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

Spring Boot JPA 1:多

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

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

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 + "]";
	}
}

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

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になってしまう。


save

保存する例。

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

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

多側のdelete

1側のEntityのIdを条件とする多側のdeleteの例。


多側のRepositoryにdeleteメソッドを定義する。

src/main/java/com/example/demo/db/repository/OneDetailRepository.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.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を記述すると、以下のようになる。

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

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の例

多側のプロパティーを条件とするJPQLの例。[2017-10-10]

src/main/java/com/example/demo/db/repository/OneExampleRepository.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;
@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の例

多側のプロパティーを条件とするcriteria APIの例。[2017-10-10]

src/main/java/com/example/demo/db/repository/OneExampleRepository.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.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);
	}
}

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