developer tip

Entity Framework는 재귀 계층과 어떻게 작동합니까?

copycodes 2020. 11. 12. 08:25
반응형

Entity Framework는 재귀 계층과 어떻게 작동합니까? Include ()가 작동하지 않는 것 같습니다.


나는 Item. Item있다 Category.

CategoryID, Name, ParentChildren. ParentChildren의하다 Category너무.

특정에 대한 LINQ to Entities 쿼리를 수행 할 때 메서드를 사용하지 않는 한 Item관련을 반환하지 않습니다 . 그러나 부모와 자식이있는 전체 범주를 가져 오지는 않습니다. 할 수는 있지만이 객체는 나무와 비슷하고 재귀 계층 구조가 있고 끝이 어디인지 모르겠습니다.CategoryInclude("Category")Include("Category.Parent")

EF Category에서 부모와 자식이있는, 부모와 자식이있는 부모 등을 완전히로드하려면 어떻게해야합니까?

이것은 전체 애플리케이션에 대한 것이 아니며 성능 고려 사항을 위해이 특정 엔티티 인 카테고리에만 필요합니다.


Include방법 을 사용하는 대신 Load.

그런 다음 for each를 수행하고 모든 자식을 반복하여 자식을로드 할 수 있습니다. 그런 다음 자녀를 통해 각각에 대해 수행하십시오.

내려가는 레벨의 수는 가지고있는 각 루프의 수에 하드 코딩됩니다.

다음은 사용의 예입니다 Load. http://msdn.microsoft.com/en-us/library/bb896249.aspx


확실히 전체 계층 구조를로드하고 싶다면, 내가 작업 한 저장 프로 시저를 작성해 보겠습니다. 계층 구조의 모든 항목을 반환하고 먼저 요청한 항목 (및 그 이후에 해당 하위 항목)을 반환하는 것입니다.

그런 다음 EF의 관계 수정이 모두 연결되었는지 확인합니다.

즉 다음과 같습니다.

// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();

저장 프로 시저를 올바르게 작성했다면 계층 구조의 모든 항목 (예 ToList():)을 구체화 하면 EF 관계 수정이 시작됩니다.

그런 다음 원하는 항목 (First ())에는 모든 하위 항목이로드되고 하위 항목이로드되어야합니다. 모두 하나의 저장 프로 시저 호출에서 채워 지므로 MARS 문제도 없습니다.

도움이 되었기를 바랍니다

알렉스


특히 카테고리에서 모든 재귀 엔티티를로드 한 경우 위험 할 수 있습니다.

Category > Item > OrderLine > Item
                  OrderHeader > OrderLine > Item
         > Item > ...

갑자기 대부분의 데이터베이스를로드하고 송장 라인, 고객, 다른 모든 송장을로드 할 수도 있습니다.

해야 할 일은 다음과 같습니다.

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select q;

foreach (Category cat in qryCategories) {
    if (!cat.Items.IsLoaded)
        cat.Items.Load();
    // This will only load product groups "once" if need be.
    if (!cat.ProductGroupReference.IsLoaded)
        cat.ProductGroupReference.Load();
    foreach (Item item in cat.Items) {
        // product group and items are guaranteed
        // to be loaded if you use them here.
    }
}

그러나 더 나은 솔루션은 쿼리를 생성하여 결과로 익명 클래스를 빌드하는 것이므로 데이터 저장소를 한 번만 조회하면됩니다.

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select new {
                        Category = q,
                        ProductGroup = q.ProductGroup,
                        Items = q.Items
                    };

이렇게하면 필요한 경우 사전 결과를 반환 할 수 있습니다.

컨텍스트는 가능한 한 짧아야합니다.


사용자가 트리를 반복적으로 드릴 다운 / 업그레이드하도록 허용하지 않는 한 계층 구조의 재귀 적로드를 원하지 않습니다. 모든 수준의 재귀는 데이터베이스로의 또 다른 이동입니다. 마찬가지로 페이지로 렌더링하거나 웹 서비스를 통해 전송할 때 계층 구조를 탐색 할 때 추가 DB 트립을 방지하기 위해 지연로드를 해제해야합니다.

대신 쿼리를 뒤집으십시오. Get CatalogInclude그 안의 항목. 이렇게하면 모든 항목이 계층 적 (탐색 속성) 및 평면화되므로 루트에 존재하는 루트가 아닌 요소를 제외하면됩니다.

이 문제가 있었고이 솔루션의 자세한 예를 다른 사람에게 제공 했습니다.


화물 자체에 부모 및 자식 속성을 추가하는 대신 각 범주를 부모와 자식으로 매핑하는 매핑 테이블을 도입하는 것이 좋습니다.

