Как Entity Framework работает с рекурсивными иерархиями? Include () кажется, не работает с ним
у меня есть Item. Item есть Category.
Category и ID,Name,Parent и Children. Parent и Children из Category тоже.
когда я делаю запрос LINQ to Entities для конкретного Item, Он не возвращает связанный Category, Если я использую Include("Category") метод. Но это не приносит полную категорию, с ее родителем и детьми. Я мог бы сделать Include("Category.Parent"), но этот объект что-то вроде дерево, у меня есть рекурсивная иерархия и я не знаю, где она заканчивается.
как я могу сделать EF полностью загрузить Category, с родителем и детьми, и родитель со своим родителем и детьми, и так далее?
это не что-то для всего приложения, для соображений производительности это было бы необходимо только для этого конкретного объекта, категории.
12 ответов:
вместо
Includeметод, который вы могли бы использоватьLoad.затем вы можете сделать для каждого и цикл через всех детей, загружая их детей. Затем для каждого через своих детей, и так далее.
количество уровней вниз вы идете будет жестко закодированы в количестве для каждого цикла у вас есть.
пример использования
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 отношения fixup пинает.и затем нужный пункт (первый()) должны иметь все его дети загружены, и у них должны быть свои дети загружены и т. д. Все они будут заполнены из этого одного вызова хранимой процедуры, поэтому проблем с 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 };таким образом, вы можете вернуть результат словаря, если это необходимо.
помните,что ваши контексты должны быть как можно короче.
вы должны скорее ввести таблицу сопоставления, которая отображает каждую категорию родителя и ребенка, вместо добавления родительского и дочернего свойства к самому грузу.
в зависимости от того, как часто вам нужна эта информация, он может быть получен по запросу. С помощью уникальных ограничений в БД вы можете избежать бесконечного количества возможных отношений.
вы не хотите делать рекурсивную загрузку иерархии, если вы не позволяете пользователю итеративно детализировать дерево: каждый уровень рекурсии-это еще одна поездка в базу данных. Точно так же вы захотите ленивую загрузку, чтобы предотвратить дальнейшие поездки БД, когда вы проходите иерархию при рендеринге на страницу или отправке через веб-сервис.
вместо этого переверните свой запрос: Get
CatalogиIncludeпредметы в нем. Это даст вам все элементы как иерархически (свойства навигации) и сглажены, поэтому теперь вам просто нужно исключить некорневые элементы, присутствующие в корне, что должно быть довольно тривиальным.у меня была эта проблема и предоставил подробный пример этого решения другому,здесь
используйте этот метод расширения, который вызывает жестко закодированную версию
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 РЕПО.
вот умная рекурсивная функция, которую я нашел здесь для этого:
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. Тогда вы можете вызвать это из своего кода.
в этом примере требуется импортировать EntityFramework.Функции из NuGet.
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;
@парламент дал мне идею для EF6. Пример для категории с методами для загрузки всех родителей до корневого узла и всех детей.
Примечание: используйте это только для не критической работы производительности. Пример с производительностью 1000 узлов от http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html.
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
а теперь для совершенно другого подхода к иерархическим данным, например для заполнения treeview.
сначала сделайте плоский запрос для всех данных, а затем постройте граф объектов в памяти:
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); } }
Comments