Kotlin
spring
Thymeleaf
spring-boot

Kotlin + SpringBoot + JPA + Thymeleafで簡単なCRUDを作る②~画面と機能作成まで~


内容



  • 前回の続き


  • こちらの記事のKotlin版です


    • Kotlin初心者なのでもっと良い書き方あれば是非コメントで教えて下さい



  • SpringBootで簡単なCRUDアプリを作る

  • 今回は以下の4画面を一通り作成して処理を通すところまで


    • 一覧画面

    • 新規作成画面

    • 編集画面

    • 参照画面




開発する機能


  1. 選手の情報を保存するplayerテーブルを作る

  2. JPAを使ってplayerテーブルの操作をできるようにする

  3. playerに関するエンドポイントの作成

  4. 各画面のテンプレート作成


1. 選手の情報を保存するplayerテーブルを作る


  • SpringBootとJPAを使っていればEntityクラスを作ると自動的にテーブルを生成してくれるのでSQLをたたく必要はない

  • playerの情報を保存するPlayer.ktsrc/main/kotlin/com/example/baseball/domainに作成する


src/main/kotlin/com/example/baseball/domain/Player.kt

package com.example.baseball.domain

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity // ①
data class Player(
@Id // ②
@GeneratedValue // ③
val id: Long? = null, // ④
val name: String? = null, // ④
val age: Int? = null, // ④
val team: String? = null, // ④
val position: String? = null // ④
)



  • ①:@EntityをつけることでDBのテーブルと紐づく

  • ②:@Idを付けた変数がテーブルのプライマーキーになる

  • ③:@GeneratedValueをつけると連番が自動で振られるようになる

  • ④:それぞれnullが入りうるので型に?をつけておく


2. JPAを使ってplayerテーブルの操作をできるようにする


  • テーブルへアクセスするための基本的な処理はJPAがが用意してくれているのでSQLを書かなくてよい

  • JPAのJpaRepositoryを継承したinterfaceを作成することで利用できる

  • findAllやsave等用意されている典型的な操作以外のことをしたくなったら、repositoryに追記していくことになる

  •  src/main/kotlin/com/example/baseball/repositoryPlayerRepository.kt作成する


src/main/kotlin/com/example/baseball/repository/PlayerRepository.kt

package com.example.baseball.repository

import com.example.baseball.domain.Player
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface PlayerRepository : JpaRepository<Player, Long> {

}



  • repositoryの処理を呼び出すserviceを作成する

  • serviceにはビジネスロジックを書いて、repositoryはDBアクセスのみという棲み分け

  • DBにアクセスするような処理を行いたい時はserviceのメソッドを呼ぶようにする

  • 現段階ではビジネスロジックはないのでrepositoryのメソッドを呼ぶ処理だけ書いておく


  • src/main/kotlin/com/example/baseball/servicePlayerService.kt作成する


src/main/kotlin/com/example/baseball/service/PlayerService.kt

package com.example.baseball.service

import com.example.baseball.repository.PlayerRepository
import org.springframework.stereotype.Service
import com.example.baseball.domain.Player

@Service
// ①
class PlayerService(private val playerRepository: PlayerRepository) {
fun findAll() = playerRepository.findAll()

fun findOne(id: Long) = playerRepository.findById(id).orElse(null)

fun save(player: Player) = playerRepository.save(player)

fun delete(id: Long) = playerRepository.deleteById(id)
}



  • ①:コンストラクタにrepositoryを宣言しておくことでBeanをインジェクトしてくれる


3. playerに関するエンドポイントの作成


  • ユーザがアクセスするURLに紐づくメソッドを作成する

  • 作成するエンドポイントは以下の6種類

HTTPメソッド
URL
Controllerのメソッド
概要

GET
/players
index()
一覧画面の表示

POST
/players
create()
データの保存

GET
/players/new
newPlayer()
新規作成画面の表示

GET
/players/1/edit
edit()
編集画面の表示

GET
/players/1
show()
参照画面の表示

PUT
/players/1
update()
データの更新

DELETE
/players/1
destroy()
データの削除



  • src/main/kotlin/com/example/baseball/controllerPlayerController.kt作成する


src/main/kotlin/com/example/baseball/controller/PlayerController.kt

package com.example.baseball.controller

import com.example.baseball.domain.Player
import com.example.baseball.service.PlayerService
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.*

