S-JIS[2017-10-08] 変更履歴

Spring Boot JPA 1:多(コレクション)

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アノテーションを付ける。

(→@OneToManyアノテーションを使用する方法


EntityだけでなくList<String>のように基本型もコレクションにして複数持つことが出来る。
ただしこの場合も、実現方法としては基本型のデータを持つテーブルが作られ、そこに複数レコード保持することになる。
(扱いとしてはEmbedded相当のようで、関連付けテーブルは作られない)


EAGERの例

@ElementCollectionのデフォルトは、fetch=LAZY(遅延)となっている。
つまり、エンティティーを取得したとき、@ElementCollectionが付けられたフィールドのデータはまだ取得されない。
(実際のSQLとしても、最初のエンティティーの取得と、フィールドのデータの取得のSQLは別々に発行される)

fetch=EAGERにすると、エンティティーを取得した際に同時にフィールドのデータも取得される。
(実際のSQLとしても、joinを使った1つのSQLしか発行されない)


例として、以下のようなEntityを操作してみる。

src/main/java/com/example/demo/db/domain/DetailEntity.java:

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〜
}

src/main/java/com/example/demo/db/domain/CollectionExampleEntity.java:

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〜
}

src/main/java/com/example/demo/db/repository/ManyExampleRepository.java:

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> {

}

save

エンティティーを保存する際は、@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のリストは変更不能なのでエラーになることがある為。


2つ以上のEAGER

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にする必要がある。


LAZYの例

@ElementCollectionのデフォルトは、fetch=LAZY(遅延)となっている。
Entityの扱い方(インスタンス生成やメソッド呼び出し方法)はEAGERの場合と同じ。

内部的には、エンティティー(下記の例のCollectionExampleEntity)を取得した時点では、@ElementCollectionが付けられたフィールドのデータ (下記の例のdetailList)はまだ取得されない。
フィールドのデータを取得しようとしたときにDBアクセスされる。

src/main/java/com/example/demo/db/domain/CollectionExampleEntity.java:

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〜
}

LazyInitializationException

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にしろ」という打開策をよく見かけるが、それは根本的な解決策ではない。

src/main/java/com/example/demo/db/service/CollectionExampleService.java:

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の操作(フィールドの取得)はトランザクションの中だけで完結させる
	}
}

Embeddedの例

エンティティーのコレクションをフィールドに持つと、実際のテーブルとしては、2つのエンティティーを関連付けるテーブル(2つのエンティティーのIdを持つテーブル)も用意される。

コレクションをEmbeddedにすると、関連テーブルは作られず、データ側のテーブルに外部キー項目が用意される。
この場合、データ側はEntityではなくEmbeddableにする。
EmbeddableはEntityではないので、Id項目は不要となる。
(Entityではないので、独立したテーブルとしてアクセスする事は出来ない)
(関連テーブルを作らずにEntityで扱いたい場合は@OneToManyアノテーションを使う)

src/main/java/com/example/demo/db/domain/DetailEmbed.java:

package com.example.demo.db.domain;

import javax.persistence.Embeddable;
@Embeddable
public class DetailEmbed {

	private String value;

	〜setter/getter〜
}

src/main/java/com/example/demo/db/domain/CollectionExampleEntity.java:

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のテーブル定義

Embeddedによって作られるテーブルのテーブル名やカラム名を定義することが出来る。

src/main/java/com/example/demo/db/domain/CollectionExampleEntity.java:

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;

JPAサンプルに戻る / JPAへ戻る / Spring Bootへ戻る / Spring Frameworkへ戻る / 技術メモへ戻る
メールの送信先:ひしだま