web-dev-qa-db-fra.com

IDENTITY_INSERT lors de l'amorçage avec EntityFramework 6 Code-First

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?

22
RikRak

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")
    }
}
10
RikRak

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.

7
Chris

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

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.

2
TomTom

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

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);
1
Kevin Kuszyk

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;
}
1
David

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;
        }
    }
}
1
tcwicks