资讯动态

Slick 3 函数式关系映射入门实战:用 Scala 集合风格操作关系数据库(52 Technologies in 2016 · 04-slick)

发布时间:2026/10/17 11:01:10 来源:尧图企业网站定制
示例工程【免费下载链接】52-technologies-in-2016Lets learn a new technology every week. A new technology blog every Sunday in 2016.项目地址https://gitcode.com/gh_mirrors/52/52-technologies-in-2016点击查看免费下载本篇技术指南是《52 Technologies in 2016》系列第 4 篇仓库目录 04-slick的完整实战讲解核心主题是Slick 3Scala Language-Integrated Connection Kit一种基于函数式编程思想、面向 Scala 的关系数据库访问库。我们将基于仓库中的tasky示例工程从零搭建 sbt 项目依次完成数据表定义、自定义列映射、Schema 创建、插入与查询最终用 ScalaTest 写出可运行的测试用例掌握Slick 不是 ORM而是函数式关系映射FRM的核心心智模型。什么是 Slick函数式关系映射FRM而非 ORMSlick 是 Scala 生态中用于操作关系数据库的强类型库其全称是Scala Language-Integrated Connection Kit。文档开篇就强调了一个关键论断Slick 不是 ORM 库。它的实现建立在函数式编程之上不会把数据库藏在 ORM 层后面而是把数据库访问的时机控制权完整交还给开发者同时允许你像操作 Scala 集合Seq、Map一样操作数据库表。Slick 的核心思想是作为开发者你不需要手写 SQL 查询。只要用库提供的构造DSL来构建查询Slick 会自动为你生成 SQL。这一点从仓库中的运行日志可以直观印证详见后文执行 Action一节的 DEBUG 日志Slick 输出了完整的create table与Preparing statement语句。文档总结了使用 Slick 的六大收益类型安全与编译期检查查询构建在编译期即被检查错误尽早暴露为任何数据库生成查询同一套 DSL 可针对不同数据库方言生成 SQL可组合Composable查询可以像函数一样组合复用内置背压Back-pressure契合响应式Reactive编程模型通过 Reactive Streams 支持流式处理同时允许你直接使用 SQLDSL 与原生 SQL 可以共存。Slick 的 API 本质上是异步的适合构建响应式应用不过它内部依然基于 JDBC——一个同步 APISlick 在异步执行层之下完成了对 JDBC 的封装与线程池调度。关于其设计渊源Slick 官方文档3.1.1 版 introduction 章节有一句值得注意的引述Slick 的 FRM 语言集成查询模型灵感来自微软的 LINQ 项目并借鉴了可追溯至 Ericsson 早期 Mnesia 工作的概念。在数据库支持方面Slick 覆盖了市面上大多数关系数据库MySQL、PostgreSQL 等开源数据库可免费使用Oracle、SQL Server、DB2 等则以闭源扩展形式提供需要购买订阅后方可使用。本教程选择H2——一个用 Java 编写的内存 SQL 数据库与应用程序运行在同一进程内非常适合入门测试与快速起步。项目环境准备搭建 tasky 的 sbt 工程仓库中 tasky 示例工程 是本文的配套代码。先在文件系统上创建目录tasky然后在其中创建 sbt 构建文件build.sbt内容如下name : tasky description : A simple task manager for humans version : 0.1.0 scalaVersion : 2.11.7 libraryDependencies com.typesafe.slick %% slick % 3.1.1 libraryDependencies com.h2database % h2 % 1.4.191 libraryDependencies ch.qos.logback % logback-classic % 1.1.3 libraryDependencies org.scalatest %% scalatest % 2.2.6 % test逐项解读这个构建文件基础信息name、description、version定义了项目身份scalaVersion : 2.11.7指定本次教程使用的 Scala 版本slick3.1.1唯一必需的依赖本次教程固定使用该版本h21.4.191H2 内存数据库驱动用于测试和入门对于真实应用应改用 MySQL 或 PostgreSQL 等logback-classic1.1.3日志后端用于输出 Slick 的执行日志后文看到的 DEBUG 日志即来自它scalatest2.2.6test作用域测试框架用于编写验证用例。随后在tasky目录内创建标准的 SBT 源码目录结构$ mkdir -p src/main/scala $ mkdir -p src/test/scala至此一个可用于 Slick 应用开发的基本 Scala SBT 工程就绪可将项目导入 IDE 继续开发。仓库中的工程还额外配置了 plugins.sbt引入了sbt-dependency-graph0.8.1插件用于可视化管理依赖图属于可选增强。定义领域对象与数据库表映射表Table是 Scala 数据类型与数据库表之间的映射桥梁。在src/main/scala下创建包datamodel并在其中创建 Scala 对象DataModel.scalapackage datamodel import slick.driver.H2Driver.api._ object DataModel { }import slick.driver.H2Driver.api._用于告诉 Slick 我们使用的是哪一个数据库 API——这里选择了 H2。仓库中的完整实现见 DataModel.scala。定义 Task 领域对象为了让示例简单易懂我们先只定义一个领域对象Taskcase classimport java.time.LocalDateTime object DataModel { case class Task( title: String, description: String , createdAt: LocalDateTime LocalDateTime.now(), dueBy: LocalDateTime, tags: Set[String] Set(), id: Long 0L) }Task包含六个字段将映射到一张存储待办任务的表字段类型说明titleString任务标题必填descriptionString任务描述默认空字符串createdAtLocalDateTime创建时间默认当前时间Java 8 时间 APIdueByLocalDateTime截止时间必填tagsSet[String]标签集合默认空集idLong主键默认 0L由数据库自增分配示例混合使用了String、Java 8LocalDateTime、Set、Long等多种类型。部分字段提供了默认值因此在构造 Task 时只需提供title和dueBy两个参数即可例如测试中常见的Task(title Learn Slick, dueBy LocalDateTime.now().plusDays(1))。定义 TaskTable 表映射接下来为Taskcase class 编写表映射object DataModel { case class Task( title: String, description: String , createdAt: LocalDateTime LocalDateTime.now(), dueBy: LocalDateTime, tags: Set[String] Set(), id: Long 0L) class TaskTable(tag: Tag) extends TableTask { def title columnString def description columnString def createdAt columnLocalDateTime def dueBy columnLocalDateTime def tags column[Set[String]](tags) def id columnLong override def * (title, description, createdAt, dueBy, tags, id) (Task.tupled, Task.unapply) } }理解TaskTable的四个关键点继承Table抽象类每个表都必须继承Table其类型参数指明表中存储的数据类型——这里是Task。构造函数需要两个必填参数tag与表名。示例中表名为taskstag是 Slick 内部用于确定单行表结构shape的机制Slick 文档对其着墨不多属于框架内部细节。列定义与领域类一一对应每个columnT都对应Task的一个字段。id是主键columnLong中的O.PrimaryKey与O.AutoInc组合让数据库为每一行自动分配递增的id。*是默认投影*方法的类型必须与Table类型参数一致这里都是Task。方法负责在六元组(title, description, createdAt, dueBy, tags, id)与Task类型之间互转需要两个函数前者把元组转为对象后者把对象转为元组。case class 天然提供tupled与unapply正好满足的需求。补充说明并非必须使用 case class普通 Scala 类同样可行只是需要自行提供与tupled、unapply等价的两个函数。文档给出了一个普通类的完整替代写法——定义一个带val构造参数的Task类并在伴生对象中实现apply、toTask、fromTask三个方法class Task( val title: String, val description: String , val createdAt: LocalDateTime LocalDateTime.now(), val dueBy: LocalDateTime, val tags: Set[String] Set[String](), val id: Long 0L) object Task { def apply(title: String, description: String , createdAt: LocalDateTime LocalDateTime.now(), dueBy: LocalDateTime, tags: Set[String] Set[String](), id: Long 0L): Task new Task(title, description, createdAt, dueBy, tags, id) def toTask(t: (String, String, LocalDateTime, LocalDateTime, Set[String], Long)): Task new Task(t._1, t._2, t._3, t._4, t._5, t._6) def fromTask(task: Task): Option[(String, String, LocalDateTime, LocalDateTime, Set[String], Long)] Some((task.title, task.description, task.createdAt, task.dueBy, task.tags, task.id)) }用 TableQuery 表示真实数据库表表定义TaskTable就绪后还需要定义一个TableQuery类型的值它才代表实际的数据库表并提供一个可与表交互的查询 DSLlazy val Tasks TableQuery[TaskTable]后续所有针对tasks表的操作建表、插入、查询都以Tasks为入口。自定义映射让 Slick 支持 LocalDateTime 与 Set[String]如果你尝试编译到目前为止的代码会编译失败。原因在于Slick 3.1.1 的列定义原生不支持 Java 8 的LocalDateTime与Set[String]类型。解决办法是编写自定义映射Custom Mapping把这两种类型转换成 Slick 能理解的基础类型。在同一个文件DataModel.scala中新建对象ColumnDataMapper仓库实现见 DataModel.scalaobject ColumnDataMapper { implicit val localDateTimeColumnType MappedColumnType.baseLocalDateTime, Timestamp, t t.toLocalDateTime ) implicit val setStringColumnType MappedColumnType.base[Set[String], String]( tags tags.mkString(,), tagsString tagsString.split(,).toSet ) }这里定义了两个映射核心是MappedColumnType.base[A, B]它需要一对互为逆操作的转换函数LocalDateTime↔java.sql.Timestamp写入时Timestamp.valueOf(ldt)读取时t.toLocalDateTimeSet[String]↔String写入时用mkString(,)以逗号拼接读取时split(,).toSet拆分回集合。映射对象中的implicit val只是声明了隐式转换的候选者还必须显式把它挂到对应的列定义上。在TaskTable中通过import datamodel.ColumnDataMapper.{localDateTimeColumnType, setStringColumnType}引入并在column调用时以第二个参数形式传入import datamodel.ColumnDataMapper.{localDateTimeColumnType, setStringColumnType} class TaskTable(tag: Tag) extends TableTask { def title columnString def description columnString def createdAt columnLocalDateTime(localDateTimeColumnType) def dueBy columnLocalDateTime(localDateTimeColumnType) def tags column[Set[String]](tags)(setStringColumnType) def id columnLong override def * (title, description, createdAt, dueBy, tags, id) (Task.tupled, Task.unapply) }加入自定义映射后工程即可通过sbt compile编译通过。Action 与 Schema 创建理解定义不等于执行Slick 把要对数据库执行的命令抽象为Action。先看第一个 Action——创建数据库 Schemalazy val Tasks TableQuery[TaskTable] val createTaskTableAction Tasks.schema.create这里有个重要的心智模型定义 Action 并不会执行它。createTaskTableAction只是待执行的建表命令真正执行需要把它交给数据库句柄见下一节。这种描述与执行分离的设计正是 Slick 异步、可组合特性的基础。执行 ActionDatabase 句柄与首个测试用例获取数据库句柄Action 必须针对某个数据库执行。Slick 提供Database类型作为代码与数据库交互的句柄val db Database.forConfig(taskydb)其中taskydb指向一个由 Typesafe Config 项目定义的配置对象。因此需要在src/test/resources目录下创建application.conf内容如下taskydb { connectionPool disabled url jdbc:h2:mem:taskydb driver org.h2.Driver keepAliveConnection true }四个配置项的作用配置项值说明connectionPooldisabled关闭连接池内存数据库场景urljdbc:h2:mem:taskydbH2 内存数据库连接地址mem:表示纯内存模式driverorg.h2.DriverJDBC 驱动类名keepAliveConnectiontrue保持连接存活避免内存库因连接关闭而丢失数据编写首个测试创建 Schema在src/test/scala下创建包datamodel编写第一个测试类。文档原始版本CreateDatabaseSpec如下package datamodel import org.scalatest.{FunSpec, Matchers} import slick.driver.H2Driver.api._ import scala.concurrent._ import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global class CreateDatabaseSpec extends FunSpec with Matchers { describe(DataModel Spec) { it(should create database) { val db Database.forConfig(taskydb) val result Await.result(db.run(DataModel.createTaskTableAction), 2 seconds) println(result) } } }理解这段测试的三个要点异步导入scala.concurrent相关导入告诉 Slick 使用ExecutionContext.Implicits.global执行代码。Slick API 完全异步数据库调用在独立的线程池中执行必须显式提供ExecutionContext创建句柄通过taskydb配置创建Database对象获得与数据库交互的句柄run方法db.run(action)执行 Action 并返回Future。由于 Slick 是异步的测试中用Await.result(..., 2 seconds)阻塞等待结果便于断言。仓库中实际的测试文件 DataModelSpec.scala 在此基础上做了工程化改进混入BeforeAndAfterEachtrait在beforeEach中统一创建数据库句柄并执行createTaskTableAction在afterEach中调用db.shutdown关闭连接避免每个用例重复建库代码。运行测试时日志会输出 Slick 生成的建表 SQL注意createdAt/dueBy列已通过自定义映射变为TIMESTAMPtags变为VARCHARid为自增主键18:50:34.955 [ScalaTest-run-running-CreateDatabaseSpec] DEBUG s.backend.DatabaseComponent.action - #1: schema.create [create table tasks (title VARCHAR NOT NULL,description VARCHAR NOT NULL,createdAt TIMESTAMP NOT NULL,dueBy TIMESTAMP NOT NULL,tags VARCHAR NOT NULL,id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY)] 18:50:35.012 [taskydb-1] DEBUG slick.jdbc.JdbcBackend.statement - Preparing statement: create table tasks (title VARCHAR NOT NULL,description VARCHAR NOT NULL,createdAt TIMESTAMP NOT NULL,dueBy TIMESTAMP NOT NULL,tags VARCHAR NOT NULL,id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY) 18:50:35.268 [taskydb-1] DEBUG slick.jdbc.JdbcBackend.benchmark - Execution of prepared statement took 23ms这段日志同时印证了三点自定义映射确实生效LocalDateTime→TIMESTAMP、Set[String]→VARCHARO.AutoInc被翻译为GENERATED BY DEFAULT AS IDENTITY(START WITH 1)以及定义与执行分离——只有在db.run真正执行时才会出现Preparing statement。由于数据库是内存模式每次运行测试都会创建一个全新的数据库。插入数据批量插入接下来编写向tasks表插入记录的测试it(should insert single task into database) { val db Database.forConfig(taskydb) val result Await.result(db.run(DataModel.insertTaskAction(Task(title Learn Slick, dueBy LocalDateTime.now().plusDays(1)))), 2 seconds) result should be(Some(1)) }测试调用insertTaskAction传入一个Task其返回值是受影响的记录数。只插入一条任务所以断言结果为Some(1)。对应的 Action 定义在DataModel对象中def insertTaskAction(tasks: Task*) Tasks tasks.toSeqinsertTaskAction接受Task的变长参数可以一次传入一条或多条任务。插入操作使用方法——它与 Scala 集合中的语义一致把序列元素整体追加进去。根据 Slick 官方文档对插入的说明返回一个 Option 类型的累计计数如果数据库系统无法为所有行提供计数则该值可能为 None这正是断言写成result should be(Some(1))而非result should be(1)的原因。仓库测试还额外验证了批量插入场景传入 3 条任务时断言Some(3)见 DataModelSpec.scala。查询数据集合风格的 Select最后查询数据库中的所有任务it(should list all tasks in the database) { val tasks Seq( Task(title Learn Slick, dueBy LocalDateTime.now().plusDays(1)), Task(title Write blog on Slick, dueBy LocalDateTime.now().plusDays(2)), Task(title Build a simple application using Slick, dueBy LocalDateTime.now().plusDays(3)) ) Await.result(db.run(DataModel.insertTaskAction(tasks: _*)), 2 seconds) val result Await.result(db.run(DataModel.listTasksAction), 2 seconds) result should have length 3 }先用insertTaskAction(tasks: _*)批量插入 3 条任务再通过listTasksAction查询并断言结果长度为 3。listTasksAction的定义极其简洁val listTasksAction Tasks.resultTasks.result会基于*默认投影生成一条select title, description, createdAt, dueBy, tags, id from tasksSQL 查询。可以看到查询的构建方式与 Scala 集合操作高度一致result即取出表中全部行这正是 Slick 像操作集合一样操作数据库理念的直观体现。更进一步map、filter、sort等集合方法在 Slick 中同样可用——它们会被翻译成对应的 SQL 子句这也是系列续篇 05-slickSlick 3 Part 2查询数据 将要深入展开的主题。小结与后续通过本篇我们完成了 Slick 3 的第一次完整实践理解了 Slick 是**函数式关系映射FRM**而非 ORM其异步 API 与定义 Action、再执行 Action的两段式心智模型搭建了taskysbt 工程配置了slick 3.1.1、H2、Logback 与 ScalaTest 依赖用Table[Task]*投影定义了Task与tasks表的映射通过TableQuery[TaskTable]获得查询入口用MappedColumnType.base为LocalDateTime与Set[String]编写了自定义列映射通过Database.forConfig(taskydb)db.run(action)执行了建表、插入与Tasks.result全表查询并用 ScalaTest 验证了结果。这只是 Slick 功能地图的一小部分——查询 DSLmap/filter/sort、连接、聚合等更丰富的查询能力将在系列后续文章中展开。仓库中完整的可运行代码位于 04-slick/tasky测试实现见 DataModelSpec.scala可以直接对照阅读与运行。赞分享示例工程【免费下载链接】52-technologies-in-2016Lets learn a new technology every week. A new technology blog every Sunday in 2016.项目地址https://gitcode.com/gh_mirrors/52/52-technologies-in-2016点击查看免费下载相关推荐一文吃透事件驱动架构Serverless Examples的S3/SQS/SNS/Kinesis全链路实战一文吃透事件驱动架构Serverless Examples的S3/SQS/SNS/Kinesis全链路实战 Serverless Examples 是 Ser示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