# 元数据配置

在大多数应用中,我们需要预先对数据表进行建模,创建数据库实体类,easysql支持从实体类中进行元数据抽取。

如果你的需求是动态构建报表查询,并不能事先对表结构建模,可以先跳过此部分,后续部分将会介绍如何使用动态的表名和字段名来构造查询。

# 基础配置

下面有一个case class组织的实体类, 可空字段使用Option类型:

case class User(id: Int, name: Option[String])

我们可以在类上添加注解Table(可以省略),在字段上添加注解Column(可以省略),主键字段添加PrimaryKeyIncrKey(对应自增主键):

@Table
case class User(@IncrKey id: Int, @Column("user_name") name: Option[String])

注解的参数为实际的数据库表名或字段名,如果代码中的名字与数据库中的名字符合驼峰命名转到蛇形命名的映射关系,则可以省略。

如果想自定义主键的生成策略,在PrimaryKeyGenerator注解中填写一个高阶函数即可,以java标准库中的UUID为例:

@Table
case class User(
    @PrimaryKeyGenerator("user_id", () => UUID.randomUUID().toString) id: String, 
    @Column("user_name") name: Option[String]
)

然后使用asTable来从实体类中生成表的代理对象,后续的大多数查询操作都要使用这个对象:

@Table
case class User(@IncrKey id: Int, @Column("user_name") name: Option[String])

val user = asTable[User]

asTable会将实体类转换为TableSchema表结构信息,这样我们就能用这些元数据来构造查询了:

// 查询
val s = select (user) from user where user.id === 1

// 增删改
val userRow = User(1, "x")
val i = insert(userRow)
val u = update(userRow)
val sv = save(userRow)
val d = delete[User](1)

有些时候,我们需要在实体类中放入一些自定义的类型,比如状态的枚举,为了让easysql知晓如何正确生成查询,并把查询结果反序列化到实体类,我们需要做一些额外的配置:

enum State {
    case Open
    case Close
}

object StateSerializer extends CustomSerializer[State, Int] {
    def toValue(x: State): Int = x match {
        case State.Open => 1
        case State.Close => 2
    }

    def fromValue(x: Any): State = x match {
        case 1 => State.Open
        case _ => State.Close
    }
}

case class User(
    @IncrKey id: Int, 
    @Column("user_name") name: Option[String]
    @CustomColumn("state", StateSerializer) state: State
)

val user = asTable[User]

我们需要编写一个object,继承CustomSerializer,第一个类型参数为自定义的类型,第二个类型参数为表的字段类型,并填写用来生成sql的toValue方法和用于反序列化的fromValue方法。

最后,在实体类中的自定义类型字段上添加注解CustomColumn,easysql会从注解信息中尽力生成类型安全的表结构信息。

更详细的查询构造方法会在后文查询构造部分介绍。

# 元数据的细节

如果我们有这样两个实体类,并为其生成表结构的元数据信息:

@Table
case class User(@IncrKey id: Int, name: Option[String])
val user = asTable[User]

@Table
case class Post(@IncrKey id: Int, userId: Int, name: Option[String])
val post = asTable[Post]

easysql会读取实体类的类型信息和注解信息,使用结构类型,生成实际的元数据代理信息,上面两个表结构的实际类型信息为:

val user: TableSchema[User] {
    val id: PrimaryKeyExpr[Int, "id"] 
    val name: ColumnExpr[String, "name"]
}

val post: TableSchema[Post] { 
    val id: PrimaryKeyExpr[Int, "id"] 
    val userId: ColumnExpr[Int, "userId"] 
    val name: ColumnExpr[String, "name"]
}

虽然我们可以使用Scala3的类型推断功能,不必显式写出生成的类型,但是了解easysql代理出的类型仍然是有意义的。

比如,我们可以给不同的查询添加上相同的sql片段(通常是为了处理逻辑删除标记、记录数据更新时间等):

inline def filterId[T <: Product](table: TableSchema[T]{ val id: PrimaryKeyExpr[Int, "id"] }) =
    select (table) from table where table.id > 0

val userQuery = filterId(user)
val postQuery = filterId(post)

使用结构类型约束参数的字段信息,即可做到这一点。需要注意的是,TableSchema的泛型参数上界为Product,并且这样的共用方法需要使用inline标记。

关于结构类型的更多信息,可以自行查看Scala3的官方文档。