リレーションシップのナビゲーション

EF Core のリレーションシップは、外部キーによって定義されます。 ナビゲーションは外部キーの上に階層化されて、リレーションシップの読み取りと操作のための自然なオブジェクト指向のビューが提供されます。 ナビゲーションを使うことで、アプリケーションでは、外部キーの値に対して起きていることに関係なく、エンティティのグラフを操作できます。

重要

複数のリレーションシップでナビゲーションを共有することはできません。 すべての外部キーは、プリンシパル側から依存側への最大 1 つのナビゲーションと、依存側からプリンシパル側への最大 1 つのナビゲーションに、関連付けることができます。

ヒント

遅延読み込みまたは変更追跡プロキシで使われていない限り、ナビゲーションを仮想にする必要はありません。

参照ナビゲーション

ナビゲーションには、参照とコレクションという 2 つの形式があります。 参照ナビゲーションは、別のエンティティへの単純なオブジェクト参照です。 これらは、一対多リレーションシップと一対一リレーションシップの "一" の側を表します。 次に例を示します。

public Blog TheBlog { get; set; }

参照ナビゲーションにはセッターが必要ですが、パブリックである必要はありません。 参照ナビゲーションを、null 以外の既定値に自動的に初期化することはできません。そうすることは、エンティティが存在しない場合に存在するとアサートすることと同じです。

C# の null 許容参照型を使う場合、オプションのリレーションシップに対しては参照ナビゲーションを null 許容にする必要があります。

public Blog? TheBlog { get; set; }

必須のリレーションシップの参照ナビゲーションには、null 許容または null 非許容のどちらにでもできます。

コレクション ナビゲーション

コレクション ナビゲーションは、.NET のコレクション型、つまり ICollection<T> を実装する任意の型のインスタンスです。 コレクションには関連エンティティ型のインスタンスが含まれ、いくつあってもかまいません。 これらは、一対多リレーションシップと多対多リレーションシップの " 多" の側を表します。 次に例を示します。

public ICollection<Post> ThePosts { get; set; }

コレクション ナビゲーションにはセッターは必要ありません。 コレクションはインラインで初期化するのが一般的であり、そのようにすると、プロパティが null かどうかをチェックする必要がなくなります。 次に例を示します。

public ICollection<Post> ThePosts { get; } = new List<Post>();

ヒント

public ICollection<Post> ThePosts => new List<Post>(); のような式形式のプロパティを誤って作成しないでください。 これを行うと、プロパティがアクセスされるたびに新しい空のコレクション インスタンスが作成されるため、ナビゲーションとして役に立たなくなります。

コレクション型

基になるコレクション インスタンスでは、ICollection<T> を実装する必要があり、動作する Add メソッドが必要です。 List<T> または HashSet<T> を使うのが一般的です。 List<T> は、少数の関連エンティティの場合に効率的であり、安定した順序を維持します。 HashSet<T> は、多数のエンティティの場合に検索が効率的になりますが、安定した順序はありません。 独自のカスタム コレクションの実装を使うこともできます。

重要

コレクションでは、参照の等価性を使う必要があります。 コレクション ナビゲーションの HashSet<T> を作成するときは、ReferenceEqualityComparer を使ってください。

コレクション ナビゲーションには配列は使用できません。配列で ICollection<T> が実装されていたとしても、Add メソッドを呼び出すと例外がスローされます。

コレクション インスタンスは ICollection<T> である必要がありますが、コレクションをそのように公開する必要はありません。 たとえば、ナビゲーションを IEnumerable<T> として公開するのが一般的です。これにより、アプリケーションのコードでランダムに変更できない読み取り専用のビューが提供されます。 次に例を示します。

public class Blog
{
    public int Id { get; set; }
    public IEnumerable<Post> ThePosts { get; } = new List<Post>();
}

このパターンのバリエーションには、必要に応じてコレクションを操作するためのメソッドが含まれます。 次に例を示します。

public class Blog
{
    private readonly List<Post> _posts = new();

    public int Id { get; set; }

    public IEnumerable<Post> Posts => _posts;

    public void AddPost(Post post) => _posts.Add(post);
}

その場合でも、アプリケーションのコードで公開されているコレクションを ICollection<T> にキャストした後、それを操作することができます。 これが問題である場合、エンティティからコレクションの防御用のコピーを返すことができます。 次に例を示します。

public class Blog
{
    private readonly List<Post> _posts = new();

    public int Id { get; set; }

    public IEnumerable<Post> Posts => _posts.ToList();

    public void AddPost(Post post) => _posts.Add(post);
}

このようにすることで得られる価値が、ナビゲーションがアクセスされるたびにコレクションのコピーを作成するオーバーヘッドを上回るほど十分に高いかどうかを、慎重に検討してください。

ヒント

