精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

攜程機票跨端 Kotlin DSL 數據庫框架 SQLlin

開發 新聞
SQLlin 目前已經在 Github 開源。

作者簡介

禹昂,攜程機票移動端資深工程師,專注于 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 (city in Cities.selectAll()) {
println("${city[Cities.id]}: ${city[Cities.name]}")
}
}

用戶需要自己定義一個表示數據庫表的對象,繼承自 Table,然后手動編寫代碼,使用屬性表示表中的列。在進行 CURD 的 SQL 構建時通過調用不同的 Table 成員函數,然后使用類似鍵值對 get/set 的方式完成 SQL 子句(clause)的構建。

以當年的角度來看,Exposed 的 API 算是相當驚艷。但以今天的眼光來看,我認為 Exposed的 API 有如下不足:

(1)數據庫不支持序列化與反序列化為對象,實際上的編程體驗仍然像在操作一個 Map。

(2)用戶需要手動定義 Table,需要編寫大量樣板代碼。

(3)API 設計與 SQL 語句本身差異較大,部分 API 接收多個 lambda 表達式作為參數,看起來括號嵌套層級多,不夠優雅。

但總的的來說 Exposed 的設計思路的方向非常棒,使用 Kotlin 語法構建自己的 DSL API,對用戶來說使用方便,且只要充分利用其潛力,API 也能設計的非常優雅。

2.2.3 SQLDelight

SQLDelight(參考鏈接 6)由 Android 界的開源先鋒 Square 開發,是我們目前調研過的最先進的 Kotlin 數據庫框架。它支持 Kotlin Multiplatform,除了 Android、iOS 這樣的移動端平臺,還通過 Kotlin/Native 直接支持 macOS、Linux 以及 Windows 等桌面端平臺,除此之外也有對 JavaScrip 以及 JVM 開發環境的支持。在所有平臺上 SQLDelight 都支持 SQLite,但在 JVM 平臺上還額外支持使用 JDBC 連接各種主流的服務端數據庫。因此 SQLDelight 是一個能滿足多種開發環境,多種技術棧的數據庫框架。

在 API 的設計上,SQLDelight 更是一騎絕塵,它使用 Kotlin 官方尚未正式發布的技術 Kotlin Compiler Plugin(后簡稱 KCP)來構建 API。用戶只需要在一個特殊的 .sq 文件中編寫自己的 SQL 語句,并給 SQL 語句起一個名字,KCP 就可以在工程編譯構建時對 SQL 語句進行語法檢查及靜態類型校驗,并生成一個函數。用戶直接在 Kotlin 代碼中調用該函數即可完成 CRUD 操作。SQLDelight 示例代碼如下圖所示:

圖片

看上去 SQLDelight 完美適合我們的場景。但實際上我們對 SQLDelight 的調研非常早,那時它會在 iOS 上帶來過大的 size 增長。攜程 app 是一個多功能聚合類 app,而機票又只是其中一個團隊, 因此在 size 的增長上會較為敏感。在近期我的調研中,在 x86 架構下 SQLDelight 帶來的包 size 增長為 200 kb,比之前有所改善。如果你準備從 0 打造一個 KMM app 或者你是某項目的基礎架構團隊的成員,我非常建議你嘗試 SQLDelight。

此外在 API 上,雖然使用 KCP 幫助開發者生成大量代碼非常驚艷,但是 SQLDelight 配置較為繁瑣,使用方式的學習成本也較高,便利性上做不到開箱即用。因此許多開發者對其也持有一些不同的觀點。

2.3 需求確定

我們調研過的庫與框架并不只有以上三款,在經過充分的對比后,我們決定仍然自己研發一款符合我們需求的 SQLite 框架,在取長補短與權衡利弊之后,我們認為它應該具有以下特性:

(1)支持 KMM(即至少支持 Android、iOS 兩個平臺)。

(2)SQL 語句必須可以在某種程度上受編譯器檢查。

(3)支持直接將對象序列化為 SQL 語句(例如 UPDATE 語句中的 SET 子句),且支持將查詢結果反序列化為 Kotlin 對象。

(4)Size 不能過大。

三、 基本設計與實現

3.1 架構設計與 module 劃分

在一個項目開發之前,我們首先需要做的是將項目的基本功能理清,然后進行適當的 module 劃分:

圖片

無論是 iOS 還是 Android,最底層調用的都是 SQLite C 庫。再往上是應用程序層,iOS 應用層可以直接調用 SQLite C API,但是在 Android 上,由于應用層的代碼運行在 ART 虛擬機上,因此我們需要通過 Android Framework 提供的 Java API 對 SQLite 進行操作。

