spring-boot/kotlin で datastore 入門


少し前までは多少プログラミングを仕事でもしていましたが、最近は仕事でできていないのでせっかくなので spring-boot と kotlin で自作アプリ用の API を作ろうと思って datastore について再度チュートリアルから始めてみました。
チュートリアルはインタラクティブ形式のCUIアプリを作る Google が公式で用意しているものを利用しています。
Java で書かれているので、Kotlin に書き換えてます。

##### ■ チュートリアル
https://codelabs.developers.google.com/codelabs/cloud-spring-datastore


# 環境

macOS Big Sur 11.1
Intellij Community 2020.3


# チュートリアル
以下のステップでチュートリアルを進めていきますが、kotlin に変更した箇所のみピックアップします。
1. Overview
2. Setup and Requirements（プロジェクトの作成とセットアップ）
3. Initialize Cloud Datastore（Datastore初期化）
4. Bootstrap a new Spring Boot Java Application（spring-bootプロジェクト作成）
5. Create the Book class（Book Entity の作成）
6. Create the BookRepository interface（Book Entity 操作用 Rpository作成）
7. Create the interactive CLI application（Shell 実行用処理作成）
8. Run the application（アプリケーション実行）
9. See what is stored in Datastore using web interface（登録内容の確認）
10. Clean up（削除）
11. Congratulations!


## チュートリアルとの変更点

#### 4. Bootstrap a new Spring Boot Java Application（spring-bootプロジェクト作成）
kotlin を利用したかったので、チュートリアルにある、spring initializr（ https://start.spring.io/ ）のページから以下の設定でダウンロードしました。
特にこだわりはないです。

* Project：Gradle Project
* Language：Kotlin
* Spring Boot：2.5.0
* Project Metadata（Packaging）：Jar
* Project Metadata（Java）：11

![Kobito.qq4G7P.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203864/136da99e-21e9-a021-21e9-14b6795da4d9.png "Kobito.qq4G7P.png")


チュートリアルでは、maven を利用していますが、gradle でプロジェクト作ったので依存の設定を変更しています。

```build.gradle.kts
dependencies {
	implementation("org.springframework.cloud:spring-cloud-gcp-starter-data-datastore:1.2.6.RELEASE")
	implementation("org.springframework.shell:spring-shell-starter:2.0.0.RELEASE")

	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}
```


#### 5. Create the Book class（Book Entity の作成）
チュートリアルが Java のコードなので、Kotlin ように書き換えます。
コードが短くなってみやすくていいですね！

```Book.kt
package app.noz.ws.example.datastore

import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity
import org.springframework.data.annotation.Id

@Entity(name = "books")
class Book(@Id var id: Long? = null, var title: String, var author: String, var year: Int) {

    override fun toString(): String {
        return "Book(id=$id, title='$title', author='$author', year=$year)"
    }

}
```


#### 6. Create the BookRepository interface（Book Entity 操作用 Rpository作成）
同様に Kotlin に書き換えます。

```BookRepository.kt
package app.noz.ws.example.datastore

import org.springframework.cloud.gcp.data.datastore.repository.DatastoreRepository

interface BookRepository: DatastoreRepository<Book, Long> {

    fun findByAuthor(author: String): List<Book>
    fun findByYearGreaterThan(year: Int): List<Book>
    fun findByAuthorAndYear(author: String, year: Int): List<Book>

}
```


#### 7. Create the interactive CLI application（Shell 実行）
同様に Kotlin に書き換えます。

