JPAで参照先のEntityが複合主キーの場合(はまり)


参画しているプロジェクトでMyBatisからJPAへの移行が予定されているので、(個人的に)JPAの勉強中です。
今回は、しばらく調査して諦めた話です。。

対象のテーブルは、以下のようなレイアウトで、マスタが有効開始日、有効終了日を持ち世代管理されています。

ユーザの本日付けの部門名、ロール名を取得したい場合、SQLだとこんな感じ。

join.sql
SELECT 
    U.USER_ID,
    D.DEPT_NM,
    R.ROLE_NM
FROM
   M_USER U
   LEFT OUTER JOIN 
    M_DEPT D ON U.DEPT_ID = D.DEPT_ID
   LEFT OUTER JOIN
   M_ROLE R ON U.ROLE_ID = R.ROLE_ID
WHERE
   TO_CHAR(SYSDATE,'yyyymmdd') BETWEEN U.START_DATE AND U.END_DATE
   AND
   TO_CHAR(SYSDATE,'yyyymmdd') BETWEEN D.START_DATE AND D.END_DATE
   AND
   TO_CHAR(SYSDATE,'yyyymmdd') BETWEEN R.START_DATE AND R.END_DATE

JPAは「単独カラムだけで主キーを構成することをお勧め」とあるので、移行するなら素直にサロゲートキーを使用したいところですが、他システムのマスタなど、こちらが勝手に変更できない場合もあります。

なので、上記SQLと同じようなことができないか?と試したのが以下。
EclipseLinkを使用しています。

MUserPK.java
@Embeddable
public class MUserPK implements Serializable {
    @Basic(optional = false)
    @Column(name = "USER_ID")
    private String userId;
    @Basic(optional = false)
    @Column(name = "START_DATE")
    private String startDate;

    public MUserPK() {
    }

    public MUserPK(String userId, String startDate) {
        this.userId = userId;
        this.startDate = startDate;
    }

//以下略
}


@JoinColumnでM_DEPTとDEPT_IDで結合。
DEPT_IDだけでは複数カラムになるので@ManyToOneを指定(すればうまくいくと思ってました)

MUser.java
@Entity
@Table(name = "M_USER")
public class MUser implements Serializable {

    private static final long serialVersionUID = 1L;
    @EmbeddedId
    protected MUserPK mUserPK;
    @Column(name = "USERNM_SEI")
    private String usernmSei;
    @Column(name = "USERNM_MEI")
    private String usernmMei;
    @Column(name = "PASSWORD")
    private String password;
    @ManyToOne
    @JoinColumns( {
        @JoinColumn(name = "DEPT_ID", referencedColumnName = "DEPT_ID", insertable = false, updatable = false)
    })
    private MDept deptId;

//以下略
}
MDeptPK.java
@Embeddable
public class MDeptPK implements Serializable {
    @Basic(optional = false)
    @Column(name = "DEPT_ID")
    private String deptId;
    @Basic(optional = false)
    @Column(name = "START_DATE")
    private String startDate;

//以下略
}

MDept.java
@Entity
@Table(name = "M_DEPT")
public class MDept implements Serializable {
    private static final long serialVersionUID = 1L;
    @EmbeddedId
    protected MDeptPK mDeptPK;
    @Column(name = "DEPT_NM")
    private String deptNm;
    @Column(name = "P_DEPT_ID")
    private String pDeptId;
    @Column(name = "END_DATE")
    private String endDate;
    @Column(name = "VERSION_NO")
    private Long versionNo;
//以下略
}

上記を実行すると以下のエラーが。。
複合主キーの場合、結合カラムをそれぞれ指定しろとな。
言われてみればそうですね。。

エラー
Internal Exception: Exception [EclipseLink-7220] (Eclipse Persistence Services - 2.5.1.v20130918-f2b9fc5): org.eclipse.persistence.exceptions.ValidationException
Exception Description: The @JoinColumns on the annotated element [field deptId] from the entity class [class testjpa.MUser] is incomplete. 
When the source entity class uses a composite primary key, 
a @JoinColumn must be specified for each join column using the @JoinColumns. 
Both the name and the referencedColumnName elements must be specified in each such @JoinColumn.

デカルト積でいいなら、以下の書き方でリレーションがないEntity同士でもJOINできるようですが、

2014/09/04修正
where句で結合指定すれば、リレーションがないEntityでもINNER JOINはできるのですね。
そしてコンストラクタ式で検索結果を任意のクラスに格納。
既存のテーブルレイアウト縛りなら、もうこれで逃げるしかないんじゃないかなあ。
LEFT OUTERしたければダミーコード入れちゃうとか。。

jpql

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("TestJPAPU");
    EntityManager em = emf.createEntityManager();

    String sql = "SELECT  new testjpa.JoinDto(" +
            "    U.mUserPK.userId, " +
            "    D.deptNm, " +
            "    R.roleNm ) " +
            "FROM " +
            "   MUser U, " +
            "   MDept D, " +
            "   MRole R " +
            "WHERE " +
            "   U.deptId = D.mDeptPK.deptId " +
            "   AND " +
            "   U.roleId = R.mRolePK.roleId " +
            "   AND " +
            "   '20140904' BETWEEN U.mUserPK.startDate AND U.endDate " +
            "   AND " +
            "   '20140904' BETWEEN D.mDeptPK.startDate AND D.endDate " +
            "   AND " +
            "   '20140904' BETWEEN R.mRolePK.startDate AND R.endDate ";

        List<JoinDto> result = em.createQuery(sql, JoinDto.class).getResultList();