再往上就到了 KMM common 層,我們希望 DSL API 的實現應該是完全平臺無關的, 因此我們需要 sqllin-dsl 的下層提供了一個叫做 sqllin-driver 的模塊,它在不同的平臺上提供不同的具體實現,但在 common source set 中提供了一層平臺無關的 lower-level SQLite API 供 sqllin-dsl 層使用。

3.2 Driver 層的技術選型與實現

sqllin-driver 在 common source set 中提供了一套通用的 API,但其在不同平臺的 source set 中需要采取不同的實現方式。在上面的架構中設計中,在 iOS source set 中可以直接調用 SQLite C API,而在 Android source set 中我們可以使用 Android Framework SQLite Java API。

使用 Android Framework SQLite Java API 有個問題,在 Android P 以下的版本上有眾多的 SQLite 參數配置都不支持,比如:日志模式、同步模式、lookaside、內存數據庫等等。如果要在低版本的 Android 系統上支持這些參數配置,我們需要自行編寫 JNI 代碼,實現一套 JVM 層的 SQLite API。

但是 Google 在 Android N 以上的版本中禁止在 NDK 開發中直接訪問系統內置的 SQLite,如果堅持這么做,開發者必須自己重新打一份 SQLite 到自己的 apk 中,這不僅會增加一部分無謂的包大小,還會讓這個項目變得過于復雜。所以最終我們仍然決定基于 Android Framework 來實現,不支持對低版本 Android 系統的 SQLite 多種個性化配置。

在 iOS 端的實現上我們也碰到了一些問題,雖然 Kotlin/Native 與 C 語言的互操作很完善,但是也非常繁瑣,比如我們在 Kotlin/Native 上做一次 open database 的操作:

fun openDatabase() = memScoped {
val dbPtr = alloc<CPointerVar<sqlite3>>()
val openResult = sqlite3_open_v2(path, dbPtr.ptr, sqliteFlags, null)
if (openResult != SQLITE_OK) {
throw IllegalStateExcepetion(sqlite3_errmsg(dbPtr.value)?.toKString() ?: “”)? }
}

由于 C 語言獨有的運行時內存的特性以及其自身的概念,我們需要使用一些繁瑣的 API 來完成對 C 的調用,比如上面示例中的:memScoped、alloc、ptr、toKString 等等。這導致這一塊的開發工作量大幅上升。

但好在我們在開源社區找到了解決方案—— SQLiter。SQLiter 是 TouchLab 的開源項目,它的作用在于使用 Kotlin 實現多個 Native 平臺統一的 SQLite lower-level API,它的 API 設計與 Android Framework SQLite Java API 有些相似,但又融合了許多 Kotlin 的語法特性。它不僅僅支持 iOS,還支持 macOS、tvOS、watchOS、Linux、Windows 等多個操作系統,抹平了包括線程鎖在內的多端差異。它同時也是 SQLDelight 在 Kotlin/Native 上的底層引擎。使用 SQLiter 可以把 Kotlin-C 之間的互操作轉化為 Kotlin 語言內的互相調用,大大節約開發成本。并且我們也能通過 SQLiter 的多平臺支持能力,擴展到除 iOS 外的多個 Native 平臺。

只要兩個平臺都可以完成對 SQLite 的操作,開發 common 層的通用 API 只需要聲明 expect API,然后在各平臺 source set 的 actual 實現中直接調用這些平臺特有的實現即可,比如說還是以 open 數據庫為例,我們在 common source set 中聲明:

public expect fun openDatabase(config: DatabaseConfiguration): DatabaseConnection

在 Android source set 中可以這樣實現:

import android.database.sqlite.SQLiteDatabase

public actual fun openDatabase(config: DatabaseConfiguration): DatabaseConnection {
SQLiteDatabase.openDatabase()
//......
}

在 Native source set 中:

import co.touchlab.sqliter.createDatabaseManager

public actual fun openDatabase(config: DatabaseConfiguration): DatabaseConnection {
createDatabaseManager(configNative).createMultiThreadedConnection()
//......
}

上面的只是示例,在 sqllin-driver 的真實實現中會更為復雜一些。

至此, sqllin-driver 的實現已經沒有太多的困難,剩余的開發工作只需要通過封裝來抹平兩邊的差異,并提供合適的 common API 即可。

3.3 DSL 設計與實現

3.3.1 基本設計

在 driver 層的實現沒有太大障礙后,我們就可以著手進行 DSL 層的設計和開發。SQLDelight 的完全生成式 DSL 實現起來過于復雜,使用 Kotlin 的語法潛力構建我們自己的 DSL 相對簡單且易于使用。在上面的調研中我們看到 Exposed 的 DSL API 設計依賴 KV 操作語法,并且不少子句的構建有太多的 lambda 表達式應用,以及過多的括號嵌套,整體使用下來寫出來的代碼與 SQL 語句相去甚遠。

在我的構思中,我希望 DSL 的設計可以盡量還原 SQL 語法,并且能最大程度的減少用戶編寫的樣板代碼。所以我初步構思了一套 DSL 語法的樣貌,這樣便于后續的實現還原:

fun sample() {
lateinit var statement: SelectStatement<Person>
database {
val table = Table<Person>(“person”)
table INSERT listOf(tom, jerry, nick)
table DELETE WHERE (name == “Jerry”)
table UPDATE SET (age = 27) WHERE (name == “Nick”)
statement = table SELECT WHERE (name == “Tom”) ORDER_BY (age to DESC)
}
val result: List<Person> = statement.getResult()
}

注意,上面的代碼是偽代碼,僅僅是初步構思。但我們在后續的實現中會盡量還原它的設計。

總的來說,用戶可以創建 Table 實例用來表示數據庫表,在所有的 SQL 語句中,Table 實例都是主語,Table 同時約束序列化與反序列化對象的類型。Table 擁有 4 個謂語,分別代表增刪改查等操作。謂語通過中綴函數實現,不同的表示操作的中綴函數接收不同類型的參數,例如我們看到 INSERT 直接接收一個對象的 List 即可完成插入操作。而 DELETE 和 SELECT 則接收 WHERE 子句來完成整條 SQL 語句的構建。此外,UPDATE 和 SELECT 語句可以連續連接多個子句, 這些多子句的連接也是通過中綴函數來實現的。最后,SELECT 語句返回了一個 SelectStatement 類型的對象,在整個 database {...} 作用域完結之后可以用它來提取查詢結果。

以這樣的方式構建出的 API 可以最大程度的還原 SQL 的語法與語序。

3.3.2 DSL 類型關系

在確定了基本的語法規則后,我們需要定義一些基本的類型關系,這無論是在面向對象編程還是函數式編程中都非常重要。這些類型關系可以在代碼編寫階段約束一些語法準則,避免將 SQL 的語法錯誤留到運行時暴露。例如,INSERT 語句不能連接子句、SELECT 語句中 ORDER BY 子句不能位于 WHERE 子句之前等等。我們以一條 SELECT語句為例來為它的每個部分定義一些類型:

圖片


Statement 、Table、Operation、Clause 我們都已經在前文討論過了。這里要解釋一下的是 ClauseElement 和 ClauseCondition。ClauseElement 表示數據庫的列名,而 ClauseCondition 則表示一個條件,條件通常會用在 WHERE 和 HAVING 子句中。基于以上的類型定義,我們可以得到一些基本的類型間的關系:

Table + Operation + Clause -> Statement
Statement + Clause -> Statement
ClauseElement + String/Number -> ClauseCondition
ClauseCondition + ClauseCondition -> ClauseCondition

當然,以上的類型在真實的代碼中都是 interface 或 abstract class,不同的 SQL 語句的類型關系有所不同,這些約束的真正實現在其子類型當中。

3.3.3 使用 Kotlin Symbol Processor 實現表與列元素生成

在 3.3.1 小節的基本設計中,Table 實例是通過構造函數創建的,每次創建時用戶都需要手動傳入數據庫的真實表名作為其參數,這并不方便易用。在 Exposed 中也有類似的 Table 設計,用戶定義自己的 class 并繼承自 Table 抽象類,還要在其中定義一些表達列名的屬性。這種設計的最大問題在于用戶總是要手動編寫大量樣板代碼。為了使這一步操作更方便,我希望 SQLlin 可以根據用戶期待序列化與反序列化的類型自動生成 Table 單例,以及其內部的列名屬性。

Kotlin Symbol Processor(后簡稱 KSP)是 Google 開發的元編程工具,基于前文所說的 KCP。它通常被用于注解處理及代碼生成,它的功能雖然不如 KCP 強大,但擁有較為完整的教程與文檔且更加易用。在 KSP 誕生之前,開發者通常使用 KAPT 來進行注解處理和代碼生成,但其二者處理 Kotlin 的階段不同,如下圖所示:

圖片

Kotlin 的編譯大概分為兩個階段,第一個階段由編譯器前端進行,它將 Kotlin 代碼編譯為中間表示碼 IR,而編譯器后端則將 IR 編譯為各平臺的產物,由此實現了 Kotlin 的跨平臺。KAPT 技術基于 Java APT 技術,它處理的是 JVM Bytecode,因此它僅僅能用于 Kotlin/JVM,無法實現跨平臺需求。并且將 Java 與 Kotlin 間的一些語法概念互相轉化相當耗時,這導致了 KAPT 的性能不夠好,導致了代碼編譯構建的耗時增加。而 KSP 處理的則是中間表示碼 IR,相當于在 Kotlin 編譯到各平臺產物之前對其進行了處理,因此可以用于跨平臺場景,并且 IR 是 Kotlin 代碼的直接編譯產物,無須概念轉換,這使得 KSP 在一些較好的工況下性能可以比 KAPT 提升兩倍之多。

那我們如何實現注解處理?我們可以定義一個注解類,用戶將注解添加到希望表示表的 data class 即可,比如:

@DBRow("person")
data class Person(
val age: Int,
val name: String,
)

字符串"person"表示數據庫中真實的表名,它作為參數傳遞給注解,這樣 KSP 就能在代碼處理階段拿到它。在 KSP 處理后,可以生成以下代碼:

// KSP generated:
object PersonTable : Table<Person>("person") {

inline operator fun <R> invoke(block: PersonTable.(table: PersonTable) -> R): R = this.block(this)

val name: ClauseString get() {}
val age: ClauseNumber get() {}
var SetClause<Person>.name: String set(value) {}
var SetClause<Person>.age: Int set(value) {}
}

我們可以發現,生成的 Table 中含有兩個 name 以及兩個 age。使用 val 聲明的屬性用于在條件語句中表示列名,而使用 var 聲明的則是 SetClause 的擴展屬性,用于在 SET 子句中設置一個新值。之所以將二者分開主要是因為如果想要在 SET 子句中使用賦值運算符 = 進行 set,那么接收的參數則必須與該屬性類型相同。舉例來說如果將屬性聲明為 ClauseString 類型,那么它的 setter 就無法接收 String 類型的參數。

有了 KSP 的助力,用戶再也無須手動編寫大量的 Table 代碼,為使用帶來了極大的便利。

3.3.4 如何實現查詢結果的反序列化

在純 Android 庫的開發中,我們通常會使用反射將某種格式的數據中的某個字段的值映射到與它名稱相同的 class 中的某個屬性,從而生成出該 class 的對象,這就是反序列化。反射是 JVM 的機制,無法跨平臺。因此我們如果要在 Kotlin Multiplatform 的環境中進行反序列化,就必須另尋他路。

在 Kotlin Multiplatform 的開發中,最常見的 JSON 和 ProtoBuf 的序列化與反序列化庫是官方的 kotlinx.serialization。它反序列化的原理是它通過 KCP 處理注解,并生成了每個被注解類的 KSerializer,KSerializer 是一個輔助類,它包含被注解類的屬性名,屬性類型等信息,kotlinx.serialization 正是通過它實現了非反射的序列化與反序列化。KCP 不僅使用門檻高,而且官方尚未正式發布(這意味著它沒有文檔且后續 API 可能會發生大的破壞性變更),因此使用 KCP 仿造編寫一個類似的功能也同樣很難。但我在調研 kotlinx.serialization 的原理時發現它開放了自定義數據格式的 API,我們可以直接復用 KSerializer。

在 sqllin-driver 中,查詢語句將會返回一個 CommonCursor,這與 Android SQLite Java API 類似。它可以進行行迭代、獲取指定列名的列號,以及 get 一些基本類型和 String 等數據,它的定義如下:

interface CommonCursor {
fun getInt(columnIndex: Int): Int
fun getString(columnIndex: Int): String?
//……
fun getColumnIndex(columnName: String): Int
fun forEachRow(block: (Int) -> Unit)
fun close()
}

而我們的目的則正是將 CommonCursor 反序列化為自己的 data class。

自定義反序列化器非常簡單,只需要繼承自 kotlinx.serialization 中提供的 AbstractDecoder 即可,核心實現如下:

@OptIn(ExperimentalSerializationApi::class)
internal class QueryDecoder(
private val cursor: CommonCursor
) : AbstractDecoder() {

private var elementIndex = 0
private var elementName = ""

override val serializersModule: SerializersModule = EmptySerializersModule()

override tailrec fun decodeElementIndex(descriptor: SerialDescriptor): Int =
if (elementIndex == descriptor.elementsCount)
CompositeDecoder.DECODE_DONE
else {
elementName = descriptor.getElementName(elementIndex)
val resultIndex = elementIndex++
if (cursorColumnIndex >= 0)
resultIndex
else
decodeElementIndex(descriptor)
}

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = QueryDecoder(cursor)

private inline val cursorColumnIndex
get() = cursor.getColumnIndex(elementName)

private inline fun <T> deserialize(block: (Int) -> T): T = cursorColumnIndex.let {
if (it >= 0) block(it) else throw SerializationException("The Cursor doesn't have this column")
}

override fun decodeBoolean(): Boolean = deserialize { cursor.getInt(it) > 0 }
override fun decodeInt(): Int = deserialize { cursor.getInt(it) }
override fun decodeString(): String = deserialize { cursor.getString(it) ?: "" }
override fun decodeDouble(): Double = deserialize { cursor.getDouble(it) }
//......
}

