Summary
It has been some time since I have used a Spring Boot repository and I thought today would be a good day to refresh this knowledge while I get to play around with Flyway.
In this tutorial we will learn how to create a small Spring Boot application using a JPA Repository and having Flyway to help us with the migrations.
All of this using this fun (see the reserved words for functions) language called Kotlin.
Prerequisites
To follow this tutorial is necessary that you have installed Java, Kotlin and Gradle.
You can learn how to do it in this post.
Excellent! Now that you have what is needed, let's continue.
Getting SpringBoot
You can get it with your vscode extension or from the Spring Initializr page.
Make sure to select the next dependencies:
- Spring Web
- Spring Data JPA
- Spring DevTools
- Flyway Migration
- MySQL Driver
After you download the generated zip file, unpack it and you can continue with the tutorial.
if you have some doubts on how to get your project going you can check out this post.
Setting up your database
In order to persist our information we are going to use Docker. In case that you don't have this great tool already installed, you can check their website and follow the steps to install.
Now that you have docker installed, you can input this command in your terminal:
docker run --rm --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=kotlin_crud_db -d mysql:latest
After the previous step is completed we need to go to our folder, the one that we unzipped, that contains the Spring Boot application we just downloaded. And identify the application.properties
file.
We will rename this file to application.yml
and its content should be:
## content of /src/main/resources/application.yml
spring:
datasource:
url: &dbURL jdbc:mysql://localhost:3306/kotlin_crud_db
username: &dbUser root
password: &dbPassword 123456
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
hibernate:
ddl-auto: update
Now we have configured our data source. To make sure we have everything running, let us go back to our terminal and type the next command:
gradle bootRun
Now you should see that in your terminal there was no error, and the spring boot app should be EXECUTING
Create Flyway migrations
Now we need to create some migration scripts so they get run by Flyway.
In the folder /src/main/resources/db/migration/
we'll create a file called V1__CreateTableTasks.sql
with content:
CREATE TABLE IF NOT EXISTS tasks(
id BIGINT(19) AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
start_date DATE,
due_date DATE,
status TINYINT NOT NULL,
priority TINYINT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=INNODB;
And we are going to create another script, in the same folder, called V2__TestData.sql
:
insert into tasks(title, status, priority, description) values ('First Task', 1, 1, 'My first task description');
insert into tasks(title, status, priority, description) values ('Another Task', 2, 2, 'Another task description');
Now let's run the command:
gradle bootRun
If you want to check what happened in the Database, you can run the following commands:
docker exec -it test-mysql mysql -u root -p
You'll get prompted Enter password:
and you need to input 123456
if you are following the tutorial exactly.
When you are inside the container you should see mysql>
. Now you can type the next command:
mysql> use kotlin_crud_db;
mysql> select * from tasks;
mysql> select * from flyway_schema_history;
Create the model
Now let's create a folder in /src/main/kotlin/com/example/demo
which is gonna be called model
.
And we are going to create a new Kotlin data class called Task
:
package com.example.demo.model
import java.time.LocalDateTime
import java.util.*
import javax.persistence.*
@Entity
@Table(name = "tasks")
data class Task(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val title: String,
val description: String? = null,
val startDate: Date? = null,
val dueDate: Date? = null,
val status: Int,
val priority: Int,
val createdAt: LocalDateTime? = LocalDateTime.now(),
val updatedAt: LocalDateTime? = LocalDateTime.now()
)
Create the JPA Repository
Now we need to create a JPA Repository where we are going to declare the TaskRepository
Interface, this is going to exist in /src/main/kotlin/com/example/demo/repository/TaskRepository.kt
:
package com.example.demo.repository
import com.example.demo.model.Task
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import javax.transaction.Transactional
@Repository
@Transactional(Transactional.TxType.MANDATORY)
interface TaskRepository : JpaRepository<Task, Long>
This will help us to handle all the interactions with the database for this tutorial. We are not going to write a single line of code to configure or to read anything from the database, besides the things we've already done.
Create the service
After the Repository is created, now we need to create our Service that will be the layer where we are going to communicate our Resource with our Repository.
We will create a file in /src/main/kotlin/com/example/demo/service/TaskService.kt
:
package com.example.demo.service
import com.example.demo.model.Task
import com.example.demo.repository.TaskRepository
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
@Service
class TaskService(private val taskRepository: TaskRepository) {
fun getTasks(): List<Task> =
taskRepository.findAll()
fun addTask(task: Task): ResponseEntity<Task> =
ResponseEntity.ok(taskRepository.save(task))
fun getTaskById(taskId: Long): ResponseEntity<Task> =
taskRepository.findById(taskId).map { task ->
ResponseEntity.ok(task)
}.orElse(ResponseEntity.notFound().build())
fun putTask(taskId: Long, newTask: Task): ResponseEntity<Task> =
taskRepository.findById(taskId).map { currentTask ->
val updatedTask: Task =
currentTask
.copy(
title = newTask.title,
description = newTask.description,
status = newTask.status,
startDate = newTask.startDate,
priority = newTask.priority,
dueDate = newTask.dueDate
)
ResponseEntity.ok().body(taskRepository.save(updatedTask))
}.orElse(ResponseEntity.notFound().build())
fun deleteTask(taskId: Long): ResponseEntity<Void> =
taskRepository.findById(taskId).map { task ->
taskRepository.delete(task)
ResponseEntity<Void>(HttpStatus.ACCEPTED)
}.orElse(ResponseEntity.notFound().build())
}
Create the resource (controller)
After creating the service we are going to tell the applcation how to map the different HTTP methods (GET, POST, PUT, DELETE).
In the file /src/main/kotlin/com/example/demo/resource/TaskResource.kt
:
package com.example.demo.resource
import com.example.demo.model.Task
import com.example.demo.service.TaskService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@RestController
@RequestMapping("/v1/api/tasks")
class TaskResource(private val taskService: TaskService) {
@GetMapping
fun getTasks(): List<Task> =
taskService.getTasks()
@PostMapping
fun addTask(@Valid @RequestBody task: Task): ResponseEntity<Task> =
taskService.addTask(task)
@GetMapping("/{id}")
fun getTaskById(@PathVariable(value="id") taskId: Long): ResponseEntity<Task> =
taskService.getTaskById(taskId)
@PutMapping("/{id}")
fun updateTaskById(
@PathVariable(value = "id") taskId: Long,
@Valid @RequestBody newTask: Task): ResponseEntity<Task> =
taskService.putTask(taskId, newTask)
@DeleteMapping("/{id}")
fun deleteTask(@PathVariable(value = "id") taskId: Long): ResponseEntity<Void> =
taskService.deleteTask(taskId)
}
Ok, great. Now... how do I test this?
I will probably follow up this tutorial with how to implement tests in a project.
But in the meantime you can test it with some good ol' curls:
Here we can test the get where we return all the tasks:
$ curl --location --request GET 'localhost:8080/v1/api/tasks'
With this other curl we can test retrieve one single task:
$ curl --location --request GET 'localhost:8080/v1/api/tasks/1'
Now we can test creating a new task with the following command:
$ curl --location --request POST 'localhost:8080/v1/api/tasks/' \
--header 'Content-Type: application/json' \
--data-raw '{
"title": "new 2nd task",
"status": 1,
"priority": 1,
"description":"2244 blah blah task"
}'
In case you want to update the task:
$ curl --location --request PUT 'localhost:8080/v1/api/tasks/2' \
--header 'Content-Type: application/json' \
--data-raw '{
"title": "blah blah new 2nd task",
"status": 1,
"priority": 1,
"description":"2244 blah blah task"
}'
And to delete the task that has the id=2 :
$ curl --location --request DELETE 'localhost:8080/v1/api/tasks/2'
Final source code
Just take me to the code already!
Alright my people, I hope you are staying safe in this COVID19 times.
We will get through this, hang in there.
Top comments (2)
Awesome, how to create findByTitle for Task?
I implemented findByTitle using the following steps:
add a new method to TaskRepository that execute a custom query using the @Query annotation:
@Query("SELECT * from tasks where title = :title", nativeQuery = true)
fun findByTitle(@param ("title") title: String): List
add a new method to TaskService that can be called from TaskResource, to execute the new repository method. This method ignores multiple rows returned, selecting the first row only to return in the response.
fun getTaskByTitle(title: String): ResponseEntity {
val tasks: List = taskRepository.findByTitle(title)
return if (tasks.isNullOrEmpty()) ResponseEntity.notFound().build()
else ResponseEntity.ok(tasks.get(0))
}
Example usage:
"localhost:8080/v1/api/tasks/title/First Task"