この最後のパターンが動作するのは、既定では、EF はバッキング フィールドを介してコレクションにアクセスするためです。 つまり、EF 自体は実際のコレクションのエンティティを追加および削除しますが、アプリケーションはコレクションの防御用のコピーのみと対話します。

コレクション ナビゲーションの初期化

コレクション ナビゲーションは、エンティティ型を使って初期化できます。次は、それを一括で行っています。

public class Blog
{
    public ICollection<Post> Posts { get; } = new List<Post>();
}

または、遅延で行うこともできます。

public class Blog
{
    private ICollection<Post>? _posts;

    public ICollection<Post> Posts => _posts ??= new List<Post>();
}

EF は、たとえばクエリの実行中にコレクション ナビゲーションにエンティティを追加する必要があると、コレクションが現在 null である場合はそれを初期化します。 作成されるインスタンスは、ナビゲーションの公開されている型によって決まります。

  • ナビゲーションが HashSet<T> として公開されている場合は、ReferenceEqualityComparer を使用する HashSet<T> のインスタンスが作成されます。
  • そうではなく、ナビゲーションがパラメーターなしのコンストラクターを持つ具象型として公開されている場合は、その具象型のインスタンスが作成されます。 これは List<T> に適用されますが、カスタム コレクション型を含む他のコレクション型にも適用されます。
  • そうではなく、ナビゲーションが IEnumerable<T>ICollection<T>、または ISet<T> として公開されている場合は、ReferenceEqualityComparer を使用する HashSet<T> のインスタンスが作成されます。
  • そうではなく、ナビゲーションが IList<T> として公開されている場合は、List<T> のインスタンスが作成されます。
  • それ以外の場合は、例外がスローされます。

注意

変更追跡プロキシなどの通知エンティティが使われている場合は、List<T>HashSet<T> の代わりに ObservableCollection<T>ObservableHashSet<T> が使われます。

重要

変更追跡に関するドキュメントで説明されているように、EF は特定のキー値を持つ任意のエンティティの 1 つのインスタンスのみを追跡します。 つまり、ナビゲーションとして使われるコレクションでは、参照等価性セマンティクスを使う必要があります。 オブジェクト等価性をオーバーライドしないエンティティ型は、既定でこれになります。 すべてのエンティティ型で動作するよう、ナビゲーションとして使うために HashSet<T> を作成するときは、必ず ReferenceEqualityComparer を使ってください。

ナビゲーションの構成

ナビゲーションは、リレーションシップの構成の一部としてモデルに含まれます。 つまり、規則により、またはモデル作成 API の HasOneHasMany などを使うことによって、そうなります。 ナビゲーションに関連付けられているほとんどの構成は、リレーションシップ自体を構成することによって行われます。

ただし、リレーションシップの全体的な構成の一部ではなく、ナビゲーション プロパティ自体に固有の構成の種類がいくつかあります。 この種類の構成は、Navigation メソッドを使って行われます。 たとえば、バッキング フィールドを使うのではなく、プロパティを介してナビゲーションにアクセスするよう EF に強制するには、次のようにします。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Navigation(e => e.Posts)
        .UsePropertyAccessMode(PropertyAccessMode.Property);

    modelBuilder.Entity<Post>()
        .Navigation(e => e.Blog)
        .UsePropertyAccessMode(PropertyAccessMode.Property);
}

注意

Navigation の呼び出しを使って、ナビゲーション プロパティを作成することはできません。 これは、以前にリレーションシップを定義したことで、または規約から作成されたナビゲーション プロパティを構成するためにのみ使用されます。

必須のナビゲーション

リレーションシップが必須の場合は、依存側からプリンシパル側へのナビゲーションは必須です。これは、外部キー プロパティが null 非許容であることを意味します。 逆に、外部キーが null 許容であり、したがってリレーションシップがオプションの場合は、ナビゲーションはオプションになります。

プリンシパル側から依存側への参照ナビゲーションは異なります。 ほとんどの場合、プリンシパル エンティティは依存エンティティなしで "常に" 存在できます。 つまり、必須リレーションシップは、少なくとも 1 つの依存エンティティが常に存在することを示すわけでは "ありません"。 プリンシパルが特定の数の依存に確実に関連付けられるようにする方法は EF モデルにはなく、リレーショナル データベースにもそれを行う標準的な方法はありません。 これが必要な場合は、アプリケーション (ビジネス) ロジックで実装する必要があります。

このルールには例外が 1 つあります。プリンシパル型と依存型がリレーショナル データベース内の同じテーブルを共有している場合、または 1 つのドキュメントに含まれている場合です。 このようなことは、所有型で、または同じテーブルを共有する非所有型で、発生する可能性があります。 この場合は、プリンシパル側から依存側へのナビゲーション プロパティを必須としてマークし、依存側が存在する必要があることを示すことができます。

ナビゲーション プロパティの必須としての構成は、Navigation メソッドを使って行います。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Navigation(e => e.BlogHeader)
        .IsRequired();
}