web-dev-qa-db-ja.com

EntityFramework 6 Code-Firstによるシード中のIDENTITY_INSERT

Auto-identity (int)列を持つエンティティがあります。データシードの一部として、システムの「標準データ」に特定の識別子の値を使用したいのですが、その後、データベースにID値を整理してもらいたいのです。

これまでのところ、IDENTITY_INSERTを挿入バッチの一部としてオンにしますが、Entity FrameworkはIdを含む挿入ステートメントを生成しません。モデルはデータベースが値を提供する必要があると考えているため、これは理にかなっていますが、この場合は値を提供したいと考えています。

モデル(疑似コード):

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

洞察や提案を提供できる人はいますか?

24
RikRak

したがって、Id列を含む独自のSQL挿入ステートメントを生成することに頼って、これを解決した可能性があります。それはひどいハックのように感じますが、動作します:-/

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

Bool DbContextをとるallowIdentityInsertsの代替コンストラクタを作成しました。そのブール値をDbContextの同じ名前のプライベートフィールドに設定しました。

私のOnModelCreatingは、その「モード」でコンテキストを作成する場合、アイデンティティー仕様を「指定解除」します

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

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

これにより、実際のデータベースID仕様を変更せずにIDを挿入できます。私はまだあなたがしたアイデンティティ挿入オン/オフトリックを使用する必要がありますが、少なくともEFはId値を送信します。

7
Chris

データベースの最初のモデルを使用する場合は、ID列のStoreGeneratedPatternプロパティをIdentityからNone

その後、私が here と答えたとき、これは役立つはずです:

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

この以前の Question によると、コンテキストのトランザクションを開始する必要があります。変更を保存した後、Identity Insert列も再宣言する必要があり、最後にトランザクションをコミットする必要があります。

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

将来のGoogle社員のために、OnModelCreating()の条件付きロジックが機能しないことを示唆する回答を見つけました。

このアプローチの主な問題は、EFがモデルをキャッシュするため、同じアプリドメインでIDをオンまたはオフに切り替えることができないことです。

私たちが採用したソリューションは、IDの挿入を許可する2番目の派生DbContextを作成することでした。このようにして、両方のモデルをキャッシュすることができ、ID値を挿入する必要がある特別な(そして願わくば)まれなケースで派生したDbContextを使用できます。

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

この派生DbContextを追加しました:

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

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

その後、次のようにSeederとともに使用されます。

var specialDbContext = new MyDbContextWhichAllowsIdentityInsert();

Seeder.Seed(specialDbContext);
2
Kevin Kuszyk

2番目のEFレベルモデルなしでは実行できません-シード用のクラスをコピーしてください。

あなたが言ったように-あなたのメタデータは、DBが値を提供することを示していますが、それはシード中には行いません。

2
TomTom

このサイトで見つかったいくつかのオプションを実験した後、次のコードは私のために機能しました(EF 6)。アイテムが既に存在する場合、最初に通常の更新を試みることに注意してください。そうでない場合は、通常の挿入を試行し、エラーがIDENTITY_INSERTが原因である場合は、回避策を試行します。また、db.SaveChangesが失敗するため、db.Database.Connection.Open()ステートメントとオプションの検証手順が失敗することにも注意してください。これはコンテキストの更新ではありませんが、私の場合は必要ないことに注意してください。お役に立てれば!

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

BranchIdという名前の整数型の列を持つBranchという名前のテーブルがあるとします。 SQL Serverの慣例により、EFは整数型の列がID列であると想定します。

したがって、列のIdentity Specificationが自動的に次のように設定されます。

  • (Is Identity)はい
  • ID増分1
  • アイデンティティSeed 1

割り当てられたID値を使用してエンティティをシードする場合は、次のようにDatabaseGeneratedOptionを使用します。

public class Branch
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int BranchId { get; set; }
    public string Description { get; set; }
}

次に、データをシードし、必要な値をBranchIdに割り当てます。

1
Kent Marsh

「クリーンに保つ」ために、このコードをDBコンテキストに追加して、話してみ​​てください。

使用シナリオの例(エンティティタイプABCStatusにID 0のデフォルトレコードを追加:

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

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

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