繁体   English   中英

实体框架 - 按属性区分

[英]Entity Framework - Disctinct by property

我有这个代码:

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);

如果不将它们全部加载到 memory 中,我该如何实现呢?

尝试使用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);

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

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

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

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

如果您遵循实体框架约定,您将拥有如下类:

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 的关系,因此它被声明为虚拟的。

为了完整性,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。

我正在尝试为每个用户分配第一个任务

问题:第一个任务是什么?

  • 那是日期最早的作业吗?
  • 那是 ID 最低的任务吗?
  • 那是数据库中首先出现的任务吗? (相当难以预测)

使用虚拟 ICollections

当使用实体框架以一对多的关系获取数据时,通常使用虚拟属性更容易。 实体框架知道您的关系,并为您创建适当的(组)连接。

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();

如果你有一个一对多的关系,并且你想要每个“一个”项目的“多”子项目,从 Select 中的“一个”开始,并使用virtual ICollection来获取“多”子项目。
另一方面,如果您想要项目,每个项目及其唯一的外键引用的父项,从“多”端开始,并使用 select 的“一”端属性的虚拟属性。

间奏曲:为什么 select 只有您计划使用的属性?

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

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

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

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

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

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

所以在上面的Select中:

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 似乎是最直观的解决方案,但您也可以使用外键 select 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 同上。 实体框架将创建一个类似于使用虚拟 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(),
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM