前書き
私は型分岐が大好きです。
// 型分岐は素晴らしい👏
if (user instanceof GeneralUser) {
// 一般ユーザー特有の処理
}
// ポリモーフィズム😞
if (user.isGeneralUser()) {
// 一般ユーザー特有の処理
}
何がしたい?
前提
以下のようなドメインモデルがあるとする。
- User には「社員」と「一般」がいる
- 社員ユーザーは, 社員番号を持つ
- 一般ユーザーは, 退会することができる
abstract class User {
protected int id;
protected UserStatus status;
protected String name;
}
class EmployeeUser extends User {
private String employeeId; // 社員番号
}
class GeneralUser extends User {
// 退会する
public void withdraw() {
this.status = UserStatus.WITHDRAWN;
}
}
※ Constructor, Getter, Setter は省略
こんなことがしたい
- User を Repository から取り出す時に User の具象型で取り出したい
- そして型分岐したい
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 必要) | × 重複あり | サブクラスが完全に独立した概念で、共通テーブルを持つ必要性が低い場合。分析系・集計をあまり行わない構造。 |
どれが正解というわけではなく、
「ドメインの複雑さ・テーブルの拡張性・読み取り頻度・パフォーマンス要件」
こういった現実的な条件を踏まえて戦略を選ぶことが重要です。
そして、型分岐を愛する我々にとって、
永続化層できちんと具象型で復元できるということは、ドメインモデルの表現力を最大限に生かせるということ。
そのためにも、適切な継承戦略の選択は避けて通れません。
アプリケーションが求める「正しさ」と「扱いやすさ」を両立するため、
自分たちのシステムに最もフィットする戦略を選んでいきましょう。