Как сделать 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 для первичного ключа.
мне нужно уметь и то и другое :
- вставьте новую запись и пусть ID будет автоматически создан для это,
- вставить новую запись с указанным 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, который может обрабатывать несколько серверов, без проблем, что каждый может догадаться из первичного ключа, сколько элементов добавлены.
Я вижу 2 проблемы:
- сделать свой
Id
поле личности с автоматически авто стоимость позволит вам записав собственный GUID. - удаление автоматически сгенерированной опции может создать повторяющиеся исключения ключа, если пользователь забудет явно создать новый идентификатор.
простое решение:
- удалить автоматически генерируемое значение
- обеспечить
Id
является ПК и и требуется - создайте новый 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) или создает исключение для повторяющегося ключа.