Como posso forçar o framework de entidade a inserir colunas de identidade?

Eu quero escrever algum código c # para inicializar meu database com alguns dados de semente. Obviamente, isso exigirá a capacidade de definir os valores de várias colunas de identidade ao inserir. Eu estou usando uma abordagem de código primeiro. Por padrão, o DbContext manipula a conexão com o database e, portanto, você não pode SET IDENTITY_INSERT [dbo].[MyTable] ON . Então, o que eu fiz até agora é usar o construtor DbContext que me permite especificar uma conexão de database a ser usada. Em seguida, defino IDENTITY_INSERT como ON nessa conexão de database e, em seguida, tento inserir meus registros usando a estrutura de entidade. Aqui está um exemplo do que eu tenho até agora:

 public class MyUserSeeder : IEntitySeeder { public void InitializeEntities(AssessmentSystemContext context, SqlConnection connection) { context.MyUsers.Add(new MyUser { MyUserId = 106, ConceptPersonId = 520476, Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginId = "520476", Password="28c923d21b68fdf129b46de949b9f7e0d03f6ced8e9404066f4f3a75e115147489c9f68195c2128e320ca9018cd711df", IsEnabled = true, SpecialRequirements = null }); try { connection.Open(); SqlCommand cmd = new SqlCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON", connection); int retVal = cmd.ExecuteNonQuery(); context.SaveChanges(); } finally { connection.Close(); } } } 

Tão perto e ainda tão longe – porque, embora cmd.ExecuteNonQuery() funciona bem, quando eu, em seguida, executar context.SaveChanges() , estou informado de que “valor explícito deve ser especificado para a coluna de identidade na tabela ‘MyUser’ quando IDENTITY_INSERT está definido como ON ou quando um usuário de replicação está inserindo em uma coluna de identidade NOT FOR REPLICATION. ”

Presumivelmente, como MyUserId (que é a coluna Identity na tabela MyUser) é a chave primária, a estrutura de entidade não tenta configurá-la quando eu chamo context.SaveChanges() , embora eu tenha dado à entidade MyUser um valor para MyUserId propriedade.

Existe uma maneira de forçar a estrutura de entidade a tentar inserir mesmo valores de chave primária para uma entidade? Ou talvez uma maneira de marcar temporariamente MyUserId como não sendo um valor de chave primária, então o EF tenta inseri-lo?

Método EF 6 , usando o artigo msdn :

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

Atualização: Para evitar erros “O valor explícito deve ser especificado para a coluna de identidade na tabela ‘Nome da tabela’ quando IDENTITY_INSERT está definido como ATIVADO ou quando um usuário de replicação está inserindo em uma coluna de identidade NOT FOR REPLICATION”, você deve alterar o valor da propriedade StoreGeneratedPattern de coluna de identidade de Identidade para Nenhum no designer de modelo.

Nota, a alteração de StoreGeneratedPattern para None falhará na inserção de object sem o ID especificado (modo normal) com o erro “Não é possível inserir um valor explícito para a coluna de identidade na tabela ‘TableName’ quando IDENTITY_INSERT está definido como OFF”.

Você não precisa fazer nenhum negócio engraçado com a conexão, você pode cortar o intermediário e apenas usar ObjectContext.ExecuteStoreCommand .

Você poderia então conseguir o que você quer fazendo isso:

 context.ExecuteStoreCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON"); 

Eu não conheço nenhuma maneira embutida de dizer à EF para configurar a inserção de identidade.

Não é perfeito, mas seria mais flexível e menos “hacky” do que sua abordagem atual.

Atualizar:

Acabei de perceber que há uma segunda parte para o seu problema. Agora que você disse ao SQL que deseja fazer inserções de identidade, a EF nem sequer está tentando inserir valores para essa identidade (por que não? Nós não dissemos isso).

Eu não tenho nenhuma experiência com uma primeira abordagem de código, mas a partir de algumas pesquisas rápidas, parece que você precisa dizer à EF que sua coluna não deve ser gerada a partir da loja. Você precisará fazer algo assim.

 Property(obj => obj.MyUserId) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None) .HasColumnName("MyUserId"); 

Espero que isso faça com que você aponte na direção certa 🙂

Pouco tarde para a festa, mas no caso de alguém encontrar esse problema no EF5 com o DB primeiro: eu não consegui fazer a solução funcionar, mas encontrei outra solução alternativa:

Antes de executar o comando .SaveChanges() , .SaveChanges() o contador de identidade da tabela:

 Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, {0})", newObject.Id-1);); Entities.YourTable.Add(newObject); Entities.SaveChanges(); 

Isso significa que .SaveChanges() precisa ser aplicado após cada adição – mas pelo menos funciona!

Aqui está a solução do problema. Eu tentei no EF6 e funcionou para mim. A seguir está um pseudo código que deve funcionar.

Primeiro de tudo você precisa criar a sobrecarga do dbcontext padrão. Se você verificar a class base, você encontrará aquela com a passagem do dbConnection existente. Verifique o código a seguir

 public MyDbContext(DbConnection existingConnection, bool contextOwnsConnection) : base(existingConnection, contextOwnsConnection = true) { //optional this.Configuration.ProxyCreationEnabled = true; this.Configuration.LazyLoadingEnabled = true; this.Database.CommandTimeout = 360; } 

E na criação de modelos, remova a opção db generated como,

 protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity() .Property(a => a.Id) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); base.OnModelCreating(modelBuilder); } 

