sql-server - 如何使用 newsequentialid 主键处理导航属性?
问题描述
我有一个使用sequentialguid键的父/子/孙关系:
CREATE TABLE [dbo].[Parent]
(
[parentid] UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID(),
[data] VARCHAR(32),
CONSTRAINT [PK_parent] PRIMARY KEY ([parentid])
)
CREATE TABLE [dbo].[Child]
(
[childid] UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID(),
[parentid] UNIQUEIDENTIFIER,
[data] VARCHAR(32)
CONSTRAINT [PK_child] PRIMARY KEY ([childid]),
CONSTRAINT [FK_parent_child] FOREIGN KEY ([parentid])
REFERENCES [dbo].[Parent] ([parentid])
)
CREATE TABLE [dbo].[grandchild]
(
[grandchildid] UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID(),
[childid] UNIQUEIDENTIFIER,
[data] VARCHAR(32)
CONSTRAINT [PK_grandchild] PRIMARY KEY ([grandchildid]),
CONSTRAINT [FK_child_grandchild] FOREIGN KEY ([childid])
REFERENCES [dbo].[child] ([childid])
)
我有一个实体框架事务:
public void SaveChild(Child aChild)
{
using (var db = new MyDbContext())
{
db.childs.Add(aChild);
db.SaveChanges();
}
}
当我用一个新的 Child 调用这个方法时,会创建一个新的 Child 记录,并带有一个新的 childid。
并创建了一个新的父记录,带有一个新的 parentid。
但事情就是这样。有时我会添加一个新的孩子和一个新的父母,有时我会添加一个新的孩子和一个现有的父母。在所有情况下,我都想添加新的孙子。
实体框架,在带有 DEFAULT NEWSEQUENTIALID() 的 Add() 上,似乎忽略了 GUID 的任何当前值并总是创建一个新值。
这会导致重复的父记录。
而我不能拥有那个。
获取子记录的 Add() 以识别父记录已经存在并更新现有记录的字段而不是创建新记录的干净方法是什么?
解决方案
只要您告诉 EF 每个表上的 PK 是一个标识列,并且您确保在关联现有实体时,数据库上下文正在跟踪该实体,它就应该起作用。(即避免将实体引用传递到读取它们的 DbContext 范围之外)
例如:对于在您的实体中使用属性表示法的 PK 声明:
public class Parent
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ParentId { get; set; }
}
这可确保 EF 将 ID 视为身份/数据库生成的值。
尤其是使用 Web 应用程序时,让人们感到困惑的第二件事是传递实体,并假设带有新 DbContext 的请求将知道它是在处理现有实体还是新实体。
例如,在一种方法中,我们读取并返回父级......
using (var context = new AppDbContext())
{
return context.Parents.Single(x => x.ParentId == parentId);
}
...然后当我们想要创建一个与父级关联的新子级时,客户端代码创建了一个子对象,关联了我们之前读取的现有父级,并将其传递回服务器:
public void CreateChild(Child child)
{
using(var context = new AppDbContext())
{
context.Children.Add(child);
context.SaveChanges();
}
// Where child.Parent == existing Parent, but DbContext inserts a new Parent.
}
同样的事情也会发生在这里:
Parent parent = null;
using (var context1 = new AppDbContext())
{
parent = context1.Parents.Single(x => x.ParentId == parentId);
}
using (var context2 = new AppDbContext())
{
Child newChild = new Child
{
Parent = parent,
Name = "Sven"
}
context2.Children.Add(newChild);
context2.SaveChanges();
}
newChild.Parent
是与 相关联的父级context1
。就其context2
而言,它是一个新的、未被追踪的实体。
您可以通过 2 种方式解决此问题:
A) 附加实体
Parent parent = null;
using (var context1 = new AppDbContext())
{
parent = context1.Parents.Single(x => x.ParentId == parentId);
}
using (var context2 = new AppDbContext())
{
context2.Parents.Attach(parent);
Child newChild = new Child
{
Parent = parent,
Name = "Sven"
}
context2.Children.Add(newChild);
context2.SaveChanges();
}
现在context2
正在跟踪parent
并将其视为现有实体。这里的问题是,Attach
如果 DbContext 实例已经在跟踪具有该 ID 的实体,则可能会失败。在上面的示例中,这不会发生,因为我们使用using
块限定了 DbContext,但是如果您使用依赖注入并将 DbContext 限定为请求,如果某些条件导致在调用此代码之前加载父级,则可能会发生这种情况. 处理分离/重新连接实体可能有点痛苦。为了安全起见,您应该检查 DbContext 是否正在跟踪实体并替换这些引用...
using (var context2 = new AppDbContext())
{
var trackedParent = context2.Parents.Local.SingleOrDefault(x => x.ParentId == parent.ParentId);
if (trackedParent == null)
context2.Parents.Attach(parent);
else
parent = trackedParent;
Child newChild = new Child
{
Parent = parent,
Name = "Sven"
}
context2.Children.Add(newChild);
context2.SaveChanges();
}
对于更复杂的实体图(父母、孙子和对其他实体的各种引用等),需要跟踪每个引用。如果某些情况导致现有实体引用滑过,则缺少一个可能会导致间歇性出现错误。
B) 不要传递实体,始终处理 ID 并让 DbContext 获取实体。与其传递实体,不如传递 DTO 和 FK。因此,当我们在客户端创建 Child 时,我们传递了一个 CreateChildViewModel,其中包含要关联的 ParentId 而不是 Parent 实体。View Models/DTO 也有助于使客户端和服务器之间的有效负载大小更紧凑。
public void CreateChild(CreateChildViewModel childVM)
{
using(var context = new AppDbContext())
{
var parent = context.Parents.Single(x => x.ParentId == childVM.ParentId);
var child = new Child
{
Parent = parent;
Name = childVM.Name;
// ...
};
context.Children.Add(child);
context.SaveChanges();
}
}
如果请求范围内的 DbContext 已经在跟踪实体引用,它将在请求时返回该引用或在数据库中查找它。传递实体的决定可能很容易避免调用数据库,但是如果实体已经被跟踪,这会导致检查和替换引用的逻辑更加复杂,(或令人讨厌的、破坏性的错误要跟踪)并且可能导致如果实体已附加并设置为已修改或使用 DbContextUpdate
方法,则数据篡改或过时数据覆盖。通过 PK 从 DbContext 中获取实体非常快。
推荐阅读
- python - 使用 python3 并连接到 sqlite3 我想显示我的数据库中的表,“。” 在“.tables”中不断给出错误,“\”没有帮助
- flutter - 在颤振小部件之间传递事件而不持有对其中之一的引用
- kotlin - Heroku 上的 Ktor Websockets - OutOfMemoryError:无法创建本机线程
- stata - 从 centile 命令保存百分位数
- mysql - 使用内部连接(Node,js,typescript)使用tyeporm返回多个对象类型
- powershell - 在 Powershell 中进行 if 条件检查
- reactjs - 如何从 /color/random API 获取随机数?
- reactjs - 如何修复工具提示以在 Dialog Hook 之外显示
- python - Mac 上的权限被拒绝
- yaml - 使用 yq 将对象添加到现有字段