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);
}
}