Skip to content

SpringDataJPA

hyukke edited this page May 16, 2016 · 18 revisions

Spring Data JPA

Description

Java による永続化に関する仕様である JPA (Java Persistence API) をサポートした O/R マッピングフレームワーク。

Usage

使用方法は、以下の通り。
バージョンは、org.springframework.boot 1.2.3による実装時に使用されていたspring-data-jpa-1.7.2を元に記載している。

Entity

@Entityを付与してエンティティを作成する。

@Entity
@Table(name = "sample_master")
public class Sample {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "sample_id")
	private Integer sampleId;
  • @GeneratedValueで ID の自動生成が可能
  • デフォルトでテーブル名やカラム名の「_(アンダースコア)」をキャメルケースに置き換えた名前がクラス名またはフィールド名になる
  • 名称に差異があるなど必要であれば@Table@Columnで名称を指定

@Enumeratedを付与することで、enumで定義した定数に変換が可能となる。

	@Enumerated(EnumType.STRING)
	private SampleStatus status;
  • EnumType#STRING 列挙名をマッピング
  • EnumType#ORDER 列挙の順序をマッピング

Abstract Class

抽象化する必要があれば、@MappedSuperclassを付与したクラスを作成して、各エンティティに継承させる。

@MappedSuperclass
public abstract class AbstractEntity {

	@CreatedDate
	@Column(updatable = false)
	@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
	private DateTime createDate;

	@CreatedBy
	@Column(updatable = false)
	private String createUsername;

	@LastModifiedDate
	@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
	private DateTime updateDate;

	@LastModifiedBy
	private String updateUsername;

	@Version
	private Integer version;
@Entity
@Table(name = "sample_master")
public class Sample extends AbstractEntity  {

Join Tables

エンティティの関係が一対多または多対一の場合は、@OneToMany@ManyToOneを付与したエンティティをフィールドにする。

@Entity
public class Sample {

	@OneToMany(mappedBy = "sample")
	@Where(clause = "delete_flg = 0")
	List<AnotherSample> anotherSamples;
@Entity
public class AnotherSample {

	@ManyToOne
	@JoinColumn(name = "sample_id", nullable = "false")
	private Sample sample;
  • @JoinColumnで結合条件を指定し、@whereで条件が指定可能

結合表がある場合は、@JoinTableで指定することができる。

	@OneToMany
	@JoinTable(
		name = "sample_attach_file",
		joinColumns = {@JoinColumn(name = "sample_id")},
		inverseJoinColumns = {@JoinColumn(name = "attach_file_id")}
	)
	List<AttachFile> sampleAttachFiles;
  • joinColumns 所有者側(このフィールドを定義している側)エンティティのカラムを指定
  • inverseJoinColumns 被所有者側エンティティのカラムを指定

Listener

@EntityListenersを付与することで、エンティティの処理に対するコールバックを実装できる。

@Configurable
public class SampleListener {
    @PrePersist
    public void doSomething() {
        // Do something... before persist entity
    }
}
@EntityListeners(SampleListener.class)
@Entity
public class Sample {

Repository

@Repositoryを付与し、JpaRepositoryをエンティティとIDをパラメータとして継承したインタフェースを作成する。
実装クラスは自動生成されるため作成しない。
基本的なエンティティへの CRUD などは、JPA が提供するリポジトリの末端に当たるインタフェースJpaRepositoryが提供してくれる。

@Repository
public interface SampleRepository extends JpaRepository<Sample, Integer> {
}

リポジトリをインジェクションする。

@Service
@Transactional
public class SampleService {

	@Autowired
	private SampleRepository repository;

Select

1件を検索する際は、#findOne(ID)を実行する。

	public Sample findOne(Integer id) {
		Sample entity = this.repository.findOne(id);

複数件を検索する際は、#findAll(Iterable<ID>)を実行する。

	public Sample findAll(Iterable<Integer> ids) {
		List<Sample> entities = this.repository.findAll(ids);

全件数を取得する場合は#count()IDの存在を確認する場合は#exists(ID)をそれぞれ実行する。

	public long count() {
		return this.repository.count();
	}

	public boolean exists(Integer id) {
		return this.repository.exists(id);
	}

Insert or Update

登録または更新する際は、#save(T)または#saveAndFlush(T)を実行する。

	public Sample register(Sample entity) {
		try {
			// Case 1
			entity = this.repository.save(entity);
			this.repository.flush();
			// Case 2
			entity = this.repository.saveAndFlush(entity);
		} catch (DataIntegrityViolationException e) {
			// Handle error
		}
  • #save(T) 永続化する操作をEntityManagerに蓄積するのみ
  • #saveAndFlush(T) 上記と同様に蓄積した後、永続層(Database)に反映するまで実施

Delete

削除する際は、#delete(ID)または#delete(T)を実行する。

	public void delete(Sample entity) {
		try {
			// Case 1
			this.repository.delete(entity.getSampleId());
			// Case 2
			this.repository.delete(entity);
		} catch (EmptyResultDataAccessException e) {
			// Handle error
		}

Find by Method Signatures

検索において、決められた命名規則に従ったメソッド名をリポジトリに定義すると、クエリーが自動生成される。
検索を表す文字列はfind...Byの他に、read...Byquery...Bycount...Byget...Byなどがある。

public interface SampleRepository extends JpaRepository<Sample, Integer> {

	// ... where ANOTHER_SAMPLE_ID = ?
	List<Sample> findByAnotherSampleId(Integer anotherSampleId);
	// ... where ANOTHER_SAMPLE_ID = ? and SAMPLE_NAME = ?
	List<Sample> findByAnotherSampleIdAndSampleName(Integer anotherSampleId, String sampleName);
	// ... where ANOTHER_SAMPLE_ID = ? or SAMPLE_NAME = ?
	List<Sample> findByAnotherSampleIdOrSampleName(Integer anotherSampleId, String sampleName);

	// ... where SAMPLE_NAME = ? order by SAMPLE_ID desc
	List<Sample> findBySampleNameOrderBySampleIdDesc(String sampleName);
	// ... where SAMPLE_NAME is not null limit 10
	Sample findTop10BySampleNameIsNotNull();

	// select distinct * ... where SAMPLE_FLAG = false
	List<Sample> findDistinctBySampleFlagFalse(String sampleName);
	// select count(*) ... where SAMPLE_NAME = ?
	Long countBySampleName(String sampleName);
  • エンティティクラス内で入れ子になっているフィールドを指定する場合は、フィールド名を前に付与

    // ... where ANOTHER_SAMPLE.SAMPLE_ID = ?
    // Entity: Sample#anotherSample -> AnotherSample#sampleId
    List<Sample> findByAnotherSampleSampleId(Integer sampleId);

Find by Query Annotation

@Queryを付与したメソッドを定義することで、 JPQL やネイティブクエリを指定することができる。

public interface SampleRepository extends JpaRepository<Sample, Integer> {

	@Query("select s from sample s where s.anotherSampleId = :anotherSampleId")
	List<Sample> findByAnotherSampleId(@Param("anotherSampleId") Integer anotherSampleId);

	@Query("select s from SAMPLE_MASTER s where s.SAMPLE_NAME = :SAMPLE_NAME")
	List<Sample> findBySampleName(@Param("sampleName") String sampleName);
  • コンパイル時に SQL の妥当性が検証されるため、不正な文であればエラーが発生

Find by Specification

Specification#toPredicate(Root, CriteriaQuery, CriteriaBuilder)を実装したオブジェクトを条件として検索することができる。
Specificationは、ドメイン駆動設計のパターンの1つで、意味を持つ条件(仕様)を表現するクラスのこと。

import static org.terasoluna.gfw.common.query.QueryEscapeUtils.*;

public class SampleSpecs {

	public static Specification<Sample> searchCondition(SearchCondition c) {
		return (sample, query, cb) -> {

			List<Predicate> pre = new ArrayList<>();

			Join<sample, AnotherSample> anotherSample = sample.join("anotherSample", JoinType.INNER);

			if (StringUtils.isNotBlank(condition.getSampleName())) {
				predicates.add(cb.like(sample.get("sampleName"), toContainingCondition(condition.getSampleName())));
			}
			equal(cb, anotherSample.get("anotherSampleId"), c.getAnotherSampleId()).ifPresent(p -> pre.add(p));

			return cb.and(pre.toArray(new Predicate[pre.size()]));
		};
	}
}

リポジトリにJpaSpecificationExecutor<T>を継承させる。

public SampleRepository extends JpaRepository<Sample, Integer>, JpaSpecificationExecutor<Sample> {

生成したSpecificationを引数に検索を実行する。

	public Sample findByCondition(SearchCondition condition) {
		List<Sample> entities = this.repository.findAll(SampleSpecs.searchCondition(condition));
Criteria Builder

検索条件の仕様( SQL における WHERE や IN などの句に該当)を組み立てる際は、javax.persistence.criteria.CriteriaBuilderを使用する。

メソッド 該当する SQL 使用例
#and(Predicate...) COL_A AND ... cb.and(preA)
#or(Predicate...) COL_A OR ... cb.or(preA)
#like(Expression, String) COL_A LIKE ?
#equal(Expression, Object) COL_A = ? cb.equal(entity.get("id"), "SAMPLE01")
#in(Expression) #value(Object) COL_A IN ( ? ) cb.in(entity.get("type")).value(list)
#greaterThanOrEqualTo(Expression, Object) COL_A >= ? cb.greaterThanOrEqualTo(entity.get("amount"), Long.valueOf(1000L))
#isNull(Expression) COL_A IS NULL cb.isNull(entity.get("id"))
#isTrue(Expression) COL_A IS TRUE cb.isTrue(entity.get("deleted"))
#nullIf(Expression, Object)
Pageable

検索時の件数を制限し、ページングを実装するには、Pageableを使用する。
Pageableを引数に取るリポジトリの検索を呼び出す。

	public Page<Sample> findPageByCondition(
	        SampleSearchCondition condition, Pageable pageable) {

		Page<Sample> result = this.repository.findAll(SampleSpecs.searchCondition(condition), pageable);
		if (result.getTotalElements() > 0 && result.getNumberOfElements() <= 0) {
			// XXX 削除された直後に呼ばれた場合に 0 件になっている可能性があるため、 1 ページ前の Pageable で再検索
			result = this.findPageByCondition(condition, pageable.previousOrFirst());
		}
		return result;
	}

上位のレイヤなどでPageableをパラメータに指定し、返却されたPageを処理する。
Page#getContent()で検索結果のリストが取得できる。

	// Spring Web MVC
	@RequestMapping(value = "search", method = RequestMethod.GET)
	public String search(
			@Validated MaintenanceRecordSearchForm form,
			BindingResult result,
			@PageableDefault(size = 30, sort = "sampleId", direction = Direction.DESC) Pageable pageable,
			Model model) {

		if (result.hasErrors()) {
			return "sample/search";
		}

		SampleSearchCondition condition = new SampleSearchCondition();
		// XXX Do something...
		Page<Sample> page = this.sampleService.findPageByCondition(condition, pageable);
		if (page.getTotalElements() > 0) {
			model.addAttribute("samples", page.getContent());
			model.addAttribute("page", page);
		} else {
			model.addAttribute(ResultMessages.info().add("success.message.key"));
		}
		return "sample/search";
	}
  • web のフレームワークであれば、@PageableDefaultを付与したパラメータを指定することで、デフォルトを設定したPageableオブジェクトを取得可能
  • size属性で件数を、sort属性で順序を、directionで昇順または降順をそれぞれ指定可能
  • 順序のみであれば@SortDefaultで指定可能

Find by Entity Manager

EntityManager#createNativeQuery(String)を使用することで、直接 SQL を記載して実行することができる。

	@Autowired
	private EntityManager entityManager;

	public List<Sample> findAll(SearchCondition condition) {
		return this.entityManager
				.createNativeQuery("select s.* from SAMPLE_MASTER s where match (s.SAMPLE_NAME, s.SAMPLE_ADDRESS) against (:keyword in boolean mode)")
				.setParameter("keyword", condition.getKeyword())
				.getResultList();
	}
  • 妥当性が検証されることによってエラーになり定義できないような SQL の文法があった場合に有効
  • データベースに対して文法が不正であったり、構文が不正になった場合は、実行時にエラーが発生
  • EntityManager#createQuery(String, T)であれば、JPQL が指定可能。

Other Components

Auditing

共通で必要になることがある作成者や更新者は、AuditorAwareを実装することで横断的に挿入することができる。

	@Bean(name = "auditorAware")
	AuditorAware<String> auditorAwareImpl() {
		return new AuditorAware<String>() {
			@Override
			public String getCurrentAuditor() {
				Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
				if (authentication == null || !authentication.isAuthenticated()) {
					return null;
				}
				return authentication.getName();
			}			
		};
	}
  • Spring Security を介してログインユーザを取得することも可能

  • org.springframework.data.jpa.domain.support.AuditingEntityListener@EntityListenersに登録することでも指定可能

// Framework set create and update user @EntityListeners(AuditingEntityListener.class) @Entity public class Sample { ```

同様に作成日や更新日は、DateTimeProviderを実装することで横断的に挿入することができる。

	@Bean(name = "dateTimeProvider")
	DateTimeProvider dateTimeProvider() {
		return new DateTimeProvider() {
			@Override
			public Calendar getNow() {
				return Calendar.getInstance(Locale.JAPAN).getTime();
			}
		};
	}

@EnableJpaAuditingで JPA を有効にする際に指定する。

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware", dateTimeProviderRef = "dateTimeProvider")
public class AppConfig {

@CreatedDateおよび@LastModifiedDateが指定されていれば、JPA によってdateTimeProviderの戻り値が設定される。
また、@CreatedByおよび@LastModifiedByが指定されていれば、auditorAwareの戻り値が設定される。

@MappedSuperclass
public abstract class AbstractEntity {

	@CreatedDate
	@Column(updatable = false)
	@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
	private DateTime createDate;

	@CreatedBy
	@Column(updatable = false)
	private String createUsername;

	@LastModifiedDate
	@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
	private DateTime updateDate;

	@LastModifiedBy
	private String updateUsername;

	@Version
	private Integer version;

Others

Exceptions

スローされるクラスとスローしてくる箇所の種別、その意味は以下の通り。

クラス 種別 意味
DataIntegrityViolationException Spring Dao 一意成約違反
EmptyResultDataAccessException Spring Dao レコードなし
IncorrectResultSizeDataAccessException Spring Dao 複数レコードあり
ObjectOptimisticLockingFailureException Spring ORM 排他制御エラー

Reference