```DatastoreApplication.kt
package app.noz.ws.example.datastore

import com.google.common.collect.Lists
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.shell.standard.ShellComponent
import org.springframework.shell.standard.ShellMethod

@ShellComponent
@SpringBootApplication
class DatastoreApplication {

	@Autowired
	private lateinit var bookRepository: BookRepository

	@ShellMethod("Saves a book to Cloud Datastore: save-book <title> <author> <year>")
	fun saveBook(title: String, author: String, year: Int): String {
		val savedBook = this.bookRepository.save(Book(title = title, author = author, year = year))
		return savedBook.toString()
	}

	@ShellMethod("Loads all books")
	fun findAllBooks(): String {
		val books = this.bookRepository.findAll()
		return Lists.newArrayList(books).toString()
	}

	@ShellMethod("Loads books by author: find-by-author <author>")
	fun findByAuthor(author: String): String {
		val books = bookRepository.findByAuthor(author)
		return books.toString()
	}

	@ShellMethod("Loads books published after a given year: find-by-year-after <year>")
	fun findByYearAfter(year: Int): String {
		val books = bookRepository.findByYearGreaterThan(year)
		return books.toString()
	}

	@ShellMethod("Loads books by author and year: find-by-author-year <author> <year>")
	fun findByAuthorYear(author: String, year: Int): String {
		val books = bookRepository.findByAuthorAndYear(author, year)
		return books.toString()
	}

	@ShellMethod("Removes all books")
	fun removeAllBooks() {
		bookRepository.deleteAll()
	}

}

fun main(args: Array<String>) {
	runApplication<DatastoreApplication>(*args)
}
```

#### 8. Run the application
コマンド経由で実行するとコンソールログが出ておかしくなってしまうため、Intellij から実行しました。
緑の「▶︎」をクリックして実行します。
![Kobito.LMW6H5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203864/e7c2132e-7ca0-4aa0-487b-adce8ba6bd0d.png "Kobito.LMW6H5.png")

```
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.5.0-SNAPSHOT)

2021-01-16 13:47:39.282  INFO 78351 --- [           main] a.n.w.e.d.DatastoreApplicationKt         : Starting DatastoreApplicationKt using Java 14.0.1 on MacBook-Pro.local with PID 78351 (/Users/yasu/Downloads/datastore/build/classes/kotlin/main started by yasu in /Users/yasu/Downloads/datastore)
2021-01-16 13:47:39.285  INFO 78351 --- [           main] a.n.w.e.d.DatastoreApplicationKt         : No active profile set, falling back to default profiles: default
2021-01-16 13:47:40.118  INFO 78351 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Datastore repositories in DEFAULT mode.
2021-01-16 13:47:40.175  INFO 78351 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 53 ms. Found 1 Datastore repository interfaces.
2021-01-16 13:47:40.767  INFO 78351 --- [           main] o.s.c.g.a.c.GcpContextAutoConfiguration  : The default project ID is api-test-300213
2021-01-16 13:47:40.845  INFO 78351 --- [           main] o.s.c.g.core.DefaultCredentialsProvider  : Default credentials provider for service account todo-test@api-test-300213.iam.gserviceaccount.com
2021-01-16 13:47:40.846  INFO 78351 --- [           main] o.s.c.g.core.DefaultCredentialsProvider  : Scopes in use by default credentials: [https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/spanner.admin, https://www.googleapis.com/auth/spanner.data, https://www.googleapis.com/auth/datastore, https://www.googleapis.com/auth/sqlservice.admin, https://www.googleapis.com/auth/devstorage.read_only, https://www.googleapis.com/auth/devstorage.read_write, https://www.googleapis.com/auth/cloudruntimeconfig, https://www.googleapis.com/auth/trace.append, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/cloud-vision, https://www.googleapis.com/auth/bigquery, https://www.googleapis.com/auth/monitoring.write]
2021-01-16 13:47:42.777  WARN 78351 --- [           main] org.jline                                : Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
2021-01-16 13:47:42.937  INFO 78351 --- [           main] a.n.w.e.d.DatastoreApplicationKt         : Started DatastoreApplicationKt in 4.112 seconds (JVM running for 5.038)
shell:>
```

##### Help コマンドの実行

```
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

Datastore Application
        find-all-books: Loads all books
        find-by-author: Loads books by author: find-by-author <author>
        find-by-author-year: Loads books by author and year: find-by-author-year <author> <year>
        find-by-year-after: Loads books published after a given year: find-by-year-after <year>
        remove-all-books: Removes all books
        save-book: Saves a book to Cloud Datastore: save-book <title> <author> <year>
```

##### 登録と表示
2件登録したあと、全件取得します。