Agora no código você precisa passar um object de conexão explicitamente,

 using (var connection = new System.Data.SqlClient.SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionStringName"].ConnectionString)) { connection.Open(); using (var context = new MyDbContext(connection, true)) { context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] ON"); context.MyTable.AddRange(objectList); context.SaveChanges(); context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] OFF"); } connection.Close(); } 

Após uma análise cuidadosa, decidi que a recusa do framework de entidades em inserir colunas de identidade é um recurso, não um bug. 🙂 Se eu fosse inserir todas as inputs no meu database, incluindo seus valores de identidade, eu também teria que criar uma entidade para cada tabela de links que a estrutura de entidades criara automaticamente para mim! Não é apenas a abordagem correta.

Então, o que estou fazendo é configurar as classs de semeadura que usam apenas o código C # e criar entidades do EF, em seguida, use um DbContext para salvar os dados recém-criados. Demora um pouco mais para pegar o SQL despejado e transformá-lo em código C #, mas não há (e não deveria) dados demais apenas para “semear” dados – deve ser uma pequena quantidade de dados que é representativa do tipo de dados que estaria em um database ativo que pode rapidamente ser colocado em um database novo para fins de debugging / desenvolvimento. Isso significa que, se eu quiser vincular entidades, preciso fazer consultas sobre o que já foi inserido ou meu código não saberia o valor de identidade gerado, por exemplo. Esse tipo de coisa aparecerá dentro do código de propagação, depois que eu tiver configurado e feito o context.SaveChanges para MyRoles :

 var roleBasic = context.MyRoles.Where(rl => rl.Name == "Basic").First(); var roleAdmin = context.MyRoles.Where(rl => rl.Name == "Admin").First(); var roleContentAuthor = context.MyRoles.Where(rl => rl.Name == "ContentAuthor").First(); MyUser thisUser = context.MyUsers.Add(new MyUser { Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginUsername = "naldred", Password="c1c966821b68fdf129c46de949b9f7e0d03f6cad8ea404066f4f3a75e11514748ac9f68695c2128e520ca0275cd711df", IsEnabled = true, SpecialRequirements = null }); thisUser.Roles.Add(roleBasic); 

Fazer isso dessa forma também aumenta a probabilidade de atualizar meus dados de propagação quando eu alterar o esquema, pois provavelmente eu quebrarei o código de propagação quando eu o alterar (se eu remover um campo ou entidade, o código de propagação existente que usa esse campo) / entidade não conseguirá compilar). Com um script SQL para fazer propagação, esse não seria o caso e nem o script SQL seria agnóstico de database.

Então eu acho que se você está tentando definir os campos de identidade das entidades para fazer dados de semeadura do database, você definitivamente adotou a abordagem errada.

Se eu estivesse realmente arrastando uma carga de dados do SQL Server para o PostgreSQL (um database ativo completo, não apenas alguns dados de propagação), eu poderia fazê-lo via EF, mas gostaria de ter dois contextos abertos ao mesmo tempo, e escreva algum código para pegar todas as várias entidades do contexto de origem e colocá-las no contexto de destino, depois salve as alterações.

Geralmente, a única vez que é apropriado inserir valores de identidade é quando você está copiando de um database para outro database dentro do mesmo DBMS (SQL Server -> SQL Server, PostgreSQL -> PostgreSQL, etc.) em um script SQL e não em código-EF (o script SQL não seria independente de DB, mas não precisaria ser; você não está indo entre DBMSs diferentes).

Essa ideia só funciona de maneira confiável se a tabela de destino estiver vazia ou se os registros estiverem sendo inseridos com IDs maiores que todos os IDs existentes na tabela!

3 anos depois, acertei um problema semelhante transferindo dados de produção para um sistema de teste. Os usuários queriam poder copiar os dados de produção no sistema de teste sempre que quisessem, portanto, em vez de configurar um trabalho de transferência no SQL Server, procurei uma maneira de realizar a transferência no aplicativo usando as classs EF existentes. Dessa forma, eu poderia fornecer aos usuários um item de menu para iniciar a transferência sempre que quisessem.

