プロパティのデフォルト値を介して関係を変更しようとすると、予期しないInvalidOperationExceptionが発生する


10

以下のサンプルコードでは、実行時に次の例外が発生しdb.Entry(a).Collection(x => x.S).IsModified = trueます。

System.InvalidOperationException: 'エンティティタイプ' B 'のインスタンスは、キー値' {Id:0} 'の別のインスタンスが既に追跡されているため追跡できません。既存のエンティティをアタッチするときは、特定のキー値を持つエンティティインスタンスが1つだけアタッチされるようにしてください。

Bのインスタンスをアタッチする代わりに追加しないのはなぜですか?

奇妙なことに、のドキュメントにIsModifiedInvalidOperationException、例外の可能性が明記されていません。無効なドキュメントまたはバグ?

私はこのコードが奇妙であることを知っていますが、奇妙なegdeのケースでefコアがどのように機能するかを理解するためだけにそれを書きました。私が欲しいのは説明ではなく、回避策です。

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}

AとBはどのように関連していますか?関係プロパティとは何ですか?
サム

回答:


8

提供されたコードのエラーの理由は次のとおりです。

Aデータベースからエンティティを作成すると、そのプロパティSは2つの新しいレコードを含むコレクションで初期化されますBIdこの新しいBエンティティのそれぞれのはに等しくなり0ます。

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

コードの行を実行した後、は遅延読み込みを使用せず、コレクションの明示的な読み込みがないため、エンティティのvar a = db.Set<A>().Single()コレクションSにはデータベースのエンティティAが含まBれてDbContext DbいませんS。エンティティには、コレクションの初期化中に作成さAれた新しいエンティティのみが含まれます。BS

IsModifed = trueコレクションSエンティティフレームワークを呼び出すと、これら2つの新しいエンティティBが変更追跡に追加されます。しかし、両方の新しいBエンティティが同じであるため、失敗しますId = 0

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

あなたは、エンティティフレームワークを追加しようとしていること、スタックトレースから見ることができるBにエンティティをIdentityMap

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

そして、エラーメッセージも、それは追跡できないことを告げるBと、エンティティがId = 0別のためBに同じでエンティティがIdすでに追跡されます。


この問題の解決方法。

この問題を解決するには、コレクションをB初期化するときにエンティティを作成するコードを削除する必要がありますS

public ICollection<B> S { get; set; } = new List<B>();

代わりSに、A作成された場所でコレクションを埋める必要があります。例えば:

db.Add(new A {S = {new B(), new B()}});

遅延読み込みを使用しない場合は、Sコレクションを明示的に読み込み、その項目を変更追跡に追加する必要があります。

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

Bのインスタンスをアタッチする代わりに追加しないのはなぜですか?

つまりDetached状態があるため、追加されるのではなく添付されます。

コード行を実行した後

var a = db.Set<A>().Single();

作成されたエンティティのインスタンスにBは状態がありDetachedます。次のコードを使用して確認できます。

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

次に設定すると

db.Entry(a).Collection(x => x.S).IsModified = true;

EFは、B変更追跡にエンティティを追加しようとします。EFCoreのソースコードから、次の引数値を持つメソッドInternalEntityEntry.SetPropertyModifiedにつながることがわかります。

  • property-私たちのBエンティティの1つ、
  • changeState = true
  • isModified = true
  • isConceptualNull = false
  • acceptChanges = true

このような引数を持つこのメソッドは、Detached Bエンティティの状態をに変更しModified、次にそれらの追跡を開始しようとします(行490〜506を参照)。そのためB、エンティティは、今の状態持っているModified彼らが装着される。このリード線(追加されていません)。


「Bのインスタンスをアタッチする代わりに追加しないのはなぜですか?」の答えはどこですか?「両方の新しいBエンティティが同じId = 0 "であるため失敗します。1と2の両方のIDでefコアが保存されるので、それは間違っていると思います。この質問の正解だとは思いません
DIlshod K

@DIlshod Kコメントをありがとう。「この問題の解決方法」のセクションでは、S提供されたコードでは遅延読み込みを使用しないため、コレクションを明示的に読み込む必要があると書きました。もちろん、EFは以前に作成されたBエンティティをデータベースに保存しました。ただし、コード行でA a = db.Set<A>().Single()はエンティティのみが読み込まAれ、コレクションにエンティティは含まれませんS。コレクションをロードするには、Seager loadingを使用する必要があります。「Bのインスタンスをアタッチする代わりに追加しないのはなぜですか?」という質問への回答を明示的に含めるように、アドレスを変更します。
Iliar Turdushev
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.