繁体   English   中英

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

[英]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 获取引用以获取相关实体通常是最好/最简单的选择。

  1. 数据库中的Users表有一个Username段不允许NULL
  2. 您正在从没有设置Username名值的OwnerIds创建新的User实体
  3. EF 正在尝试将新用户插入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.

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