プログラミング

実装・エラー解決

Spring Data Jpaと@Entityを使って複数テーブルで共通の値を親クラスにまとめて、子クラスで継承する方法

このページに書いてあること

業務用のシステムを作っていると、DBへのアクセスは非常に一般的な処理で、SpringMVCなんかを使っているとSpring Data Jpaを使ってアクセスするのがよくあるパターンかと思います。

で、DBアクセスをするときによくあるのが、

テーブルがたくさんあるけど、全てのテーブルに共通しているカラムがある。

という状況だと思います。例えば、登録日付・更新日付などが該当しそうです。
このとき、@Entityを使ったコードだとすべてのクラスに同じカラム名のコードが入ってしまうことになります。これは避けるためには共通カラムを親クラスに定義して、差分だけを子クラスが定義してやればよさそうです。

私がこの問題にぶつかったのは、テーブルのカラムは同じだがユーザによって取得先のテーブルを切り替えるという要件ででした。

結論

共通のマッピングだけを引き継ぎ、子ごとに別のテーブルへ保存するなら、親クラスに @MappedSuperclass を付けます。親クラス自体はエンティティとして管理されず、共通列は各子エンティティのテーブルに作られます。

関連記事Spring Data JPAの@MappedSuperclassと@Embeddableの違い|共通項目をどこへまとめる? Spring Data JPAで、監査項目を親から継承する場合と、住所などを所有する値として埋め込む場合を比べます。 記事を読む

これを実現したい

親クラス ParentUserEntity に共通のID・登録日・更新日を置き、子クラスが継承します。子ごとに @Table で別のテーブルを指定します。

Entityを継承したクラス図

配布例では ChildEntity が ACTIVE_USERS、ArchivedEntity が ARCHIVED_USERS を使います。

親を @Entity にすること自体は可能です。ただし、継承戦略を指定しなければ既定の単一テーブル継承となり、子ごとに別テーブルを持つ今回の要件とは結果が異なります。

子Entityのコードは以下のようになります。

実行用サンプル

単一テーブル継承と、MappedSuperclassで別テーブルへ共通列を引き継ぐ場合を、HibernateとH2で比較します。 ZIPにはソース全文・テスト・pom.xml・READMEを収録しています。JDK 17とMaven 3.9系で mvn test を実行してください。

package info.syachiku.mapping;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "ACTIVE_USERS")
public class ChildEntity extends ParentUserEntity {
    protected ChildEntity() {
    }

    public ChildEntity(String userId, String insertDate, String updateDate) {
        super(userId, insertDate, updateDate);
    }
}

親も @Entity にした比較用コードです。配布コードの SingleTableChild を保存すると、親で指定した SHARED_USERS に行が入ります。

package info.syachiku.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "SHARED_USERS")
public abstract class SingleTableParent {
    @Id
    private String userId;

    protected SingleTableParent() {
    }

    protected SingleTableParent(String userId) {
        this.userId = userId;
    }
}

親もエンティティにすると、既定の単一テーブル継承では親子が一つのテーブルを共有します。子を別テーブルに分けたいときは、共通の列定義だけを引き継ぐ構成を選びます。

次の修正例では、先に示した ChildEntity の親を、共通列を定義する ParentUserEntity にします。

@MappedSuperclassを使う

次が @MappedSuperclass を付けた親クラスの全文です。この記事の例は javax.persistence を使い、Hibernate 5.6.15.Final と H2 2.3.232 で動作を確認しました。Spring Boot 3以降のアプリでは jakarta.persistence を使う構成に合わせてください。

package info.syachiku.mapping;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public abstract class ParentUserEntity {
    @Id
    @Column(name = "USER_ID")
    private String userId;

    @Column(name = "INSERT_DATE")
    private String insertDate;

    @Column(name = "UPDATE_DATE")
    private String updateDate;

    protected ParentUserEntity() {
    }

    protected ParentUserEntity(String userId, String insertDate, String updateDate) {
        this.userId = userId;
        this.insertDate = insertDate;
        this.updateDate = updateDate;
    }
}

テストは ACTIVE_USERS と ARCHIVED_USERS の両方に同じIDの行を保存し、各テーブルから別々の更新日が読めることを確認します。

共通列を持つ親クラスだけのテーブルが作られていないことも確認します。

-実装・エラー解決
-