Spring Boot 2 で、なるべく標準的なやり方で、トラディショナルな Spring MVC による Web Application を作成するチュートリアルを数回に分けて。
の2回目です。
目次
- Spring MVC で Hello World
- Spring Data JPA でデータベースアクセス
- 登録・更新処理と Bean Validataion
- Bootstrap と Thymeleaf でページネーション
- Spring Boot DevTools で Automatic Restart
- Spring Security でログイン認証
- ファイルアップロード
- T.B.D
今回は 「Spring Data JPA でデータベースアクセス」の回となります。
Spring Data JPA の導入
Hibernate に代表されるパーシステンスレイヤのオブジェクト指向的アプローチは複雑さゆえに否定派も少なくありませんが、より低レベルでシンプルなものがベストとも言えない状況もあります。
オブジェクトデータベースがもっと広まれば状況は変わると思いますが、リレーショナルデータベースを異なるパラダイムから使うのは、どうやっても簡単ではありません。
ここでは、標準化されている JPA を Spring Data で使っていきましょう。
build.gradle
は spring-boot-starter-data-jpa
と h2
の依存を追加して以下のように定義します。
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 では AbstractPersistable
や AbstractAuditable
といった 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(); } ・・・ }
今回は利用しませんが、AbstractAuditable
は AbstractPersistable
の子クラスで、作成者・作成日時・更新者・更新日時といったフィールドが定義されています。
さて 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>
をイテレートして表示しています。
Page
は Iterable
なのでそのままループでコンテンツを取得できます。
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
へアクセスすると以下が表示されます。
今回はここまでで、次回に続きます。