C# Entity-Framework:如何在模型对象上组合 .Find 和 .Include?

Posted

技术标签:

【中文标题】C# Entity-Framework:如何在模型对象上组合 .Find 和 .Include?【英文标题】:C# Entity-Framework: How can I combine a .Find and .Include on a Model Object? 【发布时间】:2011-11-13 00:03:09 【问题描述】:

我正在做 mvcmusicstore 练习教程。在为专辑管理器创建脚手架时,我注意到了一些事情(添加删除编辑)。

我想优雅地编写代码,所以我正在寻找一种简洁的方式来编写它。

仅供参考,我正在让商店更通用:

相册 = 项目

流派 = 类别

艺术家 = 品牌

这是检索索引的方式(由 MVC 生成):

var items = db.Items.Include(i => i.Category).Include(i => i.Brand);

以下是删除项目的检索方式:

Item item = db.Items.Find(id);

第一个带回所有项目并填充项目模型中的类别和品牌模型。第二个,不填充类别和品牌。

我如何编写第二个来进行查找并填充里面的内容(最好在 1 行中)...理论上 - 类似于:

Item item = db.Items.Find(id).Include(i => i.Category).Include(i => i.Brand);

【问题讨论】:

如果有人需要在.net-core 中进行一般性操作,请参阅我的回答 【参考方案1】:

您可以先使用Include(),然后从结果查询中检索单个对象:

Item item = db.Items
              .Include(i => i.Category)
              .Include(i => i.Brand)
              .FirstOrDefault(x => x.ItemId == id);

【讨论】:

我真的推荐使用后者(SingleOrDefault),ToList 会先检索 所有 个条目,然后选择一个 如果我们有一个复合主键并且正在使用相关的查找重载,这就会崩溃。 这可行,但使用“Find”和“SingleOrDefault”是有区别的。 “Find”方法从本地跟踪存储中返回对象(如果存在),避免往返数据库,使用“SingleOrDefault”将强制查询数据库。 @Iravanchi 是正确的。这可能对用户有用,但据我所知,该操作及其副作用并不等同于 Find。 实际上并没有回答 ops 问题,因为它没有使用 .Find【参考方案2】:

您必须将 IQueryable 强制转换为 DbSet

var dbSet = (DbSet<Item>) db.Set<Item>().Include("");

return dbSet.Find(id);

【讨论】:

dbSet 中没有 .Find 或 .FindAsync。这是 EF Core 吗? ef 核心上也有 ef 6 我满怀希望,然后“InvalidCastException”【参考方案3】:

没有真正简单的方法来通过查找进行过滤。但我想出了一种接近的方法来复制该功能,但请注意我的解决方案的一些事项。

此解决方案允许您在不知道 .net-core 中的主键的情况下进行一般过滤

    Find 完全不同,因为它在查询数据库之前获取实体(如果它存在于跟踪中)。

    此外,它可以按对象过滤,因此用户不必知道主键。

    此解决方案适用于 EntityFramework Core。

    这需要访问上下文

这里有一些扩展方法可以帮助你按主键进行过滤

    public static IReadOnlyList<IProperty> GetPrimaryKeyProperties<T>(this DbContext dbContext)
    
        return dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
    

    //TODO Precompile expression so this doesn't happen everytime
    public static Expression<Func<T, bool>> FilterByPrimaryKeyPredicate<T>(this DbContext dbContext, object[] id)
    
        var keyProperties = dbContext.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var body = keyProperties
            // e => e.PK[i] == id[i]
            .Select((p, i) => Expression.Equal(
                Expression.Property(parameter, p.Name),
                Expression.Convert(
                    Expression.PropertyOrField(Expression.Constant(new  id = id[i] ), "id"),
                    p.ClrType)))
            .Aggregate(Expression.AndAlso);
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    

    public static Expression<Func<T, object[]>> GetPrimaryKeyExpression<T>(this DbContext context)
    
        var keyProperties = context.GetPrimaryKeyProperties<T>();
        var parameter = Expression.Parameter(typeof(T), "e");
        var keyPropertyAccessExpression = keyProperties.Select((p, i) => Expression.Convert(Expression.Property(parameter, p.Name), typeof(object))).ToArray();
        var selectPrimaryKeyExpressionBody = Expression.NewArrayInit(typeof(object), keyPropertyAccessExpression);

        return Expression.Lambda<Func<T, object[]>>(selectPrimaryKeyExpressionBody, parameter);
    

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this DbSet<TEntity> dbSet, DbContext context, object[] id)
        where TEntity : class
    
        return FilterByPrimaryKey(dbSet.AsQueryable(), context, id);
    

    public static IQueryable<TEntity> FilterByPrimaryKey<TEntity>(this IQueryable<TEntity> queryable, DbContext context, object[] id)
        where TEntity : class
    
        return queryable.Where(context.FilterByPrimaryKeyPredicate<TEntity>(id));
    

一旦你有了这些扩展方法,你就可以像这样过滤:

query.FilterByPrimaryKey(this._context, id);

【讨论】:

您的解决方案认为主键名为 Id。 Expression.Constant(new id = id[i] ), "id")。您创建一个具有名为 id 的属性的匿名对象。这被翻译为 Id = ... 。我实际上测试它不仅阅读它。 @vaggelanos 不,那是表达式中的临时变量。 @vaggelanos 这也适用于复合键。我的项目中有这个工作。 看它返回Expression&lt;Func&lt;T, object[]&gt;&gt;【参考方案4】:

丹尼斯的回答是使用IncludeSingleOrDefault。后者往返于数据库。

另一种方法是使用Find,结合Load,显式加载相关实体...

下面an MSDN example:

using (var context = new BloggingContext()) 
 
  var post = context.Posts.Find(2); 

  // Load the blog related to a given post 
  context.Entry(post).Reference(p => p.Blog).Load(); 

  // Load the blog related to a given post using a string  
  context.Entry(post).Reference("Blog").Load(); 

  var blog = context.Blogs.Find(1); 

  // Load the posts related to a given blog 
  context.Entry(blog).Collection(p => p.Posts).Load(); 

  // Load the posts related to a given blog  
  // using a string to specify the relationship 
  context.Entry(blog).Collection("Posts").Load(); 

当然,Find 会立即返回而不向存储区发出请求,如果该实体已被上下文加载。

【讨论】:

此方法使用Find,因此如果实体存在,实体本身就不会往返于数据库。但是,对于Loading 的每个关系,您都会有一个往返,而 SingleOrDefaultInclude 的组合会一次性加载所有内容。 当我比较 SQL 分析器中的 2 时,Find/Load 更适合我的情况(我有 1:1 的关系)。 @Iravanchi:你的意思是说如果我有 1:m 关系,它会调用 m 次商店吗?...因为没有多大意义。 不是 1:m 关系,而是多重关系。每次调用Load 函数时,都应在调用返回时填充关系。因此,如果您多次调用Load 为多个关系,每次都会有一个往返。即使对于单个关系,如果Find 方法在内存中找不到实体,它也会进行两次往返:一次用于Find,第二次用于Load。但据我所知,Include.SingleOrDefault 方法一次性获取实体和关系(但我不确定) 如果能够以某种方式遵循 Include 设计,而不是必须以不同的方式处理集合和引用,那就太好了。这使得创建一个 GetById() 外观变得更加困难,它只接受一个可选的 Expression> 集合(例如 _repo.GetById(id, x => x.MyCollection)) 介意提及您帖子的参考:msdn.microsoft.com/en-us/data/jj574232.aspx#explicit【参考方案5】:

对我没用。但我通过这样做解决了它。

var item = db.Items
             .Include(i => i.Category)
             .Include(i => i.Brand)
             .Where(x => x.ItemId == id)
             .First();

不知道这是否是一个好的解决方案。但是丹尼斯给我的另一个在.SingleOrDefault(x =&gt; x.ItemId = id);中给了我一个布尔错误@

【讨论】:

Dennis 的解决方案也必须有效。也许您在SingleOrDefault(x =&gt; x.ItemId = id) 中出现此错误只是因为错误的单= 而不是双== 是的,看起来你使用了 = 而不是 ==。语法错误;) 我都试过了 == 和 = 仍然给我一个错误 .SingleOrDefault(x => x.ItemId = id); =/ 必须是我的代码中的其他错误。但我做的方式不好?也许我不明白你的意思丹尼斯在他的代码中也有一个单一的 =。

以上是关于C# Entity-Framework:如何在模型对象上组合 .Find 和 .Include?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 模型类中设置数组属性的值

如何在c#中创建数据模型Model

如何从 Fluent Api 检索实体配置

如何在 C# 中创建基本的 IFC 文件

C# 模型的自定义设置器

C# REST API - 如何使用错误代码扩展模型状态错误