Using Catatumbo with Kotlin
Catatumbo is a great ORM framework for using Java with Google Cloud Datastore. It abstracts away Google’s clunky Datastore libraries and enables deserialization directly into Kotlin objects.
If you’re like me and love Kotlin you’ll quickly hit some issues though.
The Problem
Let’s say you have a pretty simple model to store users:
data class User(
@Identifier
val id: Long = 0,
// We don't wants users to be able to change their email
val email: String,
var isAdmin: Boolean = false,
// null when we haven't prompted the user yet
var favoriteFood: String?
)
Running this will give us an error though:
com.jmethods.catatumbo.UnsupportedConstructionStrategyException: Class User requires a public no-arg constructor or a public static method that returns a corresponding Builder instance. The name of the static method can either be newBuilder or builder
This is a great error message in that it tells us what it was expecting and a way to fix it. But first, let’s dig into why Catatumbo has these expectations in the first place.
How Catatumbo Works
When we insert our User
object Catatumbo does the following:
- Creates a new
User
instance by callingUser()
with no arguments (it doesn’t reuse theUser
we passsed in) - Populates each field of
User
using setters (e.g.setId()
) - Returns the
User
instance.
We’re failing on the first step here. User
doesn’t have a constructor that takes no arguments. A Kotlin data class only has that when all the fields have default values.
Quick&Dirty Solution
Just adding default values to User
won’t be enough though. When Catatumbo tries to overwrite val
fields like email
or id
it will fail because those are supposed to be immutable.
To make our data class fully compatible with Catatumbo we need to make every field mutable and have a default value:
data class User(
@Identifier
var id: Long = 0,
var email: String? = null,
var isAdmin: Boolean = false,
var favoriteFood:String? = null
)
This works but we’ve lost two of Kotlin’s awesome features: immutability and non-null types. Now we need to rely on developers to remember which fields are supposed to be immutable and which ones are non-nullable.
There is a better way to do this though!
The Better Solution: Builders
There is a way to deal with immutable and non-nullable fields without losing Kotlin’s advantages: the builder pattern. When inserting or loading Catatumbo does the following:
- If
User
haspublic static User builder()
orpublic static User getBuilder()
then continue, otherwise try to user setters - Call the
builder()
method to create a newUserBuilder
- For each field find a method to set it. The allowed patterns for these method names are
property()
,setProperty()
, orwithProperty()
- Call
.build()
to return a newUser
So let’s see this in action:
data class User(
@Identifier
val id: Long = 0,
val email: String,
var isAdmin: Boolean = false,
var favoriteFood: String?
) {
companion object {
@JvmStatic
fun builder(): UserBuilder = UserBuilder()
}
}
class UserBuilder {
var id: Long = 0
var email: String? = null
var isAdmin: Boolean? = false
var favoriteFood: String? = null
fun build(): User {
return User(
id = id,
email = email!!,
isAdmin = isAdmin!!,
favoriteFood = favoriteFood
)
}
}
User
’s fields are exactly the same as they were in the beginning. The only difference in the data class is that we’ve added a companion object that returns a builder. We need to annotate it with @JvmStatic
so that it gets generated as a public static method that Catatumbo recognizes.
Our UserBuilder
class looks a lot like the quick&dirty solution shown in the previous section. To get a no-arg constructor and allow mutation every property is a var
and has a default value.
You might be tempted to keep fields like email
non-nullable (e.g. var email:String = ""
). This comes with risks though. If an entity in Datastore doesn’t have email set and then when we load it, the instance of Person
will set email
to an empty string rather than erroring.
For production we’ll likely want to remove all the !!
non-null assertions and have more advanced error checking but this works to demonstrate our point.
The builder pattern isn’t perfect though. It’s mostly boilerplate and we now need to keep default values synchronized between the data class and the builder. It is on my todo list to experiment with Kotlin code generation using KotlinPoet to create these builders automatically though.
Conclusion
Catatumbo is a great library that I highly recommend using to make your life easier when dealing with Google Cloud Datastore. With a little bit of work you can get it working with Kotlin.
Have a different way of using Catatumbo with Kotlin? I’d love to hear! Get in touch with me on Twitter or LinkedIn.
Thanks to David Justo for reviewing the draft of this post!