だから余計なお世話だと言っているのに

複合主キーなんか使わないで、主キーはみんなIDパターンで通していれば少しはEclipseLinkと仲良くできただろうか。
僕だって複合主キーなんか使いたくなかった。
でも僕らはもう決して分かり合えないところまで来てしまったんだ(意味不明)。

複合主キーを使用したクラスのSqlResultSetMapping

そんなわけで今日もEclipseLink*1の愉快な挙動を紹介しちゃいます*2

/**
 * 請求エンティティの複合主キー
 */
@Embeddable
public Class BillPK {
  /** 相手先コード */
  @Column(name="PARTNER_CD")
  private String partnerCd;
  /** 請求締日 */
  @Column(name="CUTOFF_DATE")
  private Date cutoffDate;
}

/**
 * 請求エンティティ
 */
@Enitity
@NamedNativeQueries({
  @NamedNativeQuery(name="Bill.getDetail", query="SELECT "
    + "  b.PARTNER_CD AS B_PARTNER_CD,"
    + "  b.CUTOFF_DATE AS B_CUTOFF_DATE,"
    + "  b.BILL_AMOUNT AS B_BILL_AMOUNT,"
    + "  d.BILL_DETAIL_SEQ AS D_BILL_DETAIL_SEQ,"
    + "  d.BILL_DETAIL_AMOUNT AS D_BILL_DETAIL_AMOUNT "
    + "FROM "
    + "  BILL b "
    + "  INNER JOIN BILL_DETAIL d "
    + "    ON (b.PARTNER_CD = d.PARTNER_CD "
    + "    AND b.CUTOFF_DATE = d.CUTOFF_DATE)"
  )
})
@SqlResultSetMappings({
  @SqlResultSetMapping(name="Bill.getDetailResult", entities={
      @EntityResult(entityClass=Bill.class, fields={
        @FieldResult(name="pk.partnerCd", column="B_PARTNER_CD"),
        @FieldResult(name="pk.cutoffDate", column="B_CUTOFF_DATE"),
        @FieldResult(name="billAmount", column="B_BILL_AMOUNT")}),
      @EntityResult(entityClass=BillDetail.class, fields={
        @FieldResult(name="pk.partnerCd", column="B_PARTNER_CD"),
        @FieldResult(name="pk.cutoffDate", column="B_CUTOFF_DATE"),
        @FieldResult(name="pk.billDetailSeq", column="D_BILL_DETAIL_SEQ"),
        @FieldResult(name="billDetailAmount", column="D_BILL_DETAIL_AMOUNT")})
  })
})
public Class Bill {
  /** 複合主キー */
  @EmbeddedId
  private BillPK pk;
  /** 請求額 */
  @Column(name="BILL_AMOUNT")
  private long billAmount;

  @OneToMany(fetch=FetchType.LAZY)
  @JoinColumns({
    @JoinColumn(name="PARTNER_CD", referencedColumnName="PARTNER_CD"),
    @JoinColumn(name="CUTOFF_DATE", referencedColumnName="CUTOFF_DATE")})
  private BillDetail billDetails;
}

/**
 * 請求明細エンティティの複合主キー
 */
@Embeddable
public Class BillDetailPK {
  /** 相手先コード */
  @Column(name="PARTNER_CD")
  private String partnerCd;
  /** 請求締日 */
  @Column(name="CUTOFF_DATE")
  private Date cutoffDate;
  /** 請求明細シーケンス */
  @Column(name="BILL_DETAIL_SEQ")
  private int billDetailSeq;
}

/**
 * 請求明細エンティティ
 */
@Entity
@Table(name="BILL_DETAIL")
public class BillDetail {
  /** 複合主キー */
  @EmbeddedId
  private BillDetailPK pk;
  /** 明細請求額 */
  @Column(name="BILL_DETAIL_AMOUNT")
  private long billDetailAmount;
}


普通このくらいの例だとJPQLを使用しますが、今回は説明のためあえて@NamedNativeQueryと@SqlResultSetMappingを使用します。
また、SQL結果の列名が@Columnで指定した列名と一致している場合は@FieldResultの指定は省略できるのですが、諸事情でエイリアスを設定しなければならず、@Columnでの指定と一致しないこともあります。今回の例はそのような場合を想定していると考えてください。

List<Object[]> result = getEntityManager()
  .createNamedQuery("Bill.getDetail")
  .setResultSetMapping("Bill.getDetailResult")
  .getResultList();


さて、では上記のコードを実行するとresultは何になるでしょうか?


答えはこんな感じ。

[
  [null, null],
  [null, null],
  [null, null],
  …
]


びっくりです。
この子はどこかおかしいんでしょうか?

解説

実は、SQLの結果セットのマッピングには@JoinColumnで指定した情報も使われます(たぶん、LazyLoad時に使用する値を物色しているのではないかと思います)。
上記の例の場合
「請求明細エンティティとの結合に使うプロパティpartnerCdのDB上のカラムの名前は"PARTNER_CD"である」
と指定していますので、EclipseLinkはpartnerCdの値をSQLの結果セットの"PARTNER_CD"という列から探そうとします。
しかし列名にはエイリアスを指定してあるのでそんな列は見つからず、EclipseLinkはpartnerCdはnullであると判断します。
partnerCdは主キーの一部です。主キーの一部がnullである場合EclipseLinkは結果セットのマッピングを放棄し、エンティティを返す代わりにnullを返します。
その結果が上記のような現象として現れるというわけです。


@FieldResultで指定した情報が使われないというわけではないのですが、値をマッピングする順番が@FieldResultの指定→@JoinColumnの指定なので、@FieldResultの指定を正しく行っても@JoinColumnによって値が消されてしまうのです。

対策

下のように指定すればこの問題を解決することができます。

      @EntityResult(entityClass=Bill.class, fields={
        @FieldResult(name="pk.partnerCd", column="B_PARTNER_CD"),
        @FieldResult(name="pk.cutoffDate", column="B_CUTOFF_DATE"),
        @FieldResult(name="billAmount", column="B_BILL_AMOUNT"),
        @FieldResult(name="billDetails.partnerCd", column="B_PARTNER_CD"),
        @FieldResult(name="billDetails.cutoffDate", column="B_CUTOFF_DATE"),
        @FieldResult(name="billDetails.billDetailSeq", column="D_BILL_DETAIL_SEQ")}),


こうすることによって@JoinColumnの指定がSQLの結果セットのマッピングで使われなくなり、正しいカラムから値を取得できるようになります。
しかしここまでしてもエンティティを本来の構造に組み立てなおして返してくれるということはありません。あくまでもgetResultListの戻り値はObject[]です。
誰得なんだよ、この仕様。

それにしても

@SqlResultSetMappingとか@NamedNativeQueryって、つくづくアノテーションで書くものじゃないよねえ…。

*1:僕が知っているEclipseLinkは1.1.3です。今は2系が出ていて違う、ということもありえます。

*2:ソースはイメージです。動作確認してません。setter,getter,equals,hashcodeもありません。