FunDA的特點之一是以數據流方式提供逐行數據操作支持。這項功能解決了FRM如Slick數據操作以SQL批次模式為主所產生的問題。為了實現安全高效的數據行操作,我們必須把FRM產生的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例子:
1 import slick.driver.H2Driver.api._
2 import scala.concurrent.duration._
3 import scala.concurrent.Await
4
5 object TypedRow extends App {
6
7 class AlbumsTable(tag: Tag) extends Table[
8 (Long,String,String,Option[Int],Int)](tag,"ALBUMS") {
9 def id = column[Long]("ID",O.PrimaryKey)
10 def title = column[String]("TITLE")
11 def artist = column[String]("ARTIST")
12 def year = column[Option[Int]]("YEAR")
13 def company = column[Int]("COMPANY")
14 def * = (id,title,artist,year,company)
15 }
16 val albums = TableQuery[AlbumsTable]
17 class CompanyTable(tag: Tag) extends Table[(Int,String)](tag,"COMPANY") {
18 def id = column[Int]("ID",O.PrimaryKey)
19 def name = column[String]("NAME")
20 def * = (id, name)
21 }
22 val companies = TableQuery[CompanyTable]
23
24 val albumInfo = for {
25 a <- albums
26 c <- companies
27 if (a.company === c.id)
28 } yield(a.title,a.artist,a.year,c.name)
29
30 val db = Database.forConfig("h2db")
31
32 Await.result(db.run(albumInfo.result),Duration.Inf).foreach {r =>
33 println(s"${r._1} by ${r._2}, ${r._3.getOrElse(2000)} ${r._4}")
34 }
35
36 }
上面例子里的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)
我們可以在讀取數據後用這個函數來轉換行類型:
1 Await.result(db.run(albumInfo.result),Duration.Inf).map{raw =>
2 toTypedRow(raw)}.foreach {r =>
3 println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
4 }
返回行類型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)
}
}
下麵是這個函數庫的使用示範:
1 import com.bayakala.funda.rowtypes.DataRowType
2
3 val loader = FDADataRow(slick.driver.H2Driver, toTypedRow _)
4
5 loader.getTypedRows(albumInfo.result)(db).foreach {r =>
6 println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
7 }
那麼,作為一種數據行,又如何進行數據欄位的更新呢?我們應該把它當作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函數庫:
1 package com.bayakala.funda.rowtypes 2 3 import scala.concurrent.duration._ 4 import scala.concurrent.Await 5 import slick.driver.JdbcProfile 6 7 object DataRowType { 8 class FDADataRow[SOURCE, TARGET](slickProfile: JdbcProfile,convert: SOURCE => TARGET){ 9 import slickProfile.api._ 10 11 def getTypedRows(slickAction: DBIO[Iterable[SOURCE]])(slickDB: Database): Iterable[TARGET] = 12 Await.result(slickDB.run(slickAction), Duration.Inf).map(raw => convert(raw)) 13 } 14 15 object FDADataRow { 16 def apply[SOURCE, TARGET](slickProfile: JdbcProfile, converter: SOURCE => TARGET): FDADataRow[SOURCE, TARGET] = 17 new FDADataRow[SOURCE, TARGET](slickProfile, converter) 18 } 19 20 }
功能測試源代碼:
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 }