Spring Boot with DB Migration

ブログに書いていたんですが、ブログが動かなくなったのでこちらへコピー。 WP Plugin 入れすぎなのか・・・。

<p>Here, I write how to build application in Spring Boot with DB Migration. I use Kotlin and <a href="https://github.com/KenjiOhtsuka/harmonica/" target="_blank" rel="nofollow noopener noreferrer">Harmonica</a>, Kotlin database migration library.</p>
<!--more-->
<section>
  <h2>Environment</h2>
  <ul>
    <li>Kotlin 1.3.20</li>
    <li>Spring Boot 2.1.2</li>
    <li><a href="https://github.com/KenjiOhtsuka/harmonica" rel="noopener nofollow noreferrer" target="_blank">Harmonica</a> 1.1.17</li>
    <li>PostgreSQL</li>
    <li>IntelliJ IDEA</li>
    <li>JDK 11 (You can use JDK 8 or later)</li>
  </ul>
</section>
<section>
  <h2>Prepare Application Base</h2>
  <p>First, let's generate application base on <a href="https://start.spring.io/" rel="noopener nofollow noreferrer" target="_blank">Spring Initializr</a>.</p>
  <figure><a href="https://improve-future.com/spring-boot-with-db-migration.html/springinitializr_harmonica_tutorial" rel="attachment wp-att-15364"><img src="https://improve-future.com/wp-content/uploads/2019/02/SpringInitializr_Harmonica_Tutorial-1024x451.png" alt="" width="474" height="209" class="aligncenter size-large wp-image-15364" /></a></figure>
  <p>Choose the following component.</p>
  <ul>
    <li>Gradle</li>
    <li>Kotlin</li>
    <li>Spring Boot 2.1.2</li>
  </ul>
  <p>Leave the project meta data as it is.</p>
  <p>Add the following dependencies.</p>
  <ul>
    <li>Web</li>
    <li>PostgreSQL</li>
    <li>JPA</li>
    <li>Thymeleaf</li>
  </ul>
  <p>In old articles, I used kotlinx.html for template, but this time I use Thymeleaf, because most people are familiar with it.</p>
  <p>Click "Generate Project" button and extract the zip file, then you can see the file structure as follows.</p>
  <figure><pre class="lang:default decode:true " >demo/
