淺談Slick(3)- Slick201:從fp角度了解Slick


  我在上期討論里已經成功的創建了一個簡單的Slick項目,然后又嘗試使用了一些最基本的功能。Slick是一個FRM(Functional Relational Mapper),是為fp編程提供的scala SQL Query集成環境,可以讓編程人員在scala編程語言里用函數式編程模式來實現對數據庫操作的編程。在這篇討論里我想以函數式思考模式來加深了解Slick。我對fp編程模式印象最深的就是類型匹配:從參數類型和返回結果類型來了解函數功能。所以上面我所指的函數式思考方式主要是從Slick函數的類型匹配角度來分析函數所起的作用和具體使用方式。

我們先了解一下建表過程:

 1 import slick.driver.H2Driver.api._  2 object slick201 {  3   //projection case classes 表列模版
 4   case class Coffee(  5  id: Option[Long]  6  ,name: String  7  ,sup_ID: Int  8  ,price: Double  9  ,grade: Grade 10  ,total: Int 11  ) 12   case class Supplier( 13  id: Option[Int] 14  ,name: String 15  ,address: String 16  ,website: Option[String] 17  ) 18   //自定義字段
19   abstract class Grade(points: Int) 20   object Grade { 21     case object Premium extends Grade(2) 22     case object Quality extends Grade(1) 23     case object Bestbuy extends Grade(0) 24 
25     def fromInt(p: Int) = p match { 26         case 2 => Premium 27         case 1 => Quality 28         case 0 => Bestbuy 29  } 30     def toInt(g: Grade) = g match { 31       case Premium => 2
32       case Quality => 1
33       case Bestbuy => 0
34  } 35     implicit val customColumn: BaseColumnType[Grade] =
36       MappedColumnType.base[Grade,Int](Grade.toInt, Grade.fromInt) 37  } 38   //schema 表行結構定義
39   class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") { 40     def id = column[Long]("COF_ID", O.AutoInc, O.PrimaryKey) 41     def name = column[String]("COF_NAME") 42     def price = column[Double]("COF_PRICE") 43     def supID = column[Int]("COF_SUP") 44     def grade = column[Grade]("COF_GRADE", O.Default(Grade.Bestbuy)) 45     def total = column[Int]("COF_TOTAL", O.Default(0)) 46 
47     def * = (id.?,name,supID,price,grade,total) <> (Coffee.tupled, Coffee.unapply) 48 
49     def supplier = foreignKey("SUP_FK",supID,suppliers)(_.id,onUpdate = ForeignKeyAction.Restrict, onDelete = ForeignKeyAction.Cascade) 50     def nameidx = index("NM_IX",name,unique = true) 51  } 52   val coffees = TableQuery[Coffees] 53 
54   class Suppliers(tag: Tag) extends Table[Supplier](tag, "SUPPLIERS") { 55     def id = column[Int]("SUP_ID", O.PrimaryKey, O.AutoInc) 56     def name = column[String]("SUP_NAME") 57     def address = column[String]("SUP_ADDR", O.Default("-")) 58     def website = column[Option[String]]("SUP_WEB") 59 
60     def * = (id.?, name, address, website) <> (Supplier.tupled, Supplier.unapply) 61     def addidx = index("ADDR_IX",(name,address),unique = true) 62  } 63   val suppliers = TableQuery[Suppliers] 64 
65 }

我盡量把經常會遇到的情況如:定義字段、建索引、默認值、自定義字段等都作了嘗試。coffees和suppliers代表了最終的數據表Query,def * 定義了這個Query的默認返回結果字段。

所有的定義都是圍繞着表行(Table Row)結構進行的,包括:表屬性及操作(Table member methods)、字段(Column)、字段屬性(ColumnOptions)。表行定義操作方法基本都在slick.lifted.AbstractTable里、表屬性定義在slick.model命名空間里、而大部分的幫助支持函數都在slick.lifted命名空間的其它對象里。

表行的實際類型如下:

abstract class Table[T](_tableTag: Tag, _schemaName: Option[String], _tableName: String) extends AbstractTable[T](_tableTag, _schemaName, _tableName) { table => ...} /** The profile-independent superclass of all table row objects. * @tparam T Row type for this table. Make sure it matches the type of your `*` projection. */
abstract class AbstractTable[T](val tableTag: Tag, val schemaName: Option[String], val tableName: String) extends Rep[T] {...}

如上所示,Table[T] extends AbstractTable[T]。現在所有表行定義操作函數應該在slick.profile.relationalTableComponent.Table里可以找得到。值得注意的是表行的最終類型是Rep[T],T可能是case class或者Tuple,被升格(lift)到Rep[T]。所以大部分表行定義的支持函數都是在slick.lifted命名空間內的。

上面我們使用了模版對應表行定義方式,所有列都能和模版case class對應。那么在定義projection def * 時就需要使用<>函數:

  def <>[R : ClassTag](f: (U => R), g: (R => Option[U])) = new MappedProjection[R, U](shape.toNode(value), MappedScalaType.Mapper(g.andThen(_.get).asInstanceOf[Any => Any], f.asInstanceOf[Any => Any], None), implicitly[ClassTag[R]])

f,g是兩個case class <> Tuple轉換函數。在上面的例子里我們提供的是tupled和unapply,效果就是這樣的:

1  Coffee.tupled 2   //res2: ((Option[Long], String, Int, Double, Grade, Int)) => Coffee = <function1>
3  Coffee.unapply _ 4   //res3: Coffee => Option[(Option[Long], String, Int, Double, Grade, Int)] = <function1>

res2 >>> 把tuple: (...)轉成coffee,res2 >>> 把coffee轉成Option[(...)]

TableQuery[T]繼承了Query[T]:slick.lifted.Query.scala

/** Represents a database table. Profiles add extension methods to TableQuery * for operations that can be performed on tables but not on arbitrary * queries, e.g. getting the table DDL. */
class TableQuery[E <: AbstractTable[_]](cons: Tag => E) extends Query[E, E#TableElementType, Seq] {...} ... sealed trait QueryBase[T] extends Rep[T] /** An instance of Query represents a query or view, i.e. a computation of a * collection type (Rep[Seq[T]]). It is parameterized with both, the mixed * type (the type of values you see e.g. when you call map()) and the unpacked * type (the type of values that you get back when you run the query). * * Additional extension methods for queries containing a single column are * defined in [[slick.lifted.SingleColumnQueryExtensionMethods]]. */
sealed abstract class Query[+E, U, C[_]] extends QueryBase[C[U]] { self =>...}

所有Query對象里提供的函數TableQuery類都可以調用。上面例子里coffees,suppliers實際是數據庫表COFFEES,SUPPLIERS的Query實例,它們的默認字段集如:coffees.result是通過def * 定義的(除非用map或yield改變默認projection)。在slick.profile.RelationalProfile.TableQueryExtensionMethods里還有專門針對TableQuery類型的函數如schema等。

好了,來到了Query才算真正進入主題。Query可以說是Slick最核心的類型了。所有針對數據庫的讀寫操作都是通過Query產生SQL語句發送到數據庫實現的。Query是個函數式類型,即高階類型Query[A]。A代表生成SQL語句的元素,通過轉變A可以實現不同的SQL語句構建。不同功能的Query包括讀取(retreive)、插入(insert)、更新(update)、刪除(delete)都是通過Query變形(transformation)實現的。所有Query操作函數的款式:Query[A] => Query[B],是典型的函數式編程方式,也是scala集合操作函數款式。我們先從數據讀取Query開始,因為上面我們曾經提到過可以通過map來決定新的結果集結構(projection):

 1 val q1 = coffees.result  2  q1.statements.head  3   //res0: String = select "COF_ID", "COF_NAME", "COF_SUP", "COF_PRICE", "COF_GRADE", "COF_TOTAL" from "COFFEES"
 4 
 5   val q2 = coffees.map(r => (r.id, r.name)).result  6  q2.statements.head  7   //res1: String = select "COF_ID", "COF_NAME" from "COFFEES"
 8 
 9   val q3 = (for (c <- coffees) yield(c.id,c.name)).result 10  q3.statements.head 11   //res2: String = select "COF_ID", "COF_NAME" from "COFFEES"

因為map和flatMap的函數款式是:

map[A,B](Q[A])(A=>B]):Q[B], flatMap[A,B](Q[A])(A => Q[B]):Q[B]

