Springの@OneToManyと@ManyToOneの基本を解説!初心者向けリレーション・FetchType・N+1問題対策ガイド
生徒
「Springでリレーションってどうやって使うんですか?エンティティ同士を結びつける方法が知りたいです!」
先生
「リレーションには@OneToManyや@ManyToOneを使います。正しく使わないと、N+1問題などパフォーマンスの落とし穴もありますよ。」
生徒
「N+1って聞いたことあるけどよくわかりません。FetchTypeの選び方も気になります!」
先生
「では、Springのリレーション設定とFetchTypeの違い、N+1問題の対策方法について一緒に学んでいきましょう。」
1. @OneToManyと@ManyToOneの関係を理解しよう
Spring Data JPAでは、エンティティ間のリレーション(関連付け)を@OneToManyや@ManyToOneで定義します。例えば「ユーザー」と「投稿」の関係で考えてみましょう。
一人のユーザーは複数の投稿を持てます。これがOneToManyです。一方、投稿は一人のユーザーに属します。これがManyToOneです。
以下のようにエンティティを定義します。
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Post> posts;
}
@Entity
public class Post {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
2. mappedByとJoinColumnの違いを解説
mappedByは、リレーションの所有者ではない側(逆側)で使います。リレーションの主導権を相手側に任せる形です。
一方、@JoinColumnはリレーションの所有者側で使われ、外部キーを指定します。つまり、実際にテーブルにカラムが作られるのは@JoinColumnを定義した側です。
3. FetchType.LAZYとFetchType.EAGERの違いとは?
FetchTypeはデータ取得のタイミングを制御する重要な設定です。
- FetchType.LAZY:必要になるまで関連エンティティを読み込まない(遅延読み込み)
- FetchType.EAGER:親エンティティを取得すると同時に、関連エンティティも全て取得する(即時読み込み)
初心者はLAZYを基本として使い、必要に応じて明示的に取得するスタイルをおすすめします。
4. N+1問題とは?Springでよくある落とし穴
N+1問題とは、1つの親エンティティに対して、子エンティティが複数あるとき、個別にSQLが発行されてしまう問題です。
例えば、10人のユーザーがいて、それぞれの投稿を取得する場合、本来なら1回のJOINで済むところが、ユーザー取得の1回+投稿取得10回=合計11回のSQLが発行されてしまうことがあります。
これがパフォーマンスを大きく低下させる原因になります。
5. N+1問題を解決するには?JOIN FETCHの活用
JPQLでJOIN FETCHを使うことで、N+1問題を回避できます。例えば、以下のように書きます。
@Query("SELECT u FROM User u JOIN FETCH u.posts")
List<User> findAllWithPosts();
JOIN FETCHを使うことで、ユーザーと投稿を一気に取得でき、SQLの発行回数が最小限になります。
6. 実務でのFetchTypeの選び方と注意点
FetchType.EAGERは便利に見えますが、すべての関連データを毎回取得してしまい、メモリや処理速度に悪影響を及ぼす可能性があります。特にリスト型のリレーションに対しては注意が必要です。
LAZYを基本として、必要な場面でJOIN FETCHやEntityGraphを使って明示的にデータを取得するのが実務での王道パターンです。
7. JSONシリアライズ時の無限ループに注意
@OneToManyと@ManyToOneを双方向に設定したまま@RestControllerなどでJSONレスポンスを返すと、無限ループになってしまうことがあります。
これを防ぐには、以下のように@JsonManagedReferenceと@JsonBackReferenceを使うのが一般的です。
@JsonManagedReference
@OneToMany(mappedBy = "user")
private List<Post> posts;
@JsonBackReference
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
8. リレーションの方向性を意識した設計のコツ
双方向リレーションは便利ですが、すべてのエンティティに設定すると複雑になります。必要な場面に絞って使うのが設計のコツです。
例えば、投稿からユーザーを参照するだけで十分なら、@ManyToOneだけでよく、@OneToManyは省略してもかまいません。
リレーション設計は、パフォーマンスや保守性に影響するため、用途に応じた設計が重要です。