我們自定義的 Decoder 接收一個 CommonCursor 作為參數。decodeElementIndex 函數驅動著整個反序列化流程。我們通過elementIndex 在該類的眾多屬性中查找到當前對應的屬性名,再根據這個屬性名查詢到名稱相同的列名的列號,如果列號大于等于 0 則表示列名合法,直接返回 elementIndex 即可,否則進行下一輪迭代。在針對各類型的基本數據的反序列化中,我們直接調用CommonCursor 對應的 get 函數取值并返回就可以了。

關于自定義 kotlinx.serialization,我曾經寫過一篇文章詳細討論,大家可以參考(參考鏈接 7),或者查看官方文檔(參考鏈接 8)。

3.3.5 最終效果

以上基本討論完了 sqllin-dsl 設計過程中遇到的大部分難點。在真實的開發過程我們還解決了更多的問題,其中很大一部分在于類型設計。例如,某語句只能連接某子句,某子句后面不能連接某子句等等。利用 Kotlin 的語法規則可以在很大程度上保證在編譯期間暴露出我們編寫的 SQL 錯誤,并在絕大部分情況下阻止錯誤的 SQL 語句代碼通過編譯。但這不是 100% 的,使用者仍然可能使用 SQLlin 編寫出錯誤的 SQL 語句,因此充分理解 SQL 知識對那些需要使用數據庫的開發者來說非常重要。在開發完成之后,使用 sqllin-dsl 編寫的真實代碼如下所示:

fun sample() {
lateinit var statement: SelectStatement<Person>
database {
PersonTable { table ->
table INSERT listOf(tom, jerry, nick)
table DELETE WHERE (name EQ "Jerry")
table UPDATE SET {age = 27} WHERE (name NEQ "Nick")
statement = table SELECT WHERE (name EQ "Tom") ORDER_BY (age to DESC)
}
}
val result: List<Person> = statement.getResult()
}

我們大體還原了最初的設計構想,主要改變的地方有兩點,首先是 Table 現在由 KSP 直接生成,不再依賴用戶手動調用構造函數。其次是我們最終沒能使用運算符重載來實現 ClauseElement 的運算符,例如 > 和 <,原因除了重載函數的返回值類型問題,也包括如果要重載> 和 <,我們需要實現 Comparable 接口,并覆蓋 compareTo 函數。但在用戶調用 compareTo 時,它的內部無法知道用戶到底調用的是> 還是 <,因此無法準確構建正確的 SQL 語句。最終我們舍棄了運算符重載,轉而采用中綴函數實現。

在完成最終的設計后,SQLlin 的架構設計圖調整為如下所示:

圖片

我們加入了 sqllin-processor 模塊,它主要包含 KSP 相關的代碼,負責注解處理與代碼生成。在與 Native 平臺交互這邊,架構圖中添加了 SQLiter 的部分。

得益于 SQLiter 對多個 Native 平臺的支持,SQLlin 支持的平臺數量也遠超 Android、iOS 兩個移動端平臺:

  • Android
  • iOS (x64, arm32, arm64, simulatorArm64)
  • macOS (x64, arm64)
  • watchOS (x86, x64, arm32, arm64, simulatorArm64)
  • tvOS (x64, arm64, simulatorArm64)
  • Linux (x64)

四、未來計劃

SQLlin 目前已經在 Github 開源,大家可以前往它的主頁(參考鏈接 9)查看它更多的信息。

SQLlin 擁有全套的中英文文檔以及 Sample 項目供大家學習如何使用。

但 SQLlin 的開發仍未結束,它目前仍然有一些不足,例如它還有如下功能不支持:

(1)不支持子查詢,包括不支持條件語句中的子查詢和 JOIN 子查詢。

(2)不支持表創建、表刪除、增加列、刪除列等會導致數據庫結構發生變化的 SQL 語句構建。

只有將以上兩個功能開發完成,SQLlin 才基本擁有應對各種場景的能力。這兩項功能的實現會是當下 SQLlin 后續迭代的主要工作。

此外,SQLiter 除了以上提到的 SQLlin 支持的平臺外,還支持 Windows。由于目前我們是本地編譯發布,而 Kotlin 當前不支持類 Unix 系統和 Windows 系統的交差編譯,因此 SQLlin 暫時還不支持 Windows 平臺。等 SQLlin 的 Github CI/CD 配置完成后,Windows 也將加入受支持行列。

在最近的 Github issue 中我們發現,有一些開發者希望我們能考慮 JVM 后端場景,可以像 SQLDelight 一樣在 JVM 上連接后端數據庫,這是個不錯的建議,我們可以將其列為長期規劃,不過目前 SQLlin 還是需要集中精力把客戶端上的事情做好。

Kotlin Multiplatform 這項技術最近進展很快,特別是 compose-jb 在 iOS 上取得進步令人振奮。機票團隊除 UI 層以外已經基本完成了基礎架構建設,后續會繼續調研 Kotlin Multiplatform 的 UI 跨端方案,并同步推進更多的業務代碼向 KMM 的遷移。期待后續我們團隊可以為社區帶來更多的貢獻。

責任編輯:張燕妮 來源: 攜程技術
相關推薦

2025-06-24 09:44:41

2022-06-17 09:42:20

開源MMKV攜程機票

2022-05-20 11:09:15

Flybirds多端測試UI 自動化測試

2020-12-04 14:32:33

AndroidJetpackKotlin

2022-05-13 09:27:55

Widget機票業務App

2017-04-11 15:11:52

ABtestABT變量法

2022-06-03 09:21:47

Svelte前端攜程

2015-05-28 14:05:02

2022-06-10 08:35:06

項目數據庫攜程機票

2023-05-12 10:14:38

APP開發

2022-08-06 08:27:41

Trace系統機票前臺微服務架構

2023-06-06 16:01:00

Web優化

2017-04-11 15:34:41

機票前臺埋點

2024-01-19 09:21:35

攜程開源

2022-08-12 08:38:08

攜程小程序Taro跨端解決方案

2017-03-15 17:38:19

互聯網

2015-05-28 20:46:06

2015-05-28 22:46:29

2015-06-01 15:06:45

攜程數據庫災備

2023-08-25 09:51:21

前端開發
點贊
收藏

51CTO技術棧公眾號

