淺談Slick(4)- Slick301:我的Slick開發項目設置


  前面幾篇介紹里嘗試了一些Slick的功能和使用方式,看來基本可以滿足用scala語言進行數據庫操作編程的要求,而且有些代碼可以通過函數式編程模式來實現。我想,如果把Slick當作數據庫操作編程主要方式的話,可能需要先制定一套比較規范的模式來應付日常開發(也要考慮團隊開發)、測試和維護。首先從項目結構來說,我發現由Intellij-Idea IDE界面直接產生的SBT項目結構已經比較理想了。在src/main/resources是scala項目獲取配置文件的默認目錄、我們可以按照需要在src/main/scala下增加代碼子目錄(package)及在src/main/test下擺放測試代碼。配置文件application.conf、logback.xml是放在src/main/resources下的。application.conf是Slick的配置文件,logback.xml是跟蹤器logback(log4j)的配置文件。Slick把jdbc api集成到scala編程語言里,能夠支持多種數據庫。也就是說Slick提供了多種數據庫的驅動api。Slick支持在配置文件application.conf里配置數據庫功能模式,這樣我們就可以在正式部署軟件時才通過修訂application.conf里的配置來決定具體的數據庫種類和參數。當然前提是我們的程序代碼不能依賴任何特別的數據庫api。我們從表結構設定開始,先看看上篇Slick101里的例子:

 1 package com.datatech.learn.slick101  2 import slick.driver.H2Driver.api._  3 object slick101 {  4 
 5 /* ----- schema */
 6   //表字段對應模版
 7   case class AlbumModel (id: Long  8  ,title: String  9  ,year: Option[Int] 10  ,artist: String 11  ) 12   //表結構: 定義字段類型, * 代表結果集字段
13   class AlbumTable(tag: Tag) extends Table[AlbumModel](tag, "ALBUMS") { 14     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey) 15     def title = column[String]("TITLE") 16     def year = column[Option[Int]]("YEAR") 17     def artist = column[String]("ARTIST",O.Default("Unknown")) 18     def * = (id,title,year,artist) <> (AlbumModel.tupled, AlbumModel.unapply) 19  } 20   //庫表實例
21   val albums = TableQuery[AlbumTable]

我們可以看到這段代碼依賴了slick.driver.H2Driver.api,是專門針對H2 Database的了。我們可以用依賴注入(dependency injection, IOC)來解決這個依賴問題。先試試用最傳統的依賴注入方式:傳入參數來注入這個數據庫驅動依賴,把代碼放在src/main/scala/model/TableDefs.scala里:

 1 package com.bayakala.learn.slick301.model  2 import slick.driver.JdbcProfile  3 class TableDefs(val dbDriver: JdbcProfile) {  4  import dbDriver.api._  5   case class Supplier(id: Long  6  , name: String  7  , contact: Option[String]  8  , website: Option[String])  9   final class Suppliers(tag: Tag) extends Table[Supplier](tag,"SUPPLERS") { 10     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey) 11     def name = column[String]("NAME") 12     def contact = column[Option[String]]("CONTACT") 13     def website = column[Option[String]]("WEBSITE") 14     def * = (id, name, contact, website) <> (Supplier.tupled,Supplier.unapply) 15     def nidx = index("NM_IDX",name,unique = true) 16  } 17   val suppliers = TableQuery[Suppliers] 18 
19   case class Coffee(id: Long 20  ,name: String 21  ,supid: Long 22  ,price: Double 23  ,sales: Int) 24   final class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") { 25     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey) 26     def name = column[String]("NAME") 27     def supid = column[Long]("SUPID") 28     def price = column[Double]("PRICE",O.Default(0.0)) 29     def sales = column[Int]("SALES",O.Default(0)) 30     def * = (id,name,supid,price,sales) <> (Coffee.tupled, Coffee.unapply) 31     def fk_sup = foreignKey("FK_SUP",supid,suppliers)(_.id,onDelete = ForeignKeyAction.Restrict,onUpdate = ForeignKeyAction.Cascade) 32     def supidx = index("SUP_IDX",supid,unique = false) 33     def nidx = index("NM_IDX",name,unique = true) 34  } 35   val coffees = TableQuery[Coffees] 36 
37 }

