【问题标题】:Entity Framework 5 adding existing entity to nested collectionEntity Framework 5 将现有实体添加到嵌套集合
【发布时间】:2026-02-22 21:50:01
【问题描述】:

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

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

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

我的模型

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

我收到此错误:

无法创建瓶子(错误:Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。有关详细信息,请参阅内部异常。

Microsoft.Data.Sqlite.SqliteException (0x80004005):SQLite 错误 19:'NOT NULL 约束失败:Users.Username'。

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

所以我想出了这个

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。

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

【问题讨论】:

  • 添加了答案。看看有没有帮助。

标签: c# sqlite entity-framework entity-framework-core c#-5.0


【解决方案1】:

获取用户通常是正确的做法。这允许您进行关联,但也有助于验证从客户端传递的参考 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 可能一直在跟踪任何这些未来的实例- 附加用户,这将导致运行时错误,表明已在跟踪具有相同 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 获取引用以获取相关实体通常是最好/最简单的选择。

【讨论】:

    【解决方案2】:
    1. 数据库中的Users表有Username字段不允许NULL
    2. 您正在从未设置 Username 值的 OwnerIds 创建新的 User 实体
    3. EF 正在尝试将新用户插入到 Users 表中

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

    SQLite 错误 19:'NOT NULL 约束失败:Users.Username'。

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

    好吧,我假设您使用的 AddEntityAsync() 方法(我不熟悉)是一种扩展方法,而在其中,您使用的是 DbContext.Add()DbSet&lt;TEntity&gt;.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&lt;TEntity&gt;.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&lt;TEntity&gt;.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
    }
    

    然后您将能够配置/定义您的地图,例如 -

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

    【讨论】: