🔗 Spring Framework 講座【第23回】テーブル間のつながり!〜JPAのリレーションシップ(関連付け)〜

docs

前回、JPA@Query アノテーションを使って、複雑なDB検索に対応する方法を学びました。

ここまでのEntityの例は、単一のテーブル(例えば Item や User)に焦点を当てていましたが、実際の業務システムでは、複数のテーブルが関連しています。例えば、「一人のユーザー(User)は複数の注文(Order)を持つ」といった関係です。

今回は、JPAにおいて、このテーブル間の関連(リレーションシップ)を、Javaのオブジェクトの関連としてどのように表現するかを学びます。

1. リレーションシップの基本(多重度)

リレーショナルデータベースでは、テーブル間の関連を「多重度」という概念で表現します。JPAもこの概念に従い、主に以下の4つのアノテーションを提供します。

アノテーション関連の多重度意味
@OneToOne1対1ユーザーとそのプロフィール情報など
@OneToMany1対多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 属性を指定し、所有者が誰かを指定する。

上記の UserOrder のように、両方の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問題 にご期待ください!

コメント

タイトルとURLをコピーしました