如何使 Entity Framework 6 (DB First) 显式插入 Guid/UniqueIdentifier 主键?

Posted

技术标签:

【中文标题】如何使 Entity Framework 6 (DB First) 显式插入 Guid/UniqueIdentifier 主键?【英文标题】:How do I make Entity Framework 6 (DB First) explicitly insert a Guid/UniqueIdentifier primary key? 【发布时间】:2018-04-02 02:47:15 【问题描述】:

我将 Entity Framework 6 DB First 与 SQL Server 表一起使用,每个表都有一个 uniqueidentifier 主键。这些表在主键列上有一个默认值,将其设置为newid()。我相应地更新了我的 .edmx 以将这些列的 StoreGeneratedPattern 设置为 Identity。所以我可以创建新记录,将它们添加到我的数据库上下文中,然后自动生成 ID。但是现在我需要保存一个具有特定 ID 的新记录。我读过this article,它说在使用int identity PK 列时必须在保存之前执行SET IDENTITY_INSERT dbo.[TableName] ON。由于我的是 Guid 而不是实际的身份列,因此基本上已经完成了。然而,即使在我的 C# 中我将 ID 设置为正确的 Guid,该值甚至不会作为参数传递给生成的 SQL 插入,并且 SQL Server 会为主键生成一个新的 ID。

我需要两者兼得:

    插入一条新记录,让它自动创建ID, 插入具有指定 ID 的新记录。

我有#1。如何插入具有特定主键的新记录?


编辑: 保存代码摘录(注意 accountMemberSpec.ID 是我想成为 AccountMember 主键的具体 Guid 值):

IDbContextScopeFactory dbContextFactory = new DbContextScopeFactory();

using (var dbContextScope = dbContextFactory.Create())

    //Save the Account
    dbAccountMember = CRMEntity<AccountMember>.GetOrCreate(accountMemberSpec.ID);

    dbAccountMember.fk_AccountID = accountMemberSpec.AccountID;
    dbAccountMember.fk_PersonID = accountMemberSpec.PersonID;

    dbContextScope.SaveChanges();

--

public class CRMEntity<T> where T : CrmEntityBase, IGuid

    public static T GetOrCreate(Guid id)
    
        T entity;

        CRMEntityAccess<T> entities = new CRMEntityAccess<T>();

        //Get or create the address
        entity = (id == Guid.Empty) ? null : entities.GetSingle(id, null);
        if (entity == null)
        
            entity = Activator.CreateInstance<T>();
            entity.ID = id;
            entity = new CRMEntityAccess<T>().AddNew(entity);
        

        return entity;
    

--

public class CRMEntityAccess<T> where T : class, ICrmEntity, IGuid

    public virtual T AddNew(T newEntity)
    
        return DBContext.Set<T>().Add(newEntity);
    

这里是记录的,为此生成的 SQL:

DECLARE @generated_keys table([pk_AccountMemberID] uniqueidentifier)
INSERT[dbo].[AccountMembers]
([fk_PersonID], [fk_AccountID], [fk_FacilityID])
OUTPUT inserted.[pk_AccountMemberID] INTO @generated_keys
VALUES(@0, @1, @2)
SELECT t.[pk_AccountMemberID], t.[CreatedDate], t.[LastModifiedDate]
FROM @generated_keys AS g JOIN [dbo].[AccountMembers] AS t ON g.[pk_AccountMemberID] = t.[pk_AccountMemberID]
WHERE @@ROWCOUNT > 0


-- @0: '731e680c-1fd6-42d7-9fb3-ff5d36ab80d0' (Type = Guid)

-- @1: 'f6626a39-5de0-48e2-a82a-3cc31c59d4b9' (Type = Guid)

-- @2: '127527c0-42a6-40ee-aebd-88355f7ffa05' (Type = Guid)

【问题讨论】:

能否包含相关的 C# 代码? 添加了主要部分,但我认为用英文阅读更容易。 所以我先说 EF 不是我最擅长的。但据我了解,如果您将StoredGeneratedPattern 设置为identity,这告诉EF 它甚至不需要考虑您提供的PK;它将使用数据库服务器来生成值。如果该列已经有默认值newid()newsequentialid(),您可以尝试将枚举值更改为None 并看看会发生什么?我的想法是,这将阻止它假设 SQL 将创建 guid。然后即使您不提供,列默认也会。 你是对的,但这也阻止了它同时插入新的父/子记录(没有显式设置它们的 ID。)如果你添加 new Parent() 到没有身份DbContext 然后执行 parent.Children.Add(new Child()) 并保存它将在数据库中插入具有 fk_ParentID = "00000000-0000-0000-0000-000000000000" 的孩子,因为它不知道父母的 ID 是生成的标识。做同样的事情,但不要保存添加第二个孩子。现在 DbContext 将抛出一个异常,您尝试添加具有重复主键的子项,即空 Guid 我知道您可以将实体插入/更新/删除映射到存储过程。对于这种特殊情况,这可能是一个很好的选择。你所做的绝不是常态。您已经创建了一个具有唯一标识符或自动增量字段的表,并将其绑定到您的 EF 工作。它的工作方式与您的数据库设计有关。我认为您不会找到忽略 PK 约束标志或类似的东西。 【参考方案1】:

