Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table annotation support specify supper class #561

Closed
wants to merge 3 commits into from

Conversation

qumn
Copy link
Contributor

@qumn qumn commented Jun 13, 2024

@qumn
Copy link
Contributor Author

qumn commented Jun 14, 2024

According to this issue, the KSTypeNotPresentException error occurred even though the class exists in the project. The issue was temporarily resolved using by the comment

@vincentlauvlwj
Copy link
Member

vincentlauvlwj commented Jun 14, 2024

让用户指定父类有什么用处吗?可以说一说你的使用场景

@qumn
Copy link
Contributor Author

qumn commented Jun 14, 2024

对于一个Entity,对于其中的一部分字段可以在父类中通过Ktorm提供的api更灵活的实现。剩下的字段使用 KSP 生成。
比如在 DDD 里面对于主键创建一个包装类,下面的代码在父类中使用transform来实现自定义类型。

@JvmInline
value class UID(val value: Long)

interface UserBaseEntity<E : Entity<E>> : Entity<E> {
    var uid: UID
}

abstract class UserBaseTable<E : UserBaseEntity<E>>(
    tableName: String,
    alias: String? = null,
    catalog: String? = null,
    schema: String? = null,
    entityClass: KClass<E>? = null,
) : org.ktorm.schema.Table<E>(
    tableName, alias, catalog, schema, entityClass
) {
    val uid = long("uid").transform({ UID(it) }, { it.value }).primaryKey().bindTo { it.uid }
}

@Table(
    "t_user",
    superClass = UserBaseTable::class,
    ignoreProperties = ["uid"]
)
interface User: UserBaseEntity<User> {
    var username: String
    var password: String
}

生成的代码如下:

/**
 * Table t_user.
 */
public open class Users(alias: String?) : UserBaseTable<User>("t_user", alias) {
    /**
     * Column username.
     */
    public val username: Column<String> = varchar("username").bindTo { it.username }

    /**
     * Column password.
     */
    public val password: Column<String> = varchar("password").bindTo { it.password }

    /**
     * Return a new-created table object with all properties (including the table name and columns and
     * so on) being copied from this table, but applying a new alias given by the parameter.
     */
    public override fun aliased(alias: String): Users = Users(alias)

    /**
     * The default table object of t_user.
     */
    public companion object : Users(alias = null)
}

@vincentlauvlwj
Copy link
Member

想法很棒,但这方案对于一个通用框架来说还不够完美,以你的代码为例

@Table(
    "t_user",
    superClass = UserBaseTable::class,
    ignoreProperties = ["uid"]
)
interface User: UserBaseEntity<User> {
    var username: String
    var password: String
}

这里有些没必要的噪音:

  1. 通常抽象表父类和抽象实体父类都是搭配在一起使用的,既然我的实体类已经继承了 UserBaseEntity,那么表对象当然也应该继承 UserBaseTable,这是没什么疑问的,没必要在注解上单独指定
  2. ignoreProperties = ["uid"] 是因为 ktorm-ksp 默认为所有实体类的属性生成列字段,包括抽象实体类上的属性,这会导致生成的代码里也有一个 uid 列,和父类产生冲突,因此我们不得不手动把 uid 忽略掉

这两个问题会导致,如果我们项目中使用了抽象表父类,我们就不得不把这坨注解复制到每个实体类上,这种用户体验对一个公共框架来说是不可接受的

我提供一个方案你看如何?首先增加一个 @SuperTableClass 注解,使用这个注解来指定抽象表父类,我们把这个注解标注到 UserBaseEntity 上,就像这样:

@SuperTableClass(UserBaseTable::class)
interface UserBaseEntity<E : Entity<E>> : Entity<E> {
    var uid: UID
}

abstract class UserBaseTable<E : UserBaseEntity<E>>(
    tableName: String,
    alias: String? = null,
    catalog: String? = null,
    schema: String? = null,
    entityClass: KClass<E>? = null,
) : org.ktorm.schema.Table<E>(
    tableName, alias, catalog, schema, entityClass
) {
    val uid = long("uid").transform({ UID(it) }, { it.value }).primaryKey().bindTo { it.uid }
}

然后,普通的实体类,就只需要继承 UserBaseEntity,加上简单的 @Table 注解即可:

@Table
interface User : UserBaseEntity<User> {
    var username: String
    var password: String
}

Ktorm 需要能识别到 UserBaseEntity 上的注解,使用该注解指定的父类,同时自动忽略掉 UserBaseEntity 中的公共属性,避免重复生成列字段。这个方案,使用起来是不是简单很多呢?

@vincentlauvlwj
Copy link
Member

当然,为了功能的完备性和严谨性,我们还需要实现这些:

  1. @SuperTableClass 只能使用在基于 interface 的实体类上,不支持普通 class
  2. 如果把该注解标注到抽象实体类上,则如上所述,所有子类均使用指定的表父类,并且生成列字段的时候忽略抽象实体类中的公共属性(注意:生成实体类的伪构造函数和 copy 函数时仍然需要考虑抽象实体类中的公共属性,不可忽略)
  3. 如果把该注解标注到普通实体类上,则单独给该实体类指定表父类(这个场景在没有抽象实体类,只需要单独指定表父类的时候有用)
  4. 如果在实体类的继承层次中发现多个 @SuperTableClass 注解,则抛出异常提示用户

@qumn
Copy link
Contributor Author

qumn commented Jun 15, 2024

我也认为 @SuperTAbleClass 的方案会更好. 但对于第四点存在这样一种情况, 多个 @SuperTableClass 所指定的类之间存在继承链, 生成的Table 可以继承自这条继承链中的最底层.
如下代码:

@SuperTableClass(ATable::class)
Interface AEntity : Entity : {}
abstract class ATable(...) : Table(...)

@SuperTableClass(BTable::class)
Interface BEntity : AEntity : {}
abstract class BTable(...) : ATable(...)

@Table(name = "CTable")
Interface CEntity : BEntity {}

虽然 CEntity 的继承链上存在两个 @SuperTableClass, 但它们所指定的类之间存在继承关系, 因此生成的 CTable 可以继承自 这个链条中最底层的BTable , 这并不会存在冲突, 也是一个合理的需求.

Class CTable(...) : BTable(...) {}

@vincentlauvlwj
Copy link
Member

嗯,我之所以制定第 4 条规则,是考虑到 interface 的多继承,比如 interface A : B, C,但是 B 和 C 之间互相没有继承关系,这种情况就会出现冲突

当然,我们也可以实现你说的这种,自动检测出这些表对象的继承关系,当只有一条继承链的时候,使用最底层的子类,当存在多条继承链的时候才抛出异常

两种方案都 OK,考虑到开发成本,直接禁止继承也是可以接受的,我感觉这个需求并不算常见

@qumn
Copy link
Contributor Author

qumn commented Jun 16, 2024

我尝试实现了下 @SuperTableClass, #562. 因为不是很熟悉KSP, 可能会存在些问题. @vincentlauvlwj

@qumn qumn closed this Jun 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants