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

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

Мне нужно иметь возможность:

  • вставьте новую запись и пусть идентификатор будет автоматически создан для нее,
  • вставьте новую запись с указанным идентификатором.

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


Edit:
Сохранить выдержку кода (Note 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)

Ответ 1

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

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

Я вижу в вашем классе 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 будет использовать этот идентификатор.

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

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

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

Я предпочитаю свободно использовать API-интерфейс API, потому что использование свободного API позволяет мне повторно использовать классы сущностей в разных моделях баз данных, просто переписывая Dbcontext.OnModelCreating.

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

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

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

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

Простейшее решение:

  • Удалить автоматически сгенерированное значение
  • Убедитесь, что Id - это PK, а -.
  • Создайте новый 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 safe. Единственная нижняя сторона этого заключается в том, что вы должны сделать это для всех моделей.

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

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

Ответ 3

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

Как сказано здесь Как я могу заставить сущность 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 to None в дизайнере модели.

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

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

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

Ответ 4

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

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