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

Spring Boot JPA 多:1

Spring Boot 1.5.6のJPAの多:1(ManyToOne)について。


概要

あるエンティティーTから別のエンティティーM(マスター相当)を参照する(T:M=n:1、すなわち多:1)場合、JPAではエンティティーTの中でMを持つ形で書ける。

import javax.persistence.Entity;
import javax.persistence.ManyToOne;
@Entity
public TEntity {
〜
	@ManyToOne
	private MEntity m;
〜
}

フィールドにEntityクラスを直接書き、@ManyToOneアノテーションを付ける。
(このエンティティーの一覧を多側で持ちたい場合は、@OneToManyアノテーションを使用する)


多:0〜1の場合は、optionalをtrueにする。(デフォルトはtrue)

	@ManyToOne(optional = true)
	private MEntity m;

こうすると、フィールドmはnullでも良くなる。

optionalをfalseにすると必須(フィールドがnullだとDBに入れられない)になる。


ManyToOneの例

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

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

package com.example.demo.db.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Master1Entity {

	@Id
	@GeneratedValue
	private Long id;

	private String value;

	〜setter/getter〜
}

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

package com.example.demo.db.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class ManyExampleEntity {

	@Id
	@GeneratedValue
	private Long id;

	private String value;

	@ManyToOne(optional = true)
	private Master1Entity master1;

	〜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.ManyExampleEntity;
@Repository
public interface ManyExampleRepository extends JpaRepository<ManyExampleEntity, Long> {
}

save

Entityを保存する失敗例。

	@Autowired
	private ManyExampleRepository repository;
		ManyExampleEntity entity = new ManyExampleEntity();
		entity.setValue("ttt");
		{
			Master1Entity master = new Master1Entity();
			master.setValue("aaa");
			entity.setMaster1(master);
		}
		repository.save(entity);

		ManyExampleEntity result = repository.findOne(entity.getId());

↓実行結果

result=ManyExampleEntity [id=1, value=ttt, master1=Master1Entity [id=null, value=aaa]]

普通に保存できたように見えるが、取得し直したresultのMaster1Entityのidがnullのまま。
SQLのログを見ると、Master1Entityのinsertは実行されておらず、ManyExampleEntityが持つmaster1用のidもnullだった。


saveAndFlush

saveの例に対し、saveメソッドをsaveAndFlushメソッドに変えると、以下のような例外が発生する。

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.demo.db.domain.ManyExampleEntity.master1 -> com.example.demo.db.domain.Master1Entity; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.demo.db.domain.ManyExampleEntity.master1 -> com.example.demo.db.domain.Master1Entity
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:381)
〜
Caused by: java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.demo.db.domain.ManyExampleEntity.master1 -> com.example.demo.db.domain.Master1Entity
	at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1689)
〜
Caused by: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.demo.db.domain.ManyExampleEntity.master1 -> com.example.demo.db.domain.Master1Entity
	at org.hibernate.engine.spi.CascadingActions$8.noCascade(CascadingActions.java:379)
	at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:126)
	at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:150)
	at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:141)
	at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:74)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:38)
	at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1282)
	at org.hibernate.jpa.spi.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:1300)
	... 62 more

Master1Entityが保存(フラッシュ)されていないという事らしい。
Master1Entityを先に保存するか、CascadeTypeを指定すると通るようになる。


フィールドで保持するMaster1Entityを先に保存すれば問題ない。

		Master1Entity master = new Master1Entity();
		master.setValue("aaa");
		master1Repository.save(master);

		ManyExampleEntity entity = new ManyExampleEntity();
		entity.setValue("ttt");
		entity.setMaster1(master);
		repository.saveAndFlush(entity);

		ManyExampleEntity result = repository.findOne(entity.getId());

↓実行結果

result=ManyExampleEntity [id=1, value=ttt, master1=Master1Entity [id=1, value=aaa]]

Cascadeの例