정보가 얼마나 자주 필요한지에 따라 요청시 쿼리 할 수 ​​있습니다. db의 고유 한 제약을 통해 무한한 관계를 피할 수 있습니다.


하드 코딩 된 버전의를 호출하는이 확장 메서드를 사용 Include하여 동적 깊이 포함 수준을 달성하면 훌륭하게 작동합니다.

namespace System.Data.Entity
{
  using Linq;
  using Linq.Expressions;
  using Text;

  public static class QueryableExtensions
  {
    public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
      int levelIndex, Expression<Func<TEntity, TEntity>> expression)
    {
      if (levelIndex < 0)
        throw new ArgumentOutOfRangeException(nameof(levelIndex));
      var member = (MemberExpression)expression.Body;
      var property = member.Member.Name;
      var sb = new StringBuilder();
      for (int i = 0; i < levelIndex; i++)
      {
        if (i > 0)
          sb.Append(Type.Delimiter);
        sb.Append(property);
      }
      return source.Include(sb.ToString());
    }
  }
}

용법:

var affiliate = await DbContext.Affiliates
  .Include(3, a => a.Referrer)
  .SingleOrDefaultAsync(a => a.Id == affiliateId);

어쨌든 EF repo 에서 토론참여하세요 .


여기 내가 찾은 영리한 재귀 함수입니다 여기 이 작동 것이라고는 :

public partial class Category
{
    public IEnumerable<Category> AllSubcategories()
    {
        yield return this;
        foreach (var directSubcategory in Subcategories)
            foreach (var subcategory in directSubcategory.AllSubcategories())
            {
                yield return subcategory;
            }
    }
}

데이터베이스에 테이블 반환 함수를 만들고이를 DBContext에 추가 할 수도 있습니다. 그런 다음 코드에서 호출 할 수 있습니다.

이 예제에서는 nuget에서 EntityFramework.Functions를 가져와야합니다.

public class FunctionReturnType
{
    public Guid Id { get; set; } 

    public Guid AnchorId { get; set; } //the zeroPoint for the recursion

    // Add other fields as you want (add them to your tablevalued function also). 
    // I noticed that nextParentId and depth are useful
}

public class _YourDatabaseContextName_ : DbContext
{
    [TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
    public IQueryable<FunctionReturnType> RecursiveQueryFunction(
        [Parameter(DbType = "boolean")] bool param1 = true
    )
    {
        //Example how to add parameters to your function
        //TODO: Ask how to make recursive queries with SQL 
        var param1 = new ObjectParameter("param1", param1);
        return this.ObjectContext().CreateQuery<FunctionReturnType>(
            $"RecursiveQueryFunction(@{nameof(param1)})", param1);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //add both (Function returntype and the actual function) to your modelbuilder. 
        modelBuilder.ComplexType<FunctionReturnType>();
        modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);

        base.OnModelCreating(modelBuilder);
    }

    public IEnumerable<Category> GetParents(Guid id)
    {
        //this = dbContext
        return from hierarchyRow in this.RecursiveQueryFunction(true)
            join yourClass from this.Set<YourClassThatHasHierarchy>()
            on hierarchyRow.Id equals yourClass.Id
            where hierarchyRow.AnchorId == id
            select yourClass;
    }
}

이 시도

List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
                .Where(m => m.Parent == null && m.Active == true)
                .Include(m => m.Action)
                .Include(m => m.Parent).ToList();    

if (list == null)
    return null;

this.GetQuery<SiteActionMap>()
    .OrderBy(m => m.SortOrder)
    .Where(m => m.Active == true)
    .Include(m => m.Action)
    .Include(m => m.Parent)
    .ToList();

return list;

@parliament가 EF6에 대한 아이디어를주었습니다. 모든 부모를 루트 노드와 모든 자식까지로드하는 메서드가있는 범주의 예입니다.

참고 : 성능이 중요하지 않은 작업에만 사용하십시오. http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html 에서 1000 개의 노드 성능을 사용한 예입니다 .

Loading 1000 cat. with navigation properties took 15259 ms 
Loading 1000 cat. with stored procedure took 169 ms

암호:

public class Category 
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Name { get; set; }

    public int? ParentId { get; set; }

    public virtual Category Parent { get; set; }

    public virtual ICollection<Category> Children { get; set; }

    private IList<Category> allParentsList = new List<Category>();

    public IEnumerable<Category> AllParents()
    {
        var parent = Parent;
        while (!(parent is null))
        {
            allParentsList.Add(parent);
            parent = parent.Parent;
        }
        return allParentsList;
    }

    public IEnumerable<Category> AllChildren()
    {
        yield return this;
        foreach (var child in Children)
        foreach (var granChild in child.AllChildren())
        {
            yield return granChild;
        }
    }   
}

