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

时间:2017-10-20 21:12:45

标签: c# sql sql-server entity-framework guid

我正在使用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。

我需要能够同时:

  1. 插入新记录并自动为其创建ID
  2. 插入具有指定ID的新记录。
  3. 我有#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)
    

4 个答案:

答案 0 :(得分:1)

我看到了两个挑战:

  1. 使Id字段成为具有自动生成值的标识将阻止您指定自己的GUID。
  2. 如果用户忘记显式创建新ID,则删除自动生成的选项可能会产生重复的密钥例外。
  3. 最简单的解决方案

    1. 删除自动生成的值
    2. 确保Id是PK,
    3. 在模型的默认构造函数中为Id生成新的Guid。
    4. 示例模型

      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)并消除默认构造函数。恕我直言,在默认构造函数中隐藏默认值设置器永远不是一件好事。我宁愿让我的工厂方法为我做,并知道新对象填充了默认值(意图)。

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

答案 1 :(得分: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。如果没有,请提供一个。

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

我在您的班级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在其中一条评论中指出的那样,我忘了你必须告诉实体框架实体框架不应该(总是)生成一个Id。

作为一个例子,我使用一个带有Blogs和Posts的简单数据库。博客和帖子之间的一对多关系。为了表明这个想法不依赖于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,通知实体框架已经生成了主键的值。

我更喜欢流畅的API avobe使用属性,因为使用流畅的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

答案 2 :(得分:0)

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

正如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()将允许你在没有数据库生成功能的情况下创建一个新的GUID。

答案 3 :(得分:0)

解决方案是:编写自己的插入查询。我已经组织了一个快速项目来测试这个,所以这个示例与您的域名无关,但您可以使用该域名。

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返回&#34;受影响的行&#34; (在这种情况下为1),或者为重复键抛出异常。