@ManyToOneアノテーションにCascadeTypeを指定することが出来る。

CascadeTypeには色々種類があるが、例えばPERSISTを指定すると、save時に「フィールドで保持しているEntity」も保存される。
ALLを指定すると、CascadeTypeの全てを指定したことになる。
(デフォルトは、CascadeTypeの指定は一切無し)

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

package com.example.demo.db.domain;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class ManyCascadeExampleEntity {

	@Id
	@GeneratedValue
	private Long id;

	private String value;

	@ManyToOne(optional = true, cascade = CascadeType.ALL)
	private Master1Entity master1;

	〜setter/getter〜
}

保存する例:

		ManyCascadeExampleEntity entity = new ManyCascadeExampleEntity();
		entity.setValue("ttt");
		{
			Master1Entity master = new Master1Entity();
			master.setValue("aaa");
			entity.setMaster1(master);
		}
		repository.save(entity);

		ManyCascadeExampleEntity result = repository.findOne(entity.getId());

↓実行結果

result=ManyCascadeExampleEntity [id=1, value=ttt, master1=Master1Entity [id=1, value=aaa]]

JPQL

JPQLで、「Entity型のフィールド」のプロパティー(フィールド)も扱うことが出来る。

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

package com.example.demo.db.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.example.demo.db.domain.ManyExampleEntity;
@Repository
public interface ManyExampleRepository extends JpaRepository<ManyExampleEntity, Long> {
	@Query("select t from ManyExampleEntity t where t.master1.value = :value")
	public List<ManyExampleEntity> findByMaster1Value(@Param("value") String value);
}

この場合、生成されるSQLは(H2DBの場合)cross joinになる。


join句

join句を使って、結合するエンティティーを指定することも出来る。

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

	@Query("select t from ManyExampleEntity t join t.master1 m where m.value = :value")
//	@Query("select t from ManyExampleEntity t join t.master1   where t.master1.value = :value") // joinの別名を付けない方式
	public List<ManyExampleEntity> findByMaster1Value(@Param("value") String value);

この場合、生成されるSQLは(H2DBの場合)inner joinになる。


left joinやright joinにすることも出来る。

	@Query("select t from ManyExampleEntity t left join t.master1 m where m.value = :value")

普通のSQLのjoinは(任意の)結合対象のテーブル名を指定(し、onで結合カラムを指定)するものだが、
JPQLのjoinは「Entityクラス内のEntityフィールド」にしか使えない。
フィールドになっていないエンティティーに対する結合

JPQLのjoinで単純にEntity名を指定すると、例外が発生する。

	@Query("select t from ManyExampleEntity t join Master1Entity m where m.value = :value")

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'manyExampleRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Validation failed for query for method public abstract java.util.List com.example.demo.db.repository.ManyExampleRepository.findByMaster1Value(java.lang.String)!
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	〜
Caused by: java.lang.IllegalArgumentException: Validation failed for query for method public abstract java.util.List com.example.demo.db.repository.ManyExampleRepository.findByMaster1Value(java.lang.String)!
	at org.springframework.data.jpa.repository.query.SimpleJpaQuery.validateQuery(SimpleJpaQuery.java:92) ~[spring-data-jpa-1.11.6.RELEASE.jar:na]
	〜
Caused by: java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Path expected for join! [select t from com.example.demo.db.domain.ManyExampleEntity t join Master1Entity m where m.value = :value]
	at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1679) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final]
	〜
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: Path expected for join! [select t from com.example.demo.db.domain.ManyExampleEntity t join Master1Entity m where m.value = :value]
	at org.hibernate.hql.internal.ast.QuerySyntaxException.convert(QuerySyntaxException.java:74) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.hql.internal.ast.ErrorCounter.throwQueryException(ErrorCounter.java:91) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.hql.internal.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:268) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:190) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:142) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.engine.query.spi.HQLQueryPlan.(HQLQueryPlan.java:115) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.engine.query.spi.HQLQueryPlan.(HQLQueryPlan.java:76) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:150) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.internal.AbstractSessionImpl.getHQLQueryPlan(AbstractSessionImpl.java:302) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.internal.AbstractSessionImpl.createQuery(AbstractSessionImpl.java:240) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.internal.SessionImpl.createQuery(SessionImpl.java:1894) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
	at org.hibernate.jpa.spi.AbstractEntityManagerImpl.createQuery(AbstractEntityManagerImpl.java:291) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final]
	... 61 common frames omitted

