N+1問題対策として @EntityGraph(attributePaths = {"items"}) をRepositoryのメソッドに直接書いたことがある人は多いと思います。N+1問題そのものの原理はSpring Data JPAのN+1問題解決ガイドに譲りますが、このやり方には弱点があります。同じ attributePaths を複数のメソッドにコピペしていくと、関連を1つ追加したときに片方のメソッドだけ直し忘れる、ということが普通に起きます。

この問題を解決してくれるのが @NamedEntityGraph です。エンティティクラスに一度だけ定義しておけば、Repositoryのどのメソッドからも同じ名前で呼び出せるようになります。

@NamedEntityGraphの基本構文

最小構成はこんな感じです。name が識別子、attributeNodes がフェッチ対象の指定になります。

@Entity
@NamedEntityGraph(
    name = "Order.withItems",
    attributeNodes = {
        @NamedAttributeNode("items")
    }
)
public class Order {
    @Id
    private Long id;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> items;

    // ...
}

name はRepository側から文字列で参照するためのキーなので、エンティティ名.用途 のような命名にしておくと後から見てもわかりやすくなります。

@NamedAttributeNodeで複数属性をまとめて指定する

attributeNodes には配列で複数のフィールドを並べられます。OneToMany と ManyToOne が混在していても問題ありません。

@NamedEntityGraph(
    name = "Order.withItemsAndCustomer",
    attributeNodes = {
        @NamedAttributeNode("items"),
        @NamedAttributeNode("customer")
    }
)

ただし指定しすぎると結局JOINが増えてクエリが重くなるので、「この画面で本当に必要な関連か」は毎回考えたほうがいいです。

@NamedSubgraphでネストした関連をフェッチする

items の先にある product までまとめて取りたい、というように2階層以上先の関連を指定したい場合は subgraph 属性と @NamedSubgraph を組み合わせます。

@NamedEntityGraph(
    name = "Order.withItemsAndProduct",
    attributeNodes = {
        @NamedAttributeNode(value = "items", subgraph = "itemsWithProduct")
    },
    subgraphs = {
        @NamedSubgraph(
            name = "itemsWithProduct",
            attributeNodes = {
                @NamedAttributeNode("product")
            }
        )
    }
)

孫エンティティまで取りに行く分JOINも増えるので、深いsubgraphは本当に必要な階層だけに絞るのがおすすめです。

@NamedEntityGraphsで複数のグラフ定義を一元管理する

用途ごとに複数のグラフを持たせたい場合は @NamedEntityGraphs でまとめて定義できます。

@Entity
@NamedEntityGraphs({
    @NamedEntityGraph(
        name = "Order.withItems",
        attributeNodes = { @NamedAttributeNode("items") }
    ),
    @NamedEntityGraph(
        name = "Order.withCustomer",
        attributeNodes = { @NamedAttributeNode("customer") }
    )
})
public class Order {
    // ...
}

画面やユースケースごとに Order.withItems 、Order.withCustomer のように名前を分けておくと、Repository側で参照を間違えにくくなります。

EntityGraphType.LOADとFETCHの違い

ここが地味に重要なポイントです。どちらもグラフに含まれる属性をJOIN FETCHする点は同じですが、グラフに 含まれない 属性の扱いが変わります。

  • FETCH : グラフに含まれない属性は強制的にLAZY扱いになる
  • LOAD : グラフに含まれない属性はエンティティ側の既存設定(LAZY/EAGERの定義)がそのまま尊重される

既存のLAZY/EAGER設定を崩さずにグラフで指定した関連だけ追加で取りたいなら LOAD 、グラフの内容を見ればフェッチ範囲が全部わかるようにしたいなら FETCH を選ぶ、という基準で考えるとわかりやすいです。

-- LOAD: グラフ外のEAGER属性もそのままJOINされる
select o.*, i.*, c.* from orders o
  left join order_item i on i.order_id = o.id
  left join customer c on c.id = o.customer_id

-- FETCH: グラフ外のEAGER属性もLAZY扱いになり追加JOINされない
select o.*, i.* from orders o
  left join order_item i on i.order_id = o.id

Repositoryメソッドへ@NamedEntityGraphを適用する

定義したグラフは、Repositoryのメソッドに @EntityGraph を付けて value に名前を指定するだけで使えます。既存の派生クエリメソッドにもそのまま付与できます。

public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(value = "Order.withItems", type = EntityGraph.EntityGraphType.LOAD)
    Optional<Order> findById(Long id);

    @EntityGraph(value = "Order.withItemsAndCustomer", type = EntityGraph.EntityGraphType.FETCH)
    List<Order> findByStatus(OrderStatus status);
}

同じ value を複数のメソッドで指定すれば、フェッチ定義はエンティティ側の1箇所に集約されたままになります。関連を追加するときもエンティティのアノテーションを直すだけで、呼び出し側のメソッドを1つずつ探して直す必要がありません。

ad-hoc @EntityGraphとNamedEntityGraphの使い分け基準

毎回 @NamedEntityGraph にする必要はなくて、実務的には次のくらいの基準で十分だと思います。

  • 同じフェッチ定義を2箇所以上のメソッドで使うなら @NamedEntityGraph を検討する
  • 一度しか使わない単純なケースは @EntityGraph(attributePaths = {...}) のad-hoc指定で十分
  • 「このメソッドは何を一緒に取るのか」をエンティティ側に名前として残したいなら @NamedEntityGraph のほうが可読性が上がる

クエリメソッドの書き方自体で迷っている場合はSpring Data JPAのクエリメソッドガイドも参考になります。

まとめ

@NamedEntityGraph は、N+1問題対策で使っていたad-hocな @EntityGraph を整理して、複数箇所で使い回せる形にするためのものです。attributeNodes と subgraphs でフェッチ範囲を定義し、EntityGraphType でグラフ外の属性の扱いを決め、Repositoryのメソッドには名前だけ渡す。この流れを覚えておけば、関連エンティティが増えてもエンティティクラス側を直すだけで済むようになります。

エンティティの関連設計自体を見直したい場合はエンティティ関連マッピングの記事もあわせて読んでみてください。