世界のやまさ

SEKAI NO YAMASA

【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 がちょうど良さそうなので、これを写経しながら説明していきます。

クラス図はこんな感じなようです。

kobito.1404055789.402848.png

spring-data-book/doc/DomainModel.pdf at master · spring-projects/spring-data-book

今回作成するDBはこんな感じ。Customerが複数のAddressを持てるようです。Order とかはまだ作成しません。

kobito.1404132314.743252.png

ソースはGitHubに置きました。ダウンロードして解凍してください。

ソース解説

今回は一気にやることが増えています。大まかには次の通りです。

  • CustomerからAddressへの一対多を@OneToManyで表現する。
  • リポジトリのテストを作成する
  • テストデータを作成する

これらを順番に説明していきます。最終的にはこんな感じの構成になります。

kobito.1404134700.033991.png

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

2つのメソッドが追加されます。命名規則があります。

  • 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"
}

という素っ気ない返しなので、ここら辺を充実させていき、またコントローラー周りのテストを追加していきたいと思います。

情報源