No Bugs, No Life

読んだ本や、プログラミング、システム開発等のねたを中心に。文章を書く練習なので少し硬派に書くつもりだけど、どうなることやら。

JPA(EclipseLink-JPA)を用いたDBアクセス

MementoWeaver開発記(11)
前回はJPAのEntityManagerを用いて単一のテーブルへのInsert(Persist)を実装したが、関連のある複数テーブルに跨る操作は未実装だった。
今回は関連のある複数テーブルの参照を実装する。
大まかな方針として、以下の段取りですすめる。

  1. Derby上のテーブル間のリレーションを実装する。
  2. Daliを用いてDerbyテーブルからEntity群をリバース生成する。
  3. 生成されたEntity群を用いて処理(具体的には、Material(親)とTaggedMaterial(子)に格納された情報を取得)する。

Derby上のテーブル間のリレーションを実装する

まずは設計上のERを再確認。
f:id:kazyury:20130309101706p:plain
リレーションは3本引かれているが、意味的には以下の2本。

  • 素材[Material](1..1)----(0..m)タグ付素材[TaggedMaterial]
  • タグ付素材[TaggedMaterial](1..m)----(0..m)メメント[Memento]

ただし、2番目のリレーションはm:mなので関係テーブル(MementoContents)を配置して以下の形式にしているのみ。

  • TaggedMaterial(1..1)----(1..m)MementoContents(1..m)----(1..1)Memento

ERMasterから生成したDDLは以下のような形になる(関連箇所のみ抜粋)。このDDLを実行してDerby上にテーブルと関連を実装する。

/* Create Tables */

CREATE TABLE MW.MEMENTO
(
	MEMENTO_ID CHAR(8) NOT NULL,
	PRIMARY KEY (MEMENTO_ID)
);


CREATE TABLE MW.MEMENTO_CONTENTS
(
	MATERIAL_ID CHAR(14) NOT NULL,
	TAG CHAR(8) NOT NULL,
	MEMENTO_ID CHAR(8) NOT NULL,
	PRIMARY KEY (MATERIAL_ID, TAG, MEMENTO_ID)
);


CREATE TABLE MW.PREDEFINED_TAG
(
	TAG CHAR(8) NOT NULL,
	FQCN VARCHAR(256) NOT NULL,
	PRIMARY KEY (TAG)
);


CREATE TABLE MW.MATERIAL
(
	MATERIAL_ID CHAR(14) NOT NULL,
-- (省略)
	PRIMARY KEY (MATERIAL_ID)
);


CREATE TABLE MW.TAGGED_MATERIAL
(
	MATERIAL_ID CHAR(14) NOT NULL,
	TAG CHAR(8) NOT NULL,
-- (省略)
	PRIMARY KEY (MATERIAL_ID, TAG)
);

/* Create Foreign Keys */

ALTER TABLE MW.MEMENTO_CONTENTS
	ADD FOREIGN KEY (MEMENTO_ID)
	REFERENCES MW.MEMENTO (MEMENTO_ID)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;


ALTER TABLE MW.TAGGED_MATERIAL
	ADD FOREIGN KEY (MATERIAL_ID)
	REFERENCES MW.MATERIAL (MATERIAL_ID)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;


ALTER TABLE MW.MEMENTO_CONTENTS
	ADD FOREIGN KEY (MATERIAL_ID, TAG)
	REFERENCES MW.TAGGED_MATERIAL (MATERIAL_ID, TAG)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;

Daliを用いてDerbyテーブルからEntity群をリバース生成する。

次にDaliからEntityを再生成する。
Eclipseのプロジェクトフォルダを右クリックし、JPA Tools->Generate Entities from Table...を実行。
Select Tables画面では全テーブルを選択し、Next
f:id:kazyury:20130309235038p:plain
Table Associations画面では関連を(手動で?)定義するのだが、Daliが既に外部キー制約をみて初期値を設定してくれている。
f:id:kazyury:20130309235503p:plain
Dali的にはTaggedMaterialとMementoの間はm:m(ManyToMany)の関係として認識してくれている。
個人的には親テーブル(Material)が右側に表現されているのが気持ち悪いが、問題なさそうなのでNext。
自動生成キーの設定や、DBフィールド名とJavaフィールド名のマッピングなどを確認し、Finish

生成されたEntityはこのような感じになる。(抜粋)

Material.java

子エンティティであるTaggedMaterialのListに関するアクセサまでが定義されている。
デフォルトのスキーマ指定だけはしてくれないようなので、@Tableアノテーションにschema = "MW"を追加(EclipseJPA Detailビューからプルダウンできる。)

@Entity
@Table(name="MATERIAL", schema = "MW")
public class Material implements Serializable {
	private static final long serialVersionUID = 1L;
	@Id
	@Column(name="MATERIAL_ID", unique=true, nullable=false, length=14)
	private String materialId;
//(略)
	//bi-directional many-to-one association to TaggedMaterial
	@OneToMany(mappedBy="material")
	private List<TaggedMaterial> taggedMaterials;
//(略)

	public List<TaggedMaterial> getTaggedMaterials() {
		return this.taggedMaterials;
	}

	public void setTaggedMaterials(List<TaggedMaterial> taggedMaterials) {
		this.taggedMaterials = taggedMaterials;
	}

	public TaggedMaterial addTaggedMaterial(TaggedMaterial taggedMaterial) {
		getTaggedMaterials().add(taggedMaterial);
		taggedMaterial.setMaterial(this);
		return taggedMaterial;
	}

