web-dev-qa-db-ja.com

エンティティフレームワークにID列を挿入させるにはどうすればよいですか?

いくつかのシードデータを使用してデータベースを初期化するC#コードを記述したいと思います。明らかに、これには、挿入時にさまざまなID列の値を設定できる必要があります。私はコードファーストのアプローチを使用しています。デフォルトでは、DbContextがデータベース接続を処理するため、SET IDENTITY_INSERT [dbo].[MyTable] ONはできません。したがって、これまでに行ったことは、使用するDB接続を指定できるDbContextコンストラクターを使用することです。次に、そのDB接続でIDENTITY_INSERTONに設定し、エンティティフレームワークを使用してレコードを挿入しようとします。ここに私がこれまでに得たものの例を示します。

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

非常に近いが、これまでのところ-cmd.ExecuteNonQuery()は正常に動作しますが、context.SaveChanges()を実行すると、「テーブル 'MyUser'のID列に明示的な値を指定する必要がある」 IDENTITY_INSERTがONに設定されている場合、またはレプリケーションユーザーがNOT FOR REPLICATION ID列に挿入している場合。」

おそらく、MyUserId(MyUserテーブルのIdentity列)がプライマリキーであるため、MyUserを指定したにもかかわらず、エンティティフレームワークはcontext.SaveChanges()を呼び出したときにそれを設定しようとしません。 MyUserIdプロパティの値のエンティティ。

エンティティフレームワークに、エンティティのプライマリキー値さえも強制的に挿入させようとする方法はありますか?または、一時的にMyUserIdを主キー値ではないとしてマークする方法で、EFはそれを挿入しようとしますか?

51
Jez

慎重に検討した結果、IDフレームを挿入するエンティティフレームワークの拒否は、バグではなく機能であると判断しました。 :-) ID値を含むデータベースにすべてのエントリを挿入する場合、エンティティフレームワークが自動的に作成したすべてのリンクテーブルのエンティティも作成する必要があります。それは正しいアプローチではありません。

したがって、私がしているのは、C#コードを使用してEFエンティティを作成するシードクラスを設定し、DbContextを使用して新しく作成されたデータを保存することです。ダンプされたSQLを取得してC#コードに変換するには少し時間がかかりますが、「シード」データのためだけのデータはありません(そうすべきではありません)-代表的な少量のデータである必要がありますデバッグ/開発の目的で新しいDBにすばやく配置できるライブDBにあるデータの種類。これは、エンティティをリンクする場合、既に挿入されているものに対してクエリを実行する必要があることを意味します。そうしないと、生成されたID値がコードで認識されません。この種のことは、セットアップして完了したら、シードコード内に表示されますcontext.SaveChanges for 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);

また、この方法でスキーマを変更するとシードデータが更新される可能性が高くなります。これは、変更するとシードコードが壊れる可能性が高いためです(フィールドまたはエンティティを削除すると、そのフィールドを使用する既存のシードコード/ entityはコンパイルに失敗します)。シードを行うためのSQLスクリプトを使用すると、そうではなく、SQLスクリプトもデータベースに依存しません。

したがって、DBシードデータを実行するためにエンティティのIDフィールドを設定しようとすると、間違いなく間違ったアプローチを取っていると思います。

たとえば、SQL ServerからPostgreSQL(シードデータだけでなく完全なライブDB)に大量のデータを実際にドラッグしていた場合、EFを介してそれを行うことができましたが、同時に2つのコンテキストを開きたいと思います。ソースコンテキストからすべてのさまざまなエンティティを取得し、それらを宛先コンテキストに配置するコードを作成して、変更を保存します。

一般に、ID値を挿入するのが適切なのは、同じDBMS内で1つのDBから別のDBにコピーする場合(SQL Server-> SQL Server、PostgreSQL-> PostgreSQLなど)で、その後にEFコードファーストではなく、SQLスクリプト内にあります(SQLスクリプトはDBに依存しませんが、そうである必要はありません。異なるDBMS間を移動することはありません)。

2
Jez

EF 6メソッド、 msdn article を使用:

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

更新:エラーを回避するには「IDENTITY_INSERTがONに設定されている場合、またはレプリケーションユーザーがNOT FOR REPLICATION ID列に挿入する」、ID列のStoreGeneratedPatternプロパティの値をIdentityからに変更する必要がありますモデルデザイナではなし

