我有一个代码优先应用程序的问题。当我试图在数据库中插入测试项时,我在子问题上出现了一个已经存在的错误。
这是我的新项目操作代码:
public async Task<ActionResult<Event>> NewEvent(Event newEvent)
{
if (await _context.Events.CountAsync() > 0 && await _context.Events.FindAsync(newEvent.Id) is not null)
return BadRequest(new ConstraintException("Event Already Exist"));
if (newEvent.DoorPrize is not null && newEvent.DoorPrize.Count() > 0)
{
var doorPrizes = newEvent.DoorPrize.Where(d => _context.DoorPrizes.Contains(d)).ToList();
foreach (DoorPrize doorPrize in doorPrizes)
{
_context.Entry(doorPrize).State = EntityState.Detached;
}
foreach (DoorPrize doorPrize in newEvent.DoorPrize)
{
if (_context.FairlightUsers.Contains(doorPrize.Sponsor))
_context.Entry(doorPrize.Sponsor).State = EntityState.Detached;
}
}
if (newEvent.AttendeeDetails is not null && newEvent.AttendeeDetails.Count() > 0)
{
var attendeeDetails = _context.EventAttendeeDetails.Where(d => newEvent.AttendeeDetails.Contains(d)).ToList();
foreach (EventAttendeeDetail attendeeDetail in attendeeDetails)
{
_context.Entry(attendeeDetail).State = EntityState.Detached;
}
}
if (newEvent.VenueAddress is not null)
{
if (_context.Addresses.Contains(newEvent.VenueAddress))
_context.Entry(newEvent.VenueAddress).State = EntityState.Detached;
}
if (newEvent.Sponsor is not null)
{
if (_context.FairlightUsers.Contains(newEvent.Sponsor))
_context.Entry(newEvent.Sponsor).State = EntityState.Detached;
}```
I don't know why, even if I make Sponsors (the 2) to detached, the application still try to add one.
Is someone see where is my mistake ?
My goal would be to avoid any child insertion because app need to take it from existing list but I don't success to achieve this. the application always try to create children event with setting them as detached. is there a method to avoid this ?
使用分离的实体是一件麻烦事。使用分离的实体图是一件非常痛苦的事情。您的方法有几个问题,即在使用分离实体时,数据库状态应始终被视为真理点,只有在验证了分离状态的更改仍然相关后,才能应用这些更改。(也就是说,自从你当前的副本被拿走后,其他人就没有修改过实体数据。
首先,
await _context.Events.CountAsync() > 0
&& await _context.Events.FindAsync(newEvent.Id) is not null
完全没有必要。为什么要告诉DbContext执行计数并加载实体以确定实体是否存在?如果你想知道一个实体是否存在:
var doesExist = _context.Events.Any(x => x.Id == newEvent.Id);
if (doesExist)
return BadRequest(new ConstraintException("Event already exists"));
这对数据库执行IF EXISTS SELECT
,这要快得多。
并非DbContext上的每个操作都需要是async
。异步调用对于运行需要一些时间的操作非常有用。它们的额外性能成本很小,因此任何可以快速完成的操作(如获取单个实体或合理的实体图)都可以同步完成。
接下来,在处理分离的实体时,您通常不希望分离现有实体,尤其是引用,以被进入的分离实体覆盖。简而言之,您永远不应该相信进入域的数据是最新的,或者不会被错误或恶意消费者意外篡改。例如,如果你有一个web应用程序,服务器发送一个要呈现的分离实体,然后客户端显示要更改的字段,然后Form或Ajax POST(Javascript)将数据序列化到entity类中发送回服务器,很容易错过导致#nulls的值,恶意用户可以使用浏览器调试工具拦截POST,查看实体数据并进行更改,类似上面的代码可能会无意中覆盖数据。传入的内容可能看起来像一个实体,但通常不是。
相反,在不改变传入分离实体图的事实的情况下,将其视为DTO。Event中的数据将作为一个新实体,但与之相关的一切都需要决定这些数据是代表新实体还是对现有实体的引用。因此,例如,如果事件和DoorPrize之间的关系是一对多的,其中DoorPrice实体将与新事件一起创建,并且只与该实体关联,那么它应该被允许与该实体一起插入。相反,如果DoorPrize是它自己的实体,并且仅与此事件(以及其他事件)相关联,则它需要与数据状态重新关联。
二者之间的区别:数据库中的一对多(Event拥有DoorPrize)在DoorPrice表上有一个EventId。多对多(Event与DoorPrize相关联)会有一个EventDoorPrice链接表,其中包含EventId&DoorPrizeId。
在第一种情况下,如果活动被认为是新的,那么门奖应该都是新的。然而,DoorPrize和赞助商之间的关系很可能是多对多的关联,一个赞助商可能会在不同的活动中与许多不同的门奖相关联。
对于所有权,如果客户端消费者正在为实体生成新的ID(不建议使用,最好利用"标识"列之类的内容并让数据库管理),那么您可能需要检查新的DoorPrize记录在数据库中是否为而非。这里的重点不是如果发现现有的Door Prize,而是抛出一个数据异常,因为我们希望添加这些新的孩子:
如果DoorPrize是";拥有";通过事件(一对多关系),但DoorPrizeId由消费者设置,例如使用有意义的密钥或Guid.New()
var doorPrizeIds = newEvent.SelectMany(e => e.DoorPrize.Id).ToList();
var doorPrizeExists = _context.DoorPrizes.Any(dp => doorPrizeIds.Contains(dp.Id));
if (doorPrizeExists)
return BadRequest(new ConstraintException("One or more door prizes already exists"))
处理联想需要更多的关注。如果DoorPrize预计存在并且与新事件关联,那么我们需要找到它们。如果该请求需要处理可能作为该请求的一部分创建的新DoorPrize实体,那么也需要处理该请求。一般来说,最好是更原子化地处理事情,因为创建一个与门奖相关的事件就是为了做到这一点。如果有创建新Door Prize的行动,那么将通过单独的电话处理。
如果DoorPrize是";关联的";到事件(多对多关系)
var doorPrizeIds = newEvent.SelectMany(e => e.DoorPrize.Id)
.ToList();
var existingDoorPrizes = await _context.DoorPrizes
.Where(dp => doorPrizeIds.Contains(dp.Id))
.ToListAsync();
var existingDoorPrizeIds = existingDoorPrizes.Select(dp => dp.Id).ToList();
var doorPrizesToExclude = newEvent.SelectMany(e => e.DoorPrize)
.Where(dp => existingDoorPrizeIds.Contains(dp.Id))
.ToList();
foreach(var doorPrize in doorPrizesToExclude)
newEvent.DoorPrizes.Remove(doorPrize);
foreach(var doorPrize in existingDoorPrizes)
newEvent.DoorPrizes.Add(doorPrize);
这给了我们一个匹配的真实Door Prize实体的列表。我们希望将这些数据与新事件的数据联系起来。任何可能是新的门奖都将在添加活动时添加。这里的最后一步将适用于这两种情况,即将赞助商与任何新的DoorPrize联系起来。在一对多的场景中,这将是每个门的奖品,在多对多的情况中,这只是可能添加的不存在的奖品:
一对多示例:
var sponsorIds = newEvent
.SelectMany(e => e.DoorPrizes.Select(dp => dp.Sponsor.Id))
.Distinct();
var sponsors = await _context.Sponsors
.Where(s => sponsorIds.Contains(s.Id))
.ToListAsync();
foreach(var doorPrize in newEvent.DoorPrizes)
{
var sponsor = sponsors.SingleOrDefault(s => s.Id == doorPrize.Sponsor.Id);
if(sponsor == null)
return BadRequest(new ConstraintException("One or more door prizes was invalid."))
doorPrize.Sponsor = sponsor;
}
多对多示例:
一对多示例:
var newDoorPrizes = newEvent.DoorPrizes.Where(dp => !existingDoorPrizeIds.Contains(dp.Id)).ToList();
if(newDoorPrizes.Any())
{
var sponsorIds = newDoorPrizes.Select(dp => dp.Sponsor.Id))
.Distinct();
var sponsors = await _context.Sponsors
.Where(s => sponsorIds.Contains(s.Id))
.ToListAsync();
foreach(var doorPrize in newEvent.DoorPrizes)
{
var sponsor = sponsors.SingleOrDefault(s => s.Id == doorPrize.Sponsor.Id);
if(sponsor == null)
return BadRequest(new ConstraintException("One or more door prizes was invalid."))
doorPrize.Sponsor = sponsor;
}
}
为赞助商处理协会的类似操作,不同之处在于,如果DoorPrize是协会,我们只想在新增加的门奖上代替赞助商。我们从上下文跟踪实体重新关联的门奖将已经有有效的赞助商。
稍后,当您执行更新时,这是一个类似的过程,只是您希望获取现有实体,但也要通过热切加载预获取相关的详细信息:
var existingEntry = _context.Entries
.Include(e => e.DoorPrizes)
.Single(e => e.Id == entryId);
如果找不到可以捕获的条目,这将抛出,或者如果您更喜欢内联,则调用.SingleOrDefault
并检查#null以返回BadRequest。从那里开始,您可以检查现有条目的详细信息,以确定DoorPrize是否需要更新、添加或删除。同样,对于添加DoorPrize,使用相同的流程将赞助商与实际跟踪的实例重新关联。
当更新实体图(父子关系或关联)时,重要的是区分更高级别的实体";拥有";关系,或者它是否是数据库中可能已经存在的实体之间的关联。您将希望避免代码分离被跟踪的实体,然后执行诸如将传入的实体状态设置为"已修改"以进行保存之类的操作。这将导致覆盖不打算更改的数据的各种问题,或者当EF/SQL被告知执行无效操作时出现异常。