Spring Boot 1.5.6のJPAの1:多(OneToMany)について。
あるエンティティーTから別のエンティティーDを複数参照する(T:D=1:n、すなわち1:多)場合、JPAではエンティティーTの中でDのコレクションを持つ形で書ける。
import javax.persistence.Entity; import javax.persistence.OneToMany;
@Entity public TEntity { 〜 @OneToMany(mappedBy = "t") private List<DEntity> dList; 〜 }
フィールドにコレクション書き、@OneToManyアノテーションを付ける。
また、多側(DEntity)に@ManyToOneアノテーションを付けたフィールドを用意する。
1:多を記述する方法は、@OneToManyアノテーションを使う方法の他に@ElementCollectionアノテーションを使う方法がある。
私見だが、
@ElementCollectionを使う方式は「クラスの中に複数のデータを保持する」というニュアンスが強く、
@OneToManyは「RDBのテーブルの1:多を表現する」もの
と思われる。
@ElementCollectionを使う方式(Embedded以外)は、データを保持するテーブルの他に、その2つを関連付けるテーブルも生成される。
@OneToMany(および@ManyToOne)を使う方式では、多側のテーブルに外部キー(1側のテーブルのIDを保持するカラム)を持つので、関連付けテーブルは生成されない。
@OneToManyアノテーションの例。
参考:teachingprogramming.netのJPAによるリレーション: @OneToManyと@ManyToOne
package com.example.demo.db.domain; import java.util.List; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany;
@Entity public class OneExampleEntity { @Id @GeneratedValue private Long id; private String value; @OneToMany(mappedBy = "oneExample") private List<OneDetailEntity> detailList;
@OneToManyアノテーションのmappedByで、多側のEntityで定義されている自クラス(この例ではOneExampleEntity)のフィールド名を指定する。
〜setter/getter〜 @Override public String toString() { return "OneExampleEntity [id=" + id + ", value=" + value + ", detailList=" + detailList + "]"; } }
package com.example.demo.db.domain; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne;
@Entity public class OneDetailEntity { @Id @GeneratedValue private Long id; private String value; @ManyToOne @JoinColumn(name = "ONE_EXAMPLE_FK") private OneExampleEntity oneExample;
多側のEntityでは、1側のEntityを表すフィールドを用意し、@ManyToOneアノテーションを付ける。
(本来ここで欲しいのは1側のEntityのプライマリキーのみだが、JPAとしては、Entityクラスのフィールドを用意する
。実体としてのテーブルは、外部キーのみとなる)
〜setter/getter〜 @Override public String toString() { return "OneDetailEntity [id=" + id + ", value=" + value + ", oneExample=" + ((oneExample != null) ? oneExample.getId() : null) + "]"; } }
toStringメソッドでは、1側のEntity(oneExample)をそのまま出力せず、Id項目だけにしている。
oneExampleをそのまま出力してしまうと、OneExampleEntity側のtoStringメソッドでもOneDetailEntityを出力しているので循環参照となり、StackOverFlowになってしまう。
保存する例。
package com.example.demo.db.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.db.domain.OneDetailEntity; import com.example.demo.db.domain.OneExampleEntity; import com.example.demo.db.repository.OneExampleRepository; import com.example.demo.db.repository.OneDetailRepository;
@Service public class OneExampleService {
@Autowired private OneExampleRepository repository; @Autowired private OneDetailRepository detailRepository;
@Transactional public void save(OneExampleEntity entity) { if (entity.getId() == null) { repository.save(entity); // idを採番 } // 古いdetailを削除 detailRepository.deleteByOneExampleId(entity.getId()); if (entity.getDetailList() != null) { for (OneDetailEntity detail : entity.getDetailList()) { detail.setOneExample(entity); } detailRepository.save(entity.getDetailList()); } repository.saveAndFlush(entity); } }
@Autowired private OneExampleService service;
OneExampleEntity entity = new OneExampleEntity(); entity.setValue("zzz"); entity.setDetailList(new ArrayList<>(Arrays.asList(detail("aa"), detail("bb"), detail("cc")))); service.save(entity);
private OneDetailEntity detail(String value) { OneDetailEntity entity = new OneDetailEntity(); entity.setValue(value); return entity; }
1側のEntityのIdを条件とする多側のdeleteの例。
多側のRepositoryにdeleteメソッドを定義する。
package com.example.demo.db.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.example.demo.db.domain.OneDetailEntity; import com.example.demo.db.domain.OneExampleEntity;
@Repository public interface OneDetailRepository extends JpaRepository<OneDetailEntity, Long> { // EntityのIdを指定する削除 public int deleteByOneExampleId(Long id); // Entityを指定する削除 public int deleteByOneExample(OneExampleEntity entity); }
JPAの命名ルールに則り、deleteByの後にプロパティー名(フィールド名)を付けたメソッドを定義すると、そのプロパティーを条件とする削除になる。
ただ、これだと、条件に合致するOneDetailEntityを全てselectし、その数だけプライマリキー指定でdeleteを発行するようだ。
自分で削除用のJPQLを記述すると、以下のようになる。
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param;
@Query(value = "delete from OneDetailEntity t where t.oneExample.id = :id") @Modifying public int deleteByOneExampleId(@Param("id") Long id);
where条件には「t.oneExample.id
」と書いているが、実際に生成されるSQLでは、期待通りOneDetailEntityのテーブル内の外部キーのカラムになっていた。(OneExampleEntityとjoinするようなSQL文ではない)
多側のプロパティーを条件とするJPQLの例。[2017-10-10]
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;
@Repository public interface OneExampleRepository extends JpaRepository<OneExampleEntity, Long> { @Query("select t from OneExampleEntity t inner join t.detailList d where d.value = :value") public List<OneExampleEntity> findByDetailValue1(@Param("value") String value); }
多側のプロパティーを条件とする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.stereotype.Repository;
@Repository public interface OneExampleRepository extends JpaRepository<OneExampleEntity, Long>, JpaSpecificationExecutor<OneExampleEntity> { public default List<OneExampleEntity> findByDetailValue2(String value) { Specification<OneExampleEntity> spec = (root, query, cb) -> { Join<OneExampleEntity, OneDetailEntity> d = root.join("detailList", JoinType.INNER); return cb.equal(d.get("value"), value); }; return findAll(spec); } }