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に入れられない)になる。
以下のようなEntityを操作してみる。
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〜 }
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〜 }
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> { }
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だった。
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]]
@ManyToOneアノテーションにCascadeTypeを指定することが出来る。
CascadeTypeには色々種類があるが、例えばPERSISTを指定すると、save時に「フィールドで保持しているEntity」も保存される。
ALLを指定すると、CascadeTypeの全てを指定したことになる。
(デフォルトは、CascadeTypeの指定は一切無し)
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で、「Entity型のフィールド」のプロパティー(フィールド)も扱うことが出来る。
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句を使って、結合するエンティティーを指定することも出来る。
@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
「Entity型のフィールド」のプロパティー(フィールド)を条件とするcriteria APIの例。[2017-10-10]
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); } }
結合先データが存在しないレコードは以下のようにして取得できる。[2017-10-22]
public List<ManyExampleEntity> findByMaster1IsNull();
Queryで明示するなら以下のように記述する。[2017-10-22]
@Query("select t from ManyExampleEntity t where t.master1 is null") public List<ManyExampleEntity> findByMaster1IsNull();
criteria APIを使う場合は以下のように記述する。[2017-10-22]
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でその名前を指定する。
こうすると、テーブルに外部キー用の複数カラムが用意される。
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を実装しないといけないようだ。
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が複合主キーの場合(はまり)