- 무엇을 하고자 하는지
최근에 계층형 데이터(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();
질문에 설명이 부족하다면 피드백 주시면 감사하겠습니다. 우문현답도 주시면 감사하겠습니다.