首页 > 解决方案 > 如何使用 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() 以识别父记录已经存在并更新现有记录的字段而不是创建新记录的干净方法是什么?

标签: sql-serverentity-framework

解决方案


只要您告诉 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 中获取实体非常快。


推荐阅读