by 壮衣
在上一篇博文中我们列举了一些简单的例子来展示封装的API
向数据库插入一条用户数据:
def addUser1(user: User)(conn: Connection): Either[Exception, Int] =
StatementIO("insert into user (username, password) values (?, ?)",
List(user.username, user.password)).update.run(conn)
从数据库读取一条用户数据:
def getUser1(id: Int)(conn: Connection): Either[Exception, User] =
StatementIO("select id, username, password from user where id = ?", List(id))
.query.unique.run(conn)
目前我们只操作了一张表而且也没有涉及事务处理,让我们再来创建一张文章表,对应的样例类为:
case class Article(id: Int, title: String, content: String, userID: Int)
现在有这样的场景,我们需要删除用户同时需要删除用户所有的文章。假设我们先删除用户所有的文章然后再删除用户,这其中有两个删除动作,需要操作两张表。我们知道这两个操作构成一个事务,要么都删除成功,要么都删除失败,假如一个成功一个失败就需要回滚之前的删除操作。这样的场景在用封装的API该如何实现呢?来看下代码:
def deleteUser(id: Int)(conn: Connection): Either[Exception, (Int, Int)] = {
val delUser =
for {
delArticleCount <- StatementIO("delete from article where userID = ?", List(id)).update
delUserCount <- StatementIO("delete from user where id = ?", List(id)).update
} yield (delArticleCount, delUserCount)
delUser.transact(conn)
}
可以看到删除文章的SQL组成一个StatementIO,执行update产生一个ConnectionIO[Int];删除用户的SQL组成一个StatementIO,执行update产生一个ConnectionIO[Int]。两个ConnectionIO[Int]又通过for表达式(因为ConnectionIO类型实现了flatMap和map方法,所以可以应用for表达式语法糖)组合成了一个ConnectionIO[(Int, Int)]并赋值给delUser,其中第一个Int代表删除的文章数,第二个Int表示删除的用户数。最后delUser调用transact方法返回Either[Exception, (Int, Int)]。这其中我们看到StatementIO在调用update方法的时候并没有真正执行SQL而是生成一个ConnectionIO,ConnectionIO又可以自由的组合成一个新的ConnectionIO。最后ConnectionIO调用run方法或transact方法来真正执行SQL完成数据库更新或查询等操作,我们这里调用transact方法是因为我们组合的ConnectionIO代表着是一个事务操作,在其中一个失败的时候需要进行回滚。
写到这里我们还能对现有的API做进一步的优化吗?我们先来看下Scala在处理字符串中带入变量的一种写法:
s"select id, username, password from user where username='$username'"
s"delete from article where userID = ${user.id}"
对比一下我们是如何构造StatementIO,那我们能否运用Scala处理字符串的模式来处理StatementIO的构建呢?比如像如下这种写法:
sql"select id, username, password from user where username=$username"
sql"delete from article where userID = ${user.id}"
两者的区别在于一个以s开头,一个以sql开头;一个返回值是String;一个返回值是StatementIO,喔!还有一个区别sql“”方式传入字符串变量username不需要‘’。好吧,让我们在StatementIO伴生对象中看下sql方法是如何实现的:
import scala.StringContext._
implicit class SqlHelper(private val sc: StringContext) extends AnyVal {
def sql(args: Any*): StatementIO = {
val sql = sc.sqlInterpolator(treatEscapes, args)
new StatementIO(sql, args)
}
def sqlInterpolator(process: String => String, args: Seq[Any]): String = {
sc.checkLengths(args)
val pi = sc.parts.iterator
val wi = (1 to args.length).map(i => "?")
val ai = wi.iterator
val bldr = new StringBuilder(process(pi.next()))
while (ai.hasNext) {
bldr append ai.next
bldr append process(pi.next())
}
bldr.toString()
}
}
在这里我们再一次用到了隐式类这样的黑魔法,通过隐式类SqlHelper为StringContext定义了sql方法和sqlInterpolator方法。其中sqlInterpolator方法可以将字符串中的变量替换成“?”,sql 方法用替换后的字符创和传入的变量来构造StatementIO。现在我们使用sql方法再来实现之前getUser:
def getUser(id: Int)(conn: Connection): Either[Exception, User] =
sql"select id, username, password from user where id = $id"
.query.unique.run(conn)
可以看到getUser方法的实现变的越来越简洁了,但是这其中我们隐藏了一个隐式方法mappingUser:List[String] -> A。这个隐式函数作为query方法的隐式参数,我们在调用query方法的时候没有显示传入该函数,但是在作用域内我们还是得定义该函数的,让我们来看下这个mappingUser方法:
implicit def mappingUser = Mapping(
l => {
val id = l(0).toInt
val username = l(1)
val password = l(2)
User(id, username, password)
}
)
其中我们引入了Mapping类型,先来看下Mapping类型:
trait Mapping[A] extends (List[String] => A)
object Mapping {
def apply[A](f: List[String] => A) = new Mapping[A] {
override def apply(v: List[String]): A = f(v)
}
}
可以看到Mapping类型继承了函数类型List[String] => A,用于将查询的数据进行类型转换。不过这也就说每一种类型我们就得提供一个mapping方法来将List[String]转化成对应的类型,这些转化操作能由代码自动完成吗?这时候我们就需要用到Scala反射相关的知识了,让我们来重新定义StatementIO的query方法:
def query1[A: TypeTag: ClassTag]: ConnectionIO[List[A]] = {
val fa = (conn: Connection) =>
using(conn.prepareStatement(sql)) {
stmt =>
(1 to parameters.size).zip(parameters).foreach {
case (i, p) => stmt.setObject(i, p)
}
using(stmt.executeQuery()){ rs => fromResultSet[A](rs) }
}
ConnectionIO(fa)
}
在新的query方法中我们调用fromResultSet方法将ResultSet转化成List[A],那我们来看下fromResultSet方法的实现:
def fromResultSet[A: TypeTag: ClassTag](rs: ResultSet): List[A] = {
val rm = runtimeMirror(classTag[A].runtimeClass.getClassLoader)
val classTest = typeOf[A].typeSymbol.asClass
val classMirror = rm.reflectClass(classTest)
val constructor = typeOf[A].decl(termNames.CONSTRUCTOR).asMethod
val constructorMirror = classMirror.reflectConstructor(constructor)
val paramNames = constructor.paramLists.flatten.map(_.name.toString)
def loop(rs: ResultSet, res: List[A]): List[A] =
if(rs.next()) {
val constructorArgs = paramNames.map(rs.getObject)
loop(rs, res :+ constructorMirror(constructorArgs: _*).asInstanceOf[A])
} else
res
loop(rs, Nil)
}
在fromResultSet方法中通过反射获取类型A的构造参数列表,然后根据参数名作为对应表的列名在ResultSet中获取对应的值。其中我们使用了一个尾递归替代循环来遍历ResultSet。需要注意的是Scala反射包是要单独添加依赖的,例如:
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % "2.11.8"
)
现在我们可以来使用新的query方法来获取数据了:
def getUser(id: Int)(conn: Connection): Either[Exception, User] =
sql"select id, username, password from user where id = $id"
.query1[User].unique.run(conn)
在这里我们就不需要提供一个隐式方法来完成List[String]到类型A的转化了,因为在query1[A]中我们已经通过反射将类型A的字段和表的列名对应起来完成了转换。这样用户在查询的时候不需要在提供一个隐式方法,只需要提供需要转换的样例类的类型即可,在最后的测试中还是发现了一些瑕疵,这种通过反射的方式类映射表和样例类方法查询性能的表现不是很好,这个还需要优化吧。