所以不同的SQL語句基本上是通過Query[A] => Query[B]這種對高階類型內嵌元素進行轉變的函數式操作方式實現的。下面是一個帶篩選條件的Query:

 1   val q = coffees.filter(_.price > 100.0).map(r => (r.id,r.name)).result  2  q.statements.head  3   //res3: String = select "COF_ID", "COF_NAME" from "COFFEES" where "COF_PRICE" > 100.0
 4 
 5   val q4 = coffees.filter(_.price > 100.0).take(4).map(_.name).result  6  q4.statements.head  7   //res4: String = select "COF_NAME" from "COFFEES" where "COF_PRICE" > 100.0 limit 4
 8 
 9   val q5 = coffees.sortBy(_.id.desc.nullsFirst).map(_.name).drop(3).result 10  q5.statements.head 11   //res5: String = select "COF_NAME" from "COFFEES" order by "COF_ID" desc nulls first limit -1 offset 3

再復雜一點的Query,比如說join兩個表:

 1 val q6 = for {  2     (c,s) <- coffees join suppliers on (_.supID === _.id)  3   } yield(c.id,c.name,s.name)  4  q6.result.statements.head  5   //res6: String = select x2."COF_ID", x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2, "SUPPLIERS" x3 where x2."COF_SUP" = x3."SUP_ID"
 6   
 7   val q7 = for {  8     c <- coffees  9     s <- suppliers.filter(c.supID === _.id) 10   } yield(c.id,c.name,s.name) 11  q7.result.statements.head 12   //res7: String = select x2."COF_ID", x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2, "SUPPLIERS" x3 where x2."COF_SUP" = x3."SUP_ID"

還有匯總類型的Query:

1 coffees.map(_.price).max.result.statements.head 2   //res10: String = select max("COF_PRICE") from "COFFEES"
3  coffees.map(_.total).sum.result.statements.head 4   //res11: String = select sum("COF_TOTAL") from "COFFEES"
5  coffees.length.result.statements.head 6   //res12: String = select count(1) from "COFFEES"
7   coffees.filter(_.price > 100.0).exists.result.statements.head 8   //res13: String = select exists(select "COF_TOTAL", "COF_NAME", "COF_SUP", "COF_ID", "COF_PRICE", "COF_GRADE" from "COFFEES" where "COF_PRICE" > 100.0)

Query是個monad,它可以實現函數組合(functional composition)。如上所示:所有Query操作函數都是Query[A]=>Query[B]形式的。由於Query[A]里面的A類型是Rep[T]類型,是SQL語句組件類型。典型函數如flatMap的調用方式是:flatMap{a => MakeQuery(a ...)},可以看到下一個Query的構成可能依賴a值,而a的類型是表行或列定義。所以Query的函數組合就是SQL語句的組合,最終結果是產生目標SQL語句。

Slick處理數據的方式是通過組合相應的SQL語句后發送給數據庫去運算的,相關SQL語句的產生當然是通過Query來實現的:

 1   val qInsert = coffees += Coffee(Some(0),"American",101,56.0,Grade.Bestbuy,0)  2  qInsert.statements.head  3 //res10: String = insert into "COFFEES" ("COF_NAME","COF_SUP","COF_PRICE","COF_GRADE","COF_TOTAL") values (?,?,?,?,?)
 4   val qInsert2 = coffees.map{r => (r.name, r.supID, r.price)} += ("Columbia",101,102.0)  5  qInsert2.statements.head  6 //res11: String = insert into "COFFEES" ("COF_NAME","COF_SUP","COF_PRICE") values (?,?,?)
 7   val qInsert3 = (suppliers.map{r => (r.id,r.name)}).  8     returning(suppliers.map(_.id)) += (101,"The Coffee Co.,")  9  qInsert3.statements.head 10 //res12: String = insert into "SUPPLIERS" ("SUP_NAME") values (?)

從qInsert3產生的SQL語句來看:jdbc返回數據后還必須由Slick進一步處理后才能返回用戶要求的結果值。下面是一些其它更改數據的Query示范:

1   val qDelete = coffees.filter(_.price === 0.0).delete 2  qDelete.statements.head 3   //res17: String = delete from "COFFEES" where "COFFEES"."COF_PRICE" = 0.0
4   val qUpdate = for (c <- coffees if (c.name === "American")) yield c.price 5   qUpdate.update(10.0).statements.head 6   //res18: String = update "COFFEES" set "COF_PRICE" = ? where "COFFEES"."COF_NAME" = 'American'

update query必須通過for-comprehension的yield來確定更新字段。
Slick3.x最大的改進就是采用了functional I/O技術。具體做法就是引進DBIOAction類型,這是一個free monad。通過采用free monad的延遲運算模式來實現數據庫操作動作的可組合性(composablility)及多線程運算(concurrency)。

DBIOAction類型款式如下:

sealed trait DBIOAction[+R, +S <: NoStream, -E <: Effect] extends Dumpable { ...} package object dbio { /** Simplified type for a streaming [[DBIOAction]] without effect tracking */ type StreamingDBIO[+R, +T] = DBIOAction[R, Streaming[T], Effect.All] /** Simplified type for a [[DBIOAction]] without streaming or effect tracking */ type DBIO[+R] = DBIOAction[R, NoStream, Effect.All] val DBIO = DBIOAction }

DBIO[+R]和StreamingDBIO[+R,+T]分別是固定類型參數S和E的類型別名,用它們來簡化代碼。所有的數據庫操作函數包括result、insert、delete、update等都返回DBIOAction類型結果:

  def result: DriverAction[R, S, Effect.Read] = {...} def delete: DriverAction[Int, NoStream, Effect.Write] = {...} def update(value: T): DriverAction[Int, NoStream, Effect.Write] = {...} def += (value: U): DriverAction[SingleInsertResult, NoStream, Effect.Write] = {...}

上面的DriverAction是DBIOAction的子類。因為DBIOAction是個free monad,所以多個DBIOAction可以進行組合,而在過程中是不會立即產生DBIO副作用的。我們只能通過DBIOAction類型的運算器來對DBIOAction的組合進行運算才會正真進行數據庫數據讀寫。DBIOAction運算函數款式如下:

/** Run an Action asynchronously and return the result as a Future. */ final def run[R](a: DBIOAction[R, NoStream, Nothing]): Future[R] = runInternal(a, false)

run函數返回Future[R],代表在異步線程運算完成后返回R類型值。一般來講Query.result返回R類型為Seq[?]。

DBIOAction只是對數據庫操作動作的描述,不是實際的讀寫,所以DBIOAction可以進行組合。所謂組合的意思實際上就是把幾個動作連續起來。DBIOAction的函數組件除monad通用的map、flatMap、sequence等,還包括了andThen、zip等合並操作函數,andThen可以返回最后一個動作結果、zip在一個pair里返回兩個動作的結果。因為DBIOAction是monad,所以for-comprehension應該是最靈活、最強大的組合方式了。我們來試試用上面Query產生的動作來進行一些組合示范:

1   val initSupAction = suppliers.schema.create andThen qInsert3 2   val createCoffeeAction = coffees.schema.create 3   val insertCoffeeAction = qInsert zip qInsert2 4   val initSupAndCoffee = for { 5     _ <- initSupAction 6     _ <- createCoffeeAction 7     (i1,i2) <- insertCoffeeAction 8   } yield (i1,i2)

我們可以任意組合這些操作步驟,因為它們的返回結果類型都是DBIOAction[R]:一個free monad。大多數時間這些動作都是按照一定的流程順序組合的。可能有些時候下一個動作需要依賴上一個動作產生的結果,這個時候用for-comprehension是最適合的了:

 1 //先選出所有ESPRESSO開頭的coffee名稱,然后逐個刪除
 2   val delESAction = (for {  3     ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result  4     _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)  5   } yield ()).transactionally  6   //delESAction: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read ...  7 
 8   //對一個品種價格升10%
 9   def raisePriceAction(i: Long, np: Double, pc: Double) =
10     (for(c <- coffees if (c.id === i)) yield c.price).update(np * pc) 11   //raisePriceAction: raisePriceAction[](val i: Long,val np: Double,val pc: Double) => slick.driver.H2Driver.DriverAction[Int,slick.dbio.NoStream,slick.dbio.Effect.Write] 12   //對所有價格<100的coffee加價
13   val updatePriceAction = (for { 14     ips <- coffees.filter(_.price < 100.0).map(r => (r.id, r.price)).result 15     _ <- DBIO.seq{ips.map { ip => raisePriceAction(ip._1, ip._2, 110.0)}: _* } 16   } yield()).transactionally 17   //updatePriceAction: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read ...

另外,像monad的point:successful(R)可以把R升格成DBIOAction,failed(T)可以把T升格成DBIOAction[T]:

1   DBIO.successful(Supplier(Some(102),"Coffee Company","",None)) 2   //res19: slick.dbio.DBIOAction[Supplier,slick.dbio.NoStream,slick.dbio.Effect] = SuccessAction(Supplier(Some(102),Coffee Company,,None))
3 
4   DBIO.failed(new Exception("oh my god...")) 5   //res20: slick.dbio.DBIOAction[Nothing,slick.dbio.NoStream,slick.dbio.Effect] = FailureAction(java.lang.Exception: oh my god...)

DBIOAction還有比較完善的事后處理和異常處理機制:

 1 //主要示范事后處理機制用法,不必理會功能的具體目的是否有任何意義
 2  qInsert.andFinally(qDelete)  3   //res21: slick.dbio.DBIOAction[Int,slick.dbio.NoStream,slick.dbio.Effect.Write with slick.dbio.Effect.Write] = slick.dbio.SynchronousDatabaseAction$$anon$6@1d46b337
 4 
 5  updatePriceAction.cleanUp (  6     { case Some(e) => initSupAction; DBIO.failed(new Exception("oh my..."))  7       case _ => qInsert3  8  }  9       ,true
10  ) 11   //res22: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read ...
12 
13   raisePriceAction(101,10.0,110.0).asTry 14   //res23: slick.dbio.DBIOAction[scala.util.Try[Int],slick.dbio.NoStream,slick.dbio.Effect.Write] = slick.dbio.SynchronousDatabaseAction$$anon$9@60304a44

從上面的這些示范例子我們認識到DBIOAction的函數組合就是數據庫操作步驟組合、實際上就是程序的組合或者是功能組合:把一些簡單的程序組合成功能更全面的程序,然后才運算這個組合而成的程序。DBIOAction的運算函數run的函數款式如下: 

/** Run an Action asynchronously and return the result as a Future. */ final def run[R](a: DBIOAction[R, NoStream, Nothing]): Future[R] = runInternal(a, false)

對DBIOAction進行運算后的結果是個Future類型,也是一個高階類型,同樣可以用map、flatMap、sequence、andThen等泛函組件進行函數組合。可以參考下面的這個示范:

 1  import slick.jdbc.meta.MTable  2   import scala.concurrent.ExecutionContext.Implicits.global
 3  import scala.concurrent.duration.Duration  4  import scala.concurrent.{Await, Future}  5  import scala.util.{Success,Failure}  6 
 7   val db = Database.forURL("jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1", driver="org.h2.Driver")  8 
 9   def recreateCoffeeTable: Future[Unit] = { 10     db.run(MTable.getTables("Coffees")).flatMap { 11       case tables if tables.isEmpty => db.run(coffees.schema.create).andThen { 12         case Success(_) => println("coffee table created") 13         case Failure(e) => println(s"failed to create! ${e.getMessage}") 14  } 15       case _ => db.run((coffees.schema.drop andThen coffees.schema.create)).andThen { 16         case Success(_) => println("coffee table recreated") 17         case Failure(e) => println(s"failed to recreate! ${e.getMessage}") 18  } 19  } 20   }

