Entity Framework Core: 仅凭ID更新关系而不进行额外调用

18

我正在尝试理解如何处理在这个文档中所描述的'Single navigation property case'。

假设我们有两个模型。

class School
{
   public ICollection<Child> Childrens {get; set;}
   ...
}

class Child
{
    public int Id {get; set;}
    ...
}

因此,这是一种按照惯例创建的多对一关系,在Child中没有明确外键。

所以问题是,如果我们有Child实例并且知道School.Id,是否有方法可以更新这个关系而不需要额外调用数据库来获取School实例。


1
除非您的“Child”具有导航属性/父ID到父级,否则您无法这样做(即:没有原始查询)。ORM是关于对象关系的。但是,如果不先加载父项,则甚至不知道“7352”是否是有效的父项ID,因此您最终仍然需要执行此操作,否则在执行“SaveChanges()”时将从数据库提供程序获得难以解析的异常。 - Tseng
你想要实现什么目标还不清楚 - 是要改变一个已存在的子元素的父元素吗? - Ivan Stoev
@Tseng 是的,没错。我刚意识到关系方向搞反了。 - silent_coder
@Tseng 我所询问的是要执行什么操作 - 这是我需要澄清的。因为在与存根实体的多对一关系中,唯一有意义的操作是更改“SchoolId”。 - Ivan Stoev
@IvanStoev:嗯,更改SchoolId是其中之一,使用单个操作添加新的子元素是另一个有意义的操作。 - Tseng
显示剩余8条评论
2个回答

22
所以问题是如果我们有一个`Child`实例并知道`School.Id`,有没有一种方法可以更新这个关系而不需要额外调用数据库来获取`School`实例。
是的,这是可能的。您可以创建一个带有仅`Id`的伪造的`stub` `School`实体实例,将其附加到`DbContext`(这样告诉EF它已经存在),为同样的原因将`Child`实例附加,然后将`Child`添加到父集合并调用`SaveChanges`:
Child child = ...;
var schoolId = ...;

var school = new School { Id = schoolId };
context.Attach(school);
context.Attach(child);
school.Childrens.Add(child);
context.SaveChanges();

更新:实际上有另一种更清洁的方法,即使实体没有导航或FK属性,EF Core也允许您访问/修改所谓的Shadow Properties

Shadow属性是不存在于实体类中的属性。这些属性的值和状态仅在Change Tracker中维护。

只要您知道名称,就可以通过这种方式进行访问和修改。在您的情况下,如果没有配置,它将按照约定为"SchoolId"

因此,不需要虚假的School实体实例,只需确保附加了Child,然后通过ChangeTracker API简单地设置shadow属性即可:

context.Attach(child);
context.Entry(child).Property("SchoolId").CurrentValue = schoolId;
context.SaveChanges();

没想过用那种方式。个人认为这种方式不太可靠。如果 schoolId 不存在,它不会创建一个新的学校吗?而且当后面查询时,因为学校的引用将从跟踪缓存中获取,所以会产生各种奇怪的行为。 - Tseng
@Tseng 确实如此。呼叫者必须确保两个实体都存在。DbContext应该是短暂的实例,只用于此操作。那么可靠性如何?如果其中一个实体不存在,呼叫者将收到DbUpdateException,并且数据库不会进行任何更改。实际上,以上操作序列会生成单个UPDATE命令 :) - Ivan Stoev
此外,您现在的代码中有一个随机字符串,如果您的数据库字段名称更改,将不会收到任何通知。 - Rei Miyasaka

3

基于更新后的问题

不,你不能使用ORM提供的强类型功能来实现这一点,无论如何都不行。

  • Two-Way Navigation Property
  • At least a ForeignKey/Principal property(SchoolId on Child)
  • Having a shadow foreign key to the parent
  • performing a raw query (which beats the idea of having ORM for strong typing) and being DB agnostic at the same time

    // Bad!! Database specific dialect, no strong typing 
    ctx.Database.ExecuteSqlCommandAsync("UPDATE Childs SET schoolId = {0}", schoolId);
    
当您选择使用ORM时,您必须接受所选ORM框架的某些技术限制。
如果您想遵循领域驱动设计(DDD)并从实体中删除所有特定于数据库的字段,则很难将您的领域模型用作实体。
DDD和ORM之间的协同效应不太好,有更好的方法,但需要采用不同的架构方法(即:CQRS+ES(具有事件溯源的命令查询职责分离))。
这与DDD的配合更好,因为事件源自事件溯源只是简单且不可变的消息类,可以作为序列化JSON存储在数据库中并重新播放以重构域实体的状态。但这是一个不同的话题,人们可以写整整一本关于这个主题的书。
旧答案:
上述情况仅在单个数据库操作中可能发生,如果您的Child对象具有导航属性/“返回引用”到父级。
class School
{
   public ICollection<Child> Childrens {get; set;}
   ...
}

并且

class Child
{
    public int Id {get; set;}
    // this is required if you want do it in a single operation
    public int SchoolId { get; set; }
    // this one is optional
    public School { get; set; }
    ...
}

然后你可以这样做:
ctx.Childs.Add(new Child { Id = 7352, SchoolId = 5,  ... });

当然,你首先必须知道学校ID并且确认其有效性,否则如果SchoolId是无效值,该操作将会抛出异常。因此,我不建议采用这种方法。

如果你只有childId,而不是添加一个全新的孩子,你仍然需要先获取该孩子。

// childId = 7352
var child = ctx.Childs.FirstOrDefault(c => c.Id == childId);
// or use ctx.Childs.Find(childId); if there is a chance that 
// some other operation already loaded this child and it's tracked

// schoolId = 5 for example
child.SchoolId = schoolId;
ctx.SaveChanges();

ctx.Childs.Add 会忽略 Id 并尝试添加新的 Child 记录。 - Ivan Stoev
在这份文档中,描述了一个称为“单导航属性”的案例。在这种情况下,Post 中没有显式的 BlogId,但我非常有信心它会在表中被创建。所以我的问题是关于这个案例的。我认为存在一种方法,可以在将帖子添加到 Blog.Posts 集合时仅更改此“隐含”的 BlogId - silent_coder
@E-Bat 不,不是的。请滚动到正确的部分。 - silent_coder
Post类中的BlogId不是影子属性。影子属性是指在poco类中未定义但存在于数据库中的属性。无论如何...您绝不能将EF实体通过网络发送(即作为API响应)。始终使用ViewModel(或绑定模型,无论您想为Rest API称其为什么)。将EF Core Entity模型公开到WebAPI将在同一时刻使您受到严重打击,当数据库有轻微更改时,然后您的API会因为附加字段而中断。 - Tseng
1
@E-Bat:实际上的建议是不要在DbContext之外使用实体,无论是领域模型、WebAPI还是用作DTO,每个类型都应该有单独的类。否则,其他所有东西都会变得混乱,或者数据库特定的内容(ID字段)会泄漏到应用程序的其他层中。 - Tseng
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接