普通の Spring Boot 2.0 Web Applicatrion 〜 Spring Data JPA でデータベースアクセス 〜

Spring Boot 2 で、なるべく標準的なやり方で、トラディショナルな Spring MVC による Web Application を作成するチュートリアルを数回に分けて。

の2回目です。

目次

今回は 「Spring Data JPA でデータベースアクセス」の回となります。


Spring Data JPA の導入

Hibernate に代表されるパーシステンスレイヤのオブジェクト指向的アプローチは複雑さゆえに否定派も少なくありませんが、より低レベルでシンプルなものがベストとも言えない状況もあります。

オブジェクトデータベースがもっと広まれば状況は変わると思いますが、リレーショナルデータベースを異なるパラダイムから使うのは、どうやっても簡単ではありません。


ここでは、標準化されている JPA を Spring Data で使っていきましょう。

build.gradlespring-boot-starter-data-jpah2 の依存を追加して以下のように定義します。

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.0.1.RELEASE'
}

sourceCompatibility = targetCompatibility = 1.8

dependencies {
    implementation 'org.springframework.boot:spring-boot-dependencies:2.0.1.RELEASE'
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.boot:spring-boot-starter-thymeleaf'

    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'com.h2database:h2'
}

repositories {
    jcenter()
}

第一回でBOM インポートしているのでバージョン番号は不要です。


エンティティの作成

簡単な Member エンティティを作ります。


Spring Data では AbstractPersistableAbstractAuditable といった Entity の親クラスとして利用できる抽象クラスが予め用意されているので、ここでは AbstractPersistable を使うことにします。

AbstractPersistable は以下のような定義になっています。

@MappedSuperclass
public abstract class AbstractPersistable<PK extends Serializable> implements Persistable<PK> {

    @Id @GeneratedValue private @Nullable PK id;

    @Nullable
    public PK getId() { return id; }

    protected void setId(@Nullable PK id) { this.id = id; }

    @Transient
    public boolean isNew() { return null == getId(); }

    ・・・
}


今回は利用しませんが、AbstractAuditableAbstractPersistable の子クラスで、作成者・作成日時・更新者・更新日時といったフィールドが定義されています。


さて AbstractPersistable を継承し、src/main/java/stdweb/domain/Member.java を以下のように作成します。

package stdweb.domain;

import org.springframework.data.jpa.domain.AbstractPersistable;

import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Entity
public class Member extends AbstractPersistable<Long> {

    @NotNull
    @Size(min = 1, max = 25)
    private String name;

    @NotNull
    @Pattern(regexp = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$", message = "not email")
    private String email;

    protected Member() { }

    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }
}


AbstractPersistable では id 定義がされているので、個々のEntity での id 定義は不要になります。


JPA の Entity は、今では getter/setter の定義は不要です。 引数無しのコンストラクタさえ定義しておけば良いです。

モデルに対してどこからでも好きなように setter で値の更新が行えるのは望ましくないため、ここでは getter のみを定義し、setter は定義しません(画面から直接エンティティオブジェクトに値を入れたい場合に困るのですが、その話は後ほど)。


Repository の作成

Spring Data では予め以下のリポジトリクラスが用意されています。

Repository
  └ CrudRepository
     └ PagingAndSortingRepository
        └ JpaRepository


  • Repository :リポジトリのマーカーインタフェース
  • CrudRepository :一般的な CRUD 操作を定義するインターフェース
  • PagingAndSortingRepository : ページングとソーティングを行う操作を定義するインターフェース
  • JpaRepository :flush() といったJPAに特有の操作を定義するインターフェース


ここでは JpaRepository を使っていきます。

以下のような src/main/java/stdweb/domain/MemberRepository.java を作成します。

package stdweb.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByName(String name);
}


インターフェースだけ定義しておけば Spring Data が実行時に実装を自動生成してくれるので、実装クラスを用意する必要はありません。

メソッド名を Spring Data の定めるルールに従ったものにしておけば検索クエリもよろしくやってくれます(findByName のように)。

もちろん自分で定義すれば定義したものが使われます(QueryLookupStrategy.Key.USE_DECLARED_QUERY がデフォルトのため)。


サービス

サービスを作っておきます。(単純な Query 処理は直接リポジトリたたいてしまっても良いという考え方もありますが、ここでは全てサービスを経由するものとして進めます)。

以下のように src/main/java/stdweb/app/MemberService.java を作成します。

package stdweb.app;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import stdweb.domain.Member;
import stdweb.domain.MemberRepository;

@Service
public class MemberService {

    private MemberRepository repository;

    public MemberService(MemberRepository repository) {
        this.repository = repository;
    }

    public Page<Member> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }
}

サービスには先程定義したリポジトリをインジェクションします。

インジェクション方法は、フィールドインジェクション、セッターインジェクションもありますが、ここではSpringチームも推奨するコンストラクタインジェクションを使います。

Spring 4.3 からは単一のコンストラクタの場合 @Autowired も不要です。


サービスからは、先程作成した Repository の memberRepository.findAll() の結果を返すだけです。

戻り値は Page インターフェースで、ページング用に Spring Data が提供する検索結果のホルダになります。


コントローラの作成

同様にサービスメソッドの呼び出し結果を返すのみのコントローラを作成します。

src/main/java/stdweb/web/MemberController.java を作成します。

package stdweb.web;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import stdweb.app.MemberService;
import stdweb.domain.Member;


@Controller
public class MemberController {

    private final MemberService service;

    public MemberController(MemberService service) {
        this.service = service;
    }

    @GetMapping("/members")
    public String members(Model model) {
        Page<Member> results = service.findAll(PageRequest.of(0, 10));
        model.addAttribute("members", results);
        return "members/membersList";
    }
}


検索結果を members という属性名でモデルに格納して一覧画面の ID を返却します。

なお、こちらでもコンストラクタインジェクションでサービスをインジェクトしています。


一覧View

src/main/resources/templates/members/membersList.html を以下のように作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Members</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
          integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
          crossorigin="anonymous">
</head>

<body>
<div class="container">
    <h2>Members</h2>
    <table class="table table-striped table-sm">
        <thead>
        <tr>
            <th>ID</th>
            <th>name</th>
            <th>e-mail</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="member : ${members}">
            <td th:text="${member.id}"/>
            <td th:text="${member.name}"/>
            <td th:text="${member.email}"/>
        </tr>
        </tbody>
    </table>
</div>
</body>
</html>

Bootstrap の スタイルシートを CDN から取得するようにしました。あとは普通にテーブル定義しているだけです。


テーブルでは、th:each でモデルに入れた Page<Member> をイテレートして表示しています。 PageIterable なのでそのままループでコンテンツを取得できます。

Thymeleaf では th:each="member : ${members}" で当該タグを繰り返し生成できます。


初期データの投入

@SpringBootApplication@Configuration なのでクラス内で @Bean を付けて以下のように起動時に一度だけ実行する初期化ロジックを定義することができます。

@SpringBootApplication
public class Main {

    private static final Logger log = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args){
        SpringApplication.run(Main.class, args);
    }

    @Bean
    public CommandLineRunner demo(MemberRepository repository) {
        return (args) -> {
            repository.save(new Member("jack", "jack@example.com"));
            repository.save(new Member("david", "david@example.com"));

            for (Member member : repository.findAll()) {
                log.info(member.toString());
            }
        };
    }
}


実行結果

では早速実行してみましょう。

$ ./gradlew bootRun


起動したら http://localhost:8080/members へアクセスすると以下が表示されます。

f:id:Naotsugu:20180516232409p:plain



今回はここまでで、次回に続きます。