Scala對JDBC的一些封裝(2)

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)化吧坦刀。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末愧沟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鲤遥,更是在濱河造成了極大的恐慌沐寺,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盖奈,死亡現(xiàn)場離奇詭異混坞,居然都是意外死亡,警方通過查閱死者的電腦和手機钢坦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門究孕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人场钉,你說我怎么就攤上這事蚊俺。” “怎么了逛万?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵泳猬,是天一觀的道長。 經(jīng)常有香客問我宇植,道長得封,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任指郁,我火速辦了婚禮忙上,結果婚禮上,老公的妹妹穿的比我還像新娘闲坎。我一直安慰自己疫粥,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布腰懂。 她就那樣靜靜地躺著梗逮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绣溜。 梳的紋絲不亂的頭發(fā)上慷彤,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音,去河邊找鬼底哗。 笑死岁诉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的跋选。 我是一名探鬼主播涕癣,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼野建!你這毒婦竟也來了属划?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤候生,失蹤者是張志新(化名)和其女友劉穎同眯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唯鸭,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡须蜗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了目溉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片明肮。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缭付,靈堂內(nèi)的尸體忽然破棺而出柿估,到底是詐尸還是另有隱情,我是刑警寧澤陷猫,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布秫舌,位于F島的核電站,受9級特大地震影響绣檬,放射性物質發(fā)生泄漏足陨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一娇未、第九天 我趴在偏房一處隱蔽的房頂上張望墨缘。 院中可真熱鬧,春花似錦零抬、人聲如沸镊讼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狠毯。三九已至,卻和暖如春褥芒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工锰扶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留献酗,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓坷牛,卻偏偏與公主長得像罕偎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子京闰,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容