我认为这个问题没有真正的答案......

如How can I force entity framework to insert identity columns? 所说,您可以启用模式#2,但它会破坏#1。

using (var dataContext = new DataModelContainer())
using (var transaction = dataContext.Database.BeginTransaction())

  var user = new User()
  
    ID = id,
    Name = "John"
  ;

  dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] ON");

  dataContext.User.Add(user);
  dataContext.SaveChanges();

  dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF");

  transaction.Commit();

您应该在模型设计器中将标识列的 StoreGeneratedPattern 属性的值从 Identity 更改为 None。

注意,将 StoreGeneratedPattern 更改为 None 将无法插入没有指定 id 的对象

如您所见,如果不自己设置 ID,您将无法再插入。

但是,如果你往好的方面看:Guid.NewGuid() 将允许你在没有 DB 生成功能的情况下创建一个新的 GUID。

【讨论】:

关于SET IDENTITY INSERT 的所有内容都无关紧要:由于我的是 Guid 而不是实际的身份列,因此基本上已经完成了。【参考方案2】:

我看到了 2 个挑战:

    将您的 Id 字段设置为具有自动生成值的身份将阻止您指定自己的 GUID。 如果用户忘记显式创建新 ID,则删除自动生成的选项可能会导致重复键异常。

最简单的解决方案

    移除自动生成的值 确保Id 是一个PK 并且必需的 在模型的默认构造函数中为 Id 生成一个新的 Guid。

示例模型

public class Person

    public Person()
    
        this.Id = Guid.NewGuid();
    

    public Guid Id  get; set; 

用法

// "Auto id"
var person1 = new Person();

// Manual
var person2 = new Person

    Id = new Guid("5d7aead1-e8de-4099-a035-4d17abb794b7")

这将满足您的两个需求,同时保持数据库安全。唯一的缺点是您必须为所有模型执行此操作。

如果你采用这种方法,我宁愿在模型上看到一个工厂方法,它会为我提供具有默认值的对象(Id 填充)并消除默认构造函数。恕我直言,在默认构造函数中隐藏默认值设置器绝不是一件好事。我宁愿让我的工厂方法为我做这件事,并且知道新对象填充了默认值(具有 intention)。

public class Person

    public Guid Id  get; set; 

    public static Person Create()
    
        return new Person  Id = Guid.NewGuid() ;
    

用法

// New person with default values (new Id)
var person1 = Person.Create();

// Empty Guid Id
var person2 = new Person();

// Manually populated Id
var person3 = new Person  Id = Guid.NewGuid() ;

【讨论】:

谢谢凯文。这是一个很好的选择,但 Harald Coppoolse 的回答让我无需修改我的模型类(通过修改 t4 模板)就可以完成它。作为奖励,它还让我等到数据实际保存以设置我的任何 ID找到更可取的。【参考方案3】:

解决方案是:编写自己的插入查询。我已经整理了一个快速项目来对此进行测试,因此该示例与您的域无关,但您会明白的。

using (var ctx = new Model())

    var ent = new MyEntity
    
        Id = Guid.Empty,
        Name = "Test"
    ;

    try
    
        var result = ctx.Database.ExecuteSqlCommand("INSERT INTO MyEntities (Id, Name) VALUES ( @p0, @p1 )", ent.Id, ent.Name);
    
    catch (SqlException e)
    
        Console.WriteLine("id already exists");
    

ExecuteSqlCommand 返回“受影响的行数”(在本例中为 1),或者为重复键引发异常。

【讨论】:

假设 SqlException 表示“ID 已经存在”,没有任何对实际异常的进一步调查对我来说似乎是一个非常大胆的举动. 确实,但这只是“概念证明”代码,它只是为了强调重复键将引发异常而不仅仅是返回“0”(受影响的行)这一事实。 谢谢,斯特凡。我相信这会起作用(如果以 DB 优先方式完成),但它也部分违背了我们使用实体框架的目的,即不必编写和映射数百个 CRUD 存储过程。 好吧,我以为它只适用于 1 个特定实体。如果很多或全部都是这种情况,那确实是不可行的,而是最好将密钥生成策略设置为 None 并每次都设置 Guid。为了使这更容易,您可以创建一个自定义存储库,仅当 Id 尚未在业务层中设置时才会生成随机 Guid...【参考方案4】:

解决方案可能是覆盖 DbContext SaveChanges。在此函数中,查找要为其指定 ID 的所有已添加的 DbSet 条目。

如果Id尚未指定,则指定一个,如果已指定:使用指定的一个。

覆盖所有 SaveChanges:

public override void SaveChanges()

    GenerateIds();
    return base.SaveChanges();

public override async Task<int> SaveChangesAsync()

    GenerateIds();
    return await base.SaveChangesAsync();