注意我們是把JdbcProfile作為參數注入了class TableDefs里。如果TableDefs經常需要作為其它類的父類繼承的話,設計成trait能更加靈活的進行類型混合(type mixing)。這樣的需求可以用cake pattern方式進行依賴注入。我們在需要src/main/scala/config/AppConfig.scala里定義依賴界面trait DBConfig:

1 package com.bayakala.learn.slick301.config 2 import slick.driver.JdbcProfile 3 trait DBConfig { 4  val jdbcDriver: JdbcProfile 5  import jdbcDriver.api._ 6  val db: Database 7 }

后面我們可以通過實現多種DBConfig實例方式來構建開發、測試、部署等數據庫環境。為了方便示范,我們設計幾個基本的Query Action,放在src/main/scala/access/DAOs.scala里,用cake pattern注入依賴DBConfig:

 1 package com.bayakala.learn.slick301.access  2 import com.bayakala.learn.slick301.config  3 import com.bayakala.learn.slick301.config.DBConfig  4 import com.bayakala.learn.slick301.model.TableDefs  5 trait DAOs { dbconf: DBConfig =>
 6  import jdbcDriver.api._  7   //注入依賴
 8   val tables = new TableDefs(dbconf.jdbcDriver)  9  import tables._ 10   //suppliers queries
11   val createSupplierTable = suppliers.schema.create 12   val allSuppliers = suppliers.result 13  def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String]) 14   = suppliers += Supplier(id,name,address,website) 15   def insertSupbyName(n: String) = suppliers.map(_.name) += n 16   //coffees queries
17   val createCoffeeTable = coffees.schema.create 18   val allCoffees = coffees.result 19   def insertCoffee(c: (Long,String,Long,Double,Int)) =
20     coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5) 21 
22 }

dbconf: DBConfig => 的意思是在進行DAOs的實例化時必須混入(mixing)DBConfig類。

以上兩個代碼文件TableDefs.scala和DAOs.scala在注入依賴后都能夠順利通過編譯了。

我們在src/main/scala/main/Main.scala里測試運算DAOs里的query action:

 1 package com.bayakala.learn.slick301.main  2 import com.bayakala.learn.slick301.config.DBConfig  3 import com.bayakala.learn.slick301.access.DAOs  4 
 5 import scala.concurrent.{Await, Future}  6 import scala.util.{Failure, Success}  7 import scala.concurrent.duration._  8 import scala.concurrent.ExecutionContext.Implicits.global
 9 import slick.backend.DatabaseConfig 10 import slick.driver.{H2Driver, JdbcProfile} 11 object Main { 12  
13   object Actions extends DAOs with DBConfig { 14     override lazy val jdbcDriver: JdbcProfile = H2Driver 15     val dbConf: DatabaseConfig[H2Driver] = DatabaseConfig.forConfig("h2") 16     override val db = dbConf.db 17  } 18  import Actions._ 19   
20   def main(args: Array[String]) = { 21     val res = db.run(createSupplierTable).andThen { 22       case Success(_) => println("supplier table created") 23       case Failure(_) => println("unable to create supplier table") 24  } 25     Await.ready(res, 3 seconds) 26 
27     val res2 = db.run(insertSupbyName("Acme Coffee Co.")) 28     Await.ready(res2, 3 seconds) 29 
30     Await.ready(db.run(allSuppliers), 10 seconds).foreach(println) 31 
32     val res10 = db.run(createCoffeeTable).andThen { 33       case Success(_) => println("coffee table created") 34       case Failure(_) => println("unable to create coffee table") 35  } 36     Await.ready(res10, 3 seconds) 37 
38     val res11 = db.run(insertCoffee((101,"Columbia",1,158.0,0))) 39     Await.ready(res11, 3 seconds) 40 
41     Await.ready(db.run(allCoffees), 10 seconds).foreach(println) 42 
43  } 44 
45 }

