参画しているプロジェクトでMyBatisからJPAへの移行が予定されているので、(個人的に)JPAの勉強中です。
今回は、しばらく調査して諦めた話です。。
対象のテーブルは、以下のようなレイアウトで、マスタが有効開始日、有効終了日を持ち世代管理されています。
ユーザの本日付けの部門名、ロール名を取得したい場合、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を使用しています。
@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を指定(すればうまくいくと思ってました)
@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;
//以下略
}
@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;
//以下略
}
@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したければダミーコード入れちゃうとか。。
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();