by 壯衣
在上一篇博文中我們列舉了一些簡單的例子來展示封裝的API
向數(shù)據(jù)庫插入一條用戶數(shù)據(jù):
def addUser1(user: User)(conn: Connection): Either[Exception, Int] =
StatementIO("insert into user (username, password) values (?, ?)",
List(user.username, user.password)).update.run(conn)
從數(shù)據(jù)庫讀取一條用戶數(shù)據(jù):
def getUser1(id: Int)(conn: Connection): Either[Exception, User] =
StatementIO("select id, username, password from user where id = ?", List(id))
.query.unique.run(conn)
目前我們只操作了一張表而且也沒有涉及事務處理议经,讓我們再來創(chuàng)建一張文章表折欠,對應的樣例類為:
case class Article(id: Int, title: String, content: String, userID: Int)
現(xiàn)在有這樣的場景姜盈,我們需要刪除用戶同時需要刪除用戶所有的文章隔节。假設我們先刪除用戶所有的文章然后再刪除用戶枷颊,這其中有兩個刪除動作遂赠,需要操作兩張表宁否。我們知道這兩個操作構成一個事務,要么都刪除成功铸磅,要么都刪除失敗赡矢,假如一個成功一個失敗就需要回滾之前的刪除操作。這樣的場景在用封裝的API該如何實現(xiàn)呢阅仔?來看下代碼:
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吹散,執(zhí)行update產(chǎn)生一個ConnectionIO[Int];刪除用戶的SQL組成一個StatementIO八酒,執(zhí)行update產(chǎn)生一個ConnectionIO[Int]空民。兩個ConnectionIO[Int]又通過for表達式(因為ConnectionIO類型實現(xiàn)了flatMap和map方法,所以可以應用for表達式語法糖)組合成了一個ConnectionIO[(Int, Int)]并賦值給delUser丘跌,其中第一個Int代表刪除的文章數(shù)袭景,第二個Int表示刪除的用戶數(shù)唁桩。最后delUser調用transact方法返回Either[Exception, (Int, Int)]。這其中我們看到StatementIO在調用update方法的時候并沒有真正執(zhí)行SQL而是生成一個ConnectionIO耸棒,ConnectionIO又可以自由的組合成一個新的ConnectionIO荒澡。最后ConnectionIO調用run方法或transact方法來真正執(zhí)行SQL完成數(shù)據(jù)庫更新或查詢等操作,我們這里調用transact方法是因為我們組合的ConnectionIO代表著是一個事務操作与殃,在其中一個失敗的時候需要進行回滾单山。
寫到這里我們還能對現(xiàn)有的API做進一步的優(yōu)化嗎?我們先來看下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}"
兩者的區(qū)別在于一個以s開頭,一個以sql開頭爽篷;一個返回值是String悴晰;一個返回值是StatementIO,喔逐工!還有一個區(qū)別sql“”方式傳入字符串變量username不需要‘’铡溪。好吧,讓我們在StatementIO伴生對象中看下sql方法是如何實現(xiàn)的:
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 方法用替換后的字符創(chuàng)和傳入的變量來構造StatementIO」纾現(xiàn)在我們使用sql方法再來實現(xiàn)之前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方法的實現(xiàn)變的越來越簡潔了,但是這其中我們隱藏了一個隱式方法mappingUser:List[String] -> A蚓再。這個隱式函數(shù)作為query方法的隱式參數(shù)滑肉,我們在調用query方法的時候沒有顯示傳入該函數(shù),但是在作用域內(nèi)我們還是得定義該函數(shù)的摘仅,讓我們來看下這個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類型繼承了函數(shù)類型List[String] => A,用于將查詢的數(shù)據(jù)進行類型轉換实檀。不過這也就說每一種類型我們就得提供一個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方法的實現(xiàn):
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的構造參數(shù)列表签则,然后根據(jù)參數(shù)名作為對應表的列名在ResultSet中獲取對應的值须床。其中我們使用了一個尾遞歸替代循環(huán)來遍歷ResultSet。需要注意的是Scala反射包是要單獨添加依賴的渐裂,例如:
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % "2.11.8"
)
現(xiàn)在我們可以來使用新的query方法來獲取數(shù)據(jù)了:
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]中我們已經(jīng)通過反射將類型A的字段和表的列名對應起來完成了轉換钠惩。這樣用戶在查詢的時候不需要在提供一個隱式方法,只需要提供需要轉換的樣例類的類型即可族阅,在最后的測試中還是發(fā)現(xiàn)了一些瑕疵篓跛,這種通過反射的方式類映射表和樣例類方法查詢性能的表現(xiàn)不是很好,這個還需要優(yōu)化吧坦刀。