目次
Kotlin 1.1.61 + Spring で Web Application を作る (1 of 4) の続きです。
Web Page を作る
Task (作業) の登録・一覧表示ができるようにします。
下準備
kotlinx.html, PostgreSQL を使用するために、 build.gradle
を更新します。 dependencies
に2行追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
buildscript { ext.kotlin_version = '1.2.0' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } // 追加 plugins { id 'org.springframework.boot' version '1.5.8.RELEASE' } // 記述場所変更 group 'com.example' version '1.0-SNAPSHOT' apply plugin: 'kotlin' repositories { mavenCentral() jcenter() } dependencies { compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version") compile("org.springframework.boot:spring-boot-starter-web:1.5.8.RELEASE") // 追加 compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.postgresql:postgresql:42.1.4") def kotlinx_html_version = "0.6.6" compile("org.jetbrains.kotlinx:kotlinx-html-jvm:${kotlinx_html_version}") } compileKotlin { kotlinOptions.jvmTarget = "1.8" } compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } |
データベースの接続設定も必要ですので、 resources/application.properties
を作成します。
1 2 3 4 5 6 7 8 9 10 |
server.address=0.0.0.0 spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.testOnBorrow=true spring.datasource.validationQuery=SELECT 1 # Database Setting spring.datasource.url=jdbc:postgresql://localhost/example spring.datasource.username=developer spring.datasource.password=developer |
そして、今回使用するテーブルを SQL で作成しておきます。
1 2 3 4 5 6 |
CREATE TABLE task( id SERIAL PRIMARY KEY, name VARCHAR, created_at TIMESTAMP, updated_at TIMESTAMP ); |
Entity 作成
Spring の Entity とリポジトリクラスを作ります。 Kotlin らしく Getter, Setter は作りません。
1 2 3 4 5 6 7 8 |
src -main -kotlin -com.example.myapp |-domain -task |-Task.kt <- 追加するクラス -TaskRepository.kt <-追加するインターフェース |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package com.example.myapp.domain.task import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener import java.util.* import javax.persistence.* @Entity @EntityListeners(AuditingEntityListener::class) class Task { @Id @SequenceGenerator( name = "task_id_seq", sequenceName = "task_id_seq", allocationSize = 1 ) @GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "task_id_seq" ) var id: Long? = null var name: String? = null @CreatedDate @Temporal(TemporalType.TIMESTAMP) @Column(nullable = false, updatable = false) var createdAt: Date? = null @LastModifiedDate @Temporal(TemporalType.TIMESTAMP) @Column(nullable = false, updatable = false) var updatedAt: Date? = null } |
データベースからデータを取得するため、 Repositoryインターフェースも作っておきます。
1 2 3 4 5 |
package com.example.myapp.domain.task import org.springframework.data.jpa.repository.JpaRepository interface TaskRepository: JpaRepository<Task, Long> |
Entity で @CreatedDate
, @LastModifiedDate
を使用しますので、Application クラス に @EnableJpaAuditing
を付け加えておきます。
View 作成
View 部分を作ります。 共通レイアウトと内部のコンテンツに分けて、オブジェクトとして作ります。
1 2 3 4 5 6 7 8 9 |
src - main - kotlin - com.example.myapp |- presentation | - layout | - LayoutView.kt - task -TaskView.kt |
まずは LayoutView です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package com.example.myapp.presentation.layout import kotlinx.html.* import kotlinx.html.stream.appendHTML import org.springframework.web.servlet.mvc.support.RedirectAttributes import java.io.StringWriter object LayoutView { private fun HEAD.metaTags(): Unit = { meta { attributes["charset"] = "UTF-8" } meta(name = "viewport", content = "width=device-width,initial-scale=1.0") }() fun default( redirectAttributes: RedirectAttributes, pageTitle: String = "", block: DIV.() -> Unit ): String = StringWriter(). appendln("<!DOCTYPE html>"). appendHTML().html { head { title { +pageTitle } metaTags() styleLink("https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css") } body { id = "main" header("pos-f-t") { nav("navbar navbar-light bg-light") { a("/task") { classes = setOf("navbar-brand") +"Task List" } } } if (redirectAttributes.containsAttribute("success")) { div("container") { div("alert alert-success") { role = "alert" +(redirectAttributes.flashAttributes.getValue("success") as String) } } } div("container") { block() } } }.toString() } |
default メソッド がレイアウトの構造を表しています。 Kotlin で書いているので、 タグの閉じ忘れはエディタが教えてくれますし、 分岐の if, when は Kotlin と同じ文法で書くことができます。
ここではスタイルシートとして bootstrap をheaderタグに入れています。 metaタグ を別のファンクション(metaTags)としてまとめています。
Task を一覧表示する View を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
package com.example.myapp.presentation.task import com.example.myapp.domain.task.Task import com.example.myapp.presentation.core.row import com.example.myapp.presentation.layout.LayoutView import kotlinx.html.* import org.springframework.web.servlet.mvc.support.RedirectAttributes object TaskView { fun list( redirectAttributes: RedirectAttributes, taskList: List<Task> ) = LayoutView.default(redirectAttributes) { taskList.forEach { div("row") { div("col") { +(it.name ?: "") } } } hr { } div("row") { div("col") { postForm("/task") { div("form-group") { label("col-form-label") { htmlFor = "name" } textInput { classes = setOf("form-control") name = "name" } } div("form-group") { button { classes = setOf("btn btn-primary") type = ButtonType.submit +"Submit" } } } } } } } |
Taskの一覧と、Taskを追加するフォームのみ表示しています。 これで View は完成です。
kotlinx.html では、 HTMLタグと同じ名前のファンクションが用意されています。 それらは Higher-Order Function (高階関数) で、最後の引数が関数になっています。 多くはラムダを使うのですが、そのラムダの中でタグの属性を設定します。 ほとんどの属性は 属性 = 文字列
の形で設定できるようになっていますが、 予め用意されていない属性や "data-xxx"
のような独自の属性を設定するときは attributes を使用します。 また、多くのタグで、 div("form-group")
のようにタグの引数(ここでは "form-group"
)を利用してCSSクラスを設定できます。
1 2 3 |
div { attributes["data-id"] = "a" } |
kotlinx.html には div
のようにタグ名そのままのファンクションだけでなく、 postForm
や textInput
のように少しカスタマイズされたファンクションも用意されています。
CSRFトークンを form に加えたり、 DELETE, PATCH などのリクエストを送信する場合は次のようにします。
1 2 3 4 5 6 7 8 9 10 11 |
// request は HttpServletRequest のオブジェクト val csrfToken = HttpSessionCsrfTokenRepository().generateToken(request) hiddenInput(name = csrfToken.parameterName) { value = csrfToken.token } // Spring での DELETE リクエストに使用するタグ // ラムダですべての属性を設定する書き方 hiddenInput { name = "_method" value = "DELETE" } |
コントローラ作成
シンプルに追加と表示のみを考えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package com.example.myapp.presentation.task import com.example.myapp.domain.task.Task import com.example.myapp.domain.task.TaskRepository import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.* import org.springframework.web.servlet.mvc.support.RedirectAttributes @Controller @RequestMapping("task") class TaskController { @Autowired private lateinit var taskRepository: TaskRepository @ResponseBody @GetMapping fun list(attributes: RedirectAttributes): String { return TaskView.list(attributes, taskRepository.findAll()) } @PostMapping fun create( attributes: RedirectAttributes, @RequestParam name: String): String { val task = Task() task.name = name taskRepository.save(task) attributes.addFlashAttribute("success", "タスクを追加しました。") return "redirect:/task" } } |
TaskView の list
メソッドが、 String
として HTMLを返すものになっていますので、 list
メソッドには @ResponseBody
を付けています。 こうすることで、返り値のStringがそのままレスポンスとして返されます。
これで登録・一覧表示ができるようになりました。 ./gradlew bootrun
でサーバを起動し、localhost:8080
にアクセスすると下のように表示されます。(下図は3つレコードを登録した後の図)
View を見やすくする
TaskView の中身を少し見やすくしてみます。 bootstrap では <div class="row">
が頻出しますので、ひとつのメソッドにしておけると楽です。 そこで、Tag.kt
を作成して、Viewでよく使うメソッドを記述しておきます。
1 2 3 4 5 6 7 |
src - main - kotlin - com.example.myapp |- presentation - core - Tag.kt |
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.example.myapp.presentation.core import kotlinx.html.DIV import kotlinx.html.FlowContent import kotlinx.html.div fun FlowContent.row(classes : String? = null, block : DIV.() -> Unit = {}): Unit { val _classes: String if (classes.isNullOrBlank()) _classes = "row" else _classes = classes + " row" return div(_classes, block) } |
こちらはクラスでもオブジェクトでもなく、直接 Kotlin のファンクションが記載されたファイルです。
これを TaskView で次のようにしてインポートします。
1 |
import com.example.myapp.presentation.core.row |
すると、 div("row")
と書いていたのを row
で簡略化して書けるようになります。
Kotlin 1.1.61 + Spring で Web Application を作る (3 of 4) に続きます。