	public TaggedMaterial removeTaggedMaterial(TaggedMaterial taggedMaterial) {
		getTaggedMaterials().remove(taggedMaterial);
		taggedMaterial.setMaterial(null);
		return taggedMaterial;
	}
}

TaggedMaterial.java

Mementoとの関係テーブルは、@JoinTableアノテーションで表現してくれている。

@Entity
@Table(name="TAGGED_MATERIAL", schema = "MW")
public class TaggedMaterial implements Serializable {
	private static final long serialVersionUID = 1L;

	@EmbeddedId
	private TaggedMaterialPK id;
//(略)
	//bi-directional many-to-many association to Memento
	@ManyToMany
	@JoinTable(name="MEMENTO_CONTENTS"
        , joinColumns={
	  @JoinColumn(name="MATERIAL_ID", referencedColumnName="MATERIAL_ID", nullable=false),
	  @JoinColumn(name="TAG", referencedColumnName="TAG", nullable=false)
	}
	, inverseJoinColumns={
	  @JoinColumn(name="MEMENTO_ID", nullable=false)
	}
	)

	private List<Memento> mementos;

	//bi-directional many-to-one association to Material
	@ManyToOne
	@JoinColumn(name="MATERIAL_ID", nullable=false, insertable=false, updatable=false)
	private Material material;
//(略)
	public TaggedMaterialPK getId() {
		return this.id;
	}

	public void setId(TaggedMaterialPK id) {
		this.id = id;
	}
//(略)
	public List<Memento> getMementos() {
		return this.mementos;
	}

	public void setMementos(List<Memento> mementos) {
		this.mementos = mementos;
	}

	public Material getMaterial() {
		return this.material;
	}

	public void setMaterial(Material material) {
		this.material = material;
	}
}

Memento.java

TaggedMaterialとの関連については、@JoinTableなどは定義されていない様子。
これでよいのだろうか?メメント周りの実装時の確認事項としておく。

@Entity
@Table(name="MEMENTO", schema = "MW")
public class Memento implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@Column(name="MEMENTO_ID", unique=true, nullable=false, length=8)
	private String mementoId;

	//bi-directional many-to-many association to TaggedMaterial
	@ManyToMany(mappedBy="mementos")
	private List<TaggedMaterial> taggedMaterials;
//(略)
	public List<TaggedMaterial> getTaggedMaterials() {
		return this.taggedMaterials;
	}

	public void setTaggedMaterials(List<TaggedMaterial> taggedMaterials) {
		this.taggedMaterials = taggedMaterials;
	}
}

生成されたEntity群を用いた処理

MaterialテーブルへのInsert

親テーブルのみへのInsertならば前回にも記載した内容と基本的には一緒。
若干リファクタリングしたが、手直し自体は発生していない。
InstallProcessor.javaより関連箇所のみを抜粋(順不同)

EntityManager em = PersistenceUtil.getMWEntityManager();

// MaterialEntityの生成
Material materialEntity = new Material();
materialEntity.setMaterialId(formatDate(lastModifiedDate,"yyyyMMddhhmmss"));
materialEntity.setCreatedYear(Integer.parseInt(formatDate(lastModifiedDate,"yyyy")));
materialEntity.setCreatedMonth(Integer.parseInt(formatDate(lastModifiedDate,"MM")));
materialEntity.setMaterialType(Constants.MATERIAL_TYPE_JPG);
materialEntity.setMaterialState(Constants.MATERIAL_STATE_INSTALLED);

// PersistenceManagerにインストール状況の登録を要求
em.getTransaction().begin();
em.persist(materialEntity);
em.getTransaction().commit();
em.close();

Materialテーブル、TaggedMaterialテーブルからの検索(CriteriaQuery使用)

MaterialStateがMATERIAL_STATE_INSTALLEDなMaterialのみをMaterialテーブルより検索する。
また、関連するTaggedMaterialについては全件を取得する。
例によって新し物好きなので、CriteriaQueryを使用してみる。
StagingProcessor.javaより関連箇所のみを抜粋

EntityManager em = PersistenceUtil.getMWEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();

// Material表を検索し、installedされている素材の一覧を取得する。
CriteriaQuery<Material> materialQuery= cb.createQuery(Material.class);
Root<Material> material = materialQuery.from(Material.class);
materialQuery.select(material)
  .where(cb.equal(material.get("materialState"), Constants.MATERIAL_STATE_INSTALLED));

installedMaterials = em.createQuery(materialQuery).getResultList();
for (Material m:installedMaterials) {
  // TaggedMaterial表を検索し、設定されているタグの一覧を取得する。
  // -> Entity間で関連をはっているので、materialから辿ることができる。
  List<TaggedMaterial> taggedMaterialList = m.getTaggedMaterials();
  StringBuffer tags = new StringBuffer();
  for(TaggedMaterial tm:taggedMaterialList) {
    tags.append("["+tm.getId().getTag()+"]");
  }
}

どうも変数名の付け方とかはまだ気に喰わないのでこっそり直すかもしれないが、行っていることは大まかに以下の流れになっている。

  1. CriteriaBuilderをEntityManagerから取得
  2. CriteriaQueryをCriteriaBuilderから取得 #=>query
  3. query.from()で主エンティティ(?)のクラスを渡す # =>Rootオブジェクト
  4. query.select()に先ほどのRootオブジェクトを渡す
  5. query.where()で各種検索条件等を設定
  6. EntityManagerにqueryを渡して実行

どうもCriteriaとかRootとかがまだ腹に落ちていない感じがする。
Rootって名前と射影が頭の中で結びつかないからかな。
次回はCriteria API関連を整理する*1

*1:JavaFXのほうも整理しないと駄目なんだけどなかなかJPAから離れられない...。