Spring Boot 1.5.6のJPAの例。
|
|
JPAでは、テーブル相当のクラスとしてEntityクラスを用意する。
DBアクセスの為にRepositoryクラスを用意する。SQL文ひとつにつきメソッドひとつというイメージ。
それらを呼び出し、トランザクション管理する為にServiceクラスを作成する。
デフォルトでは、Entityインスタンスを保存(永続化)するだけで、自動的にテーブルが作成される。
(したがって、事前にDBにテーブルを作成しておく必要は無い)
まず、永続化(DBに保存)するデータのクラスを作る。
package com.example.demo.db.domain; import javax.persistence.Entity; //import javax.persistence.GeneratedValue; import javax.persistence.Id;
@Entity
public class JpaExampleEntity {
@Id
// @GeneratedValue
private Long id;
private String value;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public void setValue(String s) {
this.value = s;
}
public String getValue() {
return value;
}
}
永続化するクラスには@Entityアノテーションを付ける。
このクラスのデータをRDBに保存するとき、テーブルはこのEntityを元に自動的に作られる。
→テーブル名・カラム名の指定
プライマリキーとなる項目を用意し、@Idアノテーションを付ける。
型はプリミティブ型でもラッパークラスでもよい。
→複合キー
@GeneratedValueアノテーションを付けていると、INSERT時に値が自動的に採番される。
値をnull(プリミティブ型の場合は0)にしてRepositoryのsaveメソッドを呼び出すとINSERTになる。
また、Persistableインターフェースを実装してisNewメソッドを記述すると、どういう場合に新規データ扱い(INSERT)するかをコーディングすることが出来るらしい。
参考: stackoverflowのhow spring data jpa repository.save only do update
エンティティークラスに@Entityアノテーションを付けていないと、Repositoryで操作しようとしたときに例外が発生する。
java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83) 〜 Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaExampleRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not a managed type: class com.example.demo.db.domain.JpaExampleEntity at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:742) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360) at org.springframework.boot.SpringApplication.run(SpringApplication.java:303) at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120) at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98) at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116) ... 26 more Caused by: java.lang.IllegalArgumentException: Not a managed type: class com.example.demo.db.domain.JpaExampleEntity at org.hibernate.jpa.internal.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:210) at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.(JpaMetamodelEntityInformation.java:70) at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getEntityInformation(JpaEntityInformationSupport.java:68) at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:153) at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:100) at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:82) at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:199) at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:277) at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:263) at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:101) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624) ... 41 more
次に、DBアクセスするクラスを用意する。
1つのEntityに対し1つのRepositoryインターフェースを作る。
package com.example.demo.db.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.example.demo.db.domain.JpaExampleEntity;
@Repository
public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long> {
}
Repositoryアノテーションを付け、JpaRepositoryを継承する。
JpaRepositoryのジェネリクスには、対象となるEntityクラスと、そのプライマリキーの型を指定する。
JpaRepositoryには、データをINSERT/UPDATEするsaveメソッド(データが既存ならUPDATE、それ以外ならINSERT)、取得するfind系メソッド、削除するdelete系メソッドがある。
なお、Repositoryはインターフェースとして定義しているが、実行時にはSimpleJpaRepositoryというクラスが使われるようだ。
JpaRepositoryにはいくつかのメソッドが存在しているが、自分で追加することも出来る。[2017-10-01]
import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository;
@Repository
public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long> {
public List<JpaExampleEntity> findByValue(String value);
}
メソッド名を「findBy」で始め、Entityクラス内のプロパティー名(=フィールド名)を付けると、そのプロパティー(カラム)を条件とする検索になる。
メソッドの引数はプロパティー(フィールド)の型と合わせる。
このメソッドでは、検索条件に一致するデータが無い場合は空のリストが返る。
他にもメソッド名のルールが色々定義されている。[2017-10-02]
→Spring Data JPA - Reference Documentationの4.3. Query methods
もっと複雑な検索条件を指定したい場合は、クエリー(JPQL)を指定する。
Repositoryのテストクラスを作ると、動作確認することが出来る。
参考: 41.3.8 Auto-configured Data JPA tests
package com.example.demo.db.repository; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.test.context.junit4.SpringRunner; import com.example.demo.db.domain.JpaExampleEntity;
@RunWith(SpringRunner.class)
@DataJpaTest
public class JpaExampleRepositoryTest {
@Autowired
private JpaExampleRepository repository;
@Test
public void test() {
JpaExampleEntity entity1 = new JpaExampleEntity();
entity1.setId(1L);
repository.save(entity1);
JpaExampleEntity entity2 = new JpaExampleEntity();
entity2.setId(2L);
repository.save(entity2);
List<JpaExampleEntity> list = repository.findAll();
assertThat(list.size(), is(2));
}
}
テストクラスには@RunWithアノテーションでSpringRunnerクラスを指定する。
また、@DataJpaTestアノテーションを付ける。
Repositoryを使用するサービスの例。[2017-09-30]
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.JpaExampleEntity; import com.example.demo.db.repository.JpaExampleRepository;
@Service
public class JpaExampleService {
@Autowired
private JpaExampleRepository repository;
@Transactional
public void save(JpaExampleEntity entity) {
repository.save(entity);
// repository.saveAndFlush(entity);
// throw new RuntimeException("rollback test");
}
@Transactional(readOnly = true)
public JpaExampleEntity load(long id) {
return repository.findOne(id);
}
// 外部からこのメソッドを呼び出した場合、saveで例外が発生してもロールバックされない
public void save2(JpaExampleEntity entity) {
save(entity);
}
}
Repositoryのメソッドを呼び出すサービスでは、@Transactionalアノテーションを付けてトランザクション管理する。
トランザクション管理されたメソッドでは、非チェック例外(RuntimeException系)がスローされるとロールバックされる。
チェック例外だとロールバックされないので注意。
(@TransactionalアノテーションのrollbackForで例外クラスを指定すると、その例外でロールバックされるようになる)
また、同クラス内で「@Transactionalアノテーションが付いていないメソッド」から「@Transactionalアノテーションが付いているメソッド
」を呼び出した場合は、トランザクション管理されない。
(Serviceクラスの外側からメソッドを呼び出す場合はProxyクラス経由になるのでトランザクション管理される(@Transactionalアノテーションが付いているかどうかチェックされる)が、
内部から呼ぶ場合は(普通どおり)直接メソッドが呼ばれることになるので、トランザクション管理されない)
参考: NagaokaKenichiさんのSpringでのトランザクション管理
Serviceのテストクラスを作ると、動作確認することが出来る。[2017-09-30]
package com.example.demo.db.service; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import com.example.demo.db.domain.JpaExampleEntity;
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaExampleServiceTest {
@Autowired
private JpaExampleService service;
@Test
public void test() {
JpaExampleEntity entity = new JpaExampleEntity();
entity.setId(2L);
entity.setValue("abc");
try {
service.save(entity);
} catch (Exception e) {
System.out.println(e.toString());
}
JpaExampleEntity result = service.load(2); // save時にロールバックされているとnullが返る
assertEquals("abc", result.getValue());
}
}
Entityクラスにテーブル名・カラム名を付けることが出来る。[2017-09-30]
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table;
@Entity
@Table(name = "JPA_EXAMPLE")
public class JpaExampleEntity {
@Id
@GeneratedValue
@Column(name = "EXAMPLE_ID")
private Long id;
@Column(name = "VALUE", nullable = true)
private String value;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
〜
}
@Tableアノテーションをクラスに付けてテーブル名を指定する。
@Columnアノテーションをフィールドに付けてカラム名を指定する。
カラムの属性(データ長やnull可否等)も指定できる。
複数のカラムを主キーとする場合は、@Idアノテーションを複数のカラムに付ける。[2017-09-30]
また、そのEntityクラスに対するRepositoryクラスを作成する場合は、複合キーを表すクラスを作る。
package com.example.demo.db.domain; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.IdClass;
@Entity
@IdClass(CompositeKeyExampleEntity.Pk.class)
public class CompositeKeyExampleEntity {
@Id
private long key1;
@Id
private int key2;
private String value;
〜setter/getter〜
public static class Pk implements Serializable {
private static final long serialVersionUID = 1L;
private long key1;
private int key2;
〜setter/getter〜
}
}
複合キーを表すクラスはどこに作ってもいいが、staticな内部クラスにしておくのが分かりやすそう。
このクラスはSerializableにする。
また、Entityクラスに@IdClassアノテーションを付け、複合キーを表すクラスを指定する。
package com.example.demo.db.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.example.demo.db.domain.CompositeKeyExampleEntity;
@Repository
public interface CompositeKeyExampleRepository extends JpaRepository<CompositeKeyExampleEntity, CompositeKeyExampleEntity.Pk> {
}
Entityクラスで列挙型のプロパティーを持つことが出来る。[2017-09-30]
package com.example.demo.db.domain;
public enum ExampleEnum {
VALUE1, VALUE2, VALUE3;
}
package com.example.demo.db.domain; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id;
@Entity
public class EnumExampleEntity {
@Id
@GeneratedValue
private Long id;
private ExampleEnum value;
〜setter/getter〜
}
DBに保存される値は、デフォルトではordinal()の値になる。
@Enumeratedアノテーションを付けると、DBに保存される値を文字列(name())にすることが出来る。
import javax.persistence.EnumType; import javax.persistence.Enumerated;
@Enumerated(EnumType.STRING) private ExampleEnum value;
application.propertiesに列挙型変換クラスのトレースログ出力を設定すると、どういう値でDBに保存されるか分かる。
logging.level.org.hibernate.SQL=debug logging.level.org.hibernate.type.EnumType=trace
↓ログ出力例
Hibernate: insert into enum_example_entity (id, value) values (null, ?) 2017-09-30 17:16:54.748 TRACE 540 --- [ main] org.hibernate.type.EnumType : Binding [VALUE1] to parameter: [1]
列挙型を「DBに保存する値」に変換するクラスを作ることが出来る。
package com.example.demo.db.domain; import javax.persistence.AttributeConverter; import javax.persistence.Converter;
@Converter
public class ExampleEnumConverter implements AttributeConverter<ExampleEnum, Integer> {
@Override
public Integer convertToDatabaseColumn(ExampleEnum attribute) {
switch (attribute) {
case VALUE1:
return 1;
case VALUE2:
return 2;
case VALUE3:
return 3;
default:
throw new IllegalArgumentException(attribute.toString());
}
}
@Override
public ExampleEnum convertToEntityAttribute(Integer dbData) {
return ExampleEnum.valueOf("VALUE" + dbData);
}
}
AttributeConverterインターフェースの2つ目の型引数にDBに保存する型を指定する。
@Converterアノテーションを付けておくと、自動的にこのクラスが使われる。
が、テストのときはどうも読み込まれないようだorz
Entityクラスのフィールドに@Convertアノテーションを付けて変換クラスを明示することも出来る。
(これなら、テスト時にも変換クラスが使われる)
import javax.persistence.Convert;
@Convert(converter = ExampleEnumConverter.class) private ExampleEnum value;
AttributeConverterが使われた場合は、BasicBinderのログに値が出力される。
logging.level.org.hibernate.SQL=debug #logging.level.org.hibernate.type.descriptor.sql=trace logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
↓ログ出力例
Hibernate: insert into enum_example_entity (id, value) values (null, ?) 2017-09-30 17:38:48.962 TRACE 6908 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [1]
JPQLで、列挙型のフィールドも扱うことが出来る。[2017-10-01]
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; import com.example.demo.db.domain.EnumExampleEntity; import com.example.demo.db.domain.ExampleEnum;
@Repository
public interface EnumExampleRepository extends JpaRepository<EnumExampleEntity, Long> {
@Query("select t from EnumExampleEntity t where t.value = :value")
public List<EnumExampleEntity> findByValue(@Param("value") ExampleEnum value);
メソッドの引数に、普通に列挙型を指定できる。
@Query("select t from EnumExampleEntity t where t.value = com.example.demo.db.domain.ExampleEnum.VALUE2")
public List<EnumExampleEntity> findValue2();
JPQL内に列挙型の定数(列挙子)を指定したい場合、FQCNで指定すれば記述できる。
ただ、FQCNだと長いし、リファクタリングでパッケージ名やクラス名が変わったとき追随してくれない。
Java8ならインターフェースにdefaultメソッドで実装が書けるので、これを利用するといいかも。
public default List<EnumExampleEntity> findValue3() {
return findByValue(ExampleEnum.VALUE3);
}
}