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