|- build.gradle
|- gradle/
|  `- wrapper/
|     |- gradle-wrapper.jar
|     `- gradle-wrapper.properties
|- gradlew
|- gradlew.bat
|- settings.gradle
`- src/
   |- main/
   |  |- kotlin/
   |  |  `- com/
   |  |     `- example/
   |  |        `- demo/
   |  |           `- DemoApplication.kt
   |  `- resources/
   |     |- application.properties
   |     |- static/
   |     `- templates/
   `- test/
      `- kotlin/
         `- com/
            `- example/
               `- demo/
                  `- DemoApplicationTests.kt</pre></figure>
</section>
<section>
  <h2>Prepare PostgreSQL</h2>
  <p>Here is the database configuration.</p>
  <figure>
    <table>
      <tbody>
        <tr>
          <th>Database name</th>
          <td>demo</td>
        </tr>
        <tr>
          <th>User name</th>
          <td>develoepr</td>
        </tr>
        <tr>
          <th>Password</th>
          <td>developer</td>
        </tr>
        <tr>
          <th>Database port</th>
          <td>5432 (Default Port)</td>
        </tr>
      </tbody>
    </table>
  </figure>
  <p>If you don't have PostgreSQL, you can prepare it with docker as follows.</p>
  <figure><pre class="lang:sh decode:true " ><code>docker run -d -p 5432:5432 --name demo -e POSTGRES_USER=developer -e POSTGRES_PASSWORD=developer -e POSTGRES_DB=sample postgres:11</code></pre></figure>
  <p>If you want to change the listening port, please change the first <code>5432</code> to another port number.</p>
</section>
<section>
  <h2>Open the Project in IntelliJ IDEA</h2>
  <p>From the "File" menu, click "Open", and choose the file, "<code>settings.gradle</code>". You will see the dialog that asks whether you open it as a project or not. Then, please click "Open as a Project" button.</p>
  <figure><img src="https://improve-future.com/wp-content/uploads/2019/02/OpenAsProjectDialog.png" alt="" width="458" height="143" class="aligncenter size-full wp-image-15373" /></figure>
  <p>After that you will see the dialog for importing, then configure it and click "OK" button.</p>
  <p>Well, let's change the Kotlin version. Open <code>build.gradle</code> and change Kotlin version to <code>1.3.20</code>.</p>
</section>
<section>
  <h2>Migration</h2>
  <section>
    <h3>Add Module</h3>
    <p>Let's add the module. In the Project tool window, choose top directory. From the "File" menu, select "New" and "Module", the you'll see "New Module" dialog. On the dialog, please choose "Gradle" in the left pane, and choose "Kotlin (Java)" in the right pane, and click "Next" button.</p>
    <figure><a href="https://improve-future.com/spring-boot-with-db-migration.html/newmoduledialog_harmonica_tutorial" rel="attachment wp-att-15378"><img src="https://improve-future.com/wp-content/uploads/2019/02/NewModuleDialog_Harmonica_Tutorial.png" alt="" width="833" height="498" class="aligncenter size-full wp-image-15378" /></a></figure>
    <p>Next, please configure as follows, and click "Next" button and add the module.</p>
    <figure>
      <table>
        <tbody>
          <tr>
            <td>Add as module to</td>
            <td>demo</td>
          </tr>
          <tr>
            <td>GroupId</td>
            <td>com.example (Inherit)</td>
          </tr>
          <tr>
            <td>ArtifactId</td>
            <td>db</td>
          </tr>
          <tr>
            <td>Version</td>
            <td>0.0.1-SNAPSHOT (Inherit)</td>
          </tr>
        </tbody>
      </table>
    </figure>
    <figure><a href="https://improve-future.com/spring-boot-with-db-migration.html/moduleconfiguration_harmonica_tutorial" rel="attachment wp-att-15383"><img src="https://improve-future.com/wp-content/uploads/2019/02/ModuleConfiguration_Harmonica_Tutorial.png" alt="" width="833" height="498" class="aligncenter size-full wp-image-15383" /></a></figure>
    <p>Auto syncronization will work and you may get the following error output.</p>
    <figure><pre class="lang:default decode:true " >org.gradle.api.ProjectConfigurationException: A problem occurred configuring project ':db'.</pre></figure>
    <p>Now, open <code>db/build.gradle</code> and remove the version of the Kotlin plugin and add libraries.</p>
    <figure><pre class="lang:java mark:1-12,14-16,18,25,30-32 decode:true " title="db/build.gradle"><code>buildscript {
    ext {
        kotlinVersion = '1.3.20'
    }
    repositories {
        jcenter()
    }
    dependencies {
        classpath group: 'org.jetbrains.kotlin', name: 'kotlin-script-util', version: kotlinVersion
        classpath 'com.improve_future:harmonica:1.1.17'
    }
}

plugins {
    id 'org.jetbrains.kotlin.jvm'
}

apply plugin: 'jarmonica'

group 'com.example'
version '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    runtimeOnly 'org.postgresql:postgresql:42.2.5'
    implementation 'com.improve_future:harmonica:1.1.17'
    implementation group: 'org.reflections', name: 'reflections', version: '0.9.11'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}</code></pre></figure>
  </section>
  <section>
    <h3>Configuration</h3>
    <p>Set up the db and directory configuration.</p>
    <p>Add the next expression to the bottom of <code>build.gradle</code>.</p>
    <figure><pre class="lang:java decode:true " ><code>extensions.extraProperties["migrationPackage"] =
        [project.group, project.rootProject.name, project.name].join(".")</code></pre></figure>
    <p>Next, execute the following command.</p>
    <figure><pre class="lang:sh decode:true " ><code>mkdir -p db/src/main/kotlin/com/example/demo/db/config</code></pre></figure>
    <p>And create file <code>db/src/main/kotlin/com/example/demo/db/config/Default.kt</code> as follows.</p>
    <figure><pre class="lang:java decode:true " ><code>package com.example.demo.db

import com.improve_future.harmonica.core.DbConfig
import com.improve_future.harmonica.core.Dbms

config Default : DbConfig({
    dbms = Dbms.PostgreSQL
    dbName = "demo"
    host = "127.0.0.1"
    user = "developer"
    password = "developer"
    // Add port configuration if you need
    // port = 5432
})</code></pre></figure>
  </section>
  <section>
    <h3>Add and Execute Migration</h3>
    <p>Now, let's add the migration. Execute the next command.</p>
    <figure><pre class="lang:sh decode:true " ><code>./gradlew jarmonicaCreate -Pname=CreateTask</code></pre></figure>
    <p>Then, you can see the file <code>db/src/main/kotlin/com/example/demo/db/migration/Mxxxxxxxxxxxxxxxxxx_CreateTask.kt</code>. Change it as follows.</p>
    <figure><pre class="lang:java decode:true " ><code>package com.example.demo.db.migration

import com.improve_future.harmonica.core.AbstractMigration

/**
 * CreateTask
 */
class M20190209141813199_CreateTask : AbstractMigration() {
    override fun up() {
        createTable("task") {
            varchar("name", size = 200, nullable = false)
            dateTime("created_at", nullable = false)
            dateTime("updated_at", nullable = false)
        }
    }

    override fun down() {
        dropTable("task")
    }
}</code></pre></figure>
    <p>Execute migration as follows.</p>
    <figure><pre class="lang:sh decode:true " ><code>./gradlew jarmonicaUp</code></pre></figure>
    <p>You can see the output like follows.</p>
    <figure><pre class="lang:default decode:true " >&gt; Task :db:jarmonicaUp
jdbc:postgresql://127.0.0.1:5432/demo?autoReconnect=true
== [Start] Migrate up 20190209141813199 ==
Create Table: task
== [End] Migrate up 20190209141813199 ==</pre></figure>
  </section>
  <section>
    <h3>See the Database</h3>
    <p>Let's see the database with <code>psql</code>.</p>
    <figure><pre class="lang:default decode:true" >demo=# \d
                  List of relations
 Schema |        Name         |   Type   |   Owner   
--------+---------------------+----------+-----------
 public | harmonica_migration | table    | developer
 public | task                | table    | developer
 public | task_id_seq         | sequence | developer
(3 rows)</pre></figure>
    <p><code>harmonica_migration</code> table contains the executed migration versions. <code>task</code> table is what we wanted to create and <code>task_id_seq</code> sequence is the sequence for <code>task</code> table, which is created automatically.</p>
    <figure><pre class="lang:default decode:true wrap:false" >demo=# \d task
                                        Table "public.task"
   Column   |            Type             | Collation | Nullable |             Default              
------------+-----------------------------+-----------+----------+----------------------------------
 id         | integer                     |           | not null | nextval('task_id_seq'::regclass)
 name       | character varying(200)      |           | not null | 
 created_at | timestamp without time zone |           | not null | 
Indexes:
    "task_pkey" PRIMARY KEY, btree (id)</pre></figure>
    <p><code>task</code> table has <code>id</code> column, which is added automatically, and it uses the sequence.</p>
  </section>
</section>
<section>
  <h2>Build Application</h2>
  <p>From now on, create Spring Boot application. Let's create files in the follwong structure.</p>
  <figure><pre class="lang:default decode:true " >demo/
`- src/
   `- main/
      |- kotlin/
      |  `- com/
      |     `- example/
      |        `- demo/
      |           |- domain/
      |           |  `- task
      |           |     |- Task.kt
      |           |     `- TaskRepository.kt
      |           |- presentation/
      |           |  `- TaskController.kt
      |           `- DemoApplication.kt
      `- resources/
         |- application.properties
         `- templates/
            `- task/
               `- index.html</pre></figure>
  <section>
    <h3>Add configuration</h3>
    <p>Update <code>application.properties</code> and <code>DemoApplication</code> class.</p>
    <p>Add database configuration to the <code>application.properties</code>.</p>
    <figure><pre class="lang:default decode:true " ><code>spring.datasource.url=jdbc:postgresql://localhost:5432/demo
