【问题标题】:Entity Framework - Disctinct by property实体框架 - 按属性区分
【发布时间】:2021-05-20 13:48:59
【问题描述】:

我有这个代码:

var models = await _assignment
                .Include(a => a.Rule)
                .Include(a => a.User)
                .Select(x => new AssignmentResponse
                {
                    Id = x.User.Id,
                    RuleId = x.RuleId ?? -1
                })
                .ApplyFilterAndPaginationAsync(_webQueryProcessor, request.Options, cancellationToken);

我正在尝试为每个用户获取第一个分配(一个 userId 可以有多个 ruleId,但我只想要第一个)。

我的尝试(不工作):

var models = await _assignment
                .Include(a => a.Rule)
                .Include(a => a.User)
                .Distinct(a => a.User.Id)
                .Select(x => new AssignmentResponse
                {
                    Id = x.User.Id,
                    RuleId = x.RuleId ?? -1
                })
                .ApplyFilterAndPaginationAsync(_webQueryProcessor, request.Options, cancellationToken);

如何在不将它们全部加载到内存的情况下实现这一点?

【问题讨论】:

    标签: c# .net entity-framework linq


    【解决方案1】:

    所以你有一个Users 和一个Assignments 的表,并且用户和分配之间存在一对多的关系:每个用户都有零个或多个分配,每个分配只属于一个用户,即外键所指的User。

    您还有一张Rules 的表格。 AssignmentsRules 之间存在关系。我不确定这种关系:每个作业都只有一个规则吗? (一对多:规则有零个或多个分配,分配有规则的外键)。 还是存在多对多关系:每个Assignment 都有0 个或多个Rules,每个Rule 都是0 个或多个Assignments 中的一个Rule。

    因为这不是你问题的一部分,所以我不会深入探讨。

    如果你关注了entity framework conventions,你会有这样的课程:

    public class User
    {
        public int Id {get; set;}
        public string Name {get; set;}
        ... // other properties
    
        // every User has zero or more Assignments (one-to-many)
        public virtual ICollection<Assignment> Assignments {get; set;}
    }
    
    public class Assignment
    {
        public int Id {get; set;}
        public string Name {get; set;}
        public DateTime Date {get; set;}
        ... // other properties
    
        // every Assignment is the assignment of exactly one user, using foreign key:
        public int UserId {get; set;}
        public virtual User User {get; set;}
    
        // One-To-Many: Every assignment has one Rule using foreign key
        public int RuleId {get; set;}
        public virtual Rule Rule {get; set;}
    
        // alternative: many-to-many: every assignment has zero or more Rules
        public virtual ICollection<Rule> Rules {get; set;}
    }
    

    在实体框架中,非虚拟属性代表表的列;虚拟属性表示表之间的关系(一对多、多对多)。

    外键UserId 是表Assignments 中的真实列,因此它是非虚拟的。 User 不是Assignment 中的列,而是与Assignment 的关系,因此它被声明为virtual。

    为了完整性,DbContext:

    public class WorkDbContext : DbContext
    {
        public DbSet<User> Users {get; set;}
        public DbSet<AssignMent> Assignments {get; set;}
        public DbSet<Rule> Rules {get; set;}
        ...
    }
    

    因为我遵循实体框架的约定,这就是实体框架需要检测表、表的列以及表之间的关系(一对多、多对多),包括外键。只有当你想偏离约定,特别是不同的标识符时,你才需要使用 Attributes 或 fluent API。

    我正在尝试为每个用户获得 first 分配

    问题:first 作业是什么?

    • 这是日期最早的作业吗?
    • 是 ID 最低的分配吗?
    • 是数据库中最先出现的分配吗? (相当不可预测)

    使用虚拟 ICollections

    当使用实体框架以一对多的关系获取数据时,通常使用虚拟属性更容易。实体框架了解您的关系,并为您创建适当的(Group-)Join。

    using (var dbContext = new WorkDbContext())
    {
        var usersWithTheirFirstAssignment = dbContext.Users.Select(user => new
        {
            // Select only the User properties that you plan to use
            Id = user.Id,
            Name = user.Name,
            ...
    
            // Get the "first" Assignment
            FirstAssignment = ... // TODO: implement
        })
        .ToList();
    }
    

    在选择属性之前获取 FirstAssignment,如果它是最旧的日期,OrderBy Date:

    FirstAssignment = user.Assignments.OrderBy(assignment => assignment.Date)
        .Select(assignment => new {...})
        .FirstOrDefault();
    

    或者如果第一个分配是具有最低 ID 的分配:

    FirstAssignment = user.Assignments.OrderBy(assignment => assignment.Id)
        .Select(assignment => new {...})
        .FirstOrDefault();
    

    或者如果第一个分配只是数据库中的第一个分配:

    FirstAssignment = user.Assignments.Select(assignment => new {...})
        .FirstOrDefault();
    

    如果您有一个一对多的关系,并且您希望每个“one”项都有其“many”子项,请从 Select 中的“one”开始并使用 virtual ICollection 获取“many”子项目。
    另一方面,如果您想要项目,每个项目都有其唯一的外键引用的父项,从“多”端开始,并使用虚拟属性选择“单”端属性。

    Intermezzo:为什么只选择您计划使用的属性?

    数据库管理系统在选择数据方面得到了极大的优化。查询中较慢的部分之一是将所选数据从 DBMS 传输到本地进程。因此,明智的做法是尽可能限制所选数据的数量。

    如果您使用Include 选择相关类,则选择完整的相关子类,包括外键。

    因此,如果用户 [10] 有 1000 个分配,则每个分配都将有一个外键 UserId,值为 10。您将传输相同的数字 10 超过 1000 次。多么浪费处理能力。

    如果您不打算更改获取的数据,使用 Select 的另一个原因是每个 DbContext 都有一个 ChangeTracker。如果您在没有 Select 的情况下获取表行,则获取的对象与副本一起存储在 ChangeTracker 中。您会得到对原始文件的引用。如果更改值,ChangeTracker 中的原始值也会更改。当您使用SaveChanges时,ChangeTracker中的所有Originals都会与副本按值进行比较,以查看需要更新的项目。

    因此,如果您获取 1000 个用户,您的 ChangeTracker 将包含 1000 个原件和 1000 个副本。如果在调用 SaveChanges 之前只更改一个,则代码必须检查这 1000 个用户中每个用户的每个非虚拟属性,而只会更改一个用户。多么浪费处理能力!

    查询数据时,始终使用 Select 并仅选择您实际计划使用的属性。如果您打算更改获取的数据,请仅获取完整的类或使用 Include。

    所以在上面的选择中:

    FirstAssignment = user.Assignments.OrderBy(...)
        .Select(assignment => new
        {
            // Select only the Assignment properties that you plan to use
            Id = assignment.Id,
            Date = assignment.Date,
    
            // don't Select the foreign key, you already know the value:
            // UserId = assignment.UserId
    
            Rule = new
            {
                // you know the drill by now: only the rule properties that you plan to use!
                Id = assignment.Rule.Id,
                Name = assignment.Rule.Name,
                ...
            },
        })
        .FirstOrDefault(),
    

    替代方案:在Select中使用外键

    虽然在我看来使用虚拟 ICollection 是最直观的解决方案,但您也可以使用外键选择 FirstAssignment:

    var usersWithTheirFirstAssignment = dbContext.Users.Select(user => new
    {
        Id = user.Id,
        Name = user.Name,
        ...
    
        FirstAssignment = dbContext.Assignments
            .Where(assignment => assignment.UserId == user.Id)
            .Orderby(...)
            .Select(...)
            .FirstOrDefault(),
    }
    

    OrderBy 和 Select 同上。 Entity Framework 将创建一个类似于使用虚拟 ICollection 时的 GroupJoin。

    但我想做(组-)加入我自己!

    有些人不信任实体框架。他们不使用虚拟 ICollection,但更喜欢自己进行 GroupJoin。如果您想这样做,您需要执行以下操作:

    // GroupJoin Users and Assignments:
    var usersWithTheirFirstAssignment = dbContext.Users.GroupJoin(dbContext.Assignments,
    
    user => user.Id,                    // from every user take the primary key
    assignment => assignment.UserId,    // from every assignment take the foreign key to the user
    
    // parameter resultSelector: from every User and its zero or more assignments, make one new
    (user, assignmentsOfThisUser) => new
    {
        Id = user.Id,
        Name = user.Name,
        ...
    
        // the first assignment: order and select like described above
        FirstAssignment = assignmentsOfThisUser.OrderBy(...)
            .Select(...)
            .FirstOrDefault(),
    }
    

    【讨论】:

    • Jesus Harald,我在这里写了一篇完整的博文!它不仅有效,而且实际上教会了我很多东西。谢谢,伙计。
    【解决方案2】:

    尝试使用group by

    var models = await _assignment
                    .Include(a => a.Rule)
                    .Include(a => a.User)
                    .GroupBy(a => a.User.Id)
                    .Select(x => new AssignmentResponse
                    {
                        Id = x.Key,
                        RuleId = x.FirstOrDefault().RuleId ?? -1
                    })
                    .ApplyFilterAndPaginationAsync(_webQueryProcessor, request.Options, cancellationToken);
    

    此外,我认为您不需要包含规则?如果你不引用它的任何字段。

    【讨论】:

      猜你喜欢
      • 2014-03-29
      • 1970-01-01
      • 1970-01-01
      • 2016-01-10
      • 1970-01-01
      • 1970-01-01
      • 2011-06-29
      • 2013-01-24
      • 2013-09-06
      相关资源
      最近更新 更多