O aplicativo usa um database MS SQL Server 2008 e EF 6. Como os dois bancos de dados geralmente têm a mesma estrutura, eu poderia facilmente transferir dados de uma instância DbContext para outra lendo os registros de cada entidade usando AsNoTracking() e apenas Add() (ou AddRange() ) os registros para a propriedade apropriada na instância DbContext de destino.

Aqui está um DbContext com uma entidade para ilustrar:

 public class MyDataContext: DbContext { public virtual DbSet People { get; set; } } 

Para copiar os dados do People, fiz o seguinte:

 private void CopyPeople() { var records = _sourceContext.People.AsNoTracking().ToArray(); _targetContext.People.AddRange(records); _targetContext.SaveChanges(); } 

Contanto que as tabelas fossem copiadas na ordem correta (para evitar problemas com restrições de foreign keys) isso funcionou muito bem. Infelizmente, as tabelas que usam colunas de identidade tornaram as coisas um pouco difíceis, pois a EF ignorou os valores de id e deixou o SQL Server inserir o próximo valor de identidade. Para tabelas com colunas de identidade, acabei fazendo o seguinte:

  1. Leia todos os registros de uma determinada entidade
  2. Ordenar os registros por id em ordem crescente
  3. definir a semente de identidade para a tabela para o valor do primeiro id
  4. mantendo o controle do próximo valor de identidade, adicione os registros um por um. Se o id não for o mesmo que o próximo valor de identidade esperado, defina a semente de identidade para o próximo valor necessário

Contanto que a tabela esteja vazia (ou todos os novos registros possuam ids mais altos que seu id mais atual), e os ids estão em ordem crescente, EF e MS SQL irão inserir os ids requeridos e nenhum dos sistemas irá reclamar.

Aqui está um pequeno código para ilustrar:

 private void InsertRecords(Person[] people) { // setup expected id - presumption: empty table therefore 1 int expectedId = 1; // now add all people in order of ascending id foreach(var person in people.OrderBy(p => p.PersonId)) { // if the current person doesn't have the expected next id // we need to reseed the identity column of the table if (person.PersonId != expectedId) { // we need to save changes before changing the seed value _targetContext.SaveChanges(); // change identity seed: set to one less than id //(SQL Server increments current value and inserts that) _targetContext.Database.ExecuteSqlCommand( String.Format("DBCC CHECKIDENT([Person], RESEED, {0}", person.PersonId - 1) ); // update the expected id to the new value expectedId = person.PersonId; } // now add the person _targetContext.People.Add(person); // bump up the expectedId to the next value // Assumption: increment interval is 1 expectedId++; } // now save any pending changes _targetContext.SaveChanges(); } 

Usando a reflection, consegui escrever um método Load e Save que funcionou para todas as entidades no DbContext.

É um pouco um hack, mas me permite usar os methods padrão EF para ler e gravar entidades e supera o problema de como definir colunas de identidade para valores particulares sob um conjunto de circunstâncias dadas.

Espero que isso ajude alguém que enfrente um problema semelhante.

Eu sou apenas um DBA, mas sempre que algo assim aparece, considero um cheiro de código. Isto é, por que você tem algo que depende de certas linhas que possuem determinados valores de identidade? Ou seja, no seu exemplo acima, por que a Sra. Novelette precisa de um valor de identidade de 106? Ao invés de confiar em que sempre seja o caso, você pode obter seu valor de identidade e usá-lo onde quer que você tenha codificado 106. Um pouco mais pesado, mas muito mais flexível (na minha opinião).

Depois de experimentar várias opções encontradas neste site, o seguinte código funcionou para mim ( EF 6 ). Observe que primeiro tenta uma atualização normal se o item já existir. Se não, tente uma inserção normal, se o erro é devido a IDENTITY_INSERT, em seguida, tenta a solução alternativa. Observe também que db.SaveChanges falhará, portanto, a instrução db.Database.Connection.Open () e a etapa de verificação opcional. Esteja ciente de que isso não está atualizando o contexto, mas no meu caso não é necessário. Espero que isto ajude!

 public static bool UpdateLeadTime(int ltId, int ltDays) { try { using (var db = new LeadTimeContext()) { var result = db.LeadTimes.SingleOrDefault(l => l.LeadTimeId == ltId); if (result != null) { result.LeadTimeDays = ltDays; db.SaveChanges(); logger.Info("Updated ltId: {0} with ltDays: {1}.", ltId, ltDays); } else { LeadTime leadtime = new LeadTime(); leadtime.LeadTimeId = ltId; leadtime.LeadTimeDays = ltDays; try { db.LeadTimes.Add(leadtime); db.SaveChanges(); logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays); } catch (Exception ex) { logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex.Message); logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message); if (ex.InnerException.InnerException.Message.Contains("IDENTITY_INSERT")) { logger.Warn("Attempting workaround..."); try { db.Database.Connection.Open(); // required to update database without db.SaveChanges() db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] ON"); db.Database.ExecuteSqlCommand( String.Format("INSERT INTO[dbo].[LeadTime]([LeadTimeId],[LeadTimeDays]) VALUES({0},{1})", ltId, ltDays) ); db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] OFF"); logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays); // No need to save changes, the database has been updated. //db.SaveChanges(); <-- causes error } catch (Exception ex1) { logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex1.Message); logger.Warn("Inner exception message: {0}", ex1.InnerException.InnerException.Message); } finally { db.Database.Connection.Close(); //Verification if (ReadLeadTime(ltId) == ltDays) { logger.Info("Insertion verified. Workaround succeeded."); } else { logger.Info("Error!: Insert not verified. Workaround failed."); } } } } } } } catch (Exception ex) { logger.Warn("Error in UpdateLeadTime({0},{1}) was caught: {2}.", ltId.ToString(), ltDays.ToString(), ex.Message); logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message); Console.WriteLine(ex.Message); return false; } return true; } 

Existe uma maneira de forçar a estrutura de entidade a tentar inserir até mesmo valores de chave primária para uma entidade?

Sim, mas não tão claramente quanto gostaria de ver.

Supondo que você esteja usando uma chave de identidade gerada automaticamente, a EF ignorará completamente sua tentativa de armazenar o valor da chave. Isso parece ser “Por design” pelas muitas boas razões detalhadas acima, mas ainda há momentos em que você deseja controlar totalmente seus dados de semente (ou uma carga inicial). Sugiro que a EF acomode esse tipo de propagação em uma versão futura. Mas até que isso aconteça, basta escrever um pequeno código que funcione dentro da estrutura e automatize os detalhes confusos.

O Eventho VendorID é ignorado pela EF, você pode usá-lo com looping e contagem básicos para determinar quantos registros de espaço reservado adicionar entre seus registros ao vivo. Os proprietários do local recebem o próximo número de ID disponível quando são adicionados. Depois que seus registros ao vivo tiverem os IDs solicitados, você só precisará excluir o lixo.

 public class NewsprintInitializer: DropCreateDatabaseIfModelChanges { protected override void Seed(NewsprintContext context) { var vendorSeed = new List { new Vendor { VendorID = 1, Name = "#1 Papier Masson / James McClaren" }, new Vendor { VendorID = 5, Name = "#5 Abitibi-Price" }, new Vendor { VendorID = 6, Name = "#6 Kruger Inc." }, new Vendor { VendorID = 8, Name = "#8 Tembec" } }; // Add desired records AND Junk records for gaps in the IDs, because .VendorID is ignored on .Add int idx = 1; foreach (Vendor currentVendor in vendorSeed) { while (idx < currentVendor.VendorID) { context.Vendors.Add(new Vendor { Name = "**Junk**" }); context.SaveChanges(); idx++; } context.Vendors.Add(currentVendor); context.SaveChanges(); idx++; } // Cleanup (Query/Find and Remove/delete) the Junk records foreach (Vendor del in context.Vendors.Where(v => v.Name == "**Junk**")) { context.Vendors.Remove(del); } context.SaveChanges(); // setup for other classs } } 

Funcionou como esperado, exceto que eu tive que fazer “SaveChanges” frequentemente para manter os IDs em ordem.

Não consegui encontrar uma maneira de inserir registros em uma tabela. Basicamente, eu criei um script SQL com algo assim …

  sb.Append("SET IDENTITY_INSERT [dbo].[tblCustomer] ON;"); foreach(...) { var insert = string.Format("INSERT INTO [dbo].[tblCustomer] ([ID],[GivenName],[FamilyName],[NINumber],[CustomerIdent], [InputterID],[CompanyId],[Discriminator]) VALUES({0}, '{1}', '{2}', '{3}', '{4}', 2, 2, 'tblCustomer'); ", customerId, firstName, surname, nINumber, Guid.NewGuid()); sb.Append(insert); ... } sb.Append("SET IDENTITY_INSERT [dbo].[tblCustomer] OFF;"); using (var sqlConnection = new SqlConnection(connectionString)) { var svrConnection = new ServerConnection(sqlConnection); var server = new Server(svrConnection); server.ConnectionContext.ExecuteNonQuery(sb.ToString()); } 

Eu estou usando o EF 6.