Overview

CRUD operations on Spring REST resources with Kotlin

No Comments

In this practical, hands-on post I would like to share some of my experience in building REST services wih JSON and Spring(Boot) using Kotlin. All examples can be transferred to Java, and if you use the indispensable Lombok library it doesn’t even look that ugly.
Today I will concentrate on the mother of all REST use cases: a simple CRUD (create/read/update/delete) api, implemented as a SpringBoot application in Kotlin. You can get the source code here:

git clone git@gitlab.com:jsprengers/spring-rest-kotlin-tips.git

A word of warning: REST is often mistakenly referred to as a protocol. REST is an architectural style that, unlike SOAP, is much more free-format, which is a blessing as well as a curse.
Our application offers all four CRUD operations using the respective http verbs POST,GET,PUT,DELETE. A GET request retrieves either a single entity (http://localhost:8080/customers/123), or the entire list: http://localhost:8080/customers>. The latter ‘get-all’ method typically supports filtering the list in the request parameter, e.g. ?age=20, although not in this present example.
Let’s assume that we want to store a user’s name (mandatory) and date of birth (optional) in our database. For this example we use a simple hashmap in-memory storage.

data class CustomerEntity(
    var id: Long? = null, 
    var name: String, 
    var dateOfBirth: LocalDate?)

Since the mandatory numeric ID is auto-assigned (with the aid of annotations in a real production system) by the persistence implementation after constructing the entity, you must type it as a nullable Long?, because for a very brief moment it will be null. Entity classes should never be exposed through your public REST endpoints, especially not when they contain JPA or (worse) Hibernate annotations, because that would require client code to have a compilation dependency on these persistence libraries. The REST endpoint must expose data transfer objects (DTOs) of our customers, which ideally reside in a separate api deliverable. Consumers of our service can then import this api jar.
Don’t be discouraged that you have to make a DTO class separate from the entity class. With a tailor-made DTO you can supply derived properties that are not in the database. Imagine that the consumer of our service is not interested in the exact date of birth but only the customer’s current age. The DTO returned from the service will look like this:

data class CustomerDTO(val id: Long, val name: String, val age: Int?)

and the serialized response would be:

{
    "id": 4,
    "name": "Jasper Sprengers",
    "age": 47
}

Web resources in REST are not persistent entities. We’re not obliged to create a one-to-one mapping between database tables and DTO classes. Our customer resource is a uniquely identifiable entity backed by a database entry, but its properties/fields may be partly derived. You could for example have fields for gender and preferred language in the entity and a localized salutation field in your DTO. Note that the id property is a proper Long field, and not a Long?. Since the entities are retrieved from storage, the ID is guaranteed to be non-null.
The REST endpoint looks like this:

@RequestMapping("/customers")
@RestController
class CustomerEndpoint @Autowired constructor(private val repository: CustomerRepository) {
 
    @RequestMapping(method = arrayOf(RequestMethod.GET))
    fun getAllCustomers(): List<CustomerDTO> {
        return repository.getAll()
    }
 
    @RequestMapping(value = "{id}", method = arrayOf(RequestMethod.GET))
    fun getCustomerById(@PathVariable("id") id: Long): CustomerDTO {
        return repository.getById(id)
    }
}

This is all well and good, but we still haven’t POST-ed anything to our repository. How about this?

@RequestMapping(method = arrayOf(RequestMethod.POST))
 fun createCustomer(@RequestBody createRequest: CustomerDTO): CustomerDTO {
     return repository.save(createRequest)
 }

No, that won’t work. The CustomerDTO class is read-only. The database expects a precise date of birth instead of an age and it doesn’t want a unique id from you. Better to create a custom DTO class specially catered for customer creation requests.

data class CreateCustomerDTO(val name: String, val dateOfBirth: LocalDate? = null)

This contains everything the storage requires. You can even leave the date of birth blank:

{
"name":"Aristotle"
}

The endpoint method looks like this:

@RequestMapping(method = arrayOf(RequestMethod.POST))
  fun createCustomer(@RequestBody createRequest: CreateCustomerDTO): CustomerDTO {
      return repository.save(createRequest)
  }

Deleting from a resource is easy. Since that requires only the correlating entity’s unique id, the canonical format is:

@RequestMapping(value = "{id}", method = arrayOf(RequestMethod.DELETE))
   fun deleteCustomerById(@PathVariable("id") id: Long): CustomerDTO {
       return repository.removeById(id)
  }

With creation and retrieval covered, let’s move on to PUT requests for updating. Updating a persisted entity requires at least a valid ID and the new values for the database fields we want to update. CustomerDTO has this non-null id field, but it doesn’t have the dateOfBirth, only the derived age field. Fine, then we’ll just create an exact copy of the entity with a non-null id field. Not so great either: now you must provide all of the customer’s values in the PUT request, even the ones that don’t need updating. From a transactional, multi-user point of view it is safer to update only those fields that need updating. That way Alice and Bob can simulaneously update different properties in Eve’s data without conflicts. Break out of the RDBMS mindset and don’t treat a PUT request as an instruction to overwrite row 42 in the CUSTOMER table with the following values. Rather, treat the payload of the PUT request as an instruction to alter one or more properties of an entity. Can we do that with our existing DTOs? If we were dealing with only non-nullable properties, then this would be a workable solution:

data class UpdateCustomerDTO(val id: Long, val name: String?, val dateOfBirth: LocalDate?)

Any property that has a null value in the DTO should be ignored for the update. So to update a customer’s name we PUT:

{
  "id": 8,
  "name": "Aristotle the wise"
}

And our code for updating would look like this:

fun save(updateRequest: UpdateCustomerDTO): CustomerDTO {
        val entity: CustomerEntity = getEntityFromStorage(updateRequest.id)
        if (updateRequest.name != null)
            entity.name = updateRequest.name
        //same drill for all the other fields      
    }

However, our CustomerEntity contains a date of birth field which can be populated or nullified at will during the entity’s lifetime. With the above solution you cannot express that difference. Whereas Javascript and Typescript can distinguish between undefined and null object properties, strongly typed Kotlin and Java cannot. We need some means to express the three options of non-null (overwrite), null (set to null) and undefined (ignore for update). At first I thought of putting the updatable field in an optional list. Use a list of one for non-null elements, an empty list to set the value to null and leave the field out or null to ignore it.

{
  "id": 8,
  "name": ["Aristotle the wise"]
  "dateOfBirth": null
}

It’s easy to implement in JavaScript client code, but it just feels wrong. Better to create an additional generic wrapper, like so:

data class UpdateField<T>(val value: T? = null, val ignored: Boolean = false) {
 
    fun get(): T = value ?: throw IllegalStateException("value cannot be null.")
    fun getOrNull(): T? = value
 
    companion object {
        fun <T> ignore() = UpdateField<T>(ignored = true)
        fun <T> setNull() = UpdateField<T>()
        fun <T> of(value: T) = UpdateField<T>(value = value)
    }
}

Have a look at the IntegrationTest for usage of the factory methods in the companion object. Yes, it’s a bit of extra code, but it’s highly generic, hence re-usable. Since the Kotlin source code is so compact and the classes highly related I like to put all three flavours in the same Customer.kt source file

data class CustomerDTO(val id: Long, val name: String, val age: Int?)
data class CreateCustomerDTO(val name: String, val dateOfBirth: LocalDate? = null)
data class UpdateCustomerDTO(val id: Long,
                             val name: UpdateField<String> = UpdateField.ignore(),
                             val dateOfBirth: UpdateField<LocalDate> = UpdateField.ignore())

Now to update a specific field to a new value, the JSON looks like:

{
"id":8,
"name":{"value": "Aristotle the wise", "ignored": false},
"dateOfBirth":{"value": null, "ignored": true}
}

I agree that’s pretty verbose. Luckily the ignored property is false by default in the UpdateField, so you can leave it out. Any UpdateCustomer field not present in the JSON is populated with a default ‘ignore’ object, so this is equivalent to the previous JSON snippet:

{
"id":8,
"name":{"value": "Aristotle the wise"}
}

The value field is null by default, so here’s how you set a nullable field to null:

{
"id":8,
"dateOfBirth":{}
}

This however will cause an error:

{
"id":8,
"name":{}
}

Because name is a non nullable field. The actual save method in our simple hashmap-backed repository looks like this:

 fun save(customerUpdate: UpdateCustomerDTO): CustomerDTO {
        val entity: CustomerEntity = cache[customerUpdate.id] ?: throw IllegalArgumentException("No customer with id ${customerUpdate.id}")
        if (!customerUpdate.name.ignored)
            entity.name = customerUpdate.name.get()
        if (!customerUpdate.dateOfBirth.ignored)
            entity.dateOfBirth = customerUpdate.dateOfBirth.getOrNull()
        cache.put(customerUpdate.id, entity)
        return createDTO(entity)
    }

And of course if a persistent field for some reason is never eligible for updating, then don’t include it in the update dto. A final technical caveat when using Kotlin single-constructor data classes with jackson serialization: you must import the jackson-module-kotlin> module and register the KotlinModule for it to work properly. Read more.
I hope these examples can help you to make your existing REST implementations more flexible and user-friendly. If I have somehow managed to win you over to the Kotlin camp, then so much the better. If not, I’ll be back in a couple of weeks.

Jasper joined codecentric NL in 2015 but has been coding since the early eighties. Having a background in English and linguistics, he always likes to stress the importance of human language in software.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Comment

Your email address will not be published. Required fields are marked *