Actions是DAOs的實例。我們看到必須把DBConfig混入(mixin)。但是我們構建的數據庫又變成了專門針對H2的api了,這樣的話每次變動數據庫對象我們就必須重新編譯Main.scala,不符合上面我們提到的要求。我們可以把目標數據庫放到application.conf里,然后在Main.scala里用typesafe-config實時根據application.conf里的設置確定數據庫參數。src/main/resources/application.conf內容如下:

 1 app = {  2   dbconfig = h2  3 }  4 
 5 h2 {  6   driver = "slick.driver.H2Driver$"
 7  db {  8     url = "jdbc:h2:~/slickdemo;mv_store=false"
 9     driver = "org.h2.Driver"
10     connectionPool = HikariCP 11     numThreads = 10
12     maxConnections = 12
13     minConnections = 4
14     keepAliveConnection = true
15  } 16 } 17 
18 h2mem = { 19   url = "jdbc:h2:mem:slickdemo"
20   driver = org.h2.Driver 21   connectionPool = disabled 22   keepAliveConnection = true
23 } 24 
25 mysql { 26   driver = "slick.driver.MySQLDriver$"
27  db { 28     url = "jdbc:mysql://localhost/slickdemo"
29     driver = com.mysql.jdbc.Driver 30     keepAliveConnection = true
31     user="root"
32     password="123"
33     numThreads=10
34     maxConnections = 12
35     minConnections = 4
36  } 37 } 38 
39 mysqldb = { 40   dataSourceClass = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
41  properties { 42     user = "root"
43     password = "123"
44     databaseName = "slickdemo"
45     serverName = "localhost"
46  } 47   numThreads = 10
48   maxConnections = 12
49   minConnections = 4
50 } 51 
52 postgres { 53   driver = "slick.driver.PostgresDriver$"
54  db { 55     url = "jdbc:postgresql://127.0.0.1/slickdemo"
56     driver = "org.postgresql.Driver"
57     connectionPool = HikariCP 58     user = "slick"
59     password = "123"
60     numThreads = 10
61     maxConnections = 12
62     minConnections = 4
63  } 64 } 65 
66 postgressdb = { 67   dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
68   properties = { 69     databaseName = "slickdemo"
70     user = "slick"
71     password = "123"
72  } 73   connectionPool = HikariCP 74   numThreads = 10
75   maxConnections = 12
76   minConnections = 4
77 } 78 
79 mssql { 80   driver = "com.typesafe.slick.driver.ms.SQLServerDriver$"
81  db { 82     url = "jdbc:sqlserver://host:port"
83     driver = com.microsoft.sqlserver.jdbc.SQLServerDriver 84     connectionTimeout = 30 second 85     connectionPool = HikariCP 86     user = "slick"
87     password = "123"
88     numThreads = 10
89     maxConnections = 12
90     minConnections = 4
91     keepAliveConnection = true
92  } 93 } 94 
95 tsql { 96   driver = "slick.driver.H2Driver$"
97   db = ${h2mem} 98 }

現在application.conf里除了數據庫配置外又加了個app配置。我們在Main.scala里實例化DAOs時可以用typesafe-config讀取app.dbconfig值后設定jdbcDriver和db:

 1   object Actions extends DAOs with DBConfig {  2  import slick.util.ClassLoaderUtil  3  import scala.util.control.NonFatal  4  import com.typesafe.config.ConfigFactory  5 
 6     def getDbConfig: String =
 7         ConfigFactory.load().getString("app.dbconfig")  8 
 9     def getDbDriver(path: String): JdbcProfile = { 10       val config = ConfigFactory.load() 11       val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver") 12       val untypedP = try { 13         if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null) 14         else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance() 15       } catch { 16         case NonFatal(ex) =>
17           throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex) 18  } 19  untypedP.asInstanceOf[JdbcProfile] 20  } 21     
22     override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig) 23     val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig) 24     override val db = dbConf.db 25   }

現在我們只需要改變application.conf里的app.dbconfig就可以轉換目標數據庫參數了。實際上,除了數據庫配置,我們還可以在application.conf里進行其它類型的配置。然后用typesafe-config實時讀取。如果不想在application.conf進行數據庫之外的配置,可以把其它配置放在任何文件里,然后用ConfigFactory.load(path)來讀取。

另外,在軟件開發過程中跟蹤除錯也是很重要的。我們可以用logback來跟蹤Slick、HikariCP等庫的運行狀態。logback配置在src/main/resources/logback.xml:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 
 3 <configuration>
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <encoder>
 6             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
 7         </encoder>
 8     </appender>
 9 
10     <logger name="application" level="DEBUG"/>
11     <logger name="com.zaxxer.hikari" level="DEBUG"/>
12     <logger name="slick" level="DEBUG"/>
13 
14     <root level="DEBUG">
15         <appender-ref ref="STDOUT"/>
16     </root>
17 </configuration>

DEBUG值可以顯示最詳細的狀態信息。

好了,我把這次示范代碼提供在下面:

build.sbt:

 1 name := "learn-slick301"
 2 
 3 version := "1.0"
 4 
 5 scalaVersion := "2.11.8"
 6 
 7 libraryDependencies ++= Seq(  8   "com.typesafe.slick" %% "slick" % "3.1.1",  9   "com.h2database" % "h2" % "1.4.191", 10   "com.typesafe.slick" %% "slick-hikaricp" % "3.1.1", 11   "ch.qos.logback" % "logback-classic" % "1.1.7", 12   "org.typelevel" %% "cats" % "0.7.2"
13 
14 )

src/main/resources/

application.conf:

 1 app = {  2   dbconfig = h2  3 }  4 
 5 h2 {  6   driver = "slick.driver.H2Driver$"
 7  db {  8     url = "jdbc:h2:~/slickdemo;mv_store=false"
 9     driver = "org.h2.Driver"
10     connectionPool = HikariCP 11     numThreads = 10
12     maxConnections = 12
13     minConnections = 4
14     keepAliveConnection = true
15  } 16 } 17 
18 h2mem = { 19   url = "jdbc:h2:mem:slickdemo"
20   driver = org.h2.Driver 21   connectionPool = disabled 22   keepAliveConnection = true
23 } 24 
25 mysql { 26   driver = "slick.driver.MySQLDriver$"
27  db { 28     url = "jdbc:mysql://localhost/slickdemo"
29     driver = com.mysql.jdbc.Driver 30     keepAliveConnection = true
31     user="root"
32     password="123"
33     numThreads=10
34     maxConnections = 12
35     minConnections = 4
36  } 37 } 38 
39 mysqldb = { 40   dataSourceClass = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
41  properties { 42     user = "root"
43     password = "123"
44     databaseName = "slickdemo"
45     serverName = "localhost"
46  } 47   numThreads = 10
48   maxConnections = 12
49   minConnections = 4
50 } 51 
52 postgres { 53   driver = "slick.driver.PostgresDriver$"
54  db { 55     url = "jdbc:postgresql://127.0.0.1/slickdemo"
56     driver = "org.postgresql.Driver"
57     connectionPool = HikariCP 58     user = "slick"
59     password = "123"
60     numThreads = 10
61     maxConnections = 12
62     minConnections = 4
63  } 64 } 65 
66 postgressdb = { 67   dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
68   properties = { 69     databaseName = "slickdemo"
70     user = "slick"
71     password = "123"
72  } 73   connectionPool = HikariCP 74   numThreads = 10
75   maxConnections = 12
76   minConnections = 4
77 } 78 
79 mssql { 80   driver = "com.typesafe.slick.driver.ms.SQLServerDriver$"
81  db { 82     url = "jdbc:sqlserver://host:port"
83     driver = com.microsoft.sqlserver.jdbc.SQLServerDriver 84     connectionTimeout = 30 second 85     connectionPool = HikariCP 86     user = "slick"
87     password = "123"
88     numThreads = 10
89     maxConnections = 12
90     minConnections = 4
91     keepAliveConnection = true
92  } 93 } 94 
95 tsql { 96   driver = "slick.driver.H2Driver$"
97   db = ${h2mem} 98 }

logback.xml:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 
 3 <configuration>
 4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 5         <encoder>
 6             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
 7         </encoder>
 8     </appender>
 9 
10     <logger name="application" level="DEBUG"/>
11     <logger name="com.zaxxer.hikari" level="DEBUG"/>
12     <logger name="slick" level="DEBUG"/>
13 
14     <root level="DEBUG">
15         <appender-ref ref="STDOUT"/>
16     </root>
17 </configuration>

src/main/scala/config/AppConfig.scala:

1 package com.bayakala.learn.slick301.config 2 import slick.driver.JdbcProfile 3 trait DBConfig { 4  val jdbcDriver: JdbcProfile 5  import jdbcDriver.api._ 6  val db: Database 7 }

src/main/scala/model/TableDefs.scala:

 1 package com.bayakala.learn.slick301.model  2 import slick.driver.JdbcProfile  3 class TableDefs(val dbDriver: JdbcProfile) {  4  import dbDriver.api._  5   case class Supplier(id: Long  6  , name: String  7  , contact: Option[String]  8  , website: Option[String])  9   final class Suppliers(tag: Tag) extends Table[Supplier](tag,"SUPPLERS") { 10     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey) 11     def name = column[String]("NAME") 12     def contact = column[Option[String]]("CONTACT") 13     def website = column[Option[String]]("WEBSITE") 14     def * = (id, name, contact, website) <> (Supplier.tupled,Supplier.unapply) 15     def nidx = index("NM_IDX",name,unique = true) 16  } 17   val suppliers = TableQuery[Suppliers] 18 
