Entity Framework 5 将现有实体添加到嵌套集合

Entity Framework 5 adding existing entity to nested collection

我一直在尝试利用一种创建多对多关系的新方法 - nice article about EF 5 many-to-many relationships

文章指出您不再需要定义关系 class 并且框架会为您完成这项工作。

但是,几个小时以来,我一直在努力将现有实体添加到另一个实体的集合中。

我的模特

public record Bottle
{
    [Key]
    public int Id { get; set; }

    [Required]   
    public string Username { get; set; }

    // some other properties

    public Collection<User> Owners { get; set; }
}

public record User
{
    [Key]
    public int Id { get; set; }

    // some other properties

    public Collection<Bottle> Bottles { get; set; }
}

假设我想向数据库中添加一个新瓶子。我也认识那个瓶子的主人。我以为这段代码可以工作:

public async Task<int> AddBottle(BottleForAddition bottle)
{
    var bottleEntity = mapper.Map<Bottle>(bottle);
    bottleEntity.Owners = bottle
        .OwnerIds // List<int>
        .Select(id => new User { Id = id })
        .ToCollection(); // my extension method

    var createdEntity = await context.AddEntityAsync(bottleEntity);
    await context.SaveChangesAsync();

    return createdEntity.Entity.Id;
}

但遗憾的是它不起作用(BottleForAddition 是具有几乎相同属性的 DTO)。

我收到这个错误:

Unable to create bottle (error: Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.

Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'NOT NULL constraint failed: Users.Username'.

at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
at ...

所以我想到了这个

public async Task<int> AddBottle(BottleForAddition bottle)
{
    var bottleEntity = mapper.Map<Bottle>(bottle);
    bottleEntity.Owners = (await context.Users
        .Where(u => bottle.OwnerIds.Contains(u.Id))
        .ToListAsync())
        .ToCollection();

    var createdEntity = await context.AddEntityAsync(bottleEntity);

    await context.SaveChangesAsync();

    return createdEntity.Entity.Id;
}

可行,但我必须从数据库中获取 Users。

你知道更好的处理方法吗?

获取用户通常是正确的做法。这允许您建立关联,但也有助于验证从客户端传递的引用 ID 是否有效。按 ID 获取实体通常非常快,因此我会考虑避免使用 async/await 进行此操作。 async 适用于服务器响应可能“挂起”的大型或高频操作。在任何地方使用它只会导致整体操作变慢。

EF 将希望对导航属性使用代理,以用于延迟加载(不应依赖于拐杖,但有助于避免最坏情况下的错误)以及更改跟踪。

public record Bottle
{
    [Key]
    public int Id { get; set; }

    [Required]   
    public string Username { get; set; }

    // some other properties

    public virtual ICollection<User> Owners { get; set; } = new List<User>();
}

然后在适用的代码中...

var bottleEntity = mapper.Map<Bottle>(bottle);
var users = context.Users
    .Where(u => bottle.OwnerIds.Contains(u.Id))
    .ToList();

foreach(var user in users)
    bottleEntity.Users.Add(user);

// Or since dealing with a new Entity could do this...
//((List<User>)bottleEntity.Users).AddRange(users);

await context.SaveChangesAsync();

return bottleEntity.Id;

创建用户并将其附加到 DbContext 可能很诱人,而且大多数情况下这会起作用,除非 DbContext 可能一直在跟踪其中任何一个实例-be-attached 用户,这将导致运行时错误,即已跟踪具有相同 ID 的实体。

var bottleEntity = mapper.Map<Bottle>(bottle);

var proxyUsers = bottle.OwnerIds
    .Select(x => new User { Id = x }).ToList();

foreach(var user in proxyUsers)
{
    context.Users.Attach(user);
    bottleEntity.Users.Add(user);
}
await context.SaveChangesAsync();

return bottleEntity.Id;

这需要关闭所有实体跟踪,或者记住始终使用 AsNoTracking 查询实体,如果不始终如一地遵守,这可能会导致额外的工作和间歇性的错误出现。处理可能被跟踪的实体需要做更多的工作:

var bottleEntity = mapper.Map<Bottle>(bottle);

var proxyUsers = bottle.OwnerIds
    .Select(x => new User { Id = x }).ToList();
var existingUsers = context.Users.Local
    .Where(x => bottle.OwnerIds.Contains(x.Id)).ToList();
var neededProxyUsers = proxyUsers.Except(existingUsers, new UserIdComparer()).ToList();
foreach(var user in neededProxyUsers)
    context.Users.Attach(user);


var users = neededProxyUsers.Union(existingUsers).ToList();

foreach(var user in users)
    bottleEntity.Users.Add(user);

await context.SaveChangesAsync();

return bottleEntity.Id;

需要找到并引用任何现有的跟踪实体来代替附加的用户引用。这种方法的另一个警告是,为非跟踪实体创建的“代理”用户 不是 完整的用户记录,因此以后期望从 DbContext 获取用户记录的代码可以接收这些附加的代理行并导致未填充字段的空引用异常等。

因此,从 EF DbContext 获取引用以获取相关实体通常是 best/simplest 选项。

  1. 数据库中的Userstable有一个Username字段不允许NULL
  2. 您正在从未设置 Username 值的 OwnerIds 创建新的 User 实体
  3. EF 正在尝试将新用户插入 Users table

结合以上信息,您将清楚地了解为什么错误消息显示 -

SQLite Error 19: 'NOT NULL constraint failed: Users.Username'.

然后才是真正的问题,为什么 EF 会尝试插入新用户。显然,您从 OwnerIds 创建了 User 实体以将现有用户添加到列表中,而不是插入它们。

好吧,我假设您正在使用的 AddEntityAsync() 方法(我不熟悉它)是一种扩展方法,并且在其中,您使用的是 DbContext.Add()DbSet<TEntity>.Add() 方法。即使情况并非如此,显然 AddEntityAsync() 至少与他们的工作方式相似。

Add() 方法导致实体图中存在的相关实体 (Bottle) 及其所有相关实体 (Users) 被标记为 Added.标记为 Added 的实体表示 - This is a new entity and it will get inserted on the next SaveChanges call. 因此,使用您的第一种方法,EF 尝试插入您创建的 User 实体。查看详情 - DbSet<TEntity>.Add()

在第二种方法中,您首先获取了现有的 User 个实体。当您使用 DbContext 获取现有实体时,EF 将它们标记为 Unchanged。标记为 Unchanged 的实体意味着 - This entity already exists in the database and it might get updated on the next SaveChanges call. 因此,在这种情况下,Add 方法仅导致 Bottle 实体被标记为 Added 而 EF 没有不要尝试重新插入您获取的任何 User 个实体。

作为通用解决方案,在断开连接的情况下,当使用实体图(具有一个或多个相关实体)创建新实体时,请改用 Attach 方法。 Attach 方法会导致任何实体仅在未设置主键值时才被标记为 Added。否则,该实体被标记为 Unchanged。查看详情 - DbSet<TEntity>.Attach()

下面是一个例子-

var bottleEntity = mapper.Map<Bottle>(bottle);
bottleEntity.Owners = bottle
    .OwnerIds // List<int>
    .Select(id => new User { Id = id })
    .ToCollection(); // my extension method

await context.Bottles.Attach(bottleEntity);

await context.SaveChangesAsync();

与问题无关:
此外,由于您已经在使用 AutoMapper,如果您将 BottleForAddition DTO 定义为 -

public class BottleForAddition
{
    public int Id { get; set; }
    public string Username { get; set; }

    // some other properties

    public Collection<int> Owners { get; set; }     // the list of owner Id
}

然后您将能够 configure/define 您的地图 -

this.CreateMap<BottleForAddition, Bottle>();

this.CreateMap<int, User>()
    .ForMember(d => d.Id, opt => opt.MapFrom(s => s));

可以简化像-

这样的操作代码
var bottleEntity = mapper.Map<Bottle>(bottle);

await context.Bottles.Attach(bottleEntity);

await context.SaveChangesAsync();