尝试找到一种更语义化的方式来编写 Kotlin 中的 Spring 实体

Trying the find a more semantic way for writing Spring Entity in Kotlin

我有一个用 Kotlin 编写的 Spring 实体:

@Entity
class Book(
    @Id
    var id: Long? = null,
    var title: String? = null, // cannot actually be null, immutable
    var isInStock: Boolean? = null, // cannot actually be null
    var description: String? = null,
 )

所有字段必须可以为空,因为Spring需要初始化空对象。

但是,这使得实体的使用变得复杂,因为我总是必须将可为 null 的类型转换为其不可为 null 的等价物。这不是语义的:我真的很想看看,哪些字段实际上可以为空,哪些字段不能(或者在初始化过程中只能为 null)。

此外,有些字段是可变的,但有些字段在创建实体后不应更改。最好使用 Kotlin 的 valvar 来区分。

所以,我想使用以下 class:

class BetterBook(
    val id: Long,
    val title: String,
    var isInStock: Boolean,
    var description: String? = null,
 )

这样很清楚哪些字段是可变的,哪些字段可以是null.

我考虑在 Book 周围创建一个包装器。有没有人有过类似的想法?这在架构上会是一个好的解决方案吗?

JPA(不是 Spring)不需要参数构造函数和可变属性,因此在这方面您真的无能为力。

您可以将您的 JPA 实体转换或包装成看起来更像您实际想要的 类 的东西,但可能的结果是它只会使您的代码膨胀而不会使事情更易于使用。

您可能会考虑一个完全不同的持久层,它在构造函数方面更加灵活。 Spring 数据 JDBC 实际上可能符合要求。它对不可变实体有一些支持。它也与 JPA 有很大不同,JPA 可能是好事也可能是坏事,具体取决于您对 JPA 的看法。

阅读这两篇文章以了解其背后的基本概念:https://spring.io/blog/2018/09/17/introducing-spring-data-jdbc and https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates

你实际上可以做你想做的:用 val 定义 class 而不是 var 作为不可变属性,给它们 non-nullable 类型,并且有没有默认构造函数。 Spring 使用反射获取 Kotlin 代码在设置数据库值时生成的 (read-only) 属性的成员变量。对于 default-constructor(它在运行时确实需要),有一个 kotlin-jpa plugin 会在编译代码中自动为您生成它。所以基本上保持你的代码完全按照你惯用的方式编写它(就像你为 BetterBook 所做的那样),并用 @Entity 注释 class,但确保应用该插件。

我正在使用 JHipster(或 KHipster,它的 Kotlin 蓝图)。因为所有实体都是由 KHipster 生成的,所以我不想更改这些文件,因为当我重新生成实体时,我的所有更改都会被覆盖。在尝试了不同的解决方案后,我决定添加一个额外的包装层。

包装器就像实体的公关经理。它可以从实体构建,然后可以在服务层进行修改,一旦需要保存它,包装器就会返回更新后的实体。

KHipster 实体大致如下所示:

@Entity
class Agreement(
    @Id
    var id: Long? = null, // should be immuatble
    var description: String? = null, // cannot be null, immutable if `isSigned == true`
    var isSigned: Boolean? = null,
 )

直接使用这个实体很容易出错,而且不是很方便。包装器解决了所有这些问题:

class AgreementWrapper(agreement: Agreement) {
    val id = agreement.id  // NOTICE that we can now use `val` 
    var description: String = agreement.description!! // NOT NULLABLE 
        set(value) {
            if (isSigned == true) throw Exception("A signed agreement cannot be changed!")
            field = value
        }
    var isSigned = agreement.isSigned!!
        set(value) {
            if (isSigned == true && value == false) throw Exception("You cannot undo it!")
            field = value
        }

    val entity: Agreement // This will give us back the updated entity
        get() {
            return Agreement(id, description, isSigned)
        }
}

此外,包装器还可以用于创建新实体。为实体添加默认值(例如添加时间戳)也需要更改 KHipster 生成的文件,但这里没有问题。我刚刚向包装器添加了另一个构造函数,它接受必要的参数,否则使用默认值。它还可以处理验证和日志记录。

因此包装器就像实体和服务层之间的桥梁。