尝试找到一种更语义化的方式来编写 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 的 val
和 var
来区分。
所以,我想使用以下 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 生成的文件,但这里没有问题。我刚刚向包装器添加了另一个构造函数,它接受必要的参数,否则使用默认值。它还可以处理验证和日志记录。
因此包装器就像实体和服务层之间的桥梁。
我有一个用 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 的 val
和 var
来区分。
所以,我想使用以下 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 生成的文件,但这里没有问题。我刚刚向包装器添加了另一个构造函数,它接受必要的参数,否则使用默认值。它还可以处理验证和日志记录。
因此包装器就像实体和服务层之间的桥梁。