AIによる要約
@MappedSuperclassは、作成日時や更新日時のようなEntity共通のマッピングを継承させる仕組みです。@Embeddableは、住所や金額のような意味のある値を、所有Entityの一部として組み込む仕組みです。単なる重複削減ではなく、「Entityとして共通の性質か」「独立した値のまとまりか」で使い分けます。


JPAでEntityを増やすと、ID、作成日時、更新日時、住所、担当者コードなど、似たフィールドが繰り返し登場します。重複を減らすために親クラスへまとめたくなりますが、すべてを継承へ寄せると巨大なBaseEntityになりがちです。
@MappedSuperclassと@Embeddableは、どちらもテーブルへマッピングする情報を共通化できます。ただし、表している関係が違います。この記事では、監査項目と配送先住所を例に使い分けます。
この記事のポイント
@MappedSuperclassはEntityへマッピングを継承させる@Embeddableは所有Entityの一部となる値を表す- 監査項目は継承、住所や期間は埋め込みが候補になる
@Entityの継承戦略とは別の仕組みとして考える
@MappedSuperclassとは
@MappedSuperclassを付けたクラスはEntityそのものではなく、独自テーブルも持ちません。そこに宣言した永続化フィールドのマッピングが、継承したEntityへ引き継がれます。
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
}
注文Entityがこのクラスを継承すると、注文テーブルへcreated_atとupdated_atがマッピングされます。親クラス用のテーブルが作られるわけではありません。
@Entity
public class Order extends AuditableEntity {
@Id
@GeneratedValue
private Long id;
private String orderNumber;
}
Spring Data JPAの監査機能を使う場合は、プロジェクト側で@EnableJpaAuditingなど必要な設定も確認します。アノテーションを付けただけで日時が自動設定されるとは限りません。
@Embeddableとは
@Embeddableは、所有Entityの一部として保存する値の型を表します。埋め込み型自身はIDや独自テーブルを持たず、フィールドは所有Entityのテーブルへ保存されます。
@Embeddable
public class Address {
@Column(name = "shipping_postal_code")
private String postalCode;
@Column(name = "shipping_city")
private String city;
@Column(name = "shipping_street")
private String street;
protected Address() {
}
public Address(String postalCode, String city, String street) {
this.postalCode = postalCode;
this.city = city;
this.street = street;
}
}
Entity側では@Embeddedを使います。
@Entity
public class Order {
@Id
private Long id;
@Embedded
private Address shippingAddress;
}
AddressはOrderの一部ですが、Javaコード上では郵便番号、市区町村、番地を1つの意味ある値として扱えます。Stringフィールドを3つばらばらに渡すより、引数の取り違えも防ぎやすくなります。
違いを表で整理する
| 観点 | @MappedSuperclass | @Embeddable |
|---|---|---|
| 関係 | 継承する | 所有して組み込む |
| 向く内容 | Entity共通のマッピング・仕組み | 意味のある値のまとまり |
| 独自テーブル | 持たない | 持たない |
| 独自ID | 子Entity側の一部として定義可能 | 持てない |
| 代表例 | 作成日時、更新日時、共通ID | 住所、期間、金額、氏名 |
重複している列を見つけたら、まず「すべてのEntityが同じ性質として持つものか」「Entityが所有する値のまとまりか」を判断します。
監査項目はMappedSuperclassが向きやすい
作成日時、更新日時、作成者、更新者は、多くのEntityで共通する技術的な監査情報です。これらは親クラスへまとめることで、アノテーションや更新方法を統一しやすくなります。
ただし、「共通だから」と論理削除フラグ、ステータス、承認者、部署コードまで追加すると、使わないEntityにも項目が広がります。BaseEntityは何でも置き場にせず、本当に横断的な情報へ絞ります。
住所や期間はEmbeddableが向きやすい
住所、利用期間、金額、氏名などは、複数の値で1つの意味を作ります。これらは継承関係ではなく、Entityが持つ値として表現する方が自然です。
@Embeddable
public class UsagePeriod {
private LocalDate startDate;
private LocalDate endDate;
protected UsagePeriod() {
}
public boolean includes(LocalDate target) {
return !target.isBefore(startDate)
&& !target.isAfter(endDate);
}
}
日付を2つ保持するだけでなく、「期間内か」というルールも同じ型へ置けます。業務上の意味と処理を近づけられるのが利点です。
@Entityの継承とは違う
@MappedSuperclassはマッピング情報を子Entityへ引き継ぎますが、親クラス自体はEntityではありません。親型を対象にJPQLで一括検索するようなEntity継承の用途とは違います。
@Inheritanceを使うEntity継承は、単一テーブル、結合テーブルなど継承戦略の検討が必要です。共通カラムを減らしたいだけで、Entity継承へ進まないよう注意します。
よくある失敗
- 重複列を見つけるたびにBaseEntityへ追加する
- 業務上は別物の値を、列構成が同じという理由だけで共通化する
- 同じEmbeddableを1つのEntityで複数回使い、列名が衝突する
- JPA用の引数なしコンストラクタを用意していない
- Entity継承とMappedSuperclassを同じものとして説明する
請求先住所と配送先住所のように同じ埋め込み型を2回使う場合は、@AttributeOverrideで列名を分ける必要があります。既存DBへ合わせる案件では特に確認します。
現場レビューでの見られ方
// レビューコメント例 作成日時と更新日時は全Entity共通の監査項目なので、 MappedSuperclassへ寄せる候補です。 // レビューコメント例 住所はEntityの共通親クラスというより値のまとまりです。 Embeddableとして所有させる方が業務上の意味を表現できます。 // レビューコメント例 BaseEntityへ業務項目が増え続けています。 本当に全Entityが同じ理由で変更される項目か確認してください。
レビュー前のセルフチェック
確認リスト
- 共通化する対象がEntity共通の性質か、値のまとまりか
- MappedSuperclass自体をEntityとして検索しようとしていないか
- Embeddableへ独自IDを持たせようとしていないか
- 列名の衝突や既存DBとの対応を確認したか
- BaseEntityが業務項目の寄せ集めになっていないか
Spring徹底入門 第2版
Spring Frameworkの主要機能を、実務レベルで整理する。
DI、MVC、データアクセス、テスト、セキュリティまで幅広く確認できます。ピンポイント検索から体系学習へ進みたい人向けです。
- Springの全体像を整理したい
- 現場で使う機能を体系的に学びたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
@MappedSuperclassはEntity共通のマッピングを継承させ、@EmbeddableはEntityが所有する値のまとまりを表します。どちらも独自テーブルを持ちませんが、継承と所有という関係が違います。
作成日時などの横断的な監査項目はMappedSuperclass、住所や期間など業務上意味のある値はEmbeddableを候補にします。重複を消すことより、モデルを読んだ時に意味が伝わることを優先してください。
