Slick 3 代码生成实战

Slick的全称是"Scala Language-Integrated Connection Kit",它是类型安全的函数式关系型映射库,简称FRM - Functional Relational Mapping。专门用于Scala对关系型数据库的操作。它带来了函数式语言的便利性,可以通过Scala的丰富集合对数据库转换。Slick适用基于PlayAkka 框架的功能实现。

Slick底层实现了DBIO的响应式设计,它是一个Reactive Stream的实现,因此有一下一些特性:

  • Clean separation of I/O and CPU-intensive code: Isolating I/O allows you to keep your main thread pool busy with CPU-intensive parts of the application while waiting for I/O in the background.
  • Resilience under load(负荷回弹): When a database cannot keep up with the load of your application, Slick will not create more and more threads (thus making the situation worse) or lock out all kinds of I/O. Back-pressure is controlled efficiently through a queue (of configurable size) for database I/O actions, allowing a certain number of requests to build up with very little resource usage and failing immediately once this limit has been reached.
  • Reactive Streams for asynchronous streaming.
  • Efficient utilization of database resources: Slick can be tuned easily and precisely for the parallelism (number of concurrent active jobs) and resource ussage (number of currently suspended database sessions) of your database server.

Slick 3 的新特性集中在:大量使用组合的设计模式,不需要显式声明session,非阻塞,stream支持的 reactive 等 。

首先创建数据库

1
2
3
4
5
6
7
8
DROP TABLE IF EXISTS `action`;
CREATE TABLE `action` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8;

SBT配置依赖,并加入SBT Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
name := "chapa-Slick"

version := "1.0"

scalaVersion := "2.11.8"

organization := "Scala in ALG & design pattern"

// append options passed to the Scala compiler
scalacOptions ++= Seq("-deprecation", "-unchecked", "-feature")

libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.0.1",
"junit" % "junit" % "4.12",
"org.slf4j" % "slf4j-api" % "1.7.22",
"org.apache.logging.log4j" % "log4j-core" % "2.7",
"org.apache.logging.log4j" % "log4j-api" % "2.7",
"org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.7",
"com.typesafe.scala-logging" %% "scala-logging" % "3.5.0",

"mysql" % "mysql-connector-java" % "6.0.6",
"com.zaxxer" % "HikariCP" % "2.7.8",

"com.typesafe.slick" %% "slick" % "3.2.1",
"com.typesafe.slick" %% "slick-hikaricp" % "3.2.1",
"com.typesafe.slick" %% "slick-codegen" % "3.2.1"
)

slick <<= slickCodeGenTask

sourceGenerators in Compile <+= slickCodeGenTask

lazy val slick = TaskKey[Seq[File]]("gen-tables")
lazy val slickCodeGenTask = (sourceManaged, dependencyClasspath in Compile, runner in Compile, streams) map { (dir, cp, r, s) =>
val outputDir = (dir / "main/scala").getPath
val username = "root"
val password = "****"
val url = "jdbc:mysql:///test?nullNamePatternMatchesAll=true&serverTimezone=UTC"
val jdbcDriver = "com.mysql.cj.jdbc.Driver"
val slickDriver = "slick.jdbc.MySQLProfile"
val pkg = "cn.galudisu.fp.models"
toError(r.run("slick.codegen.SourceCodeGenerator", cp.files, Array(slickDriver, jdbcDriver, url, outputDir, pkg, username, password), s.log))
val fname = outputDir + "/" + "cn/galudisu/fp/models" + "/Tables.scala"
Seq(file(fname))
}

建立Dao层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.Executors

import cn.galudisu.fp.models.Tables._
import cn.galudisu.fp.models.Tables.profile.api._
import cn.galudisu.fp.models._
import slick.lifted.Query

import scala.concurrent.{ExecutionContext, ExecutionContextExecutorService, Future}

trait ActionDao {
def findById(id: Long): Future[Option[Tables.ActionRow]]
def save(name: String): Future[Long]
}

case class ActionDaoImpl(db: Database) extends ActionDao {
private implicit val ec: ExecutionContextExecutorService = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5))

override def findById(id: Long): Future[Option[Tables.ActionRow]] = {
val query: Query[Tables.Action, Tables.Action#TableElementType, Seq] = Action.filter(_.id === id)
val action = query.result.headOption
db.run(action)
}

override def save(name: String): Future[Long] = {
val query = Action.map(a => a.name)
val actions = (for {
actionInsert <- query += name
actionId <- sql"SELECT LAST_INSERT_ID()".as[(Long)].head
} yield actionId).transactionally
db.run(actions)
}
}

由于使用了HikariCP作为数据库连接池,需要一些额外配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db = {
driver = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/test?nullNamePatternMatchesAll=true&serverTimezone=UTC"
connectionTimeout = 3000
validationTimeout = 1000
connectionTestQuery = "/*ping*/ select 1"
keepAliveConnection = true
numThreads = 10
numThreads = 5
connectionTimeout = 30000
maximumPoolSize = 26
user = "root"
password = "****"
connectionPool = "HikariCP"
}

数据库需要打印输出SQL相关信息,所以加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF" monitorInterval="1800">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<ThresholdFilter level="TRACE" onMatch="ACCEPT"/>
<PatternLayout
pattern="[%-5level %date{HH:mm:ss.SSS}] %msg%n%throwable"/>
</Console>
</Appenders>
<Loggers>
<logger name="slick" level="DEBUG"/>
<Root level="INFO">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import cn.galudisu.fp.models.Tables.profile.api._
import org.scalatest._
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{Millis, Seconds, Span}

class ActionDaoSpec extends FlatSpec with Matchers with ScalaFutures {
implicit val defaultPatience: PatienceConfig =
PatienceConfig(timeout = Span(2, Seconds), interval = Span(5, Millis))

private val db = Database.forConfig("db")
private val actionDao = ActionDaoImpl(db)

"save" should "save the value without exception" in {
actionDao.save("Hello!").futureValue
}

"findById" should "find a row" in {
val actionId = actionDao.save("New Hello!").futureValue
val maybeAction = actionDao.findById(actionId).futureValue

maybeAction.isDefined should be(true)
maybeAction.get.name should be("New Hello!")
}
}

DAO模式实际上在函数式编程中并不适用,读者有兴趣,可以参考Shapeless的设计进行改造

Unit Test