spring.datasource.username=developer
spring.datasource.password=developer
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect</code></pre></figure>
    <p>Add <code>@EnableJpaAuditing</code> annotation to <code>DemoApplication</code> class. It is used for timestamp auto updating.</p>
    <figure><pre class="lang:java decode:true " title="DemoApplication.kt" ><code>package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@SpringBootApplication
@EnableJpaAuditing
class DemoApplication

fun main(args: Array&lt;String&gt;) {
    runApplication&lt;DemoApplication&gt;(*args)
}</code></pre></figure>
  </section>
  <section>
    <h3>Entity</h3>
    <p>Create an entity, which represents the record in the database. Create the file of the class , <code>Task</code>, as <code>domain/task/Task.kt</code>.</p>
    <figure><pre class="lang:java decode:true " title="domain/task/Task.kt"><code>package com.example.demo.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
}</code></pre></figure>
    <p><code>@EntityListeners</code>, <code>@CreateDate</code> and <code>@LastModifiedDate</code> are added for auto updating.</p>
  </section>
  <section>
    <h3>Repository</h3>
    <p>Create the repository interface, <code>TaskRepository</code>, for <code>Task</code>, as <code>domain/task/TaskRepository.kt</code>.</p>
    <figure><pre class="lang:java decode:true " title="domain/task/TaskRepository.kt" ><code>package com.example.demo.domain.task

import org.springframework.data.jpa.repository.JpaRepository

