-
Notifications
You must be signed in to change notification settings - Fork 0
SpringDataJPA
Java による永続化に関する仕様である JPA (Java Persistence API) をサポートした O/R マッピングフレームワーク。
使用方法は、以下の通り。
バージョンは、org.springframework.boot 1.2.3
による実装時に使用されていたspring-data-jpa-1.7.2
を元に記載している。
@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
列挙の順序をマッピング
抽象化する必要があれば、@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 {
エンティティの関係が一対多または多対一の場合は、@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
被所有者側エンティティのカラムを指定
@EntityListeners
を付与することで、エンティティの処理に対するコールバックを実装できる。
@Configurable
public class SampleListener {
@PrePersist
public void doSomething() {
// Do something... before persist entity
}
}
@EntityListeners(SampleListener.class)
@Entity
public class Sample {
@Repository
を付与し、JpaRepository
をエンティティとID
をパラメータとして継承したインタフェースを作成する。
実装クラスは自動生成されるため作成しない。
基本的なエンティティへの CRUD などは、JPA が提供するリポジトリの末端に当たるインタフェースJpaRepository
が提供してくれる。
@Repository
public interface SampleRepository extends JpaRepository<Sample, Integer> {
}
リポジトリをインジェクションする。
@Service
@Transactional
public class SampleService {
@Autowired
private SampleRepository repository;
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);
}
登録または更新する際は、#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(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
の他に、read...By
、query...By
、count...By
、get...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);
@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 の妥当性が検証されるため、不正な文であればエラーが発生
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));
検索条件の仕様( 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
を引数に取るリポジトリの検索を呼び出す。
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
で指定可能
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 が指定可能。
共通で必要になることがある作成者や更新者は、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;
スローされるクラスとスローしてくる箇所の種別、その意味は以下の通り。
クラス | 種別 | 意味 |
---|---|---|
DataIntegrityViolationException |
Spring Dao | 一意成約違反 |
EmptyResultDataAccessException |
Spring Dao | レコードなし |
IncorrectResultSizeDataAccessException |
Spring Dao | 複数レコードあり |
ObjectOptimisticLockingFailureException |
Spring ORM | 排他制御エラー |