JPA and Kotlin Null Safety

166 Views Asked by At

I have an @Embeddable with two fields, both of which on the object model are defined non-nullable (a String and an OffsetDateTime). In the database I added two nullable columns for this because I want the Embeddable itself to be optional (nullable). This seems to work as expected.

Now, I am trying to force an error by setting the String column in the database to null and would expect Kotlin to raise a NullPointerException when the object is loaded from the database. Interestingly enough this does not happen. When the entity is translated to a DTO (both fields are also defined as non-nullable), the NullPointerException is thrown but a little too late in my mind.

Watching this in the debugger I can see that the String field in the entity is indeed null, even though Kotlin's null safety feature should prohibit this.

What am I missing?

1

There are 1 best solutions below

0
João Esperancinha On

Reflection in Java is the culprit in the case you are noticing. It still surprised me to see this. I've been working in Kotlin server-side for a while with spring and JPA unfortunately displays this behavior.

I have a project on GitHub that contains code that I have created to observe this phenomenon. In this module Car Parts Kotlin, I have created a very simple REST controller, service and data layers and I am explicitly saying in the domain that Part only accepts a non-nullable name:

@Entity
@Table(name = "PARTS")
data class Part(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id: Long? = null,
    val name: String
)

So id can be null and name cannot. Before I start the test services, I'm inserting a row in the PARTS table with null for the value of name. This is the content of data.sql:

INSERT INTO PARTS(ID, NAME) values (1, NULL);

The whole example isn't really important to have a look at. Let's just focus on what is important. For this question let's focus on the example for the tests on CarPartsKotlinLauncherKotlinTest. In the beginning of the tests I'm calling the scripts necessary to create and insert data into our database:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Sql("classpath:schema.sql", "classpath:data.sql")

Then I also make something during runtime, this time a java class where I create an instance with id and name with null values:

public class MarkedUtils {
    static Part getPartWithANullField() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        return Part.class.getDeclaredConstructor().newInstance();
    }
}

Finally I create a test to test all of this:

@Test
fun `should get a null value even though it shouldn't be null`() {
    partRepository.findByIdOrNull(1).should {
        it.shouldNotBeNull().id shouldBe 1L
        val name: String = it.shouldNotBeNull().name
        name.shouldBeNull()
        changeName(name)
        val partWithANullField = getPartWithANullField()
        changeName(partWithANullField.name)
    }
}

private fun changeName(name: String) {
    Part::class.declaredMemberProperties.forEach {
        println("Field $it isMarkedNullable=${it.returnType.isMarkedNullable}")
    }
    println("This name is: $name. It shouldn't but it is null!")
    println(
        "And this is not a \"null\" string, it is actually a null value right? ${
            name.shouldBeNull().run { true }
        }"
    )
}

What this test does, long story short, is to check that the name we get from the database is null, even though, with a non-nullable property, that should never be possible. The same goes for the value we create during runtime with Java reflection.

At the end, what we get in the console, in between, is something like this:

Field val org.jesperancinha.smtd.carparts.model.jpa.Part.id: kotlin.Long? isMarkedNullable=true
Field val org.jesperancinha.smtd.carparts.model.jpa.Part.name: kotlin.String isMarkedNullable=false
This name is: null. It shouldn't but it is null!
And this is not a "null" string, it is actually a null value right? true
Field val org.jesperancinha.smtd.carparts.model.jpa.Part.id: kotlin.Long? isMarkedNullable=true
Field val org.jesperancinha.smtd.carparts.model.jpa.Part.name: kotlin.String isMarkedNullable=false
This name is: null. It shouldn't but it is null!
And this is not a "null" string, it is actually a null value right? true
 

And I agree, this is all really bizarre. And the JPA should support Kotlin and recognize the non-nullable types. Unfortunately the Spring Framework is more prepared to work with Java and the JDK does not understand non-nullable types unless we use annotations like @NotNull and things like that, but we still need Spring for that to work. Interoperability still has this problem unfortunately even though when Kotlin uses Java classes, some of the @NotNull annotations work even without Spring. There isn't much more to understand than that. I do hope that the Spring Framework standardizes this so that we don't get these kind of surprises while coding. The argument for that to be done as soon as possible is only IMO, that by allowing a null to be assigned to a Kotlin non-nullable type, people may take advantage of a rare situation where you can effectively read this kind of data any way from the database. This can lead to things like postponing data migration tasks, creating business logic wrongly around it and even creating non reliable tests.