Как 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
с родительским и дочерним, а родительский с родителями и дочерними элементами и т.д.?
Это не что-то для всего приложения, для соображений производительности это необходимо только для этой конкретной сущности - категории.
Ответы
Ответ 1
Вместо использования метода Include
вы можете использовать Load
.
Затем вы можете выполнить для каждого и пропустить всех детей, загрузив их детей. Затем сделайте для каждого через своих детей и так далее.
Количество уровней вниз будет жестко закодировано в количестве для каждой петли, которая у вас есть.
Вот пример использования Load
: http://msdn.microsoft.com/en-us/library/bb896249.aspx
Ответ 2
Если вы определенно хотите, чтобы вся иерархия загружалась, тогда, если бы это был я, я бы попытался написать хранимую процедуру, для которой нужно вернуть все элементы в иерархии, возвращая тот, который вы запрашиваете первым (и его потомки).
И затем пусть исправление отношения EF гарантирует, что все они подключены.
то есть. что-то вроде:
// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();
Если вы правильно написали свою хранимую процедуру, материализация всех элементов в иерархии (т.е. ToList()
) должна сделать исправление отношения EF.
И тогда элемент, который вы хотите (First()), должен иметь все его дочерние элементы, и они должны загружать их детей и т.д. Все будут заполнены с помощью одного вызова хранимой процедуры, поэтому проблем с MARS также не будет.
Надеюсь, что это поможет
Алекс
Ответ 3
Было бы опасно, если бы вы загрузили все рекурсивные объекты, особенно по категории, вы могли бы получить WAY больше, чем вы рассчитывали за:
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
};
Таким образом, вы можете вернуть результат словаря, если это необходимо.
Помните, что ваши контексты должны быть как можно короче.
Ответ 4
Вы могли бы ввести таблицу сопоставления, которая отображает каждую категорию как родительский и дочерний, вместо добавления родительского и дочернего свойств к самому грузу.
В зависимости от того, как часто вам нужна эта информация, она может быть запрошена по требованию. Благодаря уникальным ограничениям в db вы можете избежать бесконечного количества отношений, которые возможны.
Ответ 5
Вы не хотите выполнять рекурсивную загрузку иерархии, если только вы не позволяете пользователю итеративно развернуть/вверх по дереву: каждый уровень рекурсии - это еще одна поездка в базу данных. Аналогично, вам понадобится ленивая загрузка, чтобы предотвратить дальнейшие поездки БД, когда вы перемещаете иерархию при рендеринге на страницу или отправке через веб-сервис.
Вместо этого переверните свой запрос: Get Catalog
и Include
элементы в нем. Это приведет к тому, что все элементы будут иерархически (свойства навигации) и сплющены, поэтому теперь вам просто нужно исключить элементы, не являющиеся корневыми элементами в корне, которые должны быть довольно тривиальными.
У меня была эта проблема, и я привел подробный пример этого решения другому, здесь
Ответ 6
Используйте этот метод расширения, который вызывает жестко запрограммированную версию 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.
Ответ 7
Вот умная рекурсивная функция, которую я нашел здесь, которая будет работать для этого:
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;
}
}
}
Ответ 8
Вы также можете создать функцию tablevalued в базе данных и добавить ее в свой DBC-текст. Затем вы можете вызвать это из своего кода.
В этом примере вам нужно импортировать EntityFramework.Functions from 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;
}
}
Ответ 9
Не проще ли было бы создать представление, являющееся рекурсивным запросом ala http://www.webinade.com/web-development/create-recursive-sql-calls-for-tables-with-parent-child-отношения и просто сопоставить это. Гораздо быстрее.
Ответ 10
попробуйте это
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;
Ответ 11
@parliament дал мне идею для EF6. Пример для категории с методами для загрузки всех родителей до корня node и всех дочерних элементов.
ПРИМЕЧАНИЕ. Используйте это только для критической работы без производительности. Пример с производительностью 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;
}
}
}