初窺理解Kotlin的ORM框架Ktorm

Ktorm 是什么?

  • Ktorm

Ktorm 是直接基于純 JDBC 編寫的高效簡潔的輕量級 Kotlin ORM 框架,它提供了強類型而且靈活的 SQL DSL 和方便的序列 API,以減少我們操作數據庫的重復勞動。當然,所有的 SQL 都是自動生成的。查看更多詳細文檔,請前往官網:https://ktorm.liuwj.me

特性

  • 沒有配置文件、沒有 xml、沒有第三方依賴、輕量級、簡潔易用
  • 強類型 SQL DSL,將低級 bug 暴露在編譯期
  • 靈活的查詢,隨心所欲地精確控制所生成的 SQL
  • 實體序列 API,使用 filtermapsortedBy 等序列函數進行查詢,就像使用 Kotlin 中的原生集合一樣方便
  • 易擴展的設計,可以靈活編寫擴展,支持更多操作符、數據類型、 SQL 函數、數據庫方言等


    ktorm-example.jpg

快速開始

Ktorm 已經發布到 maven 中央倉庫和 jcenter,因此,如果你使用 maven 的話,只需要在 pom.xml 文件里面添加一個依賴:

<dependency>
    <groupId>me.liuwj.ktorm</groupId>
    <artifactId>ktorm-core</artifactId>
    <version>${ktorm.version}</version>
</dependency>

或者 gradle:

compile "me.liuwj.ktorm:ktorm-core:${ktorm.version}"

首先,創建 Kotlin object,描述你的表結構

object Departments : Table<Nothing>("t_department") {
    val id by int("id").primaryKey()
    val name by varchar("name")
    val location by varchar("location")
}

object Employees : Table<Nothing>("t_employee") {
    val id by int("id").primaryKey()
    val name by varchar("name")
    val job by varchar("job")
    val managerId by int("manager_id")
    val hireDate by date("hire_date")
    val salary by long("salary")
    val departmentId by int("department_id")
}

然后,連接到數據庫,執行一個簡單的查詢:

fun main() {
    Database.connect("jdbc:mysql://localhost:3306/ktorm", driver = "com.mysql.jdbc.Driver")

    for (row in Employees.select()) {
        println(row[Employees.name])
    }
}

現在,你可以執行這個程序了,Ktorm 會生成一條 SQL select * from t_employee,查詢表中所有的員工記錄,然后打印出他們的名字。 因為 select 函數返回的查詢對象實現了 Iterable<T> 接口,所以你可以在這里使用 for-each 循環語法。當然,任何針對 Iteralble<T> 的擴展函數也都可用,比如 Kotlin 標準庫提供的 map/filter/reduce 系列函數。

SQL DSL

讓我們在上面的查詢里再增加一點篩選條件:

val names = Employees
    .select(Employees.name)
    .where { (Employees.departmentId eq 1) and (Employees.name like "%vince%") }
    .map { row -> row[Employees.name] }
println(names)

生成的 SQL 如下:

select t_employee.name as t_employee_name 
from t_employee 
where (t_employee.department_id = ?) and (t_employee.name like ?) 

這就是 Kotlin 的魔法,使用 Ktorm 寫查詢十分地簡單和自然,所生成的 SQL 幾乎和 Kotlin 代碼一一對應。并且,Ktorm 是強類型的,編譯器會在你的代碼運行之前對它進行檢查,IDE 也能對你的代碼進行智能提示和自動補全。

基于條件的動態查詢:

val names = Employees
    .select(Employees.name)
    .whereWithConditions {
        if (someCondition) {
            it += Employees.managerId.isNull()
        }
        if (otherCondition) {
            it += Employees.departmentId eq 1
        }
    }
    .map { it.getString(1) }

聚合查詢:

val t = Employees
val salaries = t
    .select(t.departmentId, avg(t.salary))
    .groupBy(t.departmentId)
    .having { avg(t.salary) greater 100.0 }
    .associate { it.getInt(1) to it.getDouble(2) }

Union:

Employees
    .select(Employees.id)
    .unionAll(
        Departments.select(Departments.id)
    )
    .unionAll(
        Departments.select(Departments.id)
    )
    .orderBy(Employees.id.desc())

多表連接查詢:

data class Names(val name: String, val managerName: String?, val departmentName: String)

val emp = Employees.aliased("emp")
val mgr = Employees.aliased("mgr")
val dept = Departments.aliased("dept")

val results = emp
    .leftJoin(dept, on = emp.departmentId eq dept.id)
    .leftJoin(mgr, on = emp.managerId eq mgr.id)
    .select(emp.name, mgr.name, dept.name)
    .orderBy(emp.id.asc())
    .map {
        Names(
            name = it.getString(1),
            managerName = it.getString(2),
            departmentName = it.getString(3)
        )
    }

插入:

Employees.insert {
    it.name to "jerry"
    it.job to "trainee"
    it.managerId to 1
    it.hireDate to LocalDate.now()
    it.salary to 50
    it.departmentId to 1
}

更新:

Employees.update {
    it.job to "engineer"
    it.managerId to null
    it.salary to 100

    where {
        it.id eq 2
    }
}

刪除:

Employees.delete { it.id eq 4 }

更多 SQL DSL 的用法,請參考具體文檔

實體類與列綁定

除了 SQL DSL 以外,Ktorm 也支持實體對象。首先,我們需要定義實體類,然后在表對象中使用 bindTo 函數將表與實體類進行綁定。在 Ktorm 里面,我們使用接口定義實體類,繼承 Entity<E> 即可:

interface Department : Entity<Department> {
    val id: Int
    var name: String
    var location: String
}

interface Employee : Entity<Employee> {
    val id: Int?
    var name: String
    var job: String
    var manager: Employee?
    var hireDate: LocalDate
    var salary: Long
    var department: Department
}

修改前面的表對象,把數據庫中的列綁定到實體類的屬性上:

object Departments : Table<Department>("t_department") {
    val id by int("id").primaryKey().bindTo { it.id }
    val name by varchar("name").bindTo { it.name }
    val location by varchar("location").bindTo { it.location }
}

object Employees : Table<Employee>("t_employee") {
    val id by int("id").primaryKey().bindTo { it.id }
    val name by varchar("name").bindTo { it.name }
    val job by varchar("job").bindTo { it.job }
    val managerId by int("manager_id").bindTo { it.manager.id }
    val hireDate by date("hire_date").bindTo { it.hireDate }
    val salary by long("salary").bindTo { it.salary }
    val departmentId by int("department_id").references(Departments) { it.department }
}

命名規約:強烈建議使用單數名詞命名實體類,使用名詞的復數形式命名表對象,如:Employee/Employees、Department/Departments。

完成列綁定后,我們就可以使用針對實體類的各種方便的擴展函數。比如根據名字獲取 Employee 對象:

val vince = Employees.findOne { it.name eq "vince" }
println(vince)

findOne 函數接受一個 lambda 表達式作為參數,使用該 lambda 的返回值作為條件,生成一條查詢 SQL,自動 left jion 了關聯表 t_department。生成的 SQL 如下:

select * 
from t_employee 
left join t_department _ref0 on t_employee.department_id = _ref0.id 
where t_employee.name = ?

其他 find* 系列函數:

Employees.findAll()
Employees.findById(1)
Employees.findListByIds(listOf(1))
Employees.findMapByIds(listOf(1))
Employees.findList { it.departmentId eq 1 }
Employees.findOne { it.name eq "vince" }

將實體對象保存到數據庫:

val employee = Employee {
    name = "jerry"
    job = "trainee"
    manager = Employees.findOne { it.name eq "vince" }
    hireDate = LocalDate.now()
    salary = 50
    department = Departments.findOne { it.name eq "tech" }
}

Employees.add(employee)

將內存中實體對象的變化更新到數據庫:

val employee = Employees.findById(2) ?: return
employee.job = "engineer"
employee.salary = 100
employee.flushChanges()

從數據庫中刪除實體對象:

val employee = Employees.findById(2) ?: return
employee.delete()

更多實體 API 的用法,可參考列綁定實體查詢相關的文檔。

實體序列 API

除了 find* 函數以外,Ktorm 還提供了一套名為”實體序列”的 API,用來從數據庫中獲取實體對象。正如其名字所示,它的風格和使用方式與 Kotlin 標準庫中的序列 API 及其類似,它提供了許多同名的擴展函數,比如 filtermapreduce 等。

要獲取一個實體序列,我們可以在表對象上調用 asSequence 擴展函數:

val sequence = Employees.asSequence()

Ktorm 的實體序列 API,大部分都是以擴展函數的方式提供的,這些擴展函數大致可以分為兩類,它們分別是中間操作和終止操作。

中間操作

這類操作并不會執行序列中的查詢,而是修改并創建一個新的序列對象,比如 filter 函數會使用指定的篩選條件創建一個新的序列對象。下面使用 filter 獲取部門 1 中的所有員工:

val employees = Employees.asSequence().filter { it.departmentId eq 1 }.toList()

可以看到,用法幾乎與 kotlin.Sequence 完全一樣,不同的僅僅是在 lambda 表達式中的等號 == 被這里的 eq 函數代替了而已。filter 函數還可以連續使用,此時所有的篩選條件將使用 and 操作符進行連接,比如:

val employees = Employees
    .asSequence()
    .filter { it.departmentId eq 1 }
    .filter { it.managerId.isNotNull() }
    .toList()

生成 SQL:

select * 
from t_employee 
left join t_department _ref0 on t_employee.department_id = _ref0.id 
where (t_employee.department_id = ?) and (t_employee.manager_id is not null)

使用 sortedBysortedByDescending 對序列中的元素進行排序:

val employees = Employees.asSequence().sortedBy { it.salary }.toList()

使用 droptake 函數進行分頁:

val employees = Employees.asSequence().drop(1).take(1).toList()

終止操作

實體序列的終止操作會馬上執行一個查詢,獲取查詢的執行結果,然后執行一定的計算。for-each 循環就是一個典型的終止操作,下面我們使用 for-each 循環打印出序列中所有的員工:

for (employee in Employees.asSequence()) {
    println(employee)
}

生成的 SQL 如下:

select * 
from t_employee 
left join t_department _ref0 on t_employee.department_id = _ref0.id

toCollectiontoList 等方法用于將序列中的元素保存為一個集合:

val employees = Employees.asSequence().toCollection(ArrayList())

mapColumns 函數用于獲取指定列的結果:

val names = Employees.asSequenceWithoutReferences().mapColumns { it.name }

除此之外,還有 mapColumns2mapColumns3 等更多函數,它們用來同時獲取多個列的結果,這時我們需要在閉包中使用 PairTriple 包裝我們的這些字段,函數的返回值也相應變成了 List<Pair<C1?, C2?>>List<Triple<C1?, C2?, C3?>>

Employees
    .asSequenceWithoutReferences()
    .filter { it.departmentId eq 1 }
    .mapColumns2 { Pair(it.id, it.name) }
    .forEach { (id, name) ->
        println("$id:$name")
    }

生成 SQL:

select t_employee.id, t_employee.name
from t_employee 
where t_employee.department_id = ?

其他我們熟悉的序列函數也都支持,比如 foldreduceforEach 等,下面使用 fold 計算所有員工的工資總和:

val totalSalary = Employees.asSequence().fold(0L) { acc, employee -> acc + employee.salary }

序列聚合

實體序列 API 不僅可以讓我們使用類似 kotlin.Sequence 的方式獲取數據庫中的實體對象,它還支持豐富的聚合功能,讓我們可以方便地對指定字段進行計數、求和、求平均值等操作。

下面使用 aggregateColumns 函數獲取部門 1 中工資的最大值:

val max = Employees
    .asSequenceWithoutReferences()
    .filter { it.departmentId eq 1 }
    .aggregateColumns { max(it.salary) }

如果你希望同時獲取多個聚合結果,可以改用 aggregateColumns2aggregateColumns3 函數,這時我們需要在閉包中使用 PairTriple 包裝我們的這些聚合表達式,函數的返回值也相應變成了 Pair<C1?, C2?>Triple<C1?, C2?, C3?>。下面的例子獲取部門 1 中工資的平均值和極差:

val (avg, diff) = Employees
    .asSequenceWithoutReferences()
    .filter { it.departmentId eq 1 }
    .aggregateColumns2 { Pair(avg(it.salary), max(it.salary) - min(it.salary)) }

生成 SQL:

select avg(t_employee.salary), max(t_employee.salary) - min(t_employee.salary) 
from t_employee 
where t_employee.department_id = ?

除了直接使用 aggregateColumns 函數以外,Ktorm 還為序列提供了許多方便的輔助函數,他們都是基于 aggregateColumns 函數實現的,分別是 countanynoneallsumBymaxByminByaverageBy

下面改用 maxBy 函數獲取部門 1 中工資的最大值:

val max = Employees
    .asSequenceWithoutReferences()
    .filter { it.departmentId eq 1 }
    .maxBy { it.salary }

除此之外,Ktorm 還支持分組聚合,只需要先調用 groupingBy,再調用 aggregateColumns。下面的代碼可以獲取所有部門的平均工資,它的返回值類型是 Map<Int?, Double?>,其中鍵為部門 ID,值是各個部門工資的平均值:

val averageSalaries = Employees
    .asSequenceWithoutReferences()
    .groupingBy { it.departmentId }
    .aggregateColumns { avg(it.salary) }

生成 SQL:

select t_employee.department_id, avg(t_employee.salary) 
from t_employee 
group by t_employee.department_id

在分組聚合時,Ktorm 也提供了許多方便的輔助函數,它們是 eachCount(To)eachSumBy(To)eachMaxBy(To)eachMinBy(To)eachAverageBy(To)。有了這些輔助函數,上面獲取所有部門平均工資的代碼就可以改寫成:

val averageSalaries = Employees
    .asSequenceWithoutReferences()
    .groupingBy { it.departmentId }
    .eachAverageBy { it.salary }

除此之外,Ktorm 還提供了 aggregatefoldreduce 等函數,它們與 kotlin.collections.Grouping 的相應函數同名,功能也完全一樣。下面的代碼使用 fold 函數計算每個部門工資的總和:

val totalSalaries = Employees
    .asSequenceWithoutReferences()
    .groupingBy { it.departmentId }
    .fold(0L) { acc, employee -> 
        acc + employee.salary 
    }

更多實體序列 API 的用法,可參考實體序列序列聚合相關的文檔。
此文章來自https://github.com/vincentlauvlwj/Ktorm
Ktorm官方https://ktorm.liuwj.me/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。