記事目次
- 【Spring 4.0対応】Spring Boot と Spring MVC と Spring Data JPA を使って Web API を作成する (1)
- 【Spring 4.0対応】Spring Boot と Spring MVC と Spring Data JPA を使って Web API を作成する (2)
- 【Spring 4.0対応】Spring Boot と Spring MVC と Spring Data JPA を使って Web API を作成する (3)
今回使用するソース
https://github.com/nnasaki/spring-rest/tree/3
2回目の続きです。何か良い題材が無いか探してたら、spring-projects/spring-data-book がちょうど良さそうなので、これを写経しながら説明していきます。
クラス図はこんな感じなようです。

spring-data-book/doc/DomainModel.pdf at master · spring-projects/spring-data-book
今回作成するDBはこんな感じ。Customerが複数のAddressを持てるようです。Order とかはまだ作成しません。

ソースはGitHubに置きました。ダウンロードして解凍してください。
ソース解説
今回は一気にやることが増えています。大まかには次の通りです。
- CustomerからAddressへの一対多を@OneToManyで表現する。
- リポジトリのテストを作成する
- テストデータを作成する
これらを順番に説明していきます。最終的にはこんな感じの構成になります。

CustomerからAddressへの一対多を@OneToManyで表現する
説明簡略化のため、Getter/Setter は付けずにpublicで設定しています。ソースの一部を抜粋して説明しています。
AbstractEntity
@MappedSuperclass
public class AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long id;
//equals and hashCode
Idは Customer と Address クラスで共通項となるので、スーパークラスに追い出します。JPAの作法でスーパークラスには @MappedSuperclass アノテーションを付けるようです。
Address
@SuppressWarnings("UnusedDeclaration")
@Entity
public class Address extends AbstractEntity {
public String street, city, country;
先ほどの MappedSuperclass を継承して id 列を作ります。
Customer
@Entity
public class Customer extends AbstractEntity{
public String firstname, lastname;
@NotNull
@Size(max = 64)
public String password;
@Column(unique = true)
public EmailAddress emailAddress;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "customer_id")
public Set<Address> addresses = new HashSet<>();
前回の password 以外にカラムを追加します。@OneToMany アノテーションで一対多を表現します。CascadeType.ALL や orphanRemoval が何故必要かはよくわかりません。おまじないみたいなもんだとここでは思っておきます。
CustomerRepository
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long>{
Customer findByEmailAddress(EmailAddress email);
List<Customer> findByAddresses_City(String city);
}
findByEmailAddress()のように findBy〜 に検索したいカラムの名前を入れます。findByAddresses_City()のように関連性のあるテーブルの列を検索して結果を取得したいときはfindBy[他エンティティ名]_[カラム名]で作成します。アンダースコア(_)は省略可能です。複数の結果を返すのでList<>にしています。
この命名規則から外れるとSpring起動時にエラーが出て立ち上がらなくなるので注意してください。
EmailAddress
@Embeddable
public class EmailAddress {
private static final String EMAIL_REGEX = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
private static final Pattern PATTERN = Pattern.compile(EMAIL_REGEX);
@Column(name = "email")
private String value;
@Embeddable で継承ではない他クラスの埋め込み型のカラムを作れます。今回の例では一つしかないですが、複数のカラムを定義することも可能です。
@Column(name = "email") でフィールド名と別のカラム名を指定することが可能です。既存のテーブルに対して適用するときに便利です。@Entity(name = "hogehoge" も同様の効果があります。
リポジトリのテストを作成する
リポジトリの設定はこんな感じです。今回モックは使いませんが、モック化することも可能です。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class CustomerRepositoryIntegrationTest {
@Qualifier("customerRepository")
@Autowired
CustomerRepository repository;
1件保存のテストはこんな感じです。
@Test
public void savesCustomerCorrectly() {
EmailAddress email = new EmailAddress("alicia@keys.com");
Customer alicia = new Customer("Alicia", "Keys");
alicia.password = "password_test";
alicia.emailAddress = email;
alicia.add(new Address("27 Broadway", "San Francisco", "United States"));
Customer result = repository.save(alicia);
assertThat(result.id, is(notNullValue()));
}
Emailでの検索のテストです。
@Test
public void readsCustomerByEmail() {
EmailAddress email = new EmailAddress("dave@dmband.com");
Customer result = repository.findByEmailAddress(email);
assertThat(result.firstname, is("Dave"));
assertThat(result.lastname, is("Matthews"));
}
別テーブルのCityで検索します。テストデータは2行帰ってくることを期待しています。
@Test
public void findByCity() {
List<Customer> customers = repository.findByAddresses_City("New York");
assertThat(customers, hasSize(2));
assertThat(customers.get(0).firstname, is("Dave"));
}
テストデータを作成する
Spring Boot なのか Hibernate の仕様なのかよくわかってませんが、クラスパスが通っているところに import.sql を置くと、勝手に取り込んでくれるようです。今回は次のSQLを置きました。
insert into Customer (id, password, email, firstname, lastname) values (1, 'password_test', 'dave@dmband.com', 'Dave', 'Matthews');
insert into Customer (id, password, email, firstname, lastname) values (2, 'password_test', 'carter@dmband.com', 'Carter', 'Beauford');
insert into Customer (id, password, email, firstname, lastname) values (3, 'password_test', 'boyd@dmband.com', 'Boyd', 'Tinsley');
insert into Address (id, street, city, country, customer_id) values (1, '27 Broadway', 'New York', 'United States', 1);
insert into Address (id, street, city, country, customer_id) values (2, '27 Broadway', 'New York', 'United States', 1);
テストを実行する
./gradlew test で実行します。問題なければ次のような表示になります。
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
BUILD SUCCESSFUL
Total time: 5.207 secs
次回
実際に動かした場合、 http://localhost:8080/customer へのポストは { "password": "test_password" } だけ指定しても動きます。
ただ、今回追加した項目は null になってしまうのと、http://localhost:8080/customer/1 しても
{
id: 1
password: "test_password"
}
という素っ気ない返しなので、ここら辺を充実させていき、またコントローラー周りのテストを追加していきたいと思います。
情報源
- Bootstrap an application with Spring Boot – Part2 Web application | Mathias Hauser
- Spring Boot / Spring Data import.sql doesn’t run Spring-Boot-1.0.0.RC1 - Stack Overflow
- spring-data-book/doc/DomainModel.pdf at master · spring-projects/spring-data-book
- spring-data-book/jpa/src/main/java/com/oreilly/springdata/jpa/core at master · spring-projects/spring-data-book
- java - Hibernate hbm2ddl.auto possible values and what they do? - Stack Overflow
- 4.10. 遷移の永続化
COMMENT: AUTHOR: daaaaaai DATE: 12/12/2015 17:41:20 たいへん細かい点で恐縮ですが、 Spring Data JPA(というかHibernate)でOrderクラスを使おうとすると、たいていのDBでは予約語のorderとぶつかってエラーとなるので要注意ですね。MySQLの場合はSyntax Errorが出ます。
COMMENT:
AUTHOR: nnasaki
DATE: 12/12/2015 21:43:58
おお。ご指摘ありがとうございます!写経元ではテーブル名を @Table(name = "Orders") とすることで回避しているようでした。