계층형 데이터(Hierarchy Tree)를 조회하는 쿼리를 EF Core로 만드는 방법이 궁금합니다.

  • 무엇을 하고자 하는지

최근에 계층형 데이터(Hierarchy Tree)를 설계해야 할 일이 있어서 여러가지 리서치를 하면서 문제 해결에 접근 중에 있습니다.

사용하는 데이터 수가 굉장히 적고 트리의 깊이도 정확히 정해져 있어서 최대한 직관적인 데이터구조인 Adjacency List로 데이터베이스 설계를 해보려고합니다.

전체 트리 조회는 아래 블로그 글을 통해 쉽게 솔루션을 찾을 수 있었습니다. LINQ의 Queryable.Select와 Expression Tree를 이용해서 고정된 깊이를 가진 트리형태의 데이터를 조회하는 SQL을 만들 수 있는걸 확인했습니다.

대표적인 계층 데이터라고 볼 수 있는 Category라는 클래스를 예시로 들겠습니다.

public class Category
{
    public long CategoryId { get; set; }
    public string Name { get; set; } = null!;
    public long? ParentCategoryId { get; set; }
    public Category? ParentCategory { get; set; } = null!;
    public List<Category> SubCategories { get; set; } = null!;
}

위와 같이 ParentCateogryId를 ForeginKey 로 Self Referencing이 됩니다.

    private static Expression<Func<Category, Category>> GetCategoryProjection(int maxDepth, int currentDepth = 0)
    {
        currentDepth++;

        Expression<Func<Category, Category>> result = category => new Category()
        {
            CategoryId = category.CategoryId,
            Name = category.Name,
            ParentCategoryId = category.ParentCategoryId,
            ParentCategory = category.ParentCategory,
            SubCategories = currentDepth == maxDepth ? new List<Category>() :
                category.SubCategories.AsQueryable()
                .Select(GetCategoryProjection(maxDepth, currentDepth))
                .ToList()
        };

        return result;
    }

    public async Task<List<Category>> GetCategoriesWithSubCategories()
    {
        var query = _context.Categories
            .TagWith("GetCategoriesWithSubCategoriesDTO")
            .AsNoTracking()
            .Where(c => c.ParentCategoryId == null)
            .Select(GetCategoryProjection(MAX_CATEGORY_DEPTH, 0));

        return await query.ToListAsync();
    }

앞서 언급했던 위 Hierarchy data using EF Core 블로그 링크에서 위와 같이 Expression 을 이용해서 재귀적인 트리를 조회하는 쿼리를 확인할 수 있었습니다.

위와 같이 Top-Down으로 상위 트리 노드를 기준으로 하위 sub tree를 조회하는 문제는 해결되었습니다!

- 무엇이 안되는지

하지만 추가로 Bottom-Up 방식의 트리 조회를 쿼리를 ORM으로 만들고 싶은데 생각보다 쉽지않네요. LINQ에 익숙하지않고 Expression Tree를 만드는 방법을 잘모르는 건지 학습을 계속 진행 중이지만 혹시나 방법이나 조언이 있다면 알려주시면 감사하겠습니다!

질문을 간단히 요약하자면 아래와 같이 .ThenInclude 코드 수준으로 EF Core코드를 안 짤 수 있는 방법입니다!
위 블로그 솔루션처럼 Queryable.Select 를 이용해서 재귀적인 함수를 만들고 싶은데 LINQ에 대한 지식이 부족한지 쉽지가 않네요.

var category = await _context.Categories
            .Where(c => c.CategoryId == categoryId) 
            .Include(c => c.ParentCategory)
                .ThenInclude(c => c.ParentCategory)
                    .ThenInclude(c => c.ParentCategory)
                        .ThenInclude(c => c.ParentCategory)
            .FirstOrDefaultAsync();

질문에 설명이 부족하다면 피드백 주시면 감사하겠습니다. 우문현답도 주시면 감사하겠습니다.

2개의 좋아요

Dapper를 도입하시는걸 고려해 보시는건 어떤가요?

아무래도 EF에서 Hierarchy 를 힘들게 구현하는 방향보다는

Dapper 를 이용해서 쿼리 생성해서 작업하시는게 좀 더 편리하지 않을까 생각됩니다.

3개의 좋아요

Dapper에서는 recursive 한 쿼리를 쉽게 해결해줄 수 있는 방법이 있나보군요. 한번 리서치 해보겠습니다. 좋은 정보 감사합니다! :+1:

아직은 EF Core 그 자체만으로 쉽게 해결 못하는게 아직 내부 원리에 미숙해서 그런지 정말로 지원이 부족해서 그런지 판단이 안되긴하네요.

1개의 좋아요

흥미로운 주제인데 시간이 나지 않아 샘플과 함께 댓글을 달지 못하고 있습니다…

Recursive Query를 EF Core에서 지원하지 않는 한 LINQ 형태로는 불가능 하다고 개인적으로는 결론을 내렸던 기억인데 아랫글은 재귀적 데이터를 EF Core에서 어떻게 접근할 수 있는지 참고가 될 만한 글입니다.

참고하시라고 공유해요

4개의 좋아요

AutoInclude 로 간편히 처리했을 때, 순환 참조 문제 때문에 엔티티를 변경하곤 했었는데…
역시 사람은 제대로 배워야 하나 봅니다.

2개의 좋아요

감사합니다. 한번 살펴봤던 Recursive CTE 군요.

1개의 좋아요

GitHub - efcore/EFCore.SqlServer.HierarchyId: Adds hierarchyid support to the SQL Server EF Core provider 혹시 요건 고려해보신 걸까요?ㅅ?

3개의 좋아요

Path Enumeration 같은 방법론이군요. 이러한 방법론도 같이 고려중입니다!. 단독으로 사용할 것같지않지만 SubTree 탐색에 활용할 것 같네요. 좋은 정보감사합니다.

1개의 좋아요