0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JPA を使ってドメインモデルをサブタイプで永続化する

0
Last updated at Posted at 2025-11-24

前書き

私は型分岐が大好きです。

// 型分岐は素晴らしい👏
if (user instanceof GeneralUser) {
  // 一般ユーザー特有の処理
}

// ポリモーフィズム😞
if (user.isGeneralUser()) {
  // 一般ユーザー特有の処理
}

何がしたい?

前提

以下のようなドメインモデルがあるとする。

  • User には「社員」と「一般」がいる
  • 社員ユーザーは, 社員番号を持つ
  • 一般ユーザーは, 退会することができる
User.java
abstract class User {
  protected int id;
  protected UserStatus status;
  protected String name;
}
EmployeeUser.java
class EmployeeUser extends User {
  private String employeeId; // 社員番号
}
GeneralUser.java
class GeneralUser extends User {
  // 退会する
  public void withdraw() {
    this.status = UserStatus.WITHDRAWN;
  }
}

※ Constructor, Getter, Setter は省略

こんなことがしたい

  • User を Repository から取り出す時に User の具象型で取り出したい
  • そして型分岐したい
WithdrawService.java
class WithdrawService {
  private final UserRepository userRepository;

  // 退会する
  public void withdraw(int id) {
    User user = userRepository.findById(id);

    // 型分岐
    if (user instanceof GeneralUser generalUser) {
      generalUser.withdraw();
      userRepository.save(generalUser);
    } else {
      throw new Error("退会できないユーザータイプです: " + user.getClass());
    }
  }
}

ちゃんとやるなら GeneralUser implements Withdrawable で定義して user instanceof Withdrawable で分岐した方がいいという話もあるが、話の本筋に集中するため今回は控える。

永続化層の実装方法

JPA の @Inheritance を使う。継承戦略が3種類 @Inheritance(strategy = SINGLE_TABLE / JOINED / TABLE_PER_CLASS) あり、それぞれテーブル設計が異なる. それぞれのメリット・デメリットを理解してアーキテクチャ選択すること。

パターン1: 単一テーブル方式 (SINGLE_TABLE)

🧩 特徴

  • users テーブルに user_type 列を追加し、その値でサブタイプを表現する
  • 全てのデータを1つのテーブルで管理する
  • ⭕️ JOIN が不要で読み取り性能に優れる
  • ❌ サブタイプ固有のカラムは、不要な行では NULL となる

🏗️ テーブル設計

📊 データ例

📊 データ例を見る

users テーブル

id user_type name status employee_id
1 EMPLOYEE Hanako Sato ACTIVE EMP001
2 GENERAL Taro Tanaka ACTIVE
3 GENERAL Misa Ito WITHDRAWN
4 EMPLOYEE Daisuke Kato ACTIVE EMP002

🧑‍💻 JPA実装例

🧑‍💻 JPA実装例を見る
  • ℹ️ 抽象クラスに @DiscriminatorColumn を付与する
  • ℹ️ サブタイプに @DiscriminatorValue("***") を付与する
  • ⚠️ UserTypeプロパティは実装しないこと
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "user_type")
abstract class User {
  @Id
  private Integer id;

  @Enumerated(EnumType.STRING)
  private UserStatus status;

  private String name;
}

@Entity
@DiscriminatorValue("EMPLOYEE")
class EmployeeUser extends User {
  private String employeeId;
}

@Entity
@DiscriminatorValue("GENERAL")
class GeneralUser extends User {}

パターン2: 結合テーブル方式 (JOINED)

🧩 特徴

  • サブタイプごとにテーブルを作成し、サブタイプごとにデータを振り分ける
  • ⭕️ 正規化されており、NULL が存在しない
  • 🔺 読み取りに JOIN が必要となる

🏗️ テーブル設計

📊 データ例

📊 データ例を見る

users テーブル

id name status
1 Hanako Sato ACTIVE
2 Taro Tanaka ACTIVE
3 Misa Ito WITHDRAWN
4 Daisuke Kato ACTIVE

employee_users テーブル

user_id employeeId
1 EMP001
4 EMP002

general_users テーブル

※ 追加のプロパティがなくても general_users テーブルは必須。

user_id
2
3

🧑‍💻 JPA実装例

🧑‍💻 JPA実装例を見る
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
abstract class User {
  @Id
  private int id;

  @Enumerated(EnumType.STRING)
  private UserStatus status;

  private String name;
}

@Entity
@PrimaryKeyJoinColumn(name = "user_id")
class EmployeeUser extends User {
  private String employeeId;
}

@Entity
@PrimaryKeyJoinColumn(name = "user_id")
class GeneralUser extends User {}

<非推奨> パターン3: クラスごとの個別テーブル方式 (TABLE_PER_CLASS)

🧩 特徴

  • サブタイプごとに専用のテーブルを作り、共通テーブルを作成しない
  • サブタイプテーブル同士は関連を持たない
  • ❌ 共通フィールドが重複している
  • ❌ 大量検索時に UNION が必要になる
  • ❌ アプリケーション側で、テーブルをまたがる id の UNIQUE 制約に気をつける必要がある

🏗️ テーブル設計

📊 データ例

📊 データ例を見る

employee_users テーブル

id name status employeeId
1 Hanako Sato ACTIVE EMP001
4 Daisuke Kato ACTIVE EMP002

general_users テーブル

id name status
2 Taro Tanaka ACTIVE
3 Misa Ito WITHDRAWN

🧑‍💻 JPA実装例

🧑‍💻 JPA実装例を見る
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class User {
  @Id
  private int id;

  @Enumerated(EnumType.STRING)
  private UserStatus status;

  private String name;
}

@Entity
class EmployeeUser extends User {
  private String employeeId;
}

@Entity
class GeneralUser extends User {}

📌 まとめ

ユーザーの具象型をきちんとドメインとして扱い、永続化層でもそれを正しく表現したい──そんなとき JPA の継承マッピングは強力な武器になります。ただし、どの戦略を選ぶかでテーブル構造もパフォーマンス特性も大きく変わるため、「とりあえず〇〇」という姿勢は危険です。

  • SINGLE_TABLE は、高速・シンプルだが列がスパム化しやすい
  • JOINED は、正規化されて美しいが、JOIN コストがつきまとう
  • TABLE_PER_CLASS は、一見自由だが、運用まで含めると扱いが難しい
Strategy テーブル数 クエリ性能 正規化 向いている場面
SINGLE_TABLE 1 ◎ 速い × 正規化されない サブクラスの項目差が小さく、テーブル列が比較的少ない場合。高速な読み取りが重要なシステム。
JOINED 親 + 子 ○ 中間 ◎ 正規化される サブクラス間で項目差が大きい場合。データ整合性を重視し、スキーマを綺麗に保ちたい場合。
TABLE_PER_CLASS 子の数だけ △(UNION 必要) × 重複あり サブクラスが完全に独立した概念で、共通テーブルを持つ必要性が低い場合。分析系・集計をあまり行わない構造。

どれが正解というわけではなく、
「ドメインの複雑さ・テーブルの拡張性・読み取り頻度・パフォーマンス要件」
こういった現実的な条件を踏まえて戦略を選ぶことが重要です。

そして、型分岐を愛する我々にとって、
永続化層できちんと具象型で復元できるということは、ドメインモデルの表現力を最大限に生かせるということ。
そのためにも、適切な継承戦略の選択は避けて通れません。

アプリケーションが求める「正しさ」と「扱いやすさ」を両立するため、
自分たちのシステムに最もフィットする戦略を選んでいきましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?