Spring Boot 1.5.6のJPAの1:多(ElementCollection)について。
あるエンティティーTで別のエンティティーDを複数保持する(T:D=1:n、すなわち1:多)場合、JPAではエンティティーTの中でDのコレクションを持つ形で書ける。
import javax.persistence.ElementCollection; import javax.persistence.Entity;
@Entity public TEntity { 〜 @ElementCollection private List<DEntity> dList; 〜 }
フィールドにコレクション書き、@ElementCollectionアノテーションを付ける。
EntityだけでなくList<String>のように基本型もコレクションにして複数持つことが出来る。
ただしこの場合も、実現方法としては基本型のデータを持つテーブルが作られ、そこに複数レコード保持することになる。
(扱いとしてはEmbedded相当のようで、関連付けテーブルは作られない)
@ElementCollectionのデフォルトは、fetch=LAZY(遅延)となっている。
つまり、エンティティーを取得したとき、@ElementCollectionが付けられたフィールドのデータはまだ取得されない。
(実際のSQLとしても、最初のエンティティーの取得と、フィールドのデータの取得のSQLは別々に発行される)
fetch=EAGERにすると、エンティティーを取得した際に同時にフィールドのデータも取得される。
(実際のSQLとしても、joinを使った1つのSQLしか発行されない)
例として、以下のようなEntityを操作してみる。
package com.example.demo.db.domain; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id;
@Entity public class DetailEntity { @Id @GeneratedValue private Long id; private String value; 〜setter/getter〜 }
package com.example.demo.db.domain; import java.util.List; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id;
@Entity public class CollectionExampleEntity { @Id @GeneratedValue private Long id; private String value; @ElementCollection(fetch = FetchType.EAGER) private List<DetailEntity> detailList; 〜setter/getter〜 }
package com.example.demo.db.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.example.demo.db.domain.CollectionExampleEntity;
@Repository public interface CollectionExampleRepository extends JpaRepository<CollectionExampleEntity, Long> { }
エンティティーを保存する際は、@ElementCollectionを付けたフィールドのデータを先に保存しておく必要がある。
import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.springframework.beans.factory.annotation.Autowired;
@Autowired private CollectionExampleRepository repository; @Autowired private DetailRepository detailRepository;
CollectionExampleEntity entity = new CollectionExampleEntity(); entity.setValue("zzz"); entity.setDetailList(new ArrayList<>(Arrays.asList(detail("aa"), detail("bb"), detail("cc")))); detailRepository.save(entity.getDetailList()); // コレクションを先に保存 repository.saveAndFlush(entity); CollectionExampleEntity result = repository.findOne(entity.getId());
private DetailEntity detail(String value) { DetailEntity entity = new DetailEntity(); entity.setValue(value); return entity; }
detailListにセットするときに(Arrays.asListの結果を直接渡すのでなく)ArrayListにしているのは、
内部でリストを変更(削除+追加)する事があるらしく、Arrays.asListのリストは変更不能なのでエラーになることがある為。
fetch=EAGERにした@ElementCollectionアノテーションを持つフィールドが2つ以上あると、実行時にエラーになる。
Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.persistenceException(EntityManagerFactoryBuilderImpl.java:954) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final] 〜 Caused by: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.demo.db.domain.CollectionExampleEntity.masterList, com.example.demo.db.domain.CollectionExampleEntity.detailList] at org.hibernate.loader.plan.exec.internal.AbstractLoadQueryDetails.generate(AbstractLoadQueryDetails.java:178) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.plan.exec.internal.EntityLoadQueryDetails.(EntityLoadQueryDetails.java:89) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.plan.exec.internal.BatchingLoadQueryDetailsFactory.makeEntityLoadQueryDetails(BatchingLoadQueryDetailsFactory.java:61) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader. (AbstractLoadPlanBasedEntityLoader.java:82) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.plan.EntityLoader. (EntityLoader.java:103) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.plan.EntityLoader. (EntityLoader.java:38) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.plan.EntityLoader$Builder.byUniqueKey(EntityLoader.java:83) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.plan.EntityLoader$Builder.byPrimaryKey(EntityLoader.java:77) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.plan.AbstractBatchingEntityLoaderBuilder.buildNonBatchingLoader(AbstractBatchingEntityLoaderBuilder.java:30) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.loader.entity.BatchingEntityLoaderBuilder.buildLoader(BatchingEntityLoaderBuilder.java:59) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.persister.entity.AbstractEntityPersister.createEntityLoader(AbstractEntityPersister.java:2306) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.persister.entity.AbstractEntityPersister.createEntityLoader(AbstractEntityPersister.java:2328) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.persister.entity.AbstractEntityPersister.createLoaders(AbstractEntityPersister.java:3928) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.persister.entity.AbstractEntityPersister.postInstantiate(AbstractEntityPersister.java:3910) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.internal.SessionFactoryImpl. (SessionFactoryImpl.java:446) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:444) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:879) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final] ... 47 common frames omitted
EAGERにするのは最大1個にして、他はLAZYにする必要がある。
@ElementCollectionのデフォルトは、fetch=LAZY(遅延)となっている。
Entityの扱い方(インスタンス生成やメソッド呼び出し方法)はEAGERの場合と同じ。
内部的には、エンティティー(下記の例のCollectionExampleEntity)を取得した時点では、@ElementCollectionが付けられたフィールドのデータ
(下記の例のdetailList)はまだ取得されない。
フィールドのデータを取得しようとしたときにDBアクセスされる。
package com.example.demo.db.domain; import java.util.List; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id;
@Entity public class CollectionExampleEntity { @Id @GeneratedValue private Long id; private String value; @ElementCollection(fetch = FetchType.LAZY) private List<DetailEntity> detailList; 〜setter/getter〜 }
fetch=LAZYにした@ElementCollectionを使っていると、LazyInitializationExceptionが発生することがある。
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.demo.db.domain.CollectionExampleEntity.detailList, could not initialize proxy - no Session at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:587) at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:204) at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:566) at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:135) 〜
これは、コレクション(Entityのフィールド)の値をDBから取得しようとしたがDBのセッションが無い(切断されている)、という時に発生する。
要するに、トランザクションの外側でEntityのフィールドの値を取得しようとすると発生する。
したがって、トランザクションの中だけでEntityを操作していれば、この例外は発生しない。
この例外でググると「EAGERにしろ」という打開策をよく見かけるが、それは根本的な解決策ではない。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
@Service public class CollectionExampleService { @Autowired private CollectionExampleRepository repository;
@Transactional(readOnly = true) public void 何らかの処理(long id) { CollectionExampleEntity entity = repository.findOne(id); // entityの操作(フィールドの取得)はトランザクションの中だけで完結させる } }
エンティティーのコレクションをフィールドに持つと、実際のテーブルとしては、2つのエンティティーを関連付けるテーブル(2つのエンティティーのIdを持つテーブル)も用意される。
コレクションをEmbeddedにすると、関連テーブルは作られず、データ側のテーブルに外部キー項目が用意される。
この場合、データ側はEntityではなくEmbeddableにする。
EmbeddableはEntityではないので、Id項目は不要となる。
(Entityではないので、独立したテーブルとしてアクセスする事は出来ない)
(関連テーブルを作らずにEntityで扱いたい場合は@OneToManyアノテーションを使う)
package com.example.demo.db.domain; import javax.persistence.Embeddable;
@Embeddable public class DetailEmbed { private String value; 〜setter/getter〜 }
package com.example.demo.db.domain; import java.util.List; import javax.persistence.ElementCollection; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id;
@Entity
public class CollectionExampleEntity {
@Id
@GeneratedValue
private Long id;
private String value;
@ElementCollection(fetch = FetchType.LAZY)
@Embedded // このアノテーションは無くても構わないっぽい
private List<DetailEmbed> embedList;
〜setter/getter〜
}
CollectionExampleEntity entity = new CollectionExampleEntity(); entity.setValue("zzz"); entity.setEmbedList(new ArrayList<>(Arrays.asList(embed("aa"), embed("bb"), embed("cc")))); repository.saveAndFlush(entity); CollectionExampleEntity result = repository.findOne(entity.getId());
private DetailEmbed embed(String value) { DetailEmbed entity = new DetailEmbed(); entity.setValue(value); return entity; }
参考: megascusさんのJPAの@Embeddableと@ElementCollectionでEntityの責務を明確化する。
Embeddedによって作られるテーブルのテーブル名やカラム名を定義することが出来る。
import java.util.List; import javax.persistence.AttributeOverride; import javax.persistence.AttributeOverrides; import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Embedded; import javax.persistence.FetchType; import javax.persistence.JoinColumn;
@ElementCollection(fetch = FetchType.LAZY) @Embedded @CollectionTable(name = "DETAIL_EMBED", joinColumns = { @JoinColumn(name = "DETAIL_ID") }) @AttributeOverrides({ @AttributeOverride(name = "value", column = @Column(name = "DETAIL_VALUE")) }) private List<DetailEmbed> embedList;