criteria APIのjoin

「Entity型のフィールド」のプロパティー(フィールド)を条件とするcriteria APIの例。[2017-10-10]

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

package com.example.demo.db.repository;

import java.util.List;

import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface ManyExampleRepository extends JpaRepository<ManyExampleEntity, Long>, JpaSpecificationExecutor<ManyExampleEntity> {

	public default List<ManyExampleEntity> findByMaster1Value2(String value) {
		Specification<ManyExampleEntity> spec = (root, query, cb) -> {
			Join<ManyExampleEntity, Master1Entity> m = root.join("master1", JoinType.LEFT);
			return cb.equal(m.get("value"), value);
		};
		return findAll(spec);
	}
}

is null

結合先データが存在しないレコードは以下のようにして取得できる。[2017-10-22]

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

	public List<ManyExampleEntity> findByMaster1IsNull();

Queryで明示するなら以下のように記述する。[2017-10-22]

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

	@Query("select t from ManyExampleEntity t where t.master1 is null")
	public List<ManyExampleEntity> findByMaster1IsNull();

criteria APIを使う場合は以下のように記述する。[2017-10-22]

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

	public default List<ManyExampleEntity> findByMaster1IsNull() {
		Specification<ManyExampleEntity> spec = (root, query, cb) -> {
			return root.get("master1").isNull();
		};
		return findAll(spec);
	}

カラム名

@ManyToOneアノテーションを付けたEntityフィールドの場合、テーブルとしては、対象Entityの主キー(@Idを付けたフィールド)と同じ型のカラムが用意される。
このカラムの名前を明示的に付けたい場合は@JoinColumnアノテーションを使う。
(Entityの普通のフィールド(Entityでないフィールド)では、テーブルのカラム名を付けるのに@Columnアノテーションを使う)

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
@Entity
public class ManyExampleEntity {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "example_value")
	private String value;

	@ManyToOne(optional = true)
	@JoinColumn(name = "master1_fk")
	private Master1Entity master1;
〜
}

複合キー(2カラム以上を結合キーとする)の場合は@JoinColumnsアノテーションを使う。


複合キーによる結合

普通に@ManyToOneアノテーションを付けただけの場合、テーブルとしては、結合対象Entityの主キー(@Idを付けたフィールド)と同じ型の(外部キー用の)カラムが用意される。

対象Entityの(Idでない)複数のカラムをキーとして結合する場合は、結合対象Entityのカラムに名前を付け、結合元Entityフィールドに@JoinColumnsでその名前を指定する。
こうすると、テーブルに外部キー用の複数カラムが用意される。

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

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Master2Entity implements Serializable {
	private static final long serialVersionUID = 1350051284638634609L;

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "key1")
	private long key1;

	@Column(name = "key2")
	private int key2;

	private String value;

	〜setter/getter〜
}

この方式を採る場合、なぜかSerializableを実装しないといけないようだ。

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

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
@Entity
public class ManyExampleEntity {

	@Id
	@GeneratedValue
	private Long id;

	private String value;

	@ManyToOne
	@JoinColumns({
		@JoinColumn(name = "master2_key1", referencedColumnName = "key1"),
		@JoinColumn(name = "master2_key2", referencedColumnName = "key2")
	})
	private Master2Entity master2;

	〜setter/getter〜
}

参考: ko-aokiさんのJPAで参照先のEntityが複合主キーの場合(はまり)


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