wake-up-neo.com

IDENTITY_INSERT während des Seeding mit EntityFramework 6 Code-First

Ich habe eine Entität, die eine Auto-identity (int)-Spalte hat. Als Teil des Datensamens möchte ich in meinem System spezifische Bezeichnerwerte für die "Standarddaten" verwenden. Danach möchte ich, dass die Datenbank den ID-Wert aussortiert.

Bisher konnte ich IDENTITY_INSERT als Teil des Einfügungsstapels auf Ein setzen, aber Entity Framework generiert keine Einfügeanweisung, die die Id enthält. Dies ist sinnvoll, da das Modell der Ansicht ist, dass die Datenbank den Wert liefern soll. In diesem Fall möchte ich jedoch den Wert angeben.

Modell (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")
    }
}

Kann jemand Einblicke oder Anregungen geben?

22
RikRak

Ich habe dieses Problem möglicherweise gelöst, indem ich meine eigenen SQL-Einfügeanweisungen generierte, die die Id-Spalte enthalten. Es fühlt sich an wie ein schrecklicher Hack, aber es funktioniert: - /

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

Ich habe einen alternativen Konstruktor für meine DbContext erstellt, der ein bool allowIdentityInserts verwendet. Ich setze dieses bool auf ein privates Feld mit demselben Namen in der DbContext.

Meine OnModelCreating "spezifiziert" dann die Identitätsspezifikation "nicht spezifiziert", wenn der Kontext in diesem "Modus" erstellt wird.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        if(allowIdentityInsert)
        {
            modelBuilder.Entity<ChargeType>()
                .Property(x => x.Id)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        }
    }

Dadurch kann ich Ids einfügen, ohne meine tatsächliche Datenbankidentitätsspezifikation zu ändern. Ich muss immer noch den Identitäts-Ein/Aus-Trick verwenden, den Sie gemacht haben, aber zumindest wird EF Id-Werte senden.

7
Chris

Wenn Sie das Datenbankmodell zuerst verwenden, sollten Sie die StoreGeneratedPattern - Eigenschaft der ID-Spalte von Identity in None ändern.

Danach, als ich hier antwortete, sollte dies helfen:

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

Ohne ein zweites EF-Level-Modell ist dies nicht möglich - kopieren Sie die Klassen für das Seeding.

Wie Sie sagten - Ihre Metadaten besagen, dass der DB den Wert liefert, den er während des Seeding nicht liefert.

2
TomTom

Entsprechend dieser vorherigen Frage müssen Sie eine Transaktion Ihres Kontexts beginnen. Nach dem Speichern der Änderung müssen Sie auch die Spalte Identity Insert erneut formatieren und anschließend die Transaktion festschreiben.

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

Für zukünftige Googler fand ich die Antworten, die auf eine bedingte Logik in OnModelCreating() hinweisen, die für mich nicht funktionierte. 

Das Hauptproblem bei diesem Ansatz sind EF-Caches des Modells. Daher kann die Identität in derselben App-Domäne nicht aktiviert oder deaktiviert werden. 

Die von uns angenommene Lösung bestand darin, eine zweite abgeleitete DbContext zu erstellen, die das Einfügen von Identität ermöglicht. Auf diese Weise können beide Modelle zwischengespeichert werden, und Sie können die abgeleitete DbContext in speziellen (und hoffentlich) seltenen Fällen verwenden, wenn Sie Identitätswerte einfügen müssen.

Angesichts der folgenden Frage von @ 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; }   
}

Wir haben diese abgeleitete DbContext hinzugefügt:

public class MyDbContextWhichAllowsIdentityInsert : MyDbContext
{
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ReferenceThing>()
                    .Property(x => x.Id)
                    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
    }
}

Was würde dann mit der Seeder wie folgt verwendet werden:

var specialDbContext = new MyDbContextWhichAllowsIdentityInsert();

Seeder.Seed(specialDbContext);
1
Kevin Kuszyk

Nachdem ich verschiedene Optionen ausprobiert hatte, funktionierte der folgende Code für mich ( EF 6 ). Beachten Sie, dass zuerst eine normale Aktualisierung versucht wird, wenn das Element bereits vorhanden ist. Wenn dies nicht der Fall ist, versucht es ein normales Einfügen. Wenn der Fehler auf IDENTITY_INSERT zurückzuführen ist, wird die Problemumgehung versucht. Beachten Sie auch, dass db.SaveChanges fehlschlägt, daher die Anweisung db.Database.Connection.Open () und der optionale Überprüfungsschritt. Beachten Sie, dass dies nicht den Kontext aktualisiert, aber in meinem Fall ist dies nicht erforderlich. Hoffe das hilft!

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

Fügen Sie diesen Code zu Ihrem DB-Kontext hinzu, "um ihn sauber zu halten".

Beispiel für ein Anwendungsszenario (Hinzufügen von Standarddatensätzen der ID 0 zum Entitätstyp 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();
            }
        }
    }
}

Fügen Sie diese Hilfsklasse für die Erweiterungsmethode Get Table Name hinzu

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

Der Code, der dem DBContext hinzugefügt werden soll:

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