EF Core更新具有多对多集合的未跟踪实体



我面临EF Core和集合的问题;我有人读书,这些书可以由多个人读,人们可以读多本书(这是一种多对多的关系)。我的EF生成3个表BooksPersonsBookPersons

当我给新人插入他们读过的一套书时,没有问题。尽管如此,当我在数据库上下文之外重新创建其中一个人(id相同,但阅读书籍的集合发生了变化)并试图保存它时,它在多对多关系上失败了。因为现有之间的关系已经存在(不是唯一约束)

我试过:

  • 将图书收藏附加到上下文(相同错误)
  • 此人(没有错误,但也没有变化)
  • 只更改个人详细信息,不更改收藏(未跟踪的实体已保存,但我阅读的书籍未保存)

我不太喜欢管理BookPersons表或先进行查询以获取现有实体。我的目标是一次性更新一个人及其阅读的书籍。我确实知道如何用SQL编写它,但EF似乎是一个相当大的挑战。

如果您想查看我的代码,请访问:https://github.com/CasperCBroeren/EfCollectionsProblem/blob/master/Program.cs

谢谢你解释我错过了什么或没有得到

我会创建PersonBooks模型来处理这个问题,

书籍模型

public class Book
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Title { get; set; }
[ForeignKey("BookId")]
public virtual ICollection<PersonBook> PersonBooks { get; set; }
}

个人模型

public class Person
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }

[ForeignKey("PersonId")]
public virtual ICollection<PersonBook> PersonBooks { get; set; }
}

PersonBook模型

public class PersonBook
{
public int Id { get; set; }
public int PersonId { get; set; }
public int BookId { get; set; }
public virtual Person Person { get; set; }
public virtual Book Book  { get; set; }
}

然后你可以使用让所有的图书Id都由个人读取

var personId = 15; // what ever you want
db.PersonBooks.Where(a=> a.PersonId == personId);

或者通过Id 获取所有读过书的Id

var bookId= 11; // what ever you want
db.PersonBooks.Where(a=> a.BookId== bookId);

注:您可以使用例如来联系Book实体

db.PersonBooks.Where(a=> a.PersonId == personId).FirstOrDefault().Book;
EF中的一个关键因素是处理对象引用。DbContext未跟踪的任何引用都将被视为新实体。实际上,应该避免使用DbSets上的Update方法,因为它可能会导致低效且潜在危险的数据更改。

此选项:";将藏书附在上下文中";适用于单数引用,但不适用于集合。问题是,你想说的是";添加该人尚未关联的任何书";然而,DbContext不知道这个人已经与哪些书关联,除非你先获取这些信息。

。。。或者先进行查询以获取现有实体。

在大多数情况下,这实际上是应该做的事情。如果是一个简单的控制台应用程序来测试想法并了解EF的工作原理,这可能看起来有些过头了,但在现实世界的系统中,出于多种原因,这是推荐的方法。

  1. 保持有效载荷较小。以API或网站为例,允许用户将书籍与人联系起来。就数据大小而言,在服务器和客户端之间来回发送人、他们的书等的整个表示可能会变得昂贵。如果我有一个API,允许我将书籍与一个人相关联,如果这些书籍已经反映了已知的数据状态(已经存在于数据库中),那么我需要传递的只是ID。将数据传递给视图时,其想法是只传递视图所需的内容,而不是整个实体图。

  2. 确保有效载荷的安全。传递整个实体并使用Update之类的方法可能会使系统容易被篡改。Update将更新实体中的所有列,无论您是否期望或允许它们更改。通过最小化返回的数据,您可以确保只有预期的细节可以更改,并且您可以根据定义验证所提供的值是安全的。

例如,如果我有一个服务想要更新与某人相关的书籍。在UI中,我加载了John的";Jungle Book(ID:1)";,我想更新协会,所以约翰现在有了";丛林之书;以及";Tom Sawyer";。虽然我的UI现在可能允许它,但客户端浏览器确实有可能拦截对我的控制器/web服务的调用,并看到Book { ID: 1, Name: "Jungle Book" },篡改该数据以发送Book { ID: 1, Name: "Hitchhiker's Guide to the Galaxy"}。如果您确实以导致附加实体并执行Update之类操作的方式解决了此问题,那么这种篡改的后果将是攻击者可以重命名图书。这将对引用图书ID#1的每个人产生流动效应。

相反,如果我想要一个类似";UpdateBooks"可以为一个人重新分配书籍的方法,我会有一个类似这样的方法:

private void UpdateBooks(int personId, params int[] bookIds)
{
using (var context = new AppDbContext())
{
var person = context.Persons
.Include(x => x.Books)
.Single(x => x.PersonId == personId);
var existingBookIds = person.Books.Select(x => x.BookId).ToList();
var bookIdsToAdd = bookIds.Except(existingBookIds).ToList();
var bookIdsToRemove = existingBookIds.Except(bookIds).ToList();
foreach(var bookId in bookIdsToRemove)
{
var book = person.Books.Single(x => x.BookId == bookId);
person.Books.Remove(book);
}

if (bookIdsToAdd.Any())
{
var booksToAdd = context.Books
.Where(x => bookIdsToAdd.Contains(x.BookId))
.ToList();
if(booksToAdd.Count != bookIdsToAdd.Count)
{
// Handle scenario where one or more book IDs provided weren't found.
}
person.Books.AddRange(booksToAdd);
}

context.SaveChanges();
}
}

这假设EF完全在幕后处理PersonBook,其中PersonBook仅由PersonId和BookId组成,因此Person可以拥有一个图书集合,而不是PersonBook。

本例最多运行两个SELECT查询。一个用于获取Person及其当前图书,另一个用于获得任何新书(如果需要添加)。没有篡改书籍的风险,我们可以很容易地验证场景,例如传递未知的书籍ID。诱惑可能是避免查询,因为这很昂贵,但在大多数情况下,EF可以非常快速有效地提供数据。您可能需要发挥创造性,以绕过数据访问方面可能存在的性能瓶颈,这是一个例外,而不是常态。

  1. 第三个考虑因素是保持操作的原子性,尤其是对于web服务/web应用程序之类的操作。这并不适用于仅仅熟悉EF、实体等的工作方式,而是考虑更多真实世界的应用程序。而不是像UpdateBooks()那样使用更复杂的方法,而是使用像";AddBook";以及";RemoveBook";可以保持操作更快、更简单。较大方法的一个论点是,您可能希望所有操作都作为一个操作提交(或不提交),例如UpdateBooks被调用为一个大"操作"的一部分;SavePerson"方法,反映对人员及其所有相关细节的更改。在这些情况下,仍然建议具有原子操作,除了它们可以更新服务器(会话)状态而不是更新数据状态,等待"事件";保存";调用以完成将更改作为一个操作持久化,或者丢弃更改。Add/Remove方法仍然可以提供验证检查,最终设置要加载、修改和持久化的实体

相关内容

最新更新