简体   繁体   English

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

[英]Entity Framework 5 adding existing entity to nested collection

I've been trying to take advantage of a new way of creating many-to-many relationships - nice article about EF 5 many-to-many relationships .我一直在尝试利用一种创建多对多关系的新方法—— 关于 EF 5 多对多关系的好文章

The article states that you no longer need to define relation class and the framework does the job for you.文章指出您不再需要定义关系 class 并且框架会为您完成这项工作。

However, for a couple of hours now I've been struggling to add an existing entity to the collection of another entity.但是,几个小时以来,我一直在努力将现有实体添加到另一个实体的集合中。

My models我的模型

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; }
}

Say that I want to add a new bottle to the database.假设我想在数据库中添加一个新瓶子。 I also know owners of that bottle.我也认识那个瓶子的主人。 I had thought that this bit of code could work:我原以为这段代码可以工作:

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;
}

but sadly it does not work ( BottleForAddition is DTO with almost the same properties).但遗憾的是它不起作用( BottleForAddition是具有几乎相同属性的 DTO)。

I get this error:我收到此错误:

Unable to create bottle (error: Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.无法创建瓶子(错误:Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。有关详细信息,请参阅内部异常。

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

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

So I came up with this所以我想出了这个

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;
}

That works but I have to fetch User s from the database.那行得通,但我必须从数据库中获取User

Do you know about a better way how to deal with it?您知道如何处理它的更好方法吗?

Fetching the Users is generally the correct course of action.获取用户通常是正确的做法。 This allows you to make the associations but also helps validate that the reference IDs passed from the client are valid.这允许您进行关联,但也有助于验证从客户端传递的参考 ID 是否有效。 Fetching entities by ID is generally quite fast, so I'd consider avoiding async / await for this operation.通过 ID 获取实体通常非常快,所以我会考虑避免async / await进行此操作。 async is suited for large or high-frequency operations where server responsiveness could be "hung up". async适用于服务器响应可能被“挂起”的大型或高频操作。 Using it everywhere just leads to slower operations overall.在任何地方使用它只会导致整体运行速度变慢。

EF will want to use proxies for navigation properties both for lazy loading (not to be relied on as a crutch, but useful to avoid errors as a worst-case) as well as for change tracking. 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>();
}

then in the applicable code...然后在适用的代码中......

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;

It might be tempting to just create the users and attach them to the DbContext and much of the time this would work, except if there is ever the possibility that the DbContext might have been tracking an instance of any of those to-be-attached users, which will result in a runtime error that an entity with the same ID is already being tracked.只创建用户并将它们附加到 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;

This requires either turning off all entity tracking or remember to always query entities with AsNoTracking which can lead to additional work and intermitted bugs appearing if this isn't adhered to consistently.这需要关闭所有实体跟踪或记住始终使用AsNoTracking查询实体,如果不始终如一地遵守,这可能会导致额外的工作和出现间歇性错误。 To deal with possible tracked entities is a fair bit more work:处理可能被跟踪的实体需要做更多的工作:

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;

Any existing tracked entity needs to be found and referenced in place of an attached user reference.需要找到并引用任何现有的被跟踪实体来代替附加的用户引用。 The other caveat of this approach is that the "proxy" users created for non-tracked entities are not complete user records so later code expecting to get User records from the DbContext could receive these attached proxy rows and result in things like null reference exceptions etc. for fields that were not populated.这种方法的另一个警告是,为非跟踪实体创建的“代理”用户不是完整的用户记录,因此以后希望从 DbContext 获取用户记录的代码可能会接收这些附加的代理行并导致 null 引用异常等. 对于未填充的字段。

Hence, fetching the references from the EF DbContext to get the relatable entities is generally the best/simplest option.因此,从 EF DbContext 获取引用以获取相关实体通常是最好/最简单的选择。

  1. The Users table in the database has a Username field does not allow NULL数据库中的Users表有一个Username段不允许NULL
  2. You are creating new User entities from the OwnerIds which doesn't have Username value set您正在从没有设置Username名值的OwnerIds创建新的User实体
  3. EF is trying to insert a new user to the Users table EF 正在尝试将新用户插入Users

Combining the pieces of information above, you'll get a clear picture why the error message says -结合上面的信息,您将清楚地了解错误消息为何显示 -

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

Then comes the real question, why EF is trying to insert new users at all.然后是真正的问题,为什么 EF 试图插入新用户。 Obviously, you created the User entities from the OwnerIds to add already existing users to the list, not to insert them.显然,您从OwnerIds创建了User实体以将现有用户添加到列表中,而不是插入它们。

Well, I'm assuming that the AddEntityAsync() method you are using (I'm not familiar with it) is an extension method, and inside it, you are using the DbContext.Add() or DbSet<TEntity>.Add() method.好吧,我假设您正在使用的AddEntityAsync()方法(我不熟悉它)是一种扩展方法,并且在其中,您使用的是DbContext.Add()DbSet<TEntity>.Add()方法。 Even if that is no the case, apparently AddEntityAsync() at least works similarly as them.即使不是这样,显然AddEntityAsync()至少与它们类似。

The Add() method causes the entity in question ( Bottle ) and all it's related entities ( Users ) present in the entity-graph to be marked as Added . Add()方法导致实体图中存在的相关实体 ( Bottle ) 及其所有相关实体 ( Users ) 被标记为已Added An entity marked as Added implies - This is a new entity and it will get inserted on the next SaveChanges call.标记为已Added的实体意味着 - This is a new entity and it will get inserted on the next SaveChanges call. Therefore, with your first approach, EF tried to insert the User entities you created.因此,使用您的第一种方法,EF 尝试插入您创建的User实体。 See details - DbSet<TEntity>.Add()查看详细信息 - DbSet<TEntity>.Add()

In your second approach, you fetched the existing User entities first.在您的第二种方法中,您首先获取现有的User实体。 When you fetch existing entities using the DbContext , EF marks them as Unchanged .当您使用DbContext获取现有实体时,EF 将它们标记为Unchanged An entity marked as Unchanged implies - This entity already exists in the database and it might get updated on the next SaveChanges call.标记为Unchanged的实体暗示 - This entity already exists in the database and it might get updated on the next SaveChanges call. Therefore, in this case the Add method caused only the Bottle entity to be marked as Added and EF didn't try to re-insert any User entities you fetched.因此,在这种情况下, Add方法仅导致将Bottle实体标记为已Added ,并且 EF 没有尝试重新插入您获取的任何User实体。

As a general solution, in a disconnected scenario, when creating new entity with an entity-graph (with one or more related entities) use the Attach method instead.作为一般解决方案,在断开连接的情况下,当使用实体图(具有一个或多个相关实体)创建新实体时,请改用Attach方法。 The Attach method causes any entity to be marked as Added only if it doesn't have the primary-key value set.仅当没有设置主键值时, Attach方法才会将任何实体标记为已Added Otherwise, the entity is marked as Unchanged .否则,实体被标记为Unchanged See details - DbSet<TEntity>.Attach()查看详细信息 - DbSet<TEntity>.Attach()

Following is an example -以下是一个例子 -

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

Not related to the issue:与问题无关:
Also, since you are already using AutoMapper , if you define your BottleForAddition DTO as something like -此外,由于您已经在使用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
}

then you will be able to configure/define your maps like -那么您将能够配置/定义您的地图,例如 -

this.CreateMap<BottleForAddition, Bottle>();

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

which could simplify the operation code like -这可以简化操作代码,例如 -

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