FunDA(1)- Query Result Row:强类型Query结果行
FunDA的特点之一是以数据流方式提供逐行数据操作支持。这项功能解决了FRM如Slick数据操作以SQL批次模式为主所产生的问题。为了实现安全高效的数据行操作,我们必须把FRM产生的Query结果集转变成一种强类型的结果集,也就是可以字段名称进行操作的数据行类型结果集。在前面的一篇讨论中我们介绍了通过Shape来改变Slick Query结果行类型。不过这样的转变方式需要编程人员对Slick有较深的了解。更重要的是这种方式太依赖Slick的内部功能了。我们希望FunDA可以支持多种FRM,所以应当尽量避免与任何FRM的紧密耦合。看来从FRM的返回结果开始进行数据行类型格式转换是一种比较现实的选择。一般来说我们还是可以假定任何FRM的使用者对于FRM的Query结果集类型是能理解的,因为他们的主要目的就是为了使用这个结果集。那么由FunDA的使用者提供一个Query结果数据行与另一种类型的类型转换函数应该不算是什么太高的要求吧。FunDA的设计思路是由用户提供一个目标类型以及FRM Query结果数据行到这个强类型行类型的类型转换函数后由FunDA提供强类型行结果集。下面先看一个典型的Slick Query例子:
import slick.driver.H2Driver.api._ import scala.concurrent.duration._ import scala.concurrent.Await object TypedRow extends App { class AlbumsTable(tag: Tag) extends Table[ (Long,String,String,Option[Int],Int)](tag,"ALBUMS") { def id = column[Long]("ID",O.PrimaryKey) def title = column[String]("TITLE") def artist = column[String]("ARTIST") def year = column[Option[Int]]("YEAR") def company = column[Int]("COMPANY") def * = (id,title,artist,year,company) } val albums = TableQuery[AlbumsTable] class CompanyTable(tag: Tag) extends Table[(Int,String)](tag,"COMPANY") { def id = column[Int]("ID",O.PrimaryKey) def name = column[String]("NAME") def * = (id, name) } val companies = TableQuery[CompanyTable] val albumInfo = for { a <- albums c <- companies if (a.company === c.id) } yield(a.title,a.artist,a.year,c.name) val db = Database.forConfig("h2db") Await.result(db.run(albumInfo.result),Duration.Inf).foreach {r => println(s"${r._1} by ${r._2}, ${r._3.getOrElse(2000)} ${r._4}") } }
上面例子里的albumInfo返回结果行类型是个Tuple类型:(String,String,Option[Int],Int),没有字段名的,所以只能用r._1,r._2...这样的位置注明方式来选择字段。用这种形式来使用返回结果很容易造成混乱,选用字段错误。
前面提到:如果用户能提供一个返回行类型和一个转换函数如下:
1 case class AlbumRow(title: String,artist: String,year: Int,studio: String) 2 def toTypedRow(raw: (String,String,Option[Int],String)):AlbumRow = 3 AlbumRow(raw._1,raw._2,raw._3.getOrElse(2000),raw._4)
我们可以在读取数据后用这个函数来转换行类型:
Await.result(db.run(albumInfo.result),Duration.Inf).map{raw => toTypedRow(raw)}.foreach {r => println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}") }
返回行类型AlbumRow是个强类型。现在我吗可以用字段名来选择数据字段值了。不过,还是有些地方不对劲:应该是用户提供了目标行类型和转换函数后,直接调用一个函数就可以得到需要的结果集了。是的,我们就是要设计一套后台工具库来提供这个函数。
下面我们要设计FunDA的数据行类型class FDADataRow。这个类型现在基本上完全是针对Slick而设的,成功完成功能实现后期再考虑松散耦合问题。这个类型需要一个目标行类型定义和一个类型转换函数,外加一些Slick profile, database等信息。然后提供一个目标行类型结果集函数getTypedRows:
package com.bayakala.funda.rowtypes import scala.concurrent.duration._ import scala.concurrent.Await import slick.driver.JdbcProfile object DataRowType { class FDADataRow[SOURCE, TARGET](slickProfile: JdbcProfile,convert: SOURCE => TARGET){ import slickProfile.api._ def getTypedRows(slickAction: DBIO[Iterable[SOURCE]])(slickDB: Database): Iterable[TARGET] = Await.result(slickDB.run(slickAction), Duration.Inf).map(raw => convert(raw)) } object FDADataRow { def apply[SOURCE, TARGET](slickProfile: JdbcProfile, converter: SOURCE => TARGET): FDADataRow[SOURCE, TARGET] = new FDADataRow[SOURCE, TARGET](slickProfile, converter) } }
下面是这个函数库的使用示范:
import com.bayakala.funda.rowtypes.DataRowType val loader = FDADataRow(slick.driver.H2Driver, toTypedRow _) loader.getTypedRows(albumInfo.result)(db).foreach {r => println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}") }
那么,作为一种数据行,又如何进行数据字段的更新呢?我们应该把它当作immutable object用函数式方法更新:
1 def updateYear(from: AlbumRow): AlbumRow = 2 AlbumRow(from.title,from.artist,from.year+1,from.studio) 3 4 loader.getTypedRows(albumInfo.result)(db).map(updateYear).foreach {r => 5 println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}") 6 }
updateYear是个典型的函数式方法:传入AlbumRow,返回新的AlbumRow。
下面是这篇讨论中的源代码:
FunDA函数库:
package com.bayakala.funda.rowtypes import scala.concurrent.duration._ import scala.concurrent.Await import slick.driver.JdbcProfile object DataRowType { class FDADataRow[SOURCE, TARGET](slickProfile: JdbcProfile,convert: SOURCE => TARGET){ import slickProfile.api._ def getTypedRows(slickAction: DBIO[Iterable[SOURCE]])(slickDB: Database): Iterable[TARGET] = Await.result(slickDB.run(slickAction), Duration.Inf).map(raw => convert(raw)) } object FDADataRow { def apply[SOURCE, TARGET](slickProfile: JdbcProfile, converter: SOURCE => TARGET): FDADataRow[SOURCE, TARGET] = new FDADataRow[SOURCE, TARGET](slickProfile, converter) } }
功能测试源代码:
1 import slick.driver.H2Driver.api._ 2 3 import scala.concurrent.duration._ 4 import scala.concurrent.Await 5 6 object TypedRow extends App { 7 8 class AlbumsTable(tag: Tag) extends Table[ 9 (Long,String,String,Option[Int],Int)](tag,"ALBUMS") { 10 def id = column[Long]("ID",O.PrimaryKey) 11 def title = column[String]("TITLE") 12 def artist = column[String]("ARTIST") 13 def year = column[Option[Int]]("YEAR") 14 def company = column[Int]("COMPANY") 15 def * = (id,title,artist,year,company) 16 } 17 val albums = TableQuery[AlbumsTable] 18 class CompanyTable(tag: Tag) extends Table[(Int,String)](tag,"COMPANY") { 19 def id = column[Int]("ID",O.PrimaryKey) 20 def name = column[String]("NAME") 21 def * = (id, name) 22 } 23 val companies = TableQuery[CompanyTable] 24 25 val albumInfo = 26 for { 27 a <- albums 28 c <- companies 29 if (a.company === c.id) 30 } yield(a.title,a.artist,a.year,c.name) 31 32 val db = Database.forConfig("h2db") 33 34 Await.result(db.run(albumInfo.result),Duration.Inf).foreach {r => 35 println(s"${r._1} by ${r._2}, ${r._3.getOrElse(2000)} ${r._4}") 36 } 37 38 case class AlbumRow(title: String,artist: String,year: Int,studio: String) 39 def toTypedRow(raw: (String,String,Option[Int],String)):AlbumRow = 40 AlbumRow(raw._1,raw._2,raw._3.getOrElse(2000),raw._4) 41 42 Await.result(db.run(albumInfo.result),Duration.Inf).map{raw => 43 toTypedRow(raw)}.foreach {r => 44 println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}") 45 } 46 47 import com.bayakala.funda.rowtypes.DataRowType.FDADataRow 48 49 val loader = FDADataRow(slick.driver.H2Driver, toTypedRow _) 50 51 loader.getTypedRows(albumInfo.result)(db).foreach {r => 52 println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}") 53 } 54 55 def updateYear(from: AlbumRow): AlbumRow = 56 AlbumRow(from.title,from.artist,from.year+1,from.studio) 57 58 loader.getTypedRows(albumInfo.result)(db).map(updateYear).foreach {r => 59 println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}") 60 } 61 62 }