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