JPA(EclipseLink-JPA)を用いたDBアクセス
MementoWeaver開発記(11)
前回はJPAのEntityManagerを用いて単一のテーブルへのInsert(Persist)を実装したが、関連のある複数テーブルに跨る操作は未実装だった。
今回は関連のある複数テーブルの参照を実装する。
大まかな方針として、以下の段取りですすめる。
- Derby上のテーブル間のリレーションを実装する。
- Daliを用いてDerbyテーブルからEntity群をリバース生成する。
- 生成されたEntity群を用いて処理(具体的には、Material(親)とTaggedMaterial(子)に格納された情報を取得)する。
Derby上のテーブル間のリレーションを実装する
まずは設計上のERを再確認。
リレーションは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
Table Associations画面では関連を(手動で?)定義するのだが、Daliが既に外部キー制約をみて初期値を設定してくれている。
Dali的にはTaggedMaterialとMementoの間はm:m(ManyToMany)の関係として認識してくれている。
個人的には親テーブル(Material)が右側に表現されているのが気持ち悪いが、問題なさそうなのでNext。
自動生成キーの設定や、DBフィールド名とJavaフィールド名のマッピングなどを確認し、Finish
生成されたEntityはこのような感じになる。(抜粋)
Material.java
子エンティティであるTaggedMaterialのListに関するアクセサまでが定義されている。
デフォルトのスキーマ指定だけはしてくれないようなので、@Tableアノテーションにschema = "MW"を追加(EclipseのJPA 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()+"]"); } }
どうも変数名の付け方とかはまだ気に喰わないのでこっそり直すかもしれないが、行っていることは大まかに以下の流れになっている。
- CriteriaBuilderをEntityManagerから取得
- CriteriaQueryをCriteriaBuilderから取得 #=>query
- query.from()で主エンティティ(?)のクラスを渡す # =>Rootオブジェクト
- query.select()に先ほどのRootオブジェクトを渡す
- query.where()で各種検索条件等を設定
- EntityManagerにqueryを渡して実行
どうもCriteriaとかRootとかがまだ腹に落ちていない感じがする。
Rootって名前と射影が頭の中で結びつかないからかな。
次回はCriteria API関連を整理する*1。