내 제안은

var query = CreateQuery()
    .Where(entity => entity.Id == Id)
    .Include(entity => entity.Parent);
var result = await FindAsync(query);

return result.FirstOrDefault();

그리고 그것은 단일 entity및 모든이 entity.Parent엔티티를 로드 할 것임을 의미합니다 recursive.

entity is same as entity.Parent

이제 계층 적 데이터에 대한 완전히 다른 접근 방식 (예 : 트 리뷰 채우기)이 있습니다.

먼저 모든 데이터에 대해 플랫 쿼리를 수행 한 다음 메모리에 개체 그래프를 작성합니다.

  var items = this.DbContext.Items.Where(i=> i.EntityStatusId == entityStatusId).Select(a=> new ItemInfo() { 
            Id = a.Id,
            ParentId = a.ParentId,
            Name = a.Name,
            ItemTypeId = a.ItemTypeId
            }).ToList();

루트 항목을 가져옵니다.

 parent = items.FirstOrDefault(a => a.ItemTypeId == (int)Enums.ItemTypes.Root);

이제 그래프를 작성하십시오.

 this.GetDecendantsFromList(parent, items);


 private void GetDecendantsFromList(ItemInfo parent, List<ItemInfo> items)
    {
        parent.Children = items.Where(a => a.ParentId == parent.Id).ToList();
        foreach (var child in parent.Children)
        {
            this.GetDecendantsFromList(child,items);
        }
    }

public static class EntityFrameworkExtensions
{
    public static ObjectContext GetObjectContext(this DbContext context) 
    {
        ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;

        return objectContext;
    }

    public static string GetTableName<T>(this ObjectSet<T> objectSet) 
        where T : class
    {
        string sql = objectSet.ToTraceString();
        Regex regex = new Regex("FROM (?<table>.*) AS");
        Match match = regex.Match(sql);

        string table = match.Groups["table"].Value;
        return table;
    }

    public static IQueryable<T> RecursiveInclude<T>(this IQueryable<T> query, Expression<Func<T, T>> navigationPropertyExpression, DbContext context)
        where T : class
    {
        var objectContext = context.GetObjectContext();

        var entityObjectSet = objectContext.CreateObjectSet<T>();
        var entityTableName = entityObjectSet.GetTableName();
        var navigationPropertyName = ((MemberExpression)navigationPropertyExpression.Body).Member.Name;

        var navigationProperty = entityObjectSet
            .EntitySet
            .ElementType
            .DeclaredNavigationProperties
            .Where(w => w.Name.Equals(navigationPropertyName))
            .FirstOrDefault();

        var association = objectContext.MetadataWorkspace
            .GetItems<AssociationType>(DataSpace.SSpace)
            .Single(a => a.Name == navigationProperty.RelationshipType.Name);

        var pkName = association.ReferentialConstraints[0].FromProperties[0].Name;
        var fkName = association.ReferentialConstraints[0].ToProperties[0].Name;

        var sqlQuery = @"
                EXEC ('
                    ;WITH CTE AS
                    (
                        SELECT 
                            [cte1].' + @TABLE_PK + '
                            , Level = 1
                        FROM ' + @TABLE_NAME + ' [cte1]
                        WHERE [cte1].' + @TABLE_FK + ' IS NULL

                        UNION ALL

                        SELECT 
                            [cte2].' + @TABLE_PK + '
                            , Level = CTE.Level + 1
                        FROM ' + @TABLE_NAME + ' [cte2]
                            INNER JOIN CTE ON CTE.' + @TABLE_PK + ' = [cte2].' + @TABLE_FK + '
                    )
                    SELECT 
                        MAX(CTE.Level)
                    FROM CTE 
                ')
            ";

        var rawSqlQuery = context.Database.SqlQuery<int>(sqlQuery, new SqlParameter[]
            {
                new SqlParameter("TABLE_NAME", entityTableName),
                new SqlParameter("TABLE_PK", pkName),
                new SqlParameter("TABLE_FK", fkName)
            });

        var includeCount = rawSqlQuery.FirstOrDefault();

        var include = string.Empty;

        for (var i = 0; i < (includeCount - 1); i++)
        {
            if (i > 0)
                include += ".";

            include += navigationPropertyName;
        }

        return query.Include(include);
    }
}

참고 URL : https://stackoverflow.com/questions/1308158/how-does-entity-framework-work-with-recursive-hierarchies-include-seems-not-t

반응형