[英]Entity Framework 5 adding existing entity to nested collection
我一直在尝试利用一种创建多对多关系的新方法—— 关于 EF 5 多对多关系的好文章。
文章指出您不再需要定义关系 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)。
我收到此错误:
无法创建瓶子(错误:Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。有关详细信息,请参阅内部异常。
Microsoft.Data.Sqlite.SqliteException (0x80004005):SQLite 错误 19:'NOT NULL 约束:用户名失败。
在 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;
}
那行得通,但我必须从数据库中获取User
。
您知道如何处理它的更好方法吗?
获取用户通常是正确的做法。 这允许您进行关联,但也有助于验证从客户端传递的参考 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 获取用户记录的代码可能会接收这些附加的代理行并导致 null 引用异常等. 对于未填充的字段。
因此,从 EF DbContext 获取引用以获取相关实体通常是最好/最简单的选择。
Users
表有一个Username
段不允许NULL
Username
名值的OwnerIds
创建新的User
实体Users
表结合上面的信息,您将清楚地了解错误消息为何显示 -
SQLite 错误 19:'NOT NULL 约束失败: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
}
那么您将能够配置/定义您的地图,例如 -
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();
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.