好了,下面是這次討論的示范代碼:

 1 import slick.driver.H2Driver.api._  2 
 3 object slick201 {  4   //projection case classes 表列模版
 5   case class Coffee(  6  id: Option[Long]  7  ,name: String  8  ,sup_ID: Int  9  ,price: Double  10  ,grade: Grade  11  ,total: Int  12  )  13   case class Supplier(  14  id: Option[Int]  15  ,name: String  16  ,address: String  17  ,website: Option[String]  18  )  19   //自定義字段
 20   abstract class Grade(points: Int)  21   object Grade {  22     case object Premium extends Grade(2)  23     case object Quality extends Grade(1)  24     case object Bestbuy extends Grade(0)  25 
 26     def fromInt(p: Int) = p match {  27         case 2 => Premium  28         case 1 => Quality  29         case 0 => Bestbuy  30  }  31     def toInt(g: Grade) = g match {  32       case Premium => 2
 33       case Quality => 1
 34       case Bestbuy => 0
 35  }  36     implicit val customColumn: BaseColumnType[Grade] =
 37       MappedColumnType.base[Grade,Int](Grade.toInt, Grade.fromInt)  38  }  39   //schema 表行結構定義
 40   class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") {  41     def id = column[Long]("COF_ID", O.AutoInc, O.PrimaryKey)  42     def name = column[String]("COF_NAME")  43     def price = column[Double]("COF_PRICE")  44     def supID = column[Int]("COF_SUP")  45     def grade = column[Grade]("COF_GRADE", O.Default(Grade.Bestbuy))  46     def total = column[Int]("COF_TOTAL", O.Default(0))  47 
 48     def * = (id.?,name,supID,price,grade,total) <> (Coffee.tupled, Coffee.unapply)  49 
 50     def supplier = foreignKey("SUP_FK",supID,suppliers)(_.id,onUpdate = ForeignKeyAction.Restrict, onDelete = ForeignKeyAction.Cascade)  51     def nameidx = index("NM_IX",name,unique = true)  52  }  53   val coffees = TableQuery[Coffees]  54 
 55   class Suppliers(tag: Tag) extends Table[Supplier](tag, "SUPPLIERS") {  56     def id = column[Int]("SUP_ID", O.PrimaryKey, O.AutoInc)  57     def name = column[String]("SUP_NAME")  58     def address = column[String]("SUP_ADDR", O.Default("-"))  59     def website = column[Option[String]]("SUP_WEB")  60 
 61     def * = (id.?, name, address, website) <> (Supplier.tupled, Supplier.unapply)  62     def addidx = index("ADDR_IX",(name,address),unique = true)  63  }  64   val suppliers = TableQuery[Suppliers]  65 
 66   class Bars(tag: Tag) extends Table[(Int,String)](tag,"BARS") {  67     def id = column[Int]("BAR_ID",O.AutoInc,O.PrimaryKey)  68     def name = column[String]("BAR_NAME")  69     def * = (id, name)  70  }  71   val bars = TableQuery[Bars]  72 
 73  Coffee.tupled  74   //res2: ((Option[Long], String, Int, Double, Grade, Int)) => Coffee = <function1>
 75  Coffee.unapply _  76   //res3: Coffee => Option[(Option[Long], String, Int, Double, Grade, Int)] = <function1>
 77 
 78 
 79   val q1 = coffees.result  80  q1.statements.head  81   //res0: String = select "COF_ID", "COF_NAME", "COF_SUP", "COF_PRICE", "COF_GRADE", "COF_TOTAL" from "COFFEES"
 82   
 83   val q2 = coffees.map(r => (r.id, r.name)).result  84  q2.statements.head  85   //res1: String = select "COF_ID", "COF_NAME" from "COFFEES"
 86 
 87   val q3 = (for (c <- coffees) yield(c.id,c.name)).result  88  q3.statements.head  89   //res2: String = select "COF_ID", "COF_NAME" from "COFFEES"
 90 
 91 
 92   val q = coffees.filter(_.price > 100.0).map(r => (r.id,r.name)).result  93  q.statements.head  94   //res3: String = select "COF_ID", "COF_NAME" from "COFFEES" where "COF_PRICE" > 100.0
 95 
 96   val q4 = coffees.filter(_.price > 100.0).take(4).map(_.name).result  97  q4.statements.head  98   //res4: String = select "COF_NAME" from "COFFEES" where "COF_PRICE" > 100.0 limit 4
 99 
