前回、JPAの @Query アノテーションを使って、複雑なDB検索に対応する方法を学びました。
ここまでのEntityの例は、単一のテーブル(例えば Item や User)に焦点を当てていましたが、実際の業務システムでは、複数のテーブルが関連しています。例えば、「一人のユーザー(User)は複数の注文(Order)を持つ」といった関係です。
今回は、JPAにおいて、このテーブル間の関連(リレーションシップ)を、Javaのオブジェクトの関連としてどのように表現するかを学びます。
1. リレーションシップの基本(多重度)
リレーショナルデータベースでは、テーブル間の関連を「多重度」という概念で表現します。JPAもこの概念に従い、主に以下の4つのアノテーションを提供します。
| アノテーション | 関連の多重度 | 意味 |
@OneToOne | 1対1 | ユーザーとそのプロフィール情報など |
@OneToMany | 1対多 | 1つのカテゴリに複数の商品など |
@ManyToOne | 多対1 | 複数の注文は1人の顧客に紐づくなど |
@ManyToMany | 多対多 | 1つの商品が複数のタグを持ち、1つのタグが複数の商品を持つなど |
2. 1対多 / 多対1 のマッピング(最も一般的)
「一人の顧客が複数の注文を持つ」という、最も一般的な **1対多(One-to-Many)**および **多対1(Many-to-One)**の関連を見てみましょう。
この関連は、DBでは「多」側(Order テーブル)に、「1」側(User テーブル)の外部キーを持たせることで表現されます。
2-1. 多対1マッピング(Order側)
注文(Order)は、必ず一人のユーザーに紐づきます。Order Entityでは、外部キーに対応するフィールドに @ManyToOne を付けます。
Java
@Entity
@Table(name = "orders")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 他のフィールド...
// 【多対1の定義】
// Order(多)は、User(1)に紐づく
@ManyToOne
// @JoinColumn: DB上の外部キーのカラム名を指定(省略可能)
@JoinColumn(name = "user_id")
private User user; // 外部キー(user_id)が User オブジェクトとしてマッピングされる
}
2-2. 1対多マッピング(User側)
ユーザー(User)は、複数の注文を持ちます。User Entityでは、注文のリストに対応するフィールドに @OneToMany を付けます。
Java
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 【1対多の定義】
// User(1)は、複数のOrder(多)を持つ
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>(); // 複数のOrderオブジェクトのリスト
// mappedBy = "user": 関連の所有者がOrder側の「user」フィールドであることを示す
// これにより、Userテーブルには外部キーが作成されず、Order側の外部キーを利用する
}
3. リレーションシップの所有者と双方向関連
JPAでは、関連(リレーションシップ)を管理する**所有者(Owning Side)**を明確に定義する必要があります。
- 所有者: 外部キーを持つ側(上記の例では Order Entity)。DBの更新操作(INSERT/UPDATE)を行う権限を持つ。
- 非所有者: 外部キーを持たない側(上記の例では User Entity)。
mappedBy属性を指定し、所有者が誰かを指定する。
上記の User と Order のように、両方のEntityからお互いを参照できる関連を双方向関連と呼びます。双方向で関連を定義する場合、必ず**所有者(@JoinColumn 側)と非所有者(mappedBy 側)**を決めなければなりません。
4. フェッチ戦略(データの取得タイミング)
関連先のデータ(他のEntity)をいつDBから取得するかを制御するのが**フェッチ戦略(Fetch Strategy)**です。これには2種類あります。
| フェッチ戦略 | 意味 | @アノテーション |
| EAGER(即時ロード) | 親Entityをロードしたときに、関連先のデータも同時に取得する。 | @ManyToOne や @OneToOne のデフォルト |
| LAZY(遅延ロード) | 親Entityをロードしても、関連先のデータは取得しない。初めてそのデータが参照されたときにDBに問い合わせる。 | @OneToMany や @ManyToMany のデフォルト |
ベストプラクティス:
特に大量のデータが含まれる可能性がある @OneToMany では、LAZY(遅延ロード)がデフォルトであり、この設定を維持することが推奨されます。EAGERを使うと、使わないデータまで大量に取得してしまい、アプリケーションのパフォーマンスが極端に低下する原因になることがあります。
例えば、ユーザー一覧を取得する際に、そのユーザーが過去に行った全ての注文(数千件)を同時に取得する必要はないはずです。必要なときだけ取得するのが LAZY です。
Java
// User Entity の OneToMany 関連に LAZY がデフォルトで適用される
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
✅ 本日のまとめ
- JPAでは、テーブル間の関連を
@OneToOne,@OneToMany,@ManyToOne,@ManyToManyで表現する。 - 最も一般的な 多対1 関連(
@ManyToOne)を持つ側が、DB上の外部キーを持ち、関連の所有者となる。 - 双方向関連を定義する場合は、外部キーを持たない側(
@OneToMany側)でmappedBy属性を指定する。 - フェッチ戦略には EAGER(即時)と LAZY(遅延)があり、パフォーマンスのためにLAZYの使用が強く推奨される(特に
@OneToMany)。
🔔 次回予告
LAZY(遅延ロード)はパフォーマンスに優れますが、正しく使わないと**「N+1問題」**という深刻なパフォーマンス問題を引き起こす原因となります。
次回は、このN+1問題とは何か、そしてそれを解決するための JPQLのFETCH JOIN など、より実用的なデータ取得テクニックについて学びます。
次回:【第24回】パフォーマンスチューニングとN+1問題 にご期待ください!


コメント