public override async Task<int> SaveChangesAsync(System.Threading CancellationToken token)

    GenerateIds();
    return await base.SaveChangesAsync(token);

GenerateIds 应该检查您是否已经为添加的条目提供了 ID。如果没有,请提供一个。

我不确定是否所有 DbSet 都应该具有请求的功能,或者只有一些。要检查主键是否已经被填满,我需要知道主键的标识符。

我在您的班级CRMEntity 中看到您知道每个T 都有一个Id,这是因为此Id 在CRMEntityBaseIGuid 中,假设它在IGuid 中。如果它在CRMEntityBase 中,则相应地更改以下内容。

以下是小步骤;如果需要,您可以创建一个大 LINQ。

private void GenerateIds()

    // fetch all added entries that have IGuid
    IEnumerable<IGuid> addedIGuidEntries = this.ChangeTracker.Entries()
        .Where(entry => entry.State == EntityState.Added)
        .OfType<IGuid>()

    // if IGuid.Id is default: generate a new Id, otherwise leave it
    foreach (IGuid entry in addedIGuidEntries)
    
        if (entry.Id == default(Guid)
            // no value provided yet: provide it now
            entry.Id = GenerateGuidId() // TODO: implement function
        // else: Id already provided; use this Id.
    

就是这样。因为您的所有 IGuid 对象现在都有一个非默认 ID(预定义或在 GenerateId 中生成),EF 将使用该 ID。

补充:HasDatabaseGeneratedOption

正如 xr280xr 在其中一个 cmets 中指出的那样,我忘记了您必须告诉实体框架实体框架不应(总是)生成 Id。

作为示例,我对包含博客和帖子的简单数据库执行相同操作。博客和帖子之间的一对多关系。为了表明这个想法不依赖于 GUID,主键是长的。

// If an entity class is derived from ISelfGeneratedId,
// entity framework should not generate Ids
interface ISelfGeneratedId

    public long Id get; set;

class Blog : ISelfGeneratedId

    public long Id get; set;          // Primary key

    // a Blog has zero or more Posts:
    public virtual ICollection><Post> Posts get; set;

    public string Author get; set;
    ...

class Post : ISelfGeneratedId

    public long Id get; set;           // Primary Key
    // every Post belongs to one Blog:
    public long BlogId get; set;
    public virtual Blog Blog get; set;

    public string Title get; set;
    ...

现在有趣的部分:通知实体框架主键值已经生成的流畅 API。

我更喜欢 fluent API 避免使用属性,因为使用 fluent API 可以让我在不同的数据库模型中重用实体类,只需重写 Dbcontext.OnModelCreating。

例如,在某些数据库中,我喜欢我的 DateTime 对象为 DateTime2,而在某些数据库中,我需要它们是简单的 DateTime。有时我想要自己生成的 Id,有时(比如在单元测试中)我不需要。

class MyDbContext : Dbcontext

    public DbSet<Blog> Blogs get; set;
    public DbSet<Post> Posts get; set;

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    
         // Entity framework should not generate Id for Blogs:
         modelBuilder.Entity<Blog>()
             .Property(blog => blog.Id)
             .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
         // Entity framework should not generate Id for Posts:
         modelBuilder.Entity<Blog>()
             .Property(blog => blog.Id)
             .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

         ... // other fluent API
    

SaveChanges 与我上面写的类似。 GenerateIds 略有不同。在此示例中,我没有有时 Id 已填充的问题。每个添加的实现 ISelfGeneratedId 的元素都应该生成一个 Id

private void GenerateIds()

    // fetch all added entries that implement ISelfGeneratedId
    var addedIdEntries = this.ChangeTracker.Entries()
        .Where(entry => entry.State == EntityState.Added)
        .OfType<ISelfGeneratedId>()

    foreach (ISelfGeneratedId entry in addedIdEntries)
    
        entry.Id = this.GenerateId() ;// TODO: implement function
        // now you see why I need the interface:
        // I need to know the primary key
    

对于那些正在寻找简洁的 ID 生成器的人:我经常使用与 Twitter 相同的生成器,一个可以处理多个服务器的生成器,而不会出现每个人都可以从主键中猜测添加了多少项的问题。

在Nuget IdGen package

【讨论】:

这可行,但应注意,它需要将主键属性的StoreGeneratedPattern 更改回None,以便EF 将ID 包含在SQL 插入中。我曾考虑过这一点,但我的印象是,如果您尝试添加具有相同 (Guid.Empty) ID 的两条记录,DbContext 会引发异常。我以前经历过这种情况,但当时我正在附加断开连接的实体,也许这就是区别。感谢您关注细节以得出此答案!

以上是关于如何使 Entity Framework 6 (DB First) 显式插入 Guid/UniqueIdentifier 主键?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Entity Framework Code First 中使属性唯一

Entity Framework 6 不适用于时态表

Entity Framework 5/6 中的可更新视图

Entity Framework 6 设置连接字符串运行时

如何扩展 Entity Framework 6 模型

Entity Framework 6 和 SQL Server CE,代码优先