自從 Google 宣佈 Kotlin 成爲 Android 的官方語言,Kotlin 能夠說是忽然火了一波。其實不只僅是 Android,在服務端開發的領域,Kotlin 也能夠說是優點明顯。因爲其支持空安全、方法擴展、協程等衆多的優良特性,以及與 Java 幾乎完美的兼容性,選擇 Kotlin 能夠說是好處多多。html
然而,切換到 Kotlin 以後,你還在用 MyBatis 嗎?MyBatis 做爲一個 Java 的 SQL 映射框架,雖然在國內使用人數衆多,可是也受到了許多吐槽。使用 MyBatis,你必需要忍受在 XML 裏寫 SQL 這種奇怪的操做,以及在衆多 XML 與 Java 接口文件之間跳來跳去的麻煩,以及往 XML 中傳遞多個參數時的一坨坨 @Param
註解(或者你使用 Map
?那就更糟了,連基本的類型校驗都沒有,參數名也容易寫錯)。甚至,在與 Kotlin 共存的時候,還會出現一些奇怪的問題,mysql
這時,你可能想要一款專屬於 Kotlin 的 ORM 框架。它能夠充分利用 Kotlin 的各類優良特性,讓咱們寫出更加 Kotlin 的代碼。它應該是輕量級的,只須要添加依賴便可直接使用,不須要各類麻煩的配置文件。它的 SQL 最好能夠自動生成,不須要像 MyBatis 那樣每條 SQL 都本身寫,可是也給咱們保留精確控制 SQL 的能力,不至於像 Hibernate 那樣難以進行 SQL 調優。git
若是你真的這麼想的話,Ktorm 可能會適合你。Ktorm 是直接基於純 JDBC 編寫的高效簡潔的 Kotlin ORM 框架,它提供了強類型並且靈活的 SQL DSL 和方便的序列 API,以減小咱們操做數據庫的重複勞動。固然,全部的 SQL 都是自動生成的。本文的目的就是對 Ktorm 進行介紹,幫助咱們快速上手使用。github
你能夠在 Ktorm 的官網上獲取更詳細的使用文檔,若是使用遇到問題,還能夠在 GitHub 提出 issue。若是 Ktorm 對你有幫助的話,請在 GitHub 留下你的 star,也歡迎加入咱們,共同打造 Kotlin 優雅的 ORM 解決方案。sql
Ktorm 官網:ktorm.liuwj.me/
GitHub 地址:github.com/vincentlauv…數據庫
還記得咱們剛開始學編程的時候寫的第一個程序嗎,如今咱們先從 Ktorm 的 「Hello, World」 開始,瞭解如何快速地搭建一個使用 Ktorm 的項目。編程
Ktorm 已經發布到 maven 中央倉庫和 jcenter,所以,若是你使用 maven 的話,首先須要在 pom.xml
文件裏面添加一個依賴:安全
12345複製代碼 |
<dependency> <groupId>me.liuwj.ktorm</groupId> <artifactId>ktorm-core</artifactId> <version>${ktorm.version}</version></dependency>複製代碼 |
或者 gradle:bash
1複製代碼 |
compile "me.liuwj.ktorm:ktorm-core:${ktorm.version}"複製代碼 |
在使用 Ktorm 以前,咱們須要要讓它可以瞭解咱們的表結構。假設咱們有兩個表,他們分別是部門表 t_department
和員工表 t_employee
, 它們的建表 SQL 以下,咱們要如何描述這兩個表呢?閉包
123456789101112131415複製代碼 |
create table t_department( id int not null primary key auto_increment, name varchar(128) not null, location varchar(128) not null);create table t_employee( id int not null primary key auto_increment, name varchar(128) not null, job varchar(128) not null, manager_id int null, hire_date date not null, salary bigint not null, department_id int not null);複製代碼 |
通常來講,Ktorm 使用 Kotlin 中的 object 關鍵字定義一個繼承 Table
類的對象來描述表結構,上面例子中的兩個表能夠像這樣在 Ktorm 中定義:
123456789101112131415複製代碼 |
object Departments : Table<Nothing>("t_department") { val id by int("id").primaryKey() // Column<Int> val name by varchar("name") // Column<String> val location by varchar("location") // Column<String>}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")}複製代碼 |
能夠看到,Departments
和 Employees
都繼承了 Table
,而且在構造函數中指定了表名,Table
類還有一個泛型參數,它是此表綁定到的實體類的類型,在這裏咱們不須要綁定到任何實體類,所以指定爲 Nothing
便可。表中的列則使用 val 和 by 關鍵字定義爲表對象中的成員屬性,列的類型使用 int、long、varchar、date 等函數定義,它們分別對應了 SQL 中的相應類型。
定義好表結構後,咱們就可使用 Database.connect
函數鏈接到數據庫,而後執行一個簡單的查詢:
1234567複製代碼 |
fun main() { Database.connect("jdbc:mysql://localhost:3306/ktorm", driver = "com.mysql.jdbc.Driver") for (row in Employees.select()) { println(row[Employees.name]) }}複製代碼 |
這就是一個最簡單的 Ktorm 項目,這個 main
函數中只有短短三四行代碼,可是你運行它時,它卻能夠鏈接到數據庫,自動生成一條 SQL select * from t_employee
,查詢表中全部的員工記錄,而後打印出他們的名字。由於 select
函數返回的查詢對象實現了 Iterable<QueryRowSet>
接口,因此你能夠在這裏使用 for-each 循環語法。固然,任何針對 Iteralble
的擴展函數也均可用,好比 Kotlin 標準庫提供的 map/filter/reduce 系列函數。
讓咱們在上面的查詢裏再增長一點篩選條件:
12345複製代碼 |
val names = Employees .select(Employees.name) .where { (Employees.departmentId eq 1) and (Employees.name like "%vince%") } .map { row -> row[Employees.name] }println(names)複製代碼 |
生成的 SQL 以下:
123複製代碼 |
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 也能對你的代碼進行智能提示和自動補全。
實現基於條件的動態查詢也十分簡單,由於都是純 Kotlin 代碼,直接使用 if 語句就好,比 MyBatis 在 XML 裏面寫 <if>
標籤好太多。
1234567891011複製代碼 |
val names = Employees .select(Employees.name) .whereWithConditions { if (someCondition) { it += Employees.managerId.isNull() } if (otherCondition) { it += Employees.departmentId eq 1 } } .map { it.getString(1) }複製代碼 |
聚合查詢:
123456複製代碼 |
val t = Employeesval 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:
123456789複製代碼 |
Employees .select(Employees.id) .unionAll( Departments.select(Departments.id) ) .unionAll( Departments.select(Departments.id) ) .orderBy(Employees.id.desc())複製代碼 |
多表鏈接查詢:
123456789101112131415161718複製代碼 |
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) ) }複製代碼 |
插入:
12345678複製代碼 |
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}複製代碼 |
更新:
123456789複製代碼 |
Employees.update { it.job to "engineer" it.managerId to null it.salary to 100 where { it.id eq 2 }}複製代碼 |
刪除:
1複製代碼 |
Employees.delete { it.id eq 4 }複製代碼 |
這就是 Ktorm 提供的 SQL DSL,使用這套 DSL,咱們可使用純 Kotlin 代碼來編寫查詢,再也不須要在 XML 中寫 SQL,也不須要在代碼中拼接 SQL 字符串。並且,強類型的 DSL 還能讓咱們得到一些額外的好處,好比將一些低級的錯誤暴露在編譯期,以及 IDE 的智能提示和自動補全。最重要的是,它生成的 SQL 幾乎與咱們的 Kotlin 代碼一一對應,所以雖然咱們的 SQL 是自動生成的,咱們仍然對它擁有絕對的控制。
這套 DSL 幾乎能夠覆蓋咱們工做中常見的全部 SQL 的用法,好比 union、聯表、聚合等,甚至對嵌套查詢也有必定的支持。固然,確定也有一些暫時不支持的用法,好比某些數據庫中的特殊語法,或者十分複雜的查詢(如相關子查詢)。這其實十分罕見,但若是真的發生,Ktorm 也提供了一些解決方案:
ktorm-support-mysql
。固然,咱們也能本身編寫擴展。更多 SQL DSL 的用法,請參考 Ktorm 的具體文檔。
前面咱們已經介紹了 SQL DSL,可是若是隻有 DSL,Ktorm 還遠不能稱爲一個 ORM 框架。接下來咱們將介紹實體類的概念,瞭解如何將數據庫中的表與實體類進行綁定,這正是 ORM 框架的核心:對象 - 關係映射。
咱們仍然之前面的部門表 t_department
和員工表 t_employee
爲例,建立兩個 Ktorm 的實體類,分別用來表示部門和員工這兩個業務概念:
1234567891011121314151617複製代碼 |
interface Department : Entity<Department> { companion object : Entity.Factory<Department>() val id: Int var name: String var location: String}interface Employee : Entity<Employee> { companion object : Entity.Factory<Employee>() val id: Int? var name: String var job: String var manager: Employee? var hireDate: LocalDate var salary: Long var department: Department}複製代碼 |
能夠看到,Ktorm 中的實體類都繼承了 Entity<E>
接口,這個接口爲實體類注入了一些通用的方法。實體類的屬性則使用 var 或 val 關鍵字直接定義便可,根據須要肯定屬性的類型及是否爲空。
有一點可能會違揹你的直覺,Ktorm 中的實體類並非 data class,甚至也不是一個普通的 class,而是 interface。這是 Ktorm 的設計要求,經過將實體類定義爲 interface,Ktorm 纔可以實現一些特別的功能,之後你會了解到它的意義。
衆所周知,接口並不能實例化,既然實體類被定義爲接口,咱們要如何才能建立一個實體對象呢?其實很簡單,只須要像下面這樣,僞裝它有一個構造函數:
1複製代碼 |
val department = Department()複製代碼 |
有心的同窗應該已經發現,上面定義實體類接口的時候,還爲這兩個接口都增長了一個伴隨對象。這個伴隨對象重載了 Kotlin 中的 invoke
操做符,所以可使用括號像函數同樣直接調用。在 Ktorm 的內部,咱們使用了 JDK 的動態代理建立了實體對象。
還記得在上一節中咱們定義的兩個表對象嗎?如今咱們已經有了實體類,下一步就是把實體類和前面的表對象進行綁定。這個綁定其實十分簡單,只須要在聲明列以後繼續鏈式調用 bindTo
函數或 references
函數便可,下面的代碼修改了前面的兩個表對象,完成了 ORM 綁定:
123456789101112131415複製代碼 |
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。
把兩個表對象與修改前進行對比,咱們能夠發現兩處不一樣:
Table
類的泛型參數,咱們須要指定爲實體類的類型,以便 Ktorm 將表對象與實體類進行綁定;在以前,咱們設置爲 Nothing
表示不綁定到任何實體類。bindTo
或 references
函數將該列與實體類的某個屬性進行綁定;若是沒有這個調用,則不會綁定到任何屬性。列綁定的意義在於,經過查詢從數據庫中獲取實體對象的時候(如 findList
函數),Ktorm 會根據咱們的綁定配置,將某個列的數據填充到它所綁定的屬性中去;在將實體對象中的修改更新到數據庫中的時候(如 flushChanges
函數),Ktorm 也會根據咱們的綁定配置,將某個屬性的變動,同步更新到綁定它的那個列。
完成列綁定後,咱們就可使用針對實體類的各類方便的擴展函數。好比根據名字獲取員工:
12複製代碼 |
val vince = Employees.findOne { it.name eq "vince" }println(vince)複製代碼 |
findOne
函數接受一個 lambda 表達式做爲參數,使用該 lambda 的返回值做爲條件,生成一條查詢 SQL,自動 left jion 了關聯表 t_department
。生成的 SQL 以下:
1234複製代碼 |
select * from t_employee left join t_department _ref0 on t_employee.department_id = _ref0.id where t_employee.name = ?複製代碼 |
其餘 find*
系列函數:
123456複製代碼 |
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" }複製代碼 |
將實體對象保存到數據庫:
12345678910複製代碼 |
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)複製代碼 |
將內存中實體對象的變化更新到數據庫:
1234複製代碼 |
val employee = Employees.findById(2) ?: returnemployee.job = "engineer"employee.salary = 100employee.flushChanges()複製代碼 |
從數據庫中刪除實體對象:
12複製代碼 |
val employee = Employees.findById(2) ?: returnemployee.delete()複製代碼 |
更多實體 API 的用法,可參考列綁定和實體查詢相關的文檔。
能夠看到,只須要將表對象與實體類進行綁定,咱們就可使用這些方便的函數,大部分對實體對象的增刪改查操做,都只須要一個函數調用便可完成,但 Ktorm 能作到的,還遠不止於此。
除了 find*
函數之外,Ktorm 還提供了一套名爲」實體序列」的 API,用來從數據庫中獲取實體對象。正如其名字所示,它的風格和使用方式與 Kotlin 標準庫中的序列 API 及其相似,它提供了許多同名的擴展函數,好比 filter
、map
、reduce
等。
要獲取一個實體序列,咱們能夠在表對象上調用 asSequence
擴展函數:
1複製代碼 |
val sequence = Employees.asSequence()複製代碼 |
Ktorm 的實體序列 API,大部分都是以擴展函數的方式提供的,這些擴展函數大體能夠分爲兩類,它們分別是中間操做和終止操做。
這類操做並不會執行序列中的查詢,而是修改並建立一個新的序列對象,好比 filter
函數會使用指定的篩選條件建立一個新的序列對象。下面使用 filter
獲取部門 1 中的全部員工:
1複製代碼 |
val employees = Employees.asSequence().filter { it.departmentId eq 1 }.toList()複製代碼 |
能夠看到,用法幾乎與 kotlin.Sequence
徹底同樣,不一樣的僅僅是在 lambda 表達式中的等號 ==
被這裏的 eq
函數代替了而已。filter
函數還能夠連續使用,此時全部的篩選條件將使用 and
操做符進行鏈接,好比:
12345複製代碼 |
val employees = Employees .asSequence() .filter { it.departmentId eq 1 } .filter { it.managerId.isNotNull() } .toList()複製代碼 |
生成 SQL:
1234複製代碼 |
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)複製代碼 |
使用 sortedBy
或 sortedByDescending
對序列中的元素進行排序:
1複製代碼 |
val employees = Employees.asSequence().sortedBy { it.salary }.toList()複製代碼 |
使用 drop
和 take
函數進行分頁:
1複製代碼 |
val employees = Employees.asSequence().drop(1).take(1).toList()複製代碼 |
實體序列的終止操做會立刻執行一個查詢,獲取查詢的執行結果,而後執行必定的計算。for-each 循環就是一個典型的終止操做,下面咱們使用 for-each 循環打印出序列中全部的員工:
123複製代碼 |
for (employee in Employees.asSequence()) { println(employee)}複製代碼 |
生成的 SQL 以下:
123複製代碼 |
select * from t_employee left join t_department _ref0 on t_employee.department_id = _ref0.id複製代碼 |
toCollection
、toList
等方法用於將序列中的元素保存爲一個集合:
1複製代碼 |
val employees = Employees.asSequence().toCollection(ArrayList())複製代碼 |
mapColumns
函數用於獲取指定列的結果:
1複製代碼 |
val names = Employees.asSequenceWithoutReferences().mapColumns { it.name }複製代碼 |
除此以外,還有 mapColumns2
、mapColumns3
等更多函數,它們用來同時獲取多個列的結果,這時咱們須要在閉包中使用 Pair
或 Triple
包裝咱們的這些字段,函數的返回值也相應變成了 List<Pair<C1?, C2?>>
或 List<Triple<C1?, C2?, C3?>>
:
1234567複製代碼 |
Employees .asSequenceWithoutReferences() .filter { it.departmentId eq 1 } .mapColumns2 { Pair(it.id, it.name) } .forEach { (id, name) -> println("$id:$name") }複製代碼 |
生成 SQL:
123複製代碼 |
select t_employee.id, t_employee.namefrom t_employee where t_employee.department_id = ?複製代碼 |
其餘咱們熟悉的序列函數也都支持,好比 fold
、reduce
、forEach
等,下面使用 fold
計算全部員工的工資總和:
12345複製代碼 |
val totalSalary = Employees .asSequenceWithoutReferences() .fold(0L) { acc, employee -> acc + employee.salary }複製代碼 |
實體序列 API 不只可讓咱們使用相似 kotlin.Sequence
的方式獲取數據庫中的實體對象,它還支持豐富的聚合功能,讓咱們能夠方便地對指定字段進行計數、求和、求平均值等操做。
下面使用 aggregateColumns
函數獲取部門 1 中工資的最大值:
1234複製代碼 |
val max = Employees .asSequenceWithoutReferences() .filter { it.departmentId eq 1 } .aggregateColumns { max(it.salary) }複製代碼 |
若是你但願同時獲取多個聚合結果,能夠改用 aggregateColumns2
或 aggregateColumns3
函數,這時咱們須要在閉包中使用 Pair
或 Triple
包裝咱們的這些聚合表達式,函數的返回值也相應變成了 Pair<C1?, C2?>
或 Triple<C1?, C2?, C3?>
。下面的例子獲取部門 1 中工資的平均值和極差:
1234複製代碼 |
val (avg, diff) = Employees .asSequenceWithoutReferences() .filter { it.departmentId eq 1 } .aggregateColumns2 { Pair(avg(it.salary), max(it.salary) - min(it.salary)) }複製代碼 |
生成 SQL:
123複製代碼 |
select avg(t_employee.salary), max(t_employee.salary) - min(t_employee.salary) from t_employee where t_employee.department_id = ?複製代碼 |
除了直接使用 aggregateColumns
函數之外,Ktorm 還爲序列提供了許多方便的輔助函數,他們都是基於 aggregateColumns
函數實現的,分別是 count
、any
、none
、all
、sumBy
、maxBy
、minBy
、averageBy
。
下面改用 maxBy
函數獲取部門 1 中工資的最大值:
1234複製代碼 |
val max = Employees .asSequenceWithoutReferences() .filter { it.departmentId eq 1 } .maxBy { it.salary }複製代碼 |
除此以外,Ktorm 還支持分組聚合,只須要先調用 groupingBy
,再調用 aggregateColumns
。下面的代碼能夠獲取全部部門的平均工資,它的返回值類型是 Map<Int?, Double?>
,其中鍵爲部門 ID,值是各個部門工資的平均值:
1234複製代碼 |
val averageSalaries = Employees .asSequenceWithoutReferences() .groupingBy { it.departmentId } .aggregateColumns { avg(it.salary) }複製代碼 |
生成 SQL:
123複製代碼 |
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)
。有了這些輔助函數,上面獲取全部部門平均工資的代碼就能夠改寫成:
1234複製代碼 |
val averageSalaries = Employees .asSequenceWithoutReferences() .groupingBy { it.departmentId } .eachAverageBy { it.salary }複製代碼 |
除此以外,Ktorm 還提供了 aggregate
、fold
、reduce
等函數,它們與 kotlin.collections.Grouping
的相應函數同名,功能也徹底同樣。下面的代碼使用 fold
函數計算每一個部門工資的總和:
123456複製代碼 |
val totalSalaries = Employees .asSequenceWithoutReferences() .groupingBy { it.departmentId } .fold(0L) { acc, employee -> acc + employee.salary }複製代碼 |
更多實體序列 API 的用法,可參考實體序列和序列聚合相關的文檔。
本文從一個 「Hello, World」 程序開始,對 Ktorm 的幾大特性進行了介紹,它們分別是 SQL DSL、實體類與列綁定、實體序列 API 等。有了 Ktorm,咱們就可使用純 Kotlin 代碼方便地完成數據持久層的操做,不須要再使用 MyBatis 煩人的 XML。同時,因爲 Ktorm 是專一於 Kotlin 語言的框架,所以沒有兼容 Java 的包袱,可以讓咱們更加充分地使用 Kotlin 各類優越的語法特性,寫出更加優雅的代碼。既然語言都已經切換到 Kotlin,爲什麼不嘗試一下純 Kotlin 的框架呢?