J'ai une entité qui a une colonne Auto-identity (int)
. Dans le cadre de la graine de données, je souhaite utiliser des valeurs d'identificateur spécifiques pour les "données standard" de mon système, puis je souhaite que la base de données trie la valeur id.
Jusqu'à présent, j'ai pu définir le IDENTITY_INSERT
sur On dans le cadre du lot d'insertion, mais Entity Framework ne génère pas d'instruction d'insertion incluant la variable Id
. Cela semble logique car le modèle pense que la base de données devrait fournir la valeur, mais dans ce cas, je souhaite fournir la valeur.
Modèle (pseudo code):
public class ReferenceThing
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id{get;set;}
public string Name{get;set;}
}
public class Seeder
{
public void Seed (DbContext context)
{
var myThing = new ReferenceThing
{
Id = 1,
Name = "Thing with Id 1"
};
context.Set<ReferenceThing>.Add(myThing);
context.Database.Connection.Open();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON")
context.SaveChanges(); // <-- generates SQL INSERT statement
// but without Id column value
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF")
}
}
Quelqu'un peut-il offrir des idées ou des suggestions?
J'aurais donc pu résoudre ce problème en générant mes propres instructions d'insertion SQL incluant la colonne Id. Cela ressemble à un terrible bidouillage, mais cela fonctionne: - /
public class Seeder
{
public void Seed (DbContext context)
{
var myThing = new ReferenceThing
{
Id = 1,
Name = "Thing with Id 1"
};
context.Set<ReferenceThing>.Add(myThing);
context.Database.Connection.Open();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON")
// manually generate SQL & execute
context.Database.ExecuteSqlCommand("INSERT ReferenceThing (Id, Name) " +
"VALUES (@0, @1)",
myThing.Id, myThing.Name);
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF")
}
}
J'ai créé un constructeur alternatif pour ma DbContext
qui prend bool allowIdentityInserts
. Je mets ce bool à un champ privé du même nom sur le DbContext
.
Ma OnModelCreating
"ne spécifie pas" alors la spécification d'identité si je crée le contexte dans ce "mode"
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
if(allowIdentityInsert)
{
modelBuilder.Entity<ChargeType>()
.Property(x => x.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
Cela me permet d'insérer des identifiants sans changer mes spécifications d'identité de base de données réelles. Je dois encore utiliser le truc d'activation/désactivation de l'identité que vous avez utilisé, mais au moins EF enverra les valeurs Id.
Si vous utilisez le premier modèle de base de données, modifiez la propriété StoreGeneratedPattern de la colonne ID de Identity en None.
Après cela, comme je l'ai répondu ici , cela devrait aider:
using (var transaction = context.Database.BeginTransaction())
{
var myThing = new ReferenceThing
{
Id = 1,
Name = "Thing with Id 1"
};
context.Set<ReferenceThing>.Add(myThing);
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON");
context.SaveChanges();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF");
transaction.Commit();
}
Ne peut pas être fait sans un deuxième modèle de niveau EF - copiez les classes pour l'ensemencement.
Comme vous l'avez dit, vos métadonnées indiquent que la base de données fournit la valeur, ce qu'elle ne fait pas lors de l'ensemencement.
Selon cette précédente Question vous devez commencer une transaction de votre contexte. Après avoir enregistré la modification, vous devez également reformuler la colonne Insertion d'identité et, enfin, vous devez valider la transaction.
using (var transaction = context.Database.BeginTransaction())
{
var item = new ReferenceThing{Id = 418, Name = "Abrahadabra" };
context.IdentityItems.Add(item);
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
context.SaveChanges();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF");
transaction.Commit();
}
Pour les futurs utilisateurs de Google, les réponses suggérant une logique conditionnelle dans OnModelCreating()
ne fonctionnaient pas pour moi.
Le problème principal de cette approche est que EF met en cache le modèle, de sorte qu'il n'est pas possible d'activer ou de désactiver l'identité dans le même domaine d'application.
La solution que nous avons adoptée consistait à créer une seconde dérivée DbContext
permettant l'insertion d'identité. De cette façon, les deux modèles peuvent être mis en cache et vous pouvez utiliser la variable DbContext
dérivée dans les cas rares (et espérons-le) rares dans lesquels vous devez insérer des valeurs d'identité.
Compte tenu de ce qui suit de la question de @ RikRak:
public class ReferenceThing
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<ReferenceThing> ReferenceThing { get; set; }
}
Nous avons ajouté cette dérivée DbContext
:
public class MyDbContextWhichAllowsIdentityInsert : MyDbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ReferenceThing>()
.Property(x => x.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
Ce qui serait alors utilisé avec la Seeder
comme suit:
var specialDbContext = new MyDbContextWhichAllowsIdentityInsert();
Seeder.Seed(specialDbContext);
Après avoir expérimenté plusieurs options trouvées sur ce site, le code suivant a fonctionné pour moi ( EF 6 ). Notez qu'il tente d'abord une mise à jour normale si l'élément existe déjà. Si ce n'est pas le cas, essayez une insertion normale, si l'erreur est due à IDENTITY_INSERT, puis essayez la solution de contournement. Notez également que db.SaveChanges échouera, d’où l’instruction db.Database.Connection.Open () et une étape de vérification facultative. Sachez que ce n'est pas mettre à jour le contexte, mais dans mon cas, ce n'est pas nécessaire. J'espère que cela t'aides!
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;
}
Essayez d’ajouter ce code à votre contexte de base de données "pour le garder propre" afin de parler:
Exemple de scénario d'utilisation (Ajoutez les enregistrements par défaut ID 0 au type d'entité ABCStatus:
protected override void Seed(DBContextIMD context)
{
bool HasDefaultRecord;
HasDefaultRecord = false;
DBContext.ABCStatusList.Where(DBEntity => DBEntity.ID == 0).ToList().ForEach(DBEntity =>
{
DBEntity.ABCStatusCode = @"Default";
HasDefaultRecord = true;
});
if (HasDefaultRecord) { DBContext.SaveChanges(); }
else {
using (var dbContextTransaction = DBContext.Database.BeginTransaction()) {
try
{
DBContext.IdentityInsert<ABCStatus>(true);
DBContext.ABCStatusList.Add(new ABCStatus() { ID = 0, ABCStatusCode = @"Default" });
DBContext.SaveChanges();
DBContext.IdentityInsert<ABCStatus>(false);
dbContextTransaction.Commit();
}
catch (Exception ex)
{
// Log Exception using whatever framework
Debug.WriteLine(@"Insert default record for ABCStatus failed");
Debug.WriteLine(ex.ToString());
dbContextTransaction.Rollback();
DBContext.RollBack();
}
}
}
}
Ajouter cette classe d'assistance pour la méthode d'extension Get Table Name
public static class ContextExtensions
{
public static string GetTableName<T>(this DbContext context) where T : class
{
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext.GetTableName<T>();
}
public static string GetTableName<T>(this ObjectContext context) where T : class
{
string sql = context.CreateObjectSet<T>().ToTraceString();
Regex regex = new Regex(@"FROM\s+(?<table>.+)\s+AS");
Match match = regex.Match(sql);
string table = match.Groups["table"].Value;
return table;
}
}
Le code à ajouter au DBContext:
public MyDBContext(bool _EnableIdentityInsert)
: base("name=ConnectionString")
{
EnableIdentityInsert = _EnableIdentityInsert;
}
private bool EnableIdentityInsert = false;
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<DBContextIMD, Configuration>());
//modelBuilder.Entity<SomeEntity>()
// .Property(e => e.SomeProperty)
// .IsUnicode(false);
// Etc... Configure your model
// Then add the following bit
if (EnableIdentityInsert)
{
modelBuilder.Entity<SomeEntity>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<AnotherEntity>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
//Add this for Identity Insert
/// <summary>
/// Enable Identity insert for specified entity type.
/// Note you should wrap the identity insert on, the insert and the identity insert off in a transaction
/// </summary>
/// <typeparam name="T">Entity Type</typeparam>
/// <param name="On">If true sets identity insert on else set identity insert off</param>
public void IdentityInsert<T>(bool On)
where T: class
{
if (!EnableIdentityInsert)
{
throw new NotSupportedException(string.Concat(@"Cannot Enable entity insert on ", typeof(T).FullName, @" when _EnableIdentityInsert Parameter is not enabled in constructor"));
}
if (On)
{
Database.ExecuteSqlCommand(string.Concat(@"SET IDENTITY_INSERT ", this.GetTableName<T>(), @" ON"));
}
else
{
Database.ExecuteSqlCommand(string.Concat(@"SET IDENTITY_INSERT ", this.GetTableName<T>(), @" OFF"));
}
}
//Add this for Rollback changes
/// <summary>
/// Rolls back pending changes in all changed entities within the DB Context
/// </summary>
public void RollBack()
{
var changedEntries = ChangeTracker.Entries()
.Where(x => x.State != EntityState.Unchanged).ToList();
foreach (var entry in changedEntries)
{
switch (entry.State)
{
case EntityState.Modified:
entry.CurrentValues.SetValues(entry.OriginalValues);
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
case EntityState.Deleted:
entry.State = EntityState.Unchanged;
break;
}
}
}