Как сделать Entity Framework 6 (DB First) явно вставить первичный ключ Guid/UniqueIdentifier?

Я использую Entity Framework 6 DB сначала с таблицами SQL Server, каждая из которых имеет uniqueidentifier первичный ключ. Таблицы имеют значение по умолчанию для столбца первичного ключа, которое устанавливает его в newid(). Я соответственно обновил .edmx для установки StoreGeneratedPattern для этих столбцов Identity. Поэтому я могу создавать новые записи, добавлять их в контекст базы данных, и идентификаторы генерируются автоматически. Но теперь мне нужно сохранить новую запись с конкретным ID. Я читал в этой статье который говорит, что у вас есть для выполнения SET IDENTITY_INSERT dbo.[TableName] ON перед сохранением при использовании столбца int identity PK. Поскольку мои-Guid, а не столбец идентификаторов, это, по сути, уже сделано. Тем не менее, хотя в моем C# я установил ID в правильный Guid, это значение даже не передается в качестве параметра сгенерированной SQL insert и новый ID генерируется SQL Server для первичного ключа.

мне нужно уметь и то и другое :

  1. вставьте новую запись и пусть ID будет автоматически создан для это,
  2. вставить новую запись с указанным ID.

у меня # 1. Как вставить новую запись с определенным первичным ключом?


Edit:
Сохранить фрагмент кода (Примечание 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)

4 ответов


решением может быть переопределение DbContext SaveChanges. В этой функции Найти все добавленные записи из dbset объектов, которые вы хотите указать идентификатор.

если идентификатор еще не указан, укажите один, если он уже указан: используйте указанный.

переопределить все 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 должен проверить, если вы уже предоставили идентификатор для добавленных записей или нет. Если нет, предоставьте один.

Я не уверен, что все наборы dbset должен иметь запрошенную функцию или только некоторые. Чтобы проверить, заполнен ли первичный ключ, мне нужно знать идентификатор первичного ключа.

Я вижу в вашем классе CRMEntity что ты знаешь, что каждый T имеет идентификатор, это потому, что этот идентификатор находится в 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 теперь имеют идентификатор по умолчанию (либо заранее определенный, либо сгенерированный внутри GenerateId), EF будет использовать этот идентификатор.

Дополнение: HasDatabaseGeneratedOption

как указал xr280xr в одном из комментариев, я забыл, что вы должны сказать entity framework, что entity framework не должен (всегда) генерировать идентификатор.

в качестве примера я делаю то же самое с простой базы данных с блогами и постами. Отношение "один ко многим" между блогами и постами. Чтобы показать, что идея не зависит от 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;}
    ...
}

теперь интересная часть: fluent API, который сообщает Entity Framework, что значения для первичных ключей уже созданы.

Я предпочитаю fluent API avobe использование атрибутов, потому что использование fluent API позволяет мне повторно использовать классы сущностей в разных моделях баз данных, просто переписывая Для DbContext.OnModelCreating.

например, в некоторых базах данных мне нравятся объекты DateTime DateTime2, а в некоторых мне нужно, чтобы они были простыми DateTime. Иногда я хочу, чтобы созданный собственной идентификаторы, иногда (как в unit-тесты) мне это не нужно.

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 немного отличается. В этом примере у меня нет проблемы, что иногда идентификатор уже заполнен. Каждый добавленный элемент, реализующий 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


Я вижу 2 проблемы:

  1. сделать свой Id поле личности с автоматически авто стоимость позволит вам записав собственный GUID.
  2. удаление автоматически сгенерированной опции может создать повторяющиеся исключения ключа, если пользователь забудет явно создать новый идентификатор.

простое решение:

  1. удалить автоматически генерируемое значение
  2. обеспечить Id является ПК и и требуется
  3. создайте новый Guid для вашего Id в конструкторе по умолчанию для вашей модели.
Модель
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")
}

это удовлетворит обе ваши потребности, сохраняя при этом безопасность db. Единственным недостатком этого является то, что вы должны сделать это для всех моделей.

если вы пойдете с этим подходом, я бы предпочел увидеть заводской метод на модели, который даст мне объект со значениями по умолчанию (Id заполнено) и исключить конструктор по умолчанию. IMHO, скрытие установщиков значений по умолчанию в конструкторе по умолчанию никогда не является хорошей вещью. Я бы предпочел, чтобы мой фабричный метод сделал это для меня и знал, что новый объект заполнен значениями по умолчанию (с намерение).

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

Я не думаю, что для этого есть реальный ответ...

Как сказано здесь как заставить entity framework вставлять столбцы идентификаторов? вы можете включить режим #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 с Identity на None в конструкторе моделей.

Примечание, изменение StoreGeneratedPattern на None не приведет к сбою вставки объект без указанного id

Как вы можете видеть, вы больше не можете вставлять без установки самостоятельно ID.

но, если вы посмотрите на яркой стороне : Guid.NewGuid() позволит вам сделать новый GUID без функции генерации DB.


решение: напишите свой собственный запрос вставки. Я собрал быстрый проект, чтобы проверить это, поэтому пример не имеет ничего общего с вашим доменом, но вы будете использовать ideea.

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) или создает исключение для повторяющегося ключа.