類型體系是scala中最為復雜的特性,它既是scala強大的原因,也是scala號稱宇宙最復雜語言的直接原因。而且,Scala正在自我革命,新的.0規劃在很大程度上就是要在類型系統上大膽革命(參見:項目)。
我在最早構建 scala-sql 這個數據庫訪問庫的時候,主要是想把的一些實現方式和早期的esql方式遷移到scala中(參考:ORM是否必要? - 王在祥的回答 - 知乎),但第一個版本在類型化上實現得并不理想,主要存在一下的問題:
sql""中的插值是動態類型的,而在執行時sql中數據類型,則是通過反射,來決定是使用還是等基礎JDBC操作的。
case class SQLWithArgs(sql: String, args: Seq[Any]) { ... }
這種設計導致的問題是:我們可以將任意的值傳給sql,而在執行過程中,則因為類型無法識別,而產生。從而無法享受到編譯期的靜態類型檢查,這在感覺上也不符合scala的風格。
難以擴展自定義類型的支持。
在scala-sql中,我們有很多的場景需要支持自定義的數據類型,譬如:
支持 scala. 類型,而不僅僅是 java.math.類型。
支持 [Int], []等類型。
支持 joda.Date 等類型。
在 scala-sql 1.0中,雖然也支持了一定的擴展類型,但存在很嚴重的問題,其一是:這個擴展能力只能內建在內部,無法讓用戶擴展。其二是采用反射的方式來實現,代碼中大量的case match操作,很不優美。(參考:)
很蹩腳的 ORM 實現。雖然我有些反感 、JPM這樣的重量級ORM實現,但還是需要一個輕量級的ORM,不處理關系,而只完成簡單層面的字段映射。在scala-sql 1.0中,是通過反射來進行的,這一樣會出現上述的兩個問題。
在scala-sql 1.0應用了一段時間之后,我對這個庫越來越不滿意,開始在思考如何重構,建立一個更加簡單統一的類型模型,并且能夠支持用戶擴展類型體系。在這個重構的過程中,最終完成了目前的 scala-sql 2.0版本。
在做這個重構之前,我一直在思考,數據庫支持的類型,諸如int、、date等有什么共性,是否可以用一個統一的類型T來 描述呢?哪些是這個類型的基本操作呢?
基于此,我們需要這樣一個類型:
trait T {
def passIn(stmt: PreparedStatement, index: Int)
def passOut(rs: ResultSet, index: Int): T
def passOut(rs: ResultSet, name: String): T
}
問題是,我們不可能讓Int、等類型繼承這個接口,Scala的擴展方法也并不能很好的滿足這個場景。而且,即便是我們為Int、擴展了上述的方法,也并不會好用。因為的時候,我們更希望將的值賦給我們的目標變量,而不是調用目標變量的方法來改變它的值。
這個時候,scala的 Bound 類型就是非常有意義了,我們定義了:
trait JdbcValueAccessor[T] {
def passIn(stmt: PreparedStatement, index: Int, value: T)
def passOut(rs: ResultSet, index: Int): T
def passOut(rs: ResultSet, name: String): T
}
并不是一個值類型,而是一個處理某種值類型T的能力接口,可以這么讀:[]是一個處理值類型的,它可以將傳遞給,也可以從中提取。在這理,[] 就是 的一個能力綁定,為對象賦予了作為的能力。任何時候,我們需要將作為一個處理的時候,我們也需要你提供這個能力對象,完成對應的操作。
在這里,我們并不需要對、Int進行任何的改造,我們只是將數據庫訪問這種能力提取出來,作為一個,這種能力并不一定只是一個擴展方法,而可能是一個擴展方法集合。
case class SQLWithArgs(sql: String, args: Seq[JdbcValue[_]]) { ... }
case class JdbcValue[T: JdbcValueAccessor](value: T) {
def accessor: JdbcValueAccessor[T] = implicitly[JdbcValueAccessor[T]]
def passIn(stmt: PreparedStatement, index: Int) = accessor.passIn(stmt, index, value)
}
現在的sql插值參數,都是強類型的了,任何不符合的對象都不能作為插值來傳遞,而如果有了,自然,我們知道如何將這個值傳遞給了。
那么問題來了,Int、并不是一個類型啊?怎么傳遞給sql""插值呢?每次都做一次轉換?如("Hello", sor)"這樣做的話,就非常的不友好了。這時,Scala的隱式轉換就非常實用了。
?object JdbcValue {
? ?implicit def wrap[T: JdbcValueAccessor](t: T): JdbcValue[T] = JdbcValue(t)
? ?implicit def wrap[T: JdbcValueAccessor](t: Option[T]): JdbcValue[Option[T]] = JdbcValue(t)(new JdbcValueAccessor_Option[T])
?}
當我們需要將Int轉換為[Int]的時候,有一下幾個隱式轉換方法是可以派上用場的:
def wrapT: : [T] = (t)這個寫法,和下面的寫法是完全一致的,是一個語法上的甜品: def wrap(t: T)( value: [T]): [T] = (t, [[T]])
通過上面的定義,現在我們可以支持將任意的T傳遞給 sql 插值了。前提是我們為之定義了一個 [T] 的上下文綁定。在scala-sql中,我們在..sql這個對象中定義了幾乎所有的內置類型的綁定:
而要新增一種類型,你只需要參考內置類型,定義一個擴展的 [T]即可,不需要對scala-s ql 庫做任何的修改。
作為一個擴展的示例,你可以參考 框架中的一個擴展:mysql.:
case class MySqlBitSet(val mask: Long) {
def isSet(n: Int) = {
assert(n >= 0 && n < 64)
((mask >> n) & 0x1L) == 1
}
override def toString: String = s"b'${mask.toBinaryString}'"
}
object MySqlBitSet {
implicit object jdbcValueAccessor extends JdbcValueAccessor[MySqlBitSet] {
override def passIn(stmt: PreparedStatement, index: Int, value: MySqlBitSet): Unit =
stmt.setBytes(index, toByteArray(value.mask))
override def passOut(rs: ResultSet, index: Int): MySqlBitSet = { ... }
override def passOut(rs: ResultSet, name: String): MySqlBitSet = { ... }
}
}
從中提取值
上面的例子,都是介紹如何將 T 作為插值 傳給,而如果需要從 中讀取 T 時sql中數據類型,我們就會實用到 [T].了。
def rows[T : ResultSetMapper](sql: SQLWithArgs): List[T] = ...
在這里,我們要從sql執行的結果中提取 T 時,需要一個將 轉還為 T 的能力對象,我們稱之為:[T],這個對象是這樣定義的:
trait ResultSetMapper[T] {
def from(rs: ResultSet): T
}
實際上,有了這個能力對象,rows的實現是非常簡單的,這里就不贅述了。相反,如何為 T 準備一個 [T] 就要復雜的多。
先看一個簡單的實現:
implicit object ResultSetMapper_Int extends ResultSetMapper[Int] {
override def from(rs: ResultSet): Int = rs.getInt(1)
}
這個實現是從 中映射一個 Int 值,這適合與 " count(*) from table"這樣的只有一個返回字段的場景。
而對于多字段的結果集呢,以下是一個示例:
case class User(name: String, age: Int, classRoom: Int = 1)
implicit object ResultSetMapper_User extends ResultSetMapper[User] {
override def from(rs: ResultSet): User = {
val name = implictly[JdbcValueAccessor[String]].passOut(rs, "name")
val age = implicitly[JdbcValueAccessor[Int]].passOut(rs, "age")
val classRoom = implicitly[JdbcValueAccessor[Int]].passOut(rs, "classroom")
User(name, age, classRoom)
}
}
可以看出來,這個實際上也是基于 的,這樣,就可以支持User中實用任何的字段,只要這個字段有 [T] 的上下文綁定。
當然,如果,對每一個Bean,都需要編寫這樣的一個 的話,這只能算是矛盾轉移,其代碼的工作量會非常之大,沒有什么實用之處。 不過,這樣的代碼純屬體力勞動,完全可以使用 Scala 的另外一個利器:Macro,讓編譯器自動生成。這也正是 scala-sql 2.0 中所提供的。對所有的Case Class,只要滿足:
scala-sql 就可以自動的通過 macro 來生成其 。
在這個意義上,[T] 這個上下文限定統一了 ,,scala-sql 2.0 也算是完美的、統一的支持了數據類型的擴展能力。