interface TaskRepository : JpaRepository&lt;Task, Long&gt;</code></pre></figure>
  </section>
  <section>
    <h3>View template</h3>
    <p>Create the view template, which is <code>resources/templates/task/index.html</code>.</p>
    <figure> 
<pre class="lang:xhtml decode:true " title="templates/task/index.html" >&lt;!DOCTYPE html&gt;
&lt;html lang="ja" xmlns:th="http://www.thymeleaf.org"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport"
          content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Sample Application&lt;/title&gt;
    &lt;link rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootst
rap/4.1.3/css/bootstrap.min.css"
          crossorigin="anonymous"&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;header class="pos-f-t mb-2"&gt;
    &lt;nav class="navbar navbar-light bg-light"&gt;
        &lt;a th:href="@{/task}" class="navbar-brand"&gt;
            Task List
        &lt;/a&gt;
    &lt;/nav&gt;
&lt;/header&gt;
&lt;main class="container"&gt;
    &lt;div class="row" th:if="${success}"&gt;
        &lt;div class="col"&gt;
            &lt;div class="alert alert-success"
                 role="alert"
                 th:text="${success}"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="row" th:each="task : ${taskList}"&gt;
        &lt;div class="col"&gt;
            &lt;div class="d-flex"&gt;
                &lt;div class="p-2 w-100 align-self-center"
                     th:text="${task.name}"&gt;
                &lt;/div&gt;
                &lt;div class="p-2 flex-shrink-1 align-self-center"&gt;
                    &lt;form th:object="${task}"
                          th:action="@{/task/{id}(id=${task.id})}"
                          th:method="delete"&gt;
                        &lt;button class="btn btn-outline-danger btn-sm"
                                type="submit"&gt;
                            X
                        &lt;/button&gt;
                    &lt;/form&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;hr&gt;
    &lt;div class="row"&gt;
        &lt;div class="col"&gt;
            &lt;form th:method="post"
                  th:action="@{/task}"
                  th:object="${newTask}"&gt;
                &lt;div class="form-group"&gt;
                    &lt;input type="text"
                           name="name"
                           class="form-control"
                           th:field="*{name}"&gt;
                &lt;/div&gt;
                &lt;div class="form-group"&gt;
                    &lt;button class="btn btn-primary btn-block"
                            type="submit"&gt;
                        Submit
                    &lt;/button&gt;
                &lt;/div&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/main&gt;
&lt;/body&gt;
&lt;/html&gt;</pre> 
</figure>
  </section>
  <section>
    <h3>Controller</h3>
    <p>Create the controller, <code>TaskController</code>, as <code>presentation/task/TaskController.kt</code>.</p>
    <figure><pre class="lang:java decode:true " title="presentation/task/TaskController.kt" ><code>package com.example.demo.presentation.task

import com.example.demo.domain.task.Task
import com.example.demo.domain.task.TaskRepository
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Controller
import org.springframework.ui.ModelMap
import org.springframework.web.bind.annotation.*
import org.springframework.web.servlet.mvc.support.RedirectAttributes

@Controller
@RequestMapping("task")
class TaskController(
    private val taskRepository: TaskRepository
) {
    @GetMapping
    fun list(modelMap: ModelMap): String {
        modelMap["taskList"] =
            taskRepository.findAll(
                Sort.by("id").descending()
            )
        modelMap["newTask"] = Task()
        return "task/index"
    }

    @PostMapping
    fun create(
        attributes: RedirectAttributes,
        @ModelAttribute task: Task
    ): String {
        taskRepository.save(task)
        attributes.addFlashAttribute(
            "success", "The Task was successfully created."
        )
        return "redirect:/task"
    }

    @DeleteMapping("{id}")
    fun delete(
        attributes: RedirectAttributes,
        @PathVariable id: Long
    ): String {
        taskRepository.deleteById(id)
        attributes.addFlashAttribute(
            "success", "The Task was successfully deleted."
        )
        return "redirect:/task"
    }
}</code></pre></figure>
  </section>
</section>
<section>
  <h2>Run the application</h2>
  <p>Execute <code>./gradlew bootrun</code>, and after awhile, you can see the page on the browser. Please access to <code>http://localhost:8080/task</code>. And you can add and remove tasks.</p>
  <figure><figcaption style="text-align:center;">Figure: 2 tasks are added.</figcaption><img src="https://improve-future.com/wp-content/uploads/2019/02/demo_screen.png" alt="" width="373" height="364" class="aligncenter size-full wp-image-15517" /></figure>
</section>
<p>The code of this demo project is on <a href="https://github.com/KenjiOhtsuka/harmonica_demo" target="_blank" rel="nofollow noopener noreferrer">GitHub</a>.</p>