浅谈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里的例子:
package com.datatech.learn.slick101 import slick.driver.H2Driver.api._ object slick101 { /* ----- schema */ //表字段对应模版 case class AlbumModel (id: Long ,title: String ,year: Option[Int] ,artist: String ) //表结构: 定义字段类型, * 代表结果集字段 class AlbumTable(tag: Tag) extends Table[AlbumModel](tag, "ALBUMS") { def id = column[Long]("ID",O.AutoInc,O.PrimaryKey) def title = column[String]("TITLE") def year = column[Option[Int]]("YEAR") def artist = column[String]("ARTIST",O.Default("Unknown")) def * = (id,title,year,artist) <> (AlbumModel.tupled, AlbumModel.unapply) } //库表实例 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:
package com.bayakala.learn.slick301.config import slick.driver.JdbcProfile trait DBConfig { val jdbcDriver: JdbcProfile import jdbcDriver.api._ val db: Database }
后面我们可以通过实现多种DBConfig实例方式来构建开发、测试、部署等数据库环境。为了方便示范,我们设计几个基本的Query Action,放在src/main/scala/access/DAOs.scala里,用cake pattern注入依赖DBConfig:
package com.bayakala.learn.slick301.access import com.bayakala.learn.slick301.config import com.bayakala.learn.slick301.config.DBConfig import com.bayakala.learn.slick301.model.TableDefs trait DAOs { dbconf: DBConfig => import jdbcDriver.api._ //注入依赖 val tables = new TableDefs(dbconf.jdbcDriver) import tables._ //suppliers queries val createSupplierTable = suppliers.schema.create val allSuppliers = suppliers.result def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String]) = suppliers += Supplier(id,name,address,website) def insertSupbyName(n: String) = suppliers.map(_.name) += n //coffees queries val createCoffeeTable = coffees.schema.create val allCoffees = coffees.result def insertCoffee(c: (Long,String,Long,Double,Int)) = coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5) }
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:
object Actions extends DAOs with DBConfig { import slick.util.ClassLoaderUtil import scala.util.control.NonFatal import com.typesafe.config.ConfigFactory def getDbConfig: String = ConfigFactory.load().getString("app.dbconfig") def getDbDriver(path: String): JdbcProfile = { val config = ConfigFactory.load() val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver") val untypedP = try { if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null) else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance() } catch { case NonFatal(ex) => throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex) } untypedP.asInstanceOf[JdbcProfile] } override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig) val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig) override val db = dbConf.db }
现在我们只需要改变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:
name := "learn-slick301" version := "1.0" scalaVersion := "2.11.8" libraryDependencies ++= Seq( "com.typesafe.slick" %% "slick" % "3.1.1", "com.h2database" % "h2" % "1.4.191", "com.typesafe.slick" %% "slick-hikaricp" % "3.1.1", "ch.qos.logback" % "logback-classic" % "1.1.7", "org.typelevel" %% "cats" % "0.7.2" )
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:
package com.bayakala.learn.slick301.config import slick.driver.JdbcProfile trait DBConfig { val jdbcDriver: JdbcProfile import jdbcDriver.api._ val db: Database }
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:
package com.bayakala.learn.slick301.access import com.bayakala.learn.slick301.config import com.bayakala.learn.slick301.config.DBConfig import com.bayakala.learn.slick301.model.TableDefs trait DAOs { dbconf: DBConfig => import jdbcDriver.api._ //注入依赖 val tables = new TableDefs(dbconf.jdbcDriver) import tables._ //suppliers queries val createSupplierTable = suppliers.schema.create val allSuppliers = suppliers.result def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String]) = suppliers += Supplier(id,name,address,website) def insertSupbyName(n: String) = suppliers.map(_.name) += n //coffees queries val createCoffeeTable = coffees.schema.create val allCoffees = coffees.result def insertCoffee(c: (Long,String,Long,Double,Int)) = coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5) }
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 }