@Controller
@RequestMapping("/players") // ①
class PlayerController(private val playerService: PlayerService) {
@GetMapping
// ②
fun index(model: Model): String {
// ③
model.addAttribute("players", playerService.findAll())
// ④
return "players/index"
}

@GetMapping("new")
fun newPlayer(): String {
return "players/new"
}

@GetMapping("{id}/edit")
// ⑤
fun edit(@PathVariable id: Long, model: Model): String {
model.addAttribute("player", playerService.findOne(id));
return "players/edit"
}

@GetMapping("{id}")
fun show(@PathVariable id: Long, model: Model): String {
model.addAttribute("player", playerService.findOne(id));
return "players/show"
}

@PostMapping
// ⑥
fun create(@ModelAttribute player: Player): String {
playerService.save(player)
// ⑦
return "redirect:/players"
}

@PutMapping("{id}")
fun update(@PathVariable id: Long, @ModelAttribute player: Player): String {
playerService.save(player.copy(id = id))
return "redirect:/players"
}

@DeleteMapping("{id}")
fun destroy(@PathVariable id: Long): String {
playerService.delete(id)
return "redirect:/players"
}
}



  • ①:クラスに対して@RequestMappingを付けておくと、クラス内のメソッド全てに適用される


    • つまりこのクラスのメソッドは全て、http://localhost:8080/playersから始まるURLにマッピングされている



  • ②:メソッドの引数にModel型の値を設定するとModelのインスタンスが自動的に渡される

  • ③:②で受け取ったmodelに値を詰めることで、テンプレートに値を渡すことができる


    • ここではplayersというキー名でplayerのListを設定している



  • ④:returnしている文字列を元に、src/main/resources/templates/配下からファイルを見つけてユーザに返している


    • ここではsrc/main/resources/templates/players/index.htmlが返されている



  • ⑤:メソッドの引数に@PathVariableを設定するとURL上の値を取得することができる


    • ここでは、http://localhost/players/1にアクセスされるとidには1が入る



  • ⑥:メソッドの引数に@ModelAttributeをつけると送信されたリクエストのbodyの情報を取得できる

  • ⑦:"redirect:/players"とすると/playersにリダイレクトされる


    • createメソッドの処理が終わった後にhttp://localhost:8080/playersに勝手にアクセスされる感じ




4. 各画面のテンプレート作成


  • 今回作成するのはplayerに関するテンプレートなのでsrc/main/resources/templates/players以下に作成していく

  • 作成するのは以下の4画面分のファイル


    • index.html(一覧画面)

    • new.html(新規作成画面)

    • edit.html(編集画面)

    • show.html(参照画面)




静的ファイルの配置


  • テンプレートを作る前にcssとjavascriptのファイルを準備しておく

  • 今回はBootstrapを使用する


    • 最新版は4系だけど今回は3系を使う




  • ここからダウンロードして、以下のファイルを配置する


    • cssフォルダ内のbootstrap.cssを、src/main/resources/static/cssフォルダにコピー

    • jsフォルダ内のbootstrap.jssrc/main/resources/static/jsフォルダにコピー



  • Bootstrapが依存するjQueryをダウンロードして配置する

  • jQueryはここからダウンロード


    • ソースが直接出てしまう場合は右クリックで名前をつけて保存する


    • src/main/resources/static/jsフォルダにjquery.jsという名前で配置



スクリーンショット 2017-10-25 11.08.37.png


index.html


  • playerの一覧をテーブルで表示する


  • src/main/resources/templates/playersindex.htmlを作成する

index.png


src/main/resources/templates/players/index.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Listing Players - baseball</title>
<!-- ① -->
<link rel="stylesheet" href="/css/bootstrap.css" />
<script src="/js/jquery.js"></script>
<script src="/js/bootstrap.js"></script>
</head>
<body>
<div class="container">
<h1>Listing Players</h1>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>名前</th>
<th>年齢</th>
<th>チーム名</th>
<th>守備位置</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<!-- ② -->
<tr th:each="player:${players}" th:object="${player}">
<!-- ③ -->
<td th:text="*{id}"></td>
<td th:text="*{name}"></td>
<td th:text="*{age}"></td>
<td th:text="*{team}"></td>
<td th:text="*{position}"></td>
<!-- ④ -->
<td><a class="btn btn-default btn-xs" th:href="@{/players/{id}(id=*{id})}">参照</a></td>
<td><a class="btn btn-default btn-xs" th:href="@{/players/{id}/edit(id=*{id})}">編集</a></td>
<td>
<!-- ⑤ -->
<form th:action="@{/players/{id}(id=*{id})}" th:method="delete">
<input class="btn btn-default btn-xs" type="submit" value="削除" />
</form>
</td>
</tr>
</tbody>
</table>
<a class="btn btn-default" href="/players/new">新規作成</a>
</div>
</body>
</html>


  • ①:cssとjavascriptのファイルを読み込む

  • ②:Controllerから渡されたplayersに対してループ処理を行っている



    • th:each="player:${players}"のようにすることでループ処理を記述できる

    • ここの例ではListであるplayersの数だけループを回し、List内の各要素をplayerという変数名で扱う



  • ③:th:object="${player}"とすることで、このタグより子供の要素で、${player.xxx}*{xxx}と省略して書けるようになる

  • ④:th:href="xxx"とすることでhref属性に設定する値を動的にすることができる



    • /players/{id}の部分がhref属性に設定される値。{id}の部分は変数を埋め込むことができて、後続の(id=*{id})で値を設定している



  • ⑤:削除の処理はHTTPメソッドをDELETEにしてアクセスしたいので、formタグにしてth:method="delete"を付けている


new.html


  • playerの新規作成画面


  • src/main/resources/templates/playersnew.htmlを作成する

new.png


src/main/resources/templates/players/new.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>New Player - baseball</title>
<link rel="stylesheet" href="/css/bootstrap.css" />
<script src="/js/jquery.js"></script>
<script src="/js/bootstrap.js"></script>
</head>
<body>
<div class="container">
<h1>New Player</h1>
<form th:action="@{/players}" th:method="post">
<div class="form-group">
<label class="control-label">名前</label>
<input class="form-control" type="text" name="name" />
</div>
<div class="form-group">
<label class="control-label">年齢</label>
<input class="form-control" type="number" name="age" />
</div>
<div class="form-group">
<label class="control-label">チーム名</label>
<input class="form-control" type="text" name="team" />
</div>
<div class="form-group">
<label class="control-label">守備位置</label>
<input class="form-control" type="text" name="position" />
</div>
<button class="btn btn-default" type="submit">作成</button>
</form>
<div class="pull-right">
<a class="btn btn-link" href="/players">一覧画面へ</a>
</div>
</div>
</body>
</html>


edit.html


  • URLに含まれる値とidが一致するplayerの編集をする画面


  • src/main/resources/templates/playersedit.htmlを作成する

edit.png


src/main/resources/templates/players/edit.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Editing Player - baseball</title>
<link rel="stylesheet" href="/css/bootstrap.css" />
<script src="/js/jquery.js"></script>
<script src="/js/bootstrap.js"></script>
</head>
<body>
<div class="container">
<h1>Editing Player</h1>
<form th:action="@{/players/{id}(id=*{id})}" th:method="put" th:object="${player}">
<div class="form-group">
<label class="control-label">名前</label>
<!-- ① -->
<input class="form-control" type="text" th:field="*{name}" />
</div>
<div class="form-group">
<label class="control-label">年齢</label>
<input class="form-control" type="number" th:field="*{age}" />
</div>
<div class="form-group">
<label class="control-label">チーム名</label>
<input class="form-control" type="text" th:field="*{team}" />
</div>
<div class="form-group">
<label class="control-label">守備位置</label>
<input class="form-control" type="text" th:field="*{position}" />
</div>
<button class="btn btn-default" type="submit">更新</button>
</form>
<div class="pull-right">
<a class="btn btn-link" href="/players">一覧画面へ</a>
</div>
</div>
</body>
</html>


  • ①:th:fieldに設定した値は、name属性id属性に変数名、value属性に値が設定される



    • th:field="*{age}"として*{age}の値が20の場合、name="age" id="age" value="20"が自動的に設定されている




show.html


  • URLに含まれる値とidが一致するplayerの情報を参照する画面


  • src/main/resources/templates/playersshow.htmlを作成する

show.png


src/main/resources/templates/players/show.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Show Player - baseball</title>
<link rel="stylesheet" href="/css/bootstrap.css" />
<script src="/js/jquery.js"></script>
<script src="/js/bootstrap.js"></script>
</head>
<body>
<div class="container">
<h1>Show Player</h1>
<div th:object="${player}">
<div>
<label>ID</label>
<p th:text="*{id}"></p>
</div>
<div>
<label>名前</label>
<p th:text="*{name}"></p>
</div>
<div>
<label>年齢</label>
<p th:text="*{age}"></p>
</div>
<div>
<label>チーム名</label>
<p th:text="*{team}"></p>
</div>
<div>
<label>守備位置</label>
<p th:text="*{position}"></p>
</div>
</div>
<div>
<a class="btn btn-default" th:href="@{/players/{id}/edit(id=*{id})}">編集</a>
</div>
<div class="pull-right">
<a class="btn btn-link" href="/players">一覧画面へ</a>
</div>
</div>
</body>
</html>


動作確認

demo.gif


次回