JPA 和 Kotlin 空安全

问题描述 投票:0回答:1

我有一个带有两个字段的

@Embeddable
,这两个字段都在对象模型上定义为
non-nullable
(一个
String
和一个
OffsetDateTime
)。在数据库中,我为此添加了两个可为空的列,因为我希望 Embeddable 本身是可选的 (
nullable
)。这似乎按预期工作。

现在,我尝试通过将数据库中的

String
列设置为
null
来强制发生错误,并希望 Kotlin 在从数据库加载对象时引发
NullPointerException
。有趣的是,这并没有发生。当实体被翻译为
DTO
(两个字段也被定义为
non-nullable
)时,
NullPointerException
被抛出,但在我看来有点太晚了。

在调试器中观看此内容,我可以看到实体中的

String
字段确实是
null
,尽管 Kotlin 的 null 安全功能应该禁止这样做。

我错过了什么?

kotlin jpa nullpointerexception kotlin-null-safety
1个回答
0
投票

Java 中的反射是您注意到的情况的罪魁祸首。看到这一点我还是很惊讶。我已经在 Kotlin 服务器端使用 Spring 工作了一段时间,不幸的是 JPA 显示了这种行为。

我在 GitHub 上有一个项目,其中包含我为观察这种现象而创建的代码。在这个模块 Car Parts Kotlin 中,我创建了一个非常简单的 REST 控制器、服务和数据层,并且我在域中明确表示

Part
只接受不可为空的
name
:

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

因此

id
可以为空,而
name
不能为空。在开始测试服务之前,我在
PARTS
表中插入一行,其中
name
的值为 null。这是
data.sql
的内容:

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

整个示例并不重要。让我们专注于重要的事情。对于这个问题,让我们重点关注

CarPartsKotlinLauncherKotlinTest
上的测试示例。在测试开始时,我调用创建数据并将其插入数据库所需的脚本:

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

然后我还在运行时做了一些事情,这次是一个 java 类,我用

id
name
和 null 值创建一个实例:

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

最后我创建了一个测试来测试所有这些:

@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 }
        }"
    )
}

长话短说,这个测试的作用是检查我们从数据库获取的名称是否为空,尽管对于不可为空的属性,这永远不可能。我们在运行时使用 Java 反射创建的值也是如此。

最后,我们在控制台中得到的是这样的:

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
 

我同意,这真的很奇怪。 JPA 应该支持 Kotlin 并识别不可空类型。不幸的是,Spring 框架更适合与 Java 一起使用,并且 JDK 不理解不可空类型,除非我们使用像

@NotNull
这样的注释和类似的东西,但我们仍然需要 Spring 才能工作。不幸的是,即使当 Kotlin 使用 Java 类时,互操作性仍然存在这个问题,一些
@NotNull
注释即使没有 Spring 也能工作。没有什么比这更需要理解的了。我确实希望 Spring 框架能够对此进行标准化,以便我们在编码时不会遇到此类意外。 IMO 认为尽快完成此操作的论点是,通过允许将 null 分配给 Kotlin 不可空类型,人们可以利用一种罕见的情况,即您可以以任何方式有效地读取此类数据从数据库中。这可能会导致诸如推迟数据迁移任务、错误地围绕其创建业务逻辑,甚至创建不可靠的测试等问题。

© www.soinside.com 2019 - 2024. All rights reserved.