StoreGeneratedPatternをNoneに変更すると、「IDENTITY_INSERTがOFFに設定されている場合、テーブル 'TableName'のID列に明示的な値を挿入できません」というエラーで、指定されたIDなしのオブジェクトの挿入に失敗します(通常の方法)。

46
Roman O

接続で面白いビジネスをする必要はありません。仲介者を切り取って、単に ObjectContext.ExecuteStoreCommand

次に、これを行うことにより、あなたが望むものを達成することができます。

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

しかし、EFにID挿入をオンにするように指示する組み込みの方法は知りません。

それは完璧ではありませんが、現在のアプローチよりも柔軟で「ハッキング」が少ないでしょう。

更新:

私はあなたの問題に2番目の部分があることに気付きました。 SQLにIDの挿入を行うように指示したので、EFはそのIDに値を挿入しようともしていません(なぜですか?指示していません)。

コードファーストアプローチの経験はありませんが、いくつかのクイック検索から、ストアから列を生成してはならないことをEFに伝える必要があるようです。このようなことをする必要があります。

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

うまくいけば、これがあなたを正しい方向に向けさせるでしょう:-)

18
Doctor Jones

パーティーに少し遅れましたが、誰かがDBを使用してEF5でこの問題に最初に遭遇した場合:私はどちらのソリューションも動作することができませんでしたが、別の回避策を見つけました:

.SaveChanges()コマンドを実行する前に、テーブルのIDカウンターをリセットしました。

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

これは、.SaveChanges()を追加するたびに適用する必要があることを意味しますが、少なくとも機能します!

11
Peter Albert

ここに問題の解決策があります。 EF6で試してみましたが、うまくいきました。動作するはずのいくつかの擬似コードを次に示します。

まず、デフォルトのdbcontextのオーバーロードを作成する必要があります。基本クラスをチェックすると、既存のdbConnectionを渡すクラスが見つかります。次のコードを確認してください

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

そして、モデル作成で、db生成オプションを削除します。たとえば、

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

        base.OnModelCreating(modelBuilder);
    }

コードでは、接続オブジェクトを明示的に渡す必要がありますが、

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

このアイデアは、ターゲットテーブルが空であるか、テーブルに既存のすべてのIDよりも大きいIDでレコードが挿入されている場合にのみ確実に機能します!

3年後、生産データをテストシステムに転送する同様の問題が発生しました。ユーザーはいつでも本番データをテストシステムにコピーできるようにしたかったので、SQL Serverで転送ジョブを設定する代わりに、既存のEFクラスを使用してアプリケーションで転送を実行する方法を探しました。このようにして、ユーザーが望むときにいつでも転送を開始するためのメニュー項目をユーザーに提供できます。

アプリケーションはMS SQL Server 2008データベースとEF 6を使用します。2つのデータベースは一般に同じ構造であるため、AsNoTracking()を使用して各エンティティのレコードを読み取ることで、1つのDbContextインスタンスから別のインスタンスにデータを簡単に転送できると考えましたAdd()(またはAddRange())ターゲットDbContextインスタンスの適切なプロパティへのレコードのみ。

以下に、1つのエンティティを示すDbContextを示します。

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

Peopleデータをコピーするために、次のことを行いました。

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

(外部キー制約の問題を回避するために)テーブルが正しい順序でコピーされている限り、これは非常にうまく機能しました。残念ながら、EFはid値を無視し、SQL Serverに次のID値を挿入させるため、ID列を使用するテーブルは少し難しくなりました。 ID列を持つテーブルの場合、私は次のことをしました。

  1. 特定のエンティティのすべてのレコードを読み取ります
  2. IDの昇順でレコードを並べ替えます
  3. テーブルのIDシードを最初のIDの値に設定します
  4. 次のID値を追跡し、レコードを1つずつ追加します。 idが予想される次のID値と異なる場合、IDシードを次の必要な値に設定します

テーブルが空(またはすべての新しいレコードのIDが現在の最大IDよりも大きい)であり、IDが昇順である限り、EFおよびMS SQLは必要なIDを挿入し、どちらのシステムも文句を言いません。

以下に、少し説明するコードを示します。

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

リフレクションを使用して、DbContext内のすべてのエンティティに対して機能するLoadおよびSaveメソッドを記述できました。

ちょっとしたハックですが、エンティティの読み取りと書き込みに標準のEFメソッドを使用でき、特定の状況下でID列を特定の値に設定する方法の問題を克服できます。

これが同様の問題に直面している他の誰かの助けになることを願っています。

2
roadkill

このサイトで見つかったいくつかのオプションを試した後、次のコードが機能しました(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