100   val q5 = coffees.sortBy(_.id.desc.nullsFirst).map(_.name).drop(3).result 101  q5.statements.head 102   //res5: String = select "COF_NAME" from "COFFEES" order by "COF_ID" desc nulls first limit -1 offset 3
103 
104   val q6 = for { 105     (c,s) <- coffees join suppliers on (_.supID === _.id) 106   } yield(c.id,c.name,s.name) 107  q6.result.statements.head 108   //res6: String = select x2."COF_ID", x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2, "SUPPLIERS" x3 where x2."COF_SUP" = x3."SUP_ID"
109 
110   val q7 = for { 111     c <- coffees 112     s <- suppliers.filter(c.supID === _.id) 113   } yield(c.id,c.name,s.name) 114  q7.result.statements.head 115   //res7: String = select x2."COF_ID", x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2, "SUPPLIERS" x3 where x2."COF_SUP" = x3."SUP_ID"
116 
117  coffees.map(_.price).max.result.statements.head 118   //res10: String = select max("COF_PRICE") from "COFFEES"
119  coffees.map(_.total).sum.result.statements.head 120   //res11: String = select sum("COF_TOTAL") from "COFFEES"
121  coffees.length.result.statements.head 122   //res12: String = select count(1) from "COFFEES"
123   coffees.filter(_.price > 100.0).exists.result.statements.head 124   //res13: String = select exists(select "COF_TOTAL", "COF_NAME", "COF_SUP", "COF_ID", "COF_PRICE", "COF_GRADE" from "COFFEES" where "COF_PRICE" > 100.0)
125   val qInsert = coffees += Coffee(Some(0),"American",101,56.0,Grade.Bestbuy,0) 126  qInsert.statements.head 127   //res14: String = insert into "COFFEES" ("COF_NAME","COF_SUP","COF_PRICE","COF_GRADE","COF_TOTAL") values (?,?,?,?,?)
128   val qInsert2 = coffees.map{r => (r.name, r.supID, r.price)} += ("Columbia",101,102.0) 129  qInsert2.statements.head 130   //res15: String = insert into "COFFEES" ("COF_NAME","COF_SUP","COF_PRICE") values (?,?,?)
131   val qInsert3 = (suppliers.map{r => (r.id,r.name)}). 132     returning(suppliers.map(_.id)) += (101,"The Coffee Co.,") 133  qInsert3.statements.head 134   //res16: String = insert into "SUPPLIERS" ("SUP_NAME") values (?)
135 
136   val qDelete = coffees.filter(_.price === 0.0).delete 137  qDelete.statements.head 138   //res17: String = delete from "COFFEES" where "COFFEES"."COF_PRICE" = 0.0
139   val qUpdate = for (c <- coffees if (c.name === "American")) yield c.price 140   qUpdate.update(10.0).statements.head 141   //res18: String = update "COFFEES" set "COF_PRICE" = ? where "COFFEES"."COF_NAME" = 'American'
142 
143   val initSupAction = suppliers.schema.create andThen qInsert3 144   val createCoffeeAction = coffees.schema.create 145   val insertCoffeeAction = qInsert zip qInsert2 146   val initSupAndCoffee = for { 147     _ <- initSupAction 148     _ <- createCoffeeAction 149     (i1,i2) <- insertCoffeeAction 150   } yield (i1,i2) 151 
152   //先選出所有ESPRESSO開頭的coffee名稱,然后逐個刪除
153   val delESAction = (for { 154     ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result 155     _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*) 156   } yield ()).transactionally 157   //delESAction: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read with slick.dbio.Effect.Write with slick.dbio.Effect.Transactional] = CleanUpAction(AndThenAction(Vector(slick.driver.JdbcActionComponent$StartTransaction$@6e76c850, FlatMapAction(slick.driver.JdbcActionComponent$QueryActionExtensionMethodsImpl$$anon$1@2005bce5,<function1>,scala.concurrent.impl.ExecutionContextImpl@245036ad))),<function1>,true,slick.dbio.DBIOAction$sameThreadExecutionContext$@294c4c1d) 158 
159   //對一個品種價格升10%
160   def raisePriceAction(i: Long, np: Double, pc: Double) =
161     (for(c <- coffees if (c.id === i)) yield c.price).update(np * pc) 162   //raisePriceAction: raisePriceAction[](val i: Long,val np: Double,val pc: Double) => slick.driver.H2Driver.DriverAction[Int,slick.dbio.NoStream,slick.dbio.Effect.Write] 163   //對所有價格<100的coffee加價
164   val updatePriceAction = (for { 165     ips <- coffees.filter(_.price < 100.0).map(r => (r.id, r.price)).result 166     _ <- DBIO.seq{ips.map { ip => raisePriceAction(ip._1, ip._2, 110.0)}: _* } 167   } yield()).transactionally 168   //updatePriceAction: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read with slick.dbio.Effect.Write with slick.dbio.Effect.Transactional] = CleanUpAction(AndThenAction(Vector(slick.driver.JdbcActionComponent$StartTransaction$@6e76c850, FlatMapAction(slick.driver.JdbcActionComponent$QueryActionExtensionMethodsImpl$$anon$1@49c8a41f,<function1>,scala.concurrent.impl.ExecutionContextImpl@245036ad))),<function1>,true,slick.dbio.DBIOAction$sameThreadExecutionContext$@294c4c1d)
169 
170   DBIO.successful(Supplier(Some(102),"Coffee Company","",None)) 171   //res19: slick.dbio.DBIOAction[Supplier,slick.dbio.NoStream,slick.dbio.Effect] = SuccessAction(Supplier(Some(102),Coffee Company,,None))
172 
173   DBIO.failed(new Exception("oh my god...")) 174   //res20: slick.dbio.DBIOAction[Nothing,slick.dbio.NoStream,slick.dbio.Effect] = FailureAction(java.lang.Exception: oh my god...) 175 
176   //示范事后處理機制,不必理會功能的具體目的
177  qInsert.andFinally(qDelete) 178   //res21: slick.dbio.DBIOAction[Int,slick.dbio.NoStream,slick.dbio.Effect.Write with slick.dbio.Effect.Write] = slick.dbio.SynchronousDatabaseAction$$anon$6@1d46b337
179 
180  updatePriceAction.cleanUp ( 181     { case Some(e) => initSupAction; DBIO.failed(new Exception("oh my...")) 182       case _ => qInsert3 183  } 184       ,true
185  ) 186   //res22: slick.dbio.DBIOAction[Unit,slick.dbio.NoStream,slick.dbio.Effect.Read with slick.dbio.Effect.Write with slick.dbio.Effect.Transactional with slick.dbio.Effect.Write] = CleanUpAction(CleanUpAction(AndThenAction(Vector(slick.driver.JdbcActionComponent$StartTransaction$@6e76c850, FlatMapAction(slick.driver.JdbcActionComponent$QueryActionExtensionMethodsImpl$$anon$1@1f7aad00,<function1>,scala.concurrent.impl.ExecutionContextImpl@245036ad))),<function1>,true,slick.dbio.DBIOAction$sameThreadExecutionContext$@294c4c1d),<function1>,true,scala.concurrent.impl.ExecutionContextImpl@245036ad)
187 
188   raisePriceAction(101,10.0,110.0).asTry 189   //res23: slick.dbio.DBIOAction[scala.util.Try[Int],slick.dbio.NoStream,slick.dbio.Effect.Write] = slick.dbio.SynchronousDatabaseAction$$anon$9@60304a44
190 
191 
192  import slick.jdbc.meta.MTable 193   import scala.concurrent.ExecutionContext.Implicits.global
194  import scala.concurrent.duration.Duration 195  import scala.concurrent.{Await, Future} 196  import scala.util.{Success,Failure} 197 
198   val db = Database.forURL("jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1", driver="org.h2.Driver") 199 
200   def recreateCoffeeTable: Future[Unit] = { 201     db.run(MTable.getTables("Coffees")).flatMap { 202       case tables if tables.isEmpty => db.run(coffees.schema.create).andThen { 203         case Success(_) => println("coffee table created") 204         case Failure(e) => println(s"failed to create! ${e.getMessage}") 205  } 206       case _ => db.run((coffees.schema.drop andThen coffees.schema.create)).andThen { 207         case Success(_) => println("coffee table recreated") 208         case Failure(e) => println(s"failed to recreate! ${e.getMessage}") 209  } 210  } 211  } 212 
213 }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM