问题描述
我一直在尝试利用一种创建多对多关系的新方法 - 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;
}
你知道更好的处理方法吗?
解决方法
获取用户通常是正确的做法。这允许您进行关联,但也有助于验证从客户端传递的引用 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 获取引用以获取相关实体通常是最好/最简单的选择。
,- 数据库中的
Users
表有一个Username
字段不允许NULL
- 您正在从没有设置
User
值的OwnerIds
创建新的Username
实体 - EF 正在尝试向
Users
表中插入一个新用户
结合以上信息,您将清楚地了解错误消息为何显示 -
SQLite 错误 19:'NOT NULL 约束失败:Users.Username'。
那么真正的问题来了,为什么 EF 试图插入新用户。显然,您从 User
创建了 OwnerIds
实体是为了将现有用户添加到列表中,而不是插入它们。
好吧,我假设您使用的 AddEntityAsync()
方法(我不熟悉它)是一个扩展方法,并且在它内部,您使用的是 DbContext.Add()
或 {{ 1}} 方法。即使事实并非如此,显然 DbSet<TEntity>.Add()
至少与它们的工作方式相似。
AddEntityAsync()
方法导致实体图中存在的相关实体 (Add()
) 及其所有相关实体 (Bottle
) 被标记为 Users
。标记为 Added
的实体意味着 - Added
因此,使用您的第一种方法,EF 尝试插入您创建的 This is a new entity and it will get inserted on the next SaveChanges call.
实体。查看详情 - DbSet<TEntity>.Add()
在您的第二种方法中,您首先获取了现有的 User
实体。当您使用 User
获取现有实体时,EF 会将它们标记为 DbContext
。标记为 Unchanged
的实体意味着 - Unchanged
因此,在这种情况下,This entity already exists in the database and it might get updated on the next SaveChanges call.
方法仅导致 Add
实体被标记为 Bottle
而 EF 没有t 尝试重新插入您获取的任何 Added
实体。
作为一种通用解决方案,在断开连接的情况下,当使用实体图(具有一个或多个相关实体)创建新实体时,请改用 User
方法。 Attach
方法仅在没有设置主键值的情况下才会将任何实体标记为 Attach
。否则,实体被标记为 Added
。查看详情 - DbSet<TEntity>.Attach()
以下是一个例子 -
Unchanged
与问题无关:
此外,由于您已经在使用 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
DTO 定义为类似 -
BottleForAddition
然后您将能够配置/定义您的地图,例如 -
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));