記事目次
- 【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. 遷移の永続化