开源 | 携程机票跨端 Kotlin DSL 数据库框架 SQLlin

作者简介

禹昂,携程机票移动端资深工程师,专注于 Kotlin 移动端跨平台领域,Kotlin 中文社区核心成员,图书《Kotlin 编程实践》译者。

一、背景

2022年9月 Kotlin 1.7.20 发布之后,Kotlin Multiplatform Mobile(简称KMM)进入 Beta 阶段,Kotlin/Native new memory management 也变更为默认启用状态。无论从多端统一性还是性能上来看,Kotlin Multiplatform 都进入了下一个里程碑阶段。

携程机票移动端团队在2021年介绍过 KMM 技术在机票产线的落地情况(参考链接 1),2022 年年中开源了团队首个 KMM 项目—— MMKV-Kotlin(参考链接 2),并撰文(参考链接 3)详述 MMKV-Kotlin 的研发过程和一些常见问题。目前继续在 Kotlin Multiplatform 开源领域发力,打造出了基于 DSL 及 Kotlin Symbol Processor(KSP)开发的 SQLite 框架—— SQLlin。


二、需求调研


2.1 为什么要使用 SQLite 框架?

在移动端开发领域,在对 CRUD 操作有着复杂需求的数据存取场景上,SQLite 一直是首选方案。它同时内置于 Android 与 iOS 系统框架中,开发者无需增加额外的包大小。在数据的增删查改上它支持绝大部分 SQL 语法,功能足够强大。SQLite 本身是 C 语言库,虽然官方为它打造了多种语言及开发环境的 wrapper,但目前还不直接支持 Kotlin Multiplatform。因此,寻找或开发一款支持 Kotlin Multiplatform 的 SQLite 框架是我们的必选项。

但同时我们也注意到,SQLite 框架本身的意义并不仅仅在于扩展其支持的技术栈。例如,在 Android 开发中,我们有 Android Framework SQLite Java API,但是开发者们通常会在项目中使用 Jetpack Room 来操作数据库。在 iOS 开发中,开发者可以直接调用 SQLite C API,但是大家也仍然倾向于选择类似 FMDB 这样的框架。原因主要在于以下三点:

(1)SQLite 的原始 API 颗粒度较细,直接在业务代码中使用较为繁琐且容易出错。

(2)SQL 语句以字符串的形式存在于代码中,不受编译器检查。

(3)SQLite 不支持直接存取对象,将基本数据类型与对象进行转换需要编写大量样板代码。

我们期待我们未来使用的 SQLite 框架在支持 Kotlin Multiplatform 的同时可以解决掉以上三个痛点问题。

2.2 开源方案调研

在开发一个项目之前,我们通常会在开源社区寻找成熟的解决方案,如果可以完全契合我们的需求则没有必要重复造轮子。但如果我们调研的项目不完全符合我们的预期,则仍然可以学习其设计思想,为我们自己的设计与研发提供思路与参考。

2.2.1 Jetpack Room

Jetpack Room(参考链接 4)是 Google 官方提供的 SQLite 框架,最初用 Java 打造,并非专为 Kotlin 而生。它仅能用于 Android 开发,暂不支持 Kotlin Multiplatform,因此不符合我们的期望,但我们可以参考它的 API 设计:

@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)


@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>


    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>


    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
           "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User


    @Insert
    fun insertAll(vararg users: User)


    @Delete
    fun delete(user: User)
}


@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

它的 API 采用 DAO(Data Access objects)思想,它可以自动完成对象到 SQL 语句的序列化与查询结果 Cursor 到对象的反序列化。开发者只需要定义 DAO 的 interface,并用它提供的注解描述需要操作的对象即可。Room 采用 APT/KAPT(目前正在向 KSP 迁移)对注解进行处理并生成代码,可以避免用户手动编写大量样板代码。用户在使用 Room 时仅需要通过 DAO set/get 对象即可。

不过它也有一些问题。例如:查询操作与按条件的更新和删除操作,用户仍然需要编写 SQL 语句,这些 SQL 语句虽然 Android Studio 提供了高亮,但是仍然是以字符串的形式存在,不受编译器静态类型检查。

2.2.2 Exposed

Kotlin在正式发布时有一个主力卖点就是可以用来构建开发者自己的DSL。Exposed(参考链接 5)是当时官方宣传DSL的范例项目之一。Exposed主要场景是 JVM 后端,它使用 JDBC 可以连接多种数据库,包括:MySQL、Oracle、MariaDB、SQLite 等等。从场景上看 Exposed 也不符合我们的预期,但是我们仍然可以看一下它的 API 设计:

object Users : Table() {
    val id = varchar("id", 10) // Column<String>
    val name = varchar("name", length = 50) // Column<String>
    val cityId = (integer("city_id") references Cities.id).nullable() // Column<Int?>


    override val primaryKey = PrimaryKey(id, name = "PK_User_ID") // name is optional here
}


fun main() {
    Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
    transaction {
        addLogger(StdOutSqlLogger)
        Users.insert {
            it[id] = "andrey"
            it[name] = "Andrey"
            it[Users.cityId] = saintPetersburgId
        }
        Users.update({ Users.id eq "alex"}) {
            it[name] = "Alexey"
        }
        Users.deleteWhere{ Users.name like "%thing"}
        for (cit
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值