```
shell:>save-book test1 author1 2021
Book(id=5644004762845184, title='test1', author='author1', year=2021)
shell:>save-book test2 author2 2021
Book(id=5632499082330112, title='test2', author='author2', year=2021)
shell:>find-all-books
[Book(id=5632499082330112, title='test2', author='author2', year=2021), Book(id=5644004762845184, title='test1', author='author1', year=2021)]
```

#### 9. See what is stored in Datastore using web interface（登録内容の確認）
Web から見てもこんな感じでちゃんと登録されていました。
![Kobito.0q51nT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203864/b77de5ea-dcff-b86f-32b1-c433e1ea41b3.png "Kobito.0q51nT.png")

#### 10. Clean up（削除）
削除後にコマンド及びWebから確認しましたがちゃんとデータが消えていますね。

```
shell:>remove-all-books
shell:>find-all-books
[]
```
![Kobito.lbj2IN.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203864/ccb64e5e-bb3c-e017-7131-1b0c7dd184ab.png "Kobito.lbj2IN.png")


## Gradle コマンドで実行した時に不具合
Gradle コマンド使って `./gradlew bootRun` した場合、`EXECUTING [16s]` の出力が常にされておりインタラクティブコマンドがうまく動かなかったです。実行は可能ですが表示がめちゃくちゃ崩れます。。。。
原因わかる方いましたら教えてください m(_ _)m


```
% ./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.5.0-SNAPSHOT)

2021-01-16 13:56:59.649  INFO 78536 --- [           main] a.n.w.e.d.DatastoreApplicationKt         : Starting DatastoreApplicationKt using Java 14.0.1 on MacBook-Pro.local with PID 78536 (/Users/yasu/Downloads/datastore/build/classes/kotlin/main started by yasu in /Users/yasu/Downloads/datastore)
2021-01-16 13:56:59.651  INFO 78536 --- [           main] a.n.w.e.d.DatastoreApplicationKt         : No active profile set, falling back to default profiles: default
2021-01-16 13:57:00.140  INFO 78536 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Datastore repositories in DEFAULT mode.
2021-01-16 13:57:00.184  INFO 78536 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 39 ms. Found 1 Datastore repository interfaces.
2021-01-16 13:57:00.587  INFO 78536 --- [           main] o.s.c.g.a.c.GcpContextAutoConfiguration  : The default project ID is api-test-300213
2021-01-16 13:57:00.632  INFO 78536 --- [           main] o.s.c.g.core.DefaultCredentialsProvider  : Default credentials provider for service account todo-test@api-test-300213.iam.gserviceaccount.com
2021-01-16 13:57:00.632  INFO 78536 --- [           main] o.s.c.g.core.DefaultCredentialsProvider  : Scopes in use by default credentials: [https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/spanner.admin, https://www.googleapis.com/auth/spanner.data, https://www.googleapis.com/auth/datastore, https://www.googleapis.com/auth/sqlservice.admin, https://www.googleapis.com/auth/devstorage.read_only, https://www.googleapis.com/auth/devstorage.read_write, https://www.googleapis.com/auth/cloudruntimeconfig, https://www.googleapis.com/auth/trace.append, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/cloud-vision, https://www.googleapis.com/auth/bigquery, https://www.googleapis.com/auth/monitoring.write]
2021-01-16 13:57:02.198  WARN 78536 --- [           main] org.jline                                : Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
2021-01-16 13:57:02.311  INFO 78536 --- [           main] a.n.w.e.d.DatastoreApplicationKt         : Started DatastoreApplicationKt in 3.019 seconds (JVM running for 3.525)
shell:>Run
<==========---> 83% EXECUTING [16s] ← これ！！？
> :bootRun
```

# 最後に
チュートリアルを通して実践してみましたが、思ったより簡単だったんですが kotlin が初心者すぎて思った以上に時間がかかりました。
spring-boot/kotlin/datastore 使って API の方も作っていこうと思うので余力があれば Qiitaにも投稿したいと思います。

たいしたコードじゃないですがソースコードは一応 GitHub にあげておきます。
https://github.com/ynozue/DatasotreSample_kotlin