Kotlin+Spring Bootで簡単なAPIをClean Architectureで実装してみた
前回スタブで取得していたデータをちゃんとセットアップしてDBから取得できるようにします。またTODOアプリっぽいAPIを実装し、CRUD操作できるようにします。
目次
DockerでMySQL導入し、Spring BootからDB接続できるようにする
まずは既存のAPIにMySQLを導入します。今回はDockerでMySQLを選択しました。
M1 Macで起動するためにplatform: linux/x86_64を追加しています。volumesでコンテナ起動時のデータセットアップとMySQLの設定ファイルを読み込んでいます。
・docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | version: '3' services: mysql: platform: linux/x86_64 image: mysql:5.7 container_name: mysql_sample environment: MYSQL_ROOT_PASSWORD: mysql MYSQL_DATABASE: sample MYSQL_USER: docker MYSQL_PASSWORD: docker volumes: - ./initdb.d:/docker-entrypoint-initdb.d - ./my.cnf:/etc/mysql/conf.d/my.cnf ports: - 3306:3306 |
my.cnfでMySQLの設定を行います。my.cnfにcharacter-setを追加しているのはMySQLに日本語のデータを追加した際に文字化けになったのでその対応です。
参考:https://qiita.com/Asaiii12/items/69e4420d06d91374428a
・my.cnf
1 2 3 4 5 6 7 8 9 | [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_general_ci explicit-defaults-for-timestamp=1 general-log=1 general-log-file=/var/log/mysql/mysqld.log [client] default-character-set=utf8mb4 |
init.sqlではコンテナ作成時にテーブルの作成とデータ投入をしています。volumsで./initdb.d:/docker-entrypoint-initdb.dを指定すると、指定ディレクトリ配下のSQLが実行されるようになっています。
・initdb.d/init.sql
1 2 3 4 5 6 7 8 9 10 11 | USE sample; DROP TABLE IF EXISTS tasks; CREATE TABLE tasks ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NULL, `content` varchar(500) NULL, PRIMARY KEY (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO tasks (id, title, content) VALUES (1, "タイトル1", "内容1"), (2, "タイトル2", "内容2"); |
全て完了したらDockerを立ち上げます。
1 | $ docker compose up |
以下のコマンドで立ち上げたMySQLに接続し、実際にDBやテーブルが作成できるかを確認します。
1 | $ mysql -h 0.0.0.0 -u docker -p |
アプリケーションからDB接続できるようにするためにapplication.propertiesにDB接続情報を追加します。
・src/main/resources/application.properties
1 2 3 4 | spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://0.0.0.0:3306/sample spring.datasource.username=docker spring.datasource.password=docker |
Spring JPAの導入
Spring JPAを導入するためbuild.gradle.ktsのdependenciesに以下を追加し、Gradleをリビルドします。
・build.gradle.kts
1 2 3 4 5 6 | dependencies { ... implementation("org.springframework.boot:spring-boot-starter-data-jpa") } |
Entityの作成
Entityで定義されたテーブル情報が、自動インサートの対象になります。今回はTODOリストを意識しているのでidとtitle、contentのカラムを定義しています。
・entity/Task.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.example.entity import javax.persistence.* @Entity @Table(name = "tasks") data class Task ( @Id @GeneratedValue val id: Long = 0, @Column(name = "title", length = 100, nullable = false) val title: String = "", @Column(name = "content", length = 500) val content: String? = null, ) |
Repositoryの作成
JPA用のインターフェースを定義します。今回はそれぞれSQLを書いて定義しています。
・repository/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 | package com.example.repository import com.example.entity.Task import org.apache.ibatis.annotations.* import org.springframework.stereotype.Repository @Mapper @Repository interface TaskRepository { @Select("SELECT * FROM tasks;") fun findAll(): List<Task> @Select("SELECT * FROM tasks where id = #{id};") fun findById(id: Long): Task @Insert("INSERT INTO tasks(title, content) values(#{title}, #{content});") fun insertTask(title: String, content: String?): Int @Update("UPDATE tasks SET title=#{title}, content=#{content} WHERE id=#{id}") fun updateTask(id: Long, title: String, content: String?): Int @Delete("DELETE FROM tasks WHERE id=#{id}") fun deleteTask(@Param("id") id: Long): Int } |
DriverでRepositoryで定義したメソッドを呼び出す
DriverでRepositoryで定義したメソッドを呼び出し、実際にDB操作します。curlなどで確認しCRUDできていることが確認できれば完了です。
・driver/TaskDriver.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 36 37 38 39 40 41 42 | package com.example.driver import com.example.repository.TaskRepository import com.fasterxml.jackson.annotation.JsonCreator import org.springframework.stereotype.Component @Component class TaskDriver(private val taskRepository: TaskRepository) { fun getTasks(): TasksJson { return taskRepository.findAll().map { TaskJson(it.id, it.title, it.content) }.let(::TasksJson) } fun getTask(taskId: Long): TaskJson { return taskRepository.findById(taskId)?.let { TaskJson(it.id, it.title, it.content) } } fun createTask(title: String, content: String?): Int { return taskRepository.insertTask(title, content) } fun updateTask(taskJson: TaskJson): Int { return taskRepository.updateTask(taskJson.id, taskJson.title, taskJson.content) } fun deleteTask(taskId: Long): Int { return taskRepository.deleteTask(taskId) } } data class TasksJson @JsonCreator constructor( val list: List<TaskJson> ) data class TaskJson @JsonCreator constructor( val id: Long, val title: String, val content: String? ) |