19   case class Coffee(id: Long 20  ,name: String 21  ,supid: Long 22  ,price: Double 23  ,sales: Int) 24   final class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") { 25     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey) 26     def name = column[String]("NAME") 27     def supid = column[Long]("SUPID") 28     def price = column[Double]("PRICE",O.Default(0.0)) 29     def sales = column[Int]("SALES",O.Default(0)) 30     def * = (id,name,supid,price,sales) <> (Coffee.tupled, Coffee.unapply) 31     def fk_sup = foreignKey("FK_SUP",supid,suppliers)(_.id,onDelete = ForeignKeyAction.Restrict,onUpdate = ForeignKeyAction.Cascade) 32     def supidx = index("SUP_IDX",supid,unique = false) 33     def nidx = index("NM_IDX",name,unique = true) 34  } 35   val coffees = TableQuery[Coffees] 36 
37 }

src/main/scala/access/DAOs.scala:

 1 package com.bayakala.learn.slick301.access  2 import com.bayakala.learn.slick301.config  3 import com.bayakala.learn.slick301.config.DBConfig  4 import com.bayakala.learn.slick301.model.TableDefs  5 trait DAOs { dbconf: DBConfig =>
 6  import jdbcDriver.api._  7   //注入依賴
 8   val tables = new TableDefs(dbconf.jdbcDriver)  9  import tables._ 10   //suppliers queries
11   val createSupplierTable = suppliers.schema.create 12   val allSuppliers = suppliers.result 13  def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String]) 14   = suppliers += Supplier(id,name,address,website) 15   def insertSupbyName(n: String) = suppliers.map(_.name) += n 16   //coffees queries
17   val createCoffeeTable = coffees.schema.create 18   val allCoffees = coffees.result 19   def insertCoffee(c: (Long,String,Long,Double,Int)) =
20     coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5) 21 
22 }

src/main/scala/main/Main.scala:

 1 package com.bayakala.learn.slick301.main  2 import com.bayakala.learn.slick301.config.DBConfig  3 import com.bayakala.learn.slick301.access.DAOs  4 
 5 import scala.concurrent.Await  6 import scala.util.{Failure, Success}  7 import scala.concurrent.duration._  8 import scala.concurrent.ExecutionContext.Implicits.global
 9 import slick.backend.DatabaseConfig 10 import slick.driver.JdbcProfile 11 
12 object Main { 13 
14   object Actions extends DAOs with DBConfig { 15  import slick.SlickException 16  import slick.util.ClassLoaderUtil 17  import scala.util.control.NonFatal 18  import com.typesafe.config.ConfigFactory 19 
20     def getDbConfig: String =
21         ConfigFactory.load().getString("app.dbconfig") 22 
23     def getDbDriver(path: String): JdbcProfile = { 24       val config = ConfigFactory.load() 25       val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver") 26       val untypedP = try { 27         if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null) 28         else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance() 29       } catch { 30         case NonFatal(ex) =>
31           throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex) 32  } 33  untypedP.asInstanceOf[JdbcProfile] 34  } 35 
36     override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig) 37     val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig) 38     override val db = dbConf.db 39  } 40  import Actions._ 41 
42 
43   def main(args: Array[String]) = { 44 
45     val res = db.run(createSupplierTable).andThen { 46       case Success(_) => println("supplier table created") 47       case Failure(_) => println("unable to create supplier table") 48  } 49     Await.ready(res, 3 seconds) 50 
51     val res2 = db.run(insertSupbyName("Acme Coffee Co.")) 52     Await.ready(res2, 3 seconds) 53 
54     Await.ready(db.run(allSuppliers), 10 seconds).foreach(println) 55 
56     val res10 = db.run(createCoffeeTable).andThen { 57       case Success(_) => println("coffee table created") 58       case Failure(_) => println("unable to create coffee table") 59  } 60     Await.ready(res10, 3 seconds) 61 
62     val res11 = db.run(insertCoffee((101,"Columbia",1,158.0,0))) 63     Await.ready(res11, 3 seconds) 64 
65     Await.ready(db.run(allCoffees), 10 seconds).foreach(println) 66 
67   }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

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



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