scala的mysql类封装_类型体系在scala-sql项目中的应用

类型体系是scala中最为复杂的特性,它既是scala强大的原因,也是scala号称宇宙最复杂语言的直接原因。而且,Scala正在自我革命,新的Scala3.0规划在很大程度上就是要在类型系统上大胆革命(参见:http://dotty.epfl.ch项目)。

我在最早构建 scala-sql 这个数据库访问库的时候,主要是想把groovy的一些实现方式和早期的esql方式迁移到scala中(参考:ORM是否必要? - 王在祥的回答 - 知乎

https://www.zhihu.com/question/23244681/answer/78839922),但第一个版本在类型化上实现得并不理想,主要存在一下的问题:

sql"interpolation" 中的插值是动态类型的,而在执行时,则是通过反射,来决定是使用setInt还是setString等基础JDBC操作的。

case class SQLWithArgs(sql: String, args: Seq[Any]) { ... }

这种设计导致的问题是:我们可以将任意的值传给sql,而在执行过程中,则因为类型无法识别,而产生RuntimeException。从而无法享受到编译期的静态类型检查,这在感觉上也不符合scala的风格。

难以扩展自定义类型的支持。在scala-sql中,我们有很多的场景需要支持自定义的数据类型,譬如:

支持 scala.BigDecimal 类型,而不仅仅是 java.math.BigDecimal类型。

支持 Option[Int], Option[String]等类型。

支持 joda.Date 等类型。

在 scala-sql 1.0中,虽然也支持了一定的扩展类型,但存在很严重的问题,其一是:这个扩展能力只能内建在内部,无法让用户扩展。其二是采用反射的方式来实现,代码中大量的case match操作,很不优美。(参考:https://github.com/wangzaixiang/scala-sql/blob/v1.0.6/src/main/scala/wangzx/scala_commons/sql/RichConnection.scala)

很蹩脚的 ORM 实现。虽然我有些反感 Hibernate、JPM这样的重量级ORM实现,但还是需要一个轻量级的ORM,不处理关系,而只完成简单层面的字段映射。在scala-sql 1.0中,是通过反射来进行mapping的,这一样会出现上述的两个问题。

在scala-sql 1.0应用了一段时间之后,我对这个库越来越不满意,开始在思考如何重构,建立一个更加简单统一的类型模型,并且能够支持用户扩展类型体系。在这个重构的过程中,最终完成了目前的 scala-sql 2.0版本。

在做这个重构之前,我一直在思考,数据库支持的类型,诸如int、string、date等有什么共性,是否可以用一个统一的类型 T 来 描述呢?哪些是这个类型的基本操作呢?

作为 parameter传入给 Statement。

从 ResultSet 中获取值。

基于此,我们需要这样一个类型:

trait T {

def passIn(stmt: PreparedStatement, index: Int)

def passOut(rs: ResultSet, index: Int): T

def passOut(rs: ResultSet, name: String): T

}

问题是,我们不可能让 Int、String等类型继承这个接口,Scala的扩展方法也并不能很好的满足这个场景。而且,即便是我们为Int、String扩展了上述的方法,也并不会好用。因为passout的时候,我们更希望将passOut的值赋给我们的目标变量,而不是调用目标变量的方法来改变它的值。

这个时候,scala的 Context 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

}

JdbcValueAccessor 并不是一个值类型,而是一个处理某种值类型T的能力接口,可以这么读:JdbcValueAccessor[String] 是一个处理String值类型的JdbcValueAccessor,它可以将String传递给Statement,也可以从ResultSet中提取String。在这理,JdbcValueAccessor[String] 就是 String 的一个能力绑定,为String对象赋予了作为JdbcValue的能力。任何时候,我们需要将String作为一个JdbcValue处理的时候,我们也需要你提供这个能力对象,完成对应的操作。

在这里,我们并不需要对String、Int进行任何的改造,我们只是将数据库访问这种能力提取出来,作为一个JdbcValueAccessor,这种能力并不一定只是一个扩展方法,而可能是一个扩展方法集合。

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插值参数,都是强类型的了,任何不符合JdbcValue的对象都不能作为插值来传递,而如果有了JdbcValue,自然,我们知道如何将这个值传递给Statement了。

那么问题来了,Int、String并不是一个JdbcValue类型啊?怎么传递给sql""插值呢?每次都做一次转换?如JdbcValue("Hello", StringJdbcValueAccessor)"这样做的话,就非常的不友好了。这时,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 转换为 JdbcValue[Int] 的时候,有一下几个隐式转换方法是可以派上用场的:

全局的implicit def convertIntToJdbcValue(i: Int): JdbcValue[Int]

在 object Int 中的 implicit def convertIntToJdbcValue(i: Int): JdbcValue[Int] 这个不现实了。

在 object JdbcValue中的 implicit def convertIntToJdbcValue(i: Int): JdbcValue[Int] 如果采用这种方法,我们需要为每一种类型,都在object JdbcValue中定义一个implicit方法,这样做仍然存在问题:无法支持用户扩展的类型。

在 object JdbcValue中,implicit def wrap[T: JdbcValueAccessor](t: T): JdbcValue[T] = JdbcValue(t) 通过这个方法,我们可以把任意的 T 都转换为 JdbcValue[T],前提是存在相应的 JdbcValueAccessor 上下文绑定。 而这,其实是另外一个隐式值了。

implicit def wrap[T: JdbcValueAccessor](t: T): JdbcValue[T] = JdbcValue(t)

这个写法,和下面的写法是完全一致的,是一个语法上的甜品:

implicit def wrap(t: T)(implicit value: JdbcValueAccessor[T]): JdbcValue[T] = JdbcValue(t, implicitly[JdbcValueAccessor[T]])

通过上面的定义,现在我们可以支持将任意的T传递给 sql 插值了。前提是我们为之定义了一个 JdbcValueAccessor[T] 的上下文绑定。在scala-sql中,我们在 wangzx.scala_commons.sql 这个package对象中定义了几乎所有的内置类型的绑定:

Boolean

Byte

Short

Int

Long

Float

Double

BigDecimal & scala.BigDecimal

Date、Timestamp

String

Array[Byte]

Option[T: JdbcValueAccessor] 所有的JdbcValue类型,都可以作为Option[T]传递。

而要新增一种类型,你只需要参考内置类型,定义一个扩展的 JdbcValueAccessor[T]即可,不需要对scala-s ql 库做任何的修改。

作为一个扩展的示例,你可以参考 框架中的一个扩展:mysql.MySqlBitSet:

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 = { ... }

}

}

从ResultSet中提取值

上面的例子,都是介绍如何将 T 作为插值 传给PreparedStatement, 而如果需要从 ResultSet中读取 T 时,我们就会实用到 JdbcValueAccessor[T].passOut了。

def rows[T : ResultSetMapper](sql: SQLWithArgs): List[T] = ...

在这里,我们要从sql执行的结果中提取 T 时,需要一个将 ResultSet 转还为 T 的能力对象,我们称之为:ResultSetMapper[T],这个对象是这样定义的:

trait ResultSetMapper[T] {

def from(rs: ResultSet): T

}

实际上,有了这个能力对象,rows的实现是非常简单的,这里就不赘述了。相反,如何为 T 准备一个 ResultSetMapper[T] 就要复杂的多。

先看一个简单的实现:

implicit object ResultSetMapper_Int extends ResultSetMapper[Int] {

override def from(rs: ResultSet): Int = rs.getInt(1)

}

这个实现是从 ResultSet 中映射一个 Int 值,这适合与 "select 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)

}

}

可以看出来,这个mapper实际上也是基于 JdbcValueAccessor的,这样,就可以支持User中实用任何的字段,只要这个字段有 JdbcValueAccessor[T] 的上下文绑定。

当然,如果,对每一个Bean,都需要编写这样的一个 Mapper 的话,这只能算是矛盾转移,其代码的工作量会非常之大,没有什么实用之处。 不过,这样的代码纯属体力劳动,完全可以使用 Scala 的另外一个利器:Macro,让编译器自动生成。这也正是 scala-sql 2.0 中所提供的。对所有的Case Class,只要满足:

每个字段的类型 T 都满足 JdbcValueAccessor[T] 上下文限定

scala-sql 就可以自动的通过 macro 来生成其 ResultSetMapper。

在这个意义上,JdbcValueAccessor[T] 这个上下文限定统一了 passIn,passOut,scala-sql 2.0 也算是完美的、统一的支持了数据类型的扩展能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值