Kotlin Help

Add database support for Spring Boot project

In this part of the tutorial, you'll add and configure a database to your project using JDBC. In JVM applications, you use JDBC to interact with databases. For convenience, the Spring Framework provides the JdbcTemplate class that simplifies the use of JDBC and helps to avoid common errors.

Add database support

The common practice in Spring Framework based applications is to implement the database access logic within the so-called service layer – this is where business logic lives. In Spring, you should mark classes with the @Service annotation to imply that the class belongs to the service layer of the application. In this application, you will create the MessageService class for this purpose.

In the DemoApplication.kt file, create the MessageService class as follows:

import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate @Service class MessageService(val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message) { db.update( "insert into messages values ( ?, ? )", message.id, message.text ) } }
Constructor argument and dependency injection – (val db: JdbcTemplate)

A class in Kotlin has a primary constructor. It can also have one or more secondary constructors. The primary constructor is a part of the class header, and it goes after the class name and optional type parameters. In our case, the constructor is (val db: JdbcTemplate).

val db: JdbcTemplate is the constructor's argument:

@Service class MessageService(val db: JdbcTemplate)
Trailing lambda and SAM conversion

The findMessages() function calls the query() function of the JdbcTemplate class. The query() function takes two arguments: an SQL query as a String instance, and a callback that will map one object per row:

db.query("...", RowMapper { ... } )


The RowMapper interface declares only one method, so it is possible to implement it via lambda expression by omitting the name of the interface. The Kotlin compiler knows the interface that the lambda expression needs to be converted to because you use it as a parameter for the function call. This is known as SAM conversion in Kotlin:

db.query("...", { ... } )


After the SAM conversion, the query function ends up with two arguments: a String at the first position, and a lambda expression at the last position. According to the Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses. Such syntax is also known as trailing lambda:

db.query("...") { ... }
Underscore for unused lambda argument

For a lambda with multiple parameters, you can use the underscore _ character to replace the names of the parameters you don't use.

Hence, the final syntax for query function call looks like this:

db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) }

Update the MessageController class

Update MessageController to use the new MessageService class:

import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.PostMapping @RestController class MessageController(val service: MessageService) { @GetMapping("/") fun index(): List<Message> = service.findMessages() @PostMapping("/") fun post(@RequestBody message: Message) { service.save(message) } }
@PostMapping annotation

The method responsible for handling HTTP POST requests needs to be annotated with @PostMapping annotation. To be able to convert the JSON sent as HTTP Body content into an object, you need to use the @RequestBody annotation for the method argument. Thanks to having Jackson library in the classpath of the application, the conversion happens automatically.

Update the MessageService class

The id for Message class was declared as a nullable String:

data class Message(val id: String?, val text: String)

It would not be correct to store the null as an id value in the database though: you need to handle this situation gracefully.

Update your code to generate a new value when the id is null while storing the messages in the database:

import java.util.UUID @Service class MessageService(val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message) { val id = message.id ?: UUID.randomUUID().toString() db.update( "insert into messages values ( ?, ? )", id, message.text ) } }
Elvis operator – ?:

The code message.id ?: UUID.randomUUID().toString() uses the Elvis operator (if-not-null-else shorthand) ?:. If the expression to the left of ?: is not null, the Elvis operator returns it; otherwise, it returns the expression to the right. Note that the expression on the right-hand side is evaluated only if the left-hand side is null.

The application code is ready to work with the database. It is now required to configure the data source.

Configure the database

Configure the database in the application:

  1. Create schema.sql file in the src/main/resources directory. It will store the database object definitions:

    Create database schema
  2. Update the src/main/resources/schema.sql file with the following code:

    CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL );

    It creates the messages table with two columns: id and text. The table structure matches the structure of the Message class.

  3. Open the application.properties file located in the src/main/resources folder and add the following application properties:

    spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:file:./data/testdb spring.datasource.username=name spring.datasource.password=password spring.sql.init.schema-locations=classpath:schema.sql spring.sql.init.mode=always

    These settings enable the database for the Spring Boot application.
    See the full list of common application properties in the Spring documentation.

Add messages to database via HTTP request