天天做夜夜做人人爱精品| 精品av中文字幕在线毛片| 欧美成人69av| 日韩av有码在线| 最近中文字幕一区二区| 国产黄色小视频在线| 成人黄色在线网站| 国产精品久久久久影院日本| 中文字幕五月天| 爽爽窝窝午夜精品一区二区| 欧美喷潮久久久xxxxx| 男人的天堂avav| 成人精品福利| 成人av在线播放网站| 国产精品久久久久久久久久ktv | 久久久综合网站| 国产日韩欧美在线播放| 欧美三日本三级少妇99| 久久综合电影| 亚洲另类欧美自拍| 青青草精品在线| 欧美色片在线观看| 亚洲国产视频a| 中文字幕一区综合| 久久久久久女乱国产| 国产成人免费视频网站高清观看视频| 欧美一区二三区| 久久久久久久中文字幕| 久久综合88| 亚洲一级片在线看| 无码人妻aⅴ一区二区三区| 日本高清久久| 欧美图区在线视频| 99福利在线观看| 深夜国产在线播放| 综合色中文字幕| 日韩精品久久久| 亚洲av成人无码网天堂| 国产99一区视频免费| 91精品美女在线| 久久精品国产亚洲av麻豆蜜芽| 日韩视频一区| 欧美精品国产精品日韩精品| 久久久久久久久久97| av中文字幕一区二区| 国产视频一区在线| 免费黄色三级网站| av一级亚洲| 精品国产制服丝袜高跟| 亚洲最大天堂网| 国产精品.xx视频.xxtv| 欧美中文字幕不卡| 香蕉视频网站入口| 国产高清视频免费在线观看| 日本黄色一级视频| 亚洲免费播放| 午夜精品在线视频| 久久免费激情视频| 国产日韩欧美一区二区三区在线观看| 欧美激情18p| 国产亚洲成人精品| 亚洲大胆av| 538国产精品一区二区免费视频| 日韩欧美三级在线观看| 亚洲精选在线| 日本在线观看天堂男亚洲 | 成人h猎奇视频网站| 中文字幕在线观看视频一区| 另类小说视频一区二区| 国产一区红桃视频| 国产高清视频免费观看| 懂色av一区二区三区免费看| 福利精品视频| 亚洲色大成网站www| 久久久美女艺术照精彩视频福利播放| 欧美精品一区二区三区四区五区 | 91精品国产色综合久久不卡粉嫩| 欧美丝袜第三区| 色啦啦av综合| 一区二区三区欧洲区| 亚洲精品美女久久久| 日本爱爱爱视频| 国产精品久久观看| 久久网这里都是精品| 在线成人高清不卡| 日韩中文字幕a| 欧美第一在线视频| 日韩av在线免费观看| 亚洲精品国产熟女久久久| 欧美激情偷拍自拍| 国语自产偷拍精品视频偷| 欧美日韩一级黄色片| 激情欧美一区二区| 国产在线精品二区| 爱久久·www| 樱花影视一区二区| 日本精品久久久久中文字幕| 在线日韩三级| 亚洲精品v天堂中文字幕| 亚洲综合第一区| 亚洲视频狠狠| 国产日韩欧美视频| 色综合成人av| 亚洲男帅同性gay1069| 欧美精品一区免费| 精品国产乱码一区二区三区| 亚洲精品一区二区在线| 国产女人18水真多毛片18精品| 国产精品毛片在线| 99久久久精品免费观看国产| 国产黄在线看| 五月天中文字幕一区二区| 亚洲成人av在线播放| 国产精品日韩一区二区三区 | 日本一级片免费看| 久久99国产精品免费| 久久伊人一区| 黄色成人在线网| 欧美日韩免费高清一区色橹橹| 亚洲图片综合网| 欧美精品国产一区二区| 国产精品女主播| 婷婷伊人综合中文字幕| 一区二区三区欧美视频| 欧美精品aaaa| 亚洲精品3区| 久久久久久久一区二区| 国产精品熟女久久久久久| 久久先锋资源网| 免费看一级大黄情大片| 欧洲精品99毛片免费高清观看 | 亚洲精品专区| 91黄色精品| 很黄的网站在线观看| 欧美三级一区二区| 久久国产柳州莫菁门| 国产精品普通话对白| 97免费资源站| aa在线视频| 日本视频一区二区三区| 欧美天天综合色影久久精品| 中文字幕免费高清在线| 国产一区二区电影在线观看| 97超级碰碰人国产在线观看| 狠狠人妻久久久久久综合麻豆| 亚洲日本在线天堂| 精品综合久久久久| 午夜av一区| 成人网欧美在线视频| 精品51国产黑色丝袜高跟鞋| 欧美日韩精品二区第二页| 极品蜜桃臀肥臀-x88av| 日韩在线一区二区| 日韩亚洲不卡在线| 91精品国产66| 在线a欧美视频| 亚洲一区二区人妻| 日韩一区欧美一区| 丰满人妻一区二区三区53视频| 婷婷综合在线| 3d动漫啪啪精品一区二区免费| 中文字幕免费高清电视剧网站在线观看| 7799精品视频| 麻豆91精品91久久久| 成人美女视频在线观看| 午夜免费福利小电影| 欧美人成在线观看ccc36| 欧美影院久久久| 九色视频网站在线观看| 欧美日韩视频在线观看一区二区三区 | 国产日韩一区二区三区| 国产精品蜜芽在线观看| 亚洲大胆人体在线| 中文字幕精品无码一区二区| 久久九九影视网| jizz大全欧美jizzcom| 999久久久免费精品国产| 亚洲综合一区二区不卡| 不卡一本毛片| 亚洲欧美中文日韩在线| 一区二区三区在线免费观看视频| 亚洲视频综合在线| 国产综合内射日韩久| 久久精品123| 亚洲国产欧美不卡在线观看| 日韩成人久久| 欧美又大又硬又粗bbbbb| 3p视频在线观看| 欧美tk—视频vk| 亚洲成人av影片| 国产精品久久久久精k8 | 免费高清一区二区三区| 亚洲自拍电影| 成人在线国产精品| 麻豆国产在线| 色妞久久福利网| 欧美中文字幕在线观看| 视频一区 中文字幕| 91成人在线精品| 一区二区国产精品精华液| 99久久婷婷国产精品综合| 亚洲成人av免费看| 精品二区视频| 日韩一区二区电影在线观看| 成人激情久久| 国产999精品| 美女日批视频在线观看| 一区二区亚洲欧洲国产日韩| 韩国av免费在线| 欧美日韩国产另类不卡| 国产成人无码精品亚洲| 1024亚洲合集| 欧美图片一区二区| 国产v日产∨综合v精品视频| 国产一级做a爰片久久| 亚洲日本成人| 国产午夜精品视频一区二区三区| 久久av影视| 国产一区二区免费电影| 婷婷久久免费视频| 国产成人在线一区二区| xxx性欧美| 久久国产精品电影| 在线a人片免费观看视频| 亚洲老板91色精品久久| 色网站免费观看| 日韩欧美国产成人一区二区| 91麻豆成人精品国产免费网站| 欧美性猛交xxxxx免费看| 精品人妻在线播放| 亚洲欧美日韩中文播放 | 在线视频欧美精品| 欧美午夜免费| 亚洲性视频在线| 91欧美激情另类亚洲| 成人开心激情| 日本精品免费一区二区三区| av电影免费在线看| 欧美激情啊啊啊| 18videosex性欧美麻豆| 久久久久999| 免费观看久久久久| 中文字幕在线观看日韩| 国产一级在线| 亚洲女人初尝黑人巨大| 欧洲一级在线观看| 亚洲奶大毛多的老太婆| 日本护士...精品国| 亚洲国产古装精品网站| wwwav国产| 影音先锋一区| 国产91精品高潮白浆喷水| 波多野结衣影片| 国产大片一区二区| 欧美在线xxx| 永久免费看黄网站| 欧美a在线观看| 91亚洲永久免费精品| av在线精品| 2022国产精品| 高清日韩欧美| 好看的日韩精品视频在线| 欧美顶级毛片在线播放| 久久久影院一区二区三区| 久久93精品国产91久久综合| 色综合影院在线观看| 婷婷综合五月| 人妻无码一区二区三区四区| 亚洲片区在线| 免费在线观看毛片网站| 日本不卡免费在线视频| 99九九精品视频| 国产成人在线影院| 亚洲欧美日本一区| 91丝袜美腿高跟国产极品老师 | 九九九久久久久久| 美足av综合网| 日韩美女在线观看| **精品中文字幕一区二区三区| 91文字幕巨乱亚洲香蕉| 日韩高清一级| 伊人狠狠色丁香综合尤物| 国产一区二区三区四区老人| 黄色片视频在线免费观看| 久久超级碰视频| 香蕉久久久久久av成人| 国产亚洲一区二区在线观看| 97精品在线播放| 亚洲成a人在线观看| 日韩熟女一区二区| 91精品视频网| 四虎在线免费观看| 精品国产自在精品国产浪潮| 爱看av在线| 国产一区视频在线播放| 岛国精品一区| 亚洲狠狠婷婷综合久久久| 国语精品一区| 中文字幕 91| 成a人片国产精品| 波多野结衣久久久久| 欧美日韩激情视频8区| 91精品在线视频观看| 日韩精品视频在线播放| 国产黄a三级三级三级av在线看| …久久精品99久久香蕉国产| 亚洲精品aa| 欧美日韩国产一二| 欧美激情综合| 一个色综合久久| 久久亚洲一级片| 国产亚洲第一页| 欧美精品一级二级| 久久免费看视频| 午夜精品久久久久久久99热| 人人精品久久| 欧美男人的天堂| 1024成人| 国产chinesehd精品露脸| 国产精品天天摸av网| 狠狠人妻久久久久久| 亚洲福利视频网站| 天堂av最新在线| 成人h视频在线| 成人不卡免费视频| 韩国成人漫画| 国产又爽又黄的激情精品视频| 国产一区二区久久久久| 欧美亚洲一级二级| 黄色av成人| 中文字幕1234区| 午夜a一级毛片亚洲欧洲| 日韩免费视频在线观看| 成人台湾亚洲精品一区二区 | 9191成人精品久久| 国产高清在线看| 2021久久精品国产99国产精品| 一区二区三区视频播放| 成人毛片100部免费看| 黄一区二区三区| 99久久久无码国产精品不卡| 欧美伊人久久久久久久久影院| 黄色在线免费观看大全| 欧美最猛性xxxxx亚洲精品| 久久精品色综合| 久在线观看视频| 99精品视频在线播放观看| 日韩欧美高清在线观看| 亚洲成人中文字幕| 偷拍自拍在线看| 久久婷婷人人澡人人喊人人爽| 激情婷婷亚洲| 国产精品无码在线| 精品久久久久久中文字幕| 日韩中文字幕免费在线观看| 欧美精品videofree1080p| 99re91这里只有精品| 久久久久久久久影视| 国产成人鲁色资源国产91色综| 青青草原免费观看| 欧美精品一区二区三区高清aⅴ| 乱插在线www| 精品欧美一区二区精品久久| 国产日韩视频| 黄色三级生活片| 欧美日韩电影一区| 免费黄色电影在线观看| 91精品天堂| 国产一区成人| 国产在线免费av| 制服视频三区第一页精品| 国产黄色小视频在线| 国产精品推荐精品| 欧美中文字幕| 黄色片在线观看免费| 7878成人国产在线观看| av剧情在线观看| 日韩伦理一区二区三区av在线| 久久国产精品一区二区| 久久久一二三区| 国产婷婷成人久久av免费高清 | 欧美性高跟鞋xxxxhd| av资源种子在线观看| 91成人在线看| 香蕉久久夜色精品国产| 26uuu成人网| 精品一区二区三区四区| 亚洲国产综合在线观看| 欧美一级视频免费看| 欧美国产一区二区| 亚洲老妇色熟女老太| 国产盗摄xxxx视频xxx69| 亚洲国产精品久久久天堂| 日本五十肥熟交尾| 欧美精品v国产精品v日韩精品| av丝袜在线| 一区二区三区四区国产| 成人免费观看av| 亚洲天堂视频网| 97国产suv精品一区二区62|