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

How do I make Entity Framework 6 (DB First) explicitly insert a Guid/UniqueIdentifier primary key?

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

我需要两者兼顾:

  1. 插入一条新记录并让其自动创建ID,
  2. 插入一条具有指定 ID 的新记录。

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


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

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)

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

如这里所说 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();
}

you should change value of StoreGeneratedPattern property of identity column from Identity to None in model designer.

Note, changing of StoreGeneratedPattern to None will fail inserting of object without specified id

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

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

我看到 2 个挑战:

  1. 将您的 Id 字段设为具有自动生成值的身份将阻止您指定自己的 GUID。
  2. 如果用户忘记明确创建新 ID,删除自动生成选项可能会产生重复键异常。

最简单的解决方案

  1. 删除自动生成的值
  2. 确保 Id 是 PK 并且 必需的
  3. 在模型的默认构造函数中为您的 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() };

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

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");
    }
}

ExecuteSqlCommandreturns"rows affected"(本例为1),否则抛出重复键异常。

一个解决方案可能是覆盖 DbContext SaveChanges。在此函数中查找要指定其 Id 的 DbSets 的所有添加条目。

如果还没有指定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 都应该具有请求的功能,或者只有一些。要检查主键是否已经填充,我需要知道主键的标识符。

我在你的 class CRMEntity 中看到你知道每个 T 都有一个 Id,这是因为这个 Id 在 CRMEntityBase 中,或者在 IGuid,我们假设它在 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 在其中一条评论中指出的那样,我忘记了您必须告诉 entity framework entity framework 不应该(总是)生成一个 Id。

例如,我对一个包含博客和帖子的简单数据库执行相同的操作。博客和帖子之间的一对多关系。为了说明思路不依赖GUID,主键是long。

// 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 通知 Entity Framework 主键的值已经生成。

我更喜欢 fluent API 避免使用属性,因为使用 fluent API 允许我在不同的数据库模型中重用实体 classes,只需通过重写 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