You should use an HTTP client to work with previously created endpoints. In IntelliJ IDEA, use the embedded HTTP client:

  1. Run the application. Once the application is up and running, you can execute POST requests to store messages in the database. Create the requests.http file in the src/main/resources folder and add the following HTTP requests:

    ### Post "Hello!" POST http://localhost:8080/ Content-Type: application/json { "text": "Hello!" } ### Post "Bonjour!" POST http://localhost:8080/ Content-Type: application/json { "text": "Bonjour!" } ### Post "Privet!" POST http://localhost:8080/ Content-Type: application/json { "text": "Privet!" } ### Get all the messages GET http://localhost:8080/
  2. Execute all POST requests. Use the green Run icon in the gutter next to the request declaration. These requests write the text messages to the database:

    Execute POST request
  3. Execute the GET request and see the result in the Run tool window:

    Execute GET requests

Alternative way to execute requests

You can also use any other HTTP client or the cURL command-line tool. For example, run the following commands in the terminal to get the same result:

curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Hello!\" }" curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Bonjour!\" }" curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Privet!\" }" curl -X GET --location "http://localhost:8080"

Retrieve messages by id

Extend the functionality of the application to retrieve the individual messages by id.

  1. In the MessageService class, add the new function findMessageById(id: String) to retrieve the individual messages by id:

    import org.springframework.jdbc.core.query @Service class MessageService(val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun findMessageById(id: String): List<Message> = db.query("select * from messages where id = ?", id) { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message) { val id = message.id ?: UUID.randomUUID().toString() db.update( "insert into messages values ( ?, ? )", id, message.text ) } }
  2. Add the new index(...) function with the id parameter to the MessageController class:

    import org.springframework.web.bind.annotation.* @RestController class MessageController(val service: MessageService) { @GetMapping("/") fun index(): List<Message> = service.findMessages() @GetMapping("/{id}") fun index(@PathVariable id: String): List<Message> = service.findMessageById(id) @PostMapping("/") fun post(@RequestBody message: Message) { service.save(message) } }
    Retrieving a value from the context path

    The message id is retrieved from the context path by the Spring Framework as you annotated the new function by @GetMapping("/{id}"). By annotating the function argument with @PathVariable, you tell the framework to use the retrieved value as a function argument. The new function makes a call to MessageService to retrieve the individual message by its id.

    vararg argument position in the parameter list

    The query() function takes three arguments:

    • SQL query string that requires a parameter to run

    • `id`, which is a parameter of type String

    • RowMapper instance is implemented by a lambda expression

    The second parameter for the query() function is declared as a variable argument (vararg). In Kotlin, the position of the variable arguments parameter is not required to be the last in the parameters list.

Here is a complete code of the DemoApplication.kt:

package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import java.util.UUID import org.springframework.jdbc.core.query import org.springframework.web.bind.annotation.* @SpringBootApplication class DemoApplication fun main(args: Array<String>) { runApplication<DemoApplication>(*args) } @RestController class MessageController(val service: MessageService) { @GetMapping("/") fun index(): List<Message> = service.findMessages() @GetMapping("/{id}") fun index(@PathVariable id: String): List<Message> = service.findMessageById(id) @PostMapping("/") fun post(@RequestBody message: Message) { service.save(message) } } data class Message(val id: String?, val text: String) @Service class MessageService(val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun findMessageById(id: String): List<Message> = db.query("select * from messages where id = ?", id) { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message) { val id = message.id ?: UUID.randomUUID().toString() db.update( "insert into messages values ( ?, ? )", id, message.text ) } }

Run the application

The Spring application is ready to run:

  1. Run the application again.

  2. Open the requests.http file and add the new GET request:

    ### Get the message by its id GET http://localhost:8080/id
  3. Execute the GET request to retrieve all the messages from the database.

  4. In the Run tool window copy one of the ids and add it to the request, like this:

    ### Get the message by its id GET http://localhost:8080/f16c1d2e-08dc-455c-abfe-68440229b84f
  5. Execute the GET request and see the result in the Run tool window:

    Retrieve message by its id

Next step

The final step shows you how to use more popular connection to database using Spring Data.

Proceed to the next chapter

Last modified: 22 February 2024