S-JIS[2017-09-15/2017-10-22] 変更履歴

Spring Boot JPA サンプル

Spring Boot 1.5.6のJPAの例。


概要

JPAでは、テーブル相当のクラスとしてEntityクラスを用意する。
DBアクセスの為にRepositoryクラスを用意する。SQL文ひとつにつきメソッドひとつというイメージ。
それらを呼び出し、トランザクション管理する為にServiceクラスを作成する。

デフォルトでは、Entityインスタンスを保存(永続化)するだけで、自動的にテーブルが作成される。
(したがって、事前にDBにテーブルを作成しておく必要は無い)


Entity

まず、永続化(DBに保存)するデータのクラスを作る。

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

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

Repository

次に、DBアクセスするクラスを用意する。
1つのEntityに対し1つのRepositoryインターフェースを作る。

src/main/java/com/example/demo/db/repository/JpaExampleRepository.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.JpaExampleEntity;
@Repository
public interface JpaExampleRepository extends JpaRepository<JpaExampleEntity, Long> {

}

Repositoryアノテーションを付け、JpaRepositoryを継承する。
JpaRepositoryのジェネリクスには、対象となるEntityクラスと、そのプライマリキーの型を指定する。

JpaRepositoryには、データをINSERT/UPDATEするsaveメソッド(データが既存ならUPDATE、それ以外ならINSERT)、取得するfind系メソッド、削除するdelete系メソッドがある。


なお、Repositoryはインターフェースとして定義しているが、実行時にはSimpleJpaRepositoryというクラスが使われるようだ。


Repositoryへのメソッド追加

JpaRepositoryにはいくつかのメソッドが存在しているが、自分で追加することも出来る。[2017-10-01]

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

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のテスト

Repositoryのテストクラスを作ると、動作確認することが出来る。

参考: 41.3.8 Auto-configured Data JPA tests

src/test/java/com/example/demo/db/repository/JpaExampleRepositoryTest.java:

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アノテーションを付ける。


Service

Repositoryを使用するサービスの例。[2017-09-30]

src/main/java/com/example/demo/db/service/JpaExampleService.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.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のテスト

Serviceのテストクラスを作ると、動作確認することが出来る。[2017-09-30]

src/test/java/com/example/demo/db/service/JpaExampleService.java:

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可否等)も指定できる。

結合用のカラムの場合は@JoinColumnを使う


複合キー

複数のカラムを主キーとする場合は、@Idアノテーションを複数のカラムに付ける。[2017-09-30]
また、そのEntityクラスに対するRepositoryクラスを作成する場合は、複合キーを表すクラスを作る。

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

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アノテーションを付け、複合キーを表すクラスを指定する。


src/main/java/com/example/demo/db/repository/CompositeKeyExampleRepository.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.CompositeKeyExampleEntity;
@Repository
public interface CompositeKeyExampleRepository extends JpaRepository<CompositeKeyExampleEntity, CompositeKeyExampleEntity.Pk> {

}

列挙型

Entityクラスで列挙型のプロパティーを持つことが出来る。[2017-09-30]

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

package com.example.demo.db.domain;
public enum ExampleEnum {

	VALUE1, VALUE2, VALUE3;
}

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

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に保存されるか分かる。

src/main/resources/application.properties:

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に保存する値」に変換するクラスを作ることが出来る。

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

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アノテーションを付けて変換クラスを明示することも出来る。
(これなら、テスト時にも変換クラスが使われる)

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

import javax.persistence.Convert;
	@Convert(converter = ExampleEnumConverter.class)
	private ExampleEnum value;

AttributeConverterが使われた場合は、BasicBinderのログに値が出力される。

src/main/resources/application.properties:

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

JPQLで、列挙型のフィールドも扱うことが出来る。[2017-10-01]

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

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

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