使用Scala开发现代应用程序:使用Slick进行数据库访问

本文是我们名为“ 使用Scala开发现代应用程序 ”的学院课程的一部分。

在本课程中,我们提供一个框架和工具集,以便您可以开发现代的Scala应用程序。 我们涵盖了广泛的主题,从SBT构建和响应式应用程序到测试和数据库访问。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

当然,我们正处在数据存储蓬勃发展的时代。 在过去的几年中,出现了无数的NoSQLNewSQL解决方案,即使是最近几天,新的解决方案也时不时出现在这里。

但是, 关系数据库形式的长期参与者仍在绝大多数软件系统中使用。 防弹和经过战斗测试,它们是存储对业务数据至关重要的第一选择。

1.简介

一般而言,当我们谈论Java和JVM平台时, JDBC是与关系数据库进行交互的标准方式。 这样,几乎每个关系数据库供应商都提供JDBC驱动程序实现,因此可以从应用程序代码连接到数据库引擎。

在许多方面, JDBC是一种老式的规范,几乎不适合现代的反应式编程范例。 尽管进行了一些讨论以修改规范,但实际上并没有朝着这个方向进行任何积极的工作,至少是在公开场合。

2.数据库访问,功能方式

无法加快规格的更改并不意味着什么也不能做。 JVM上有许多不同的框架和库可用于访问关系数据库 。 它们中的一些目标是尽可能地接近SQL ,而另一些目标则是提供关系模型到编程语言结构的“无缝”映射(所谓的ORM类或对象关系映射解决方案)。 虽然公平地说,但是它们大多数还是建立在JDBC抽象之上。

在这方面, Slick (或完整的Scala Language-Integrated Connection Kit )是一个库,用于提供从Scala应用程序访问关系数据库的功能。 它在很大程度上基于功能编程范例,因此通常被称为功能关系映射 (或FRM )库。 Slick的最终承诺是通过对集合的常规操作来重塑访问关系数据库的方式,这对于每个Scala开发人员都非常熟悉,并且特别强调类型安全。

尽管不久之前发布3.1.1 ,但它是一个相对较年轻的库,仍处于达到一定成熟度的道路上。 许多早期采用者确实记得2.x3.x版本之间的区别有多重要。 幸运的是,事情变得更加稳定和顺畅,即将发布的3.2版本的第一个里程碑仅用了几周的时间。

Slick与时俱进,是完全异步的库。 而且,正如我们很快就会看到的那样,它也实现了反应流规范

3.配置

Slick完全支持许多流行的开源和商业关系数据库引擎。 为了演示我们将至少使用其中两个:用于生产部署的MySQL和用于集成测试的H2

感谢Slick ,在针对单个关系数据库引擎开发应用程序时,很容易上手。 但是由于数据库功能的差异,以JDBC驱动程序独立的方式配置和使用Slick有点棘手。 因为我们的目标是至少两个不同的引擎MySQLH2 ,所以我们肯定会走这条路。

Slick中配置数据库连接的典型方法是在application.conf文件中有一个专用的命名部分,例如:

db {
  driver = "slick.driver.MySQLDriver$"
  
  db {
  	url = "jdbc:mysql://localhost:3306/test?user=root&password=password"
  	driver = com.mysql.jdbc.Driver
  	maxThreads = 5
  }
}

对于通过JDBC使用关系数据库的 JVM开发人员,该配置应该看起来很熟悉。 值得一提的是, Slick使用出色的HikariCP库支持开箱即用的数据库连接池。 maxThreads设置提示Slick配置最大大小为5连接池。

如果您好奇为什么配置中有两个驱动程序设置,这就是原因。 第一个驱动程序设置标识特定于Slick的 JDBC配置文件( Slick驱动程序),而第二个驱动程序则指出要使用的JDBC驱动程序实现。

为了照顾这个配置,我们将定义一个专用的DbConfiguration特性,尽管引入这种特性的目的目前可能还不那么明显:

trait DbConfiguration {
  lazy val config = DatabaseConfig.forConfig[JdbcProfile]("db")
}

4.表映射

可以说, 关系数据库的第一件事就是数据建模。 本质上,它转换为数据库模式,表,它们之间的关系和约束的创建。 幸运的是, Slick使它变得非常容易。

作为练习,让我们构建一个示例应用程序来管理用户及其地址,由这两个类表示。

case class User(id: Option[Int], email: String, 
  firstName: Option[String], lastName: Option[String])

case class Address(id: Option[Int], userId: Int, 
  addressLine: String, city: String, postalCode: String)

反过来,我们的关系数据模型将仅由两个表USERSADDRESSES 。 让我们使用Slick功能在Scala中进行调整

trait UsersTable { this: Db =>
  import config.driver.api._
  
  private class Users(tag: Tag) extends Table[User](tag, "USERS") {
    // Columns
    def id = column[Int]("USER_ID", O.PrimaryKey, O.AutoInc)
    def email = column[String]("USER_EMAIL", O.Length(512))
    def firstName = column[Option[String]]("USER_FIRST_NAME", O.Length(64)) 
    def lastName = column[Option[String]]("USER_LAST_NAME", O.Length(64))
    
    // Indexes
    def emailIndex = index("USER_EMAIL_IDX", email, true)
    
    // Select
    def * = (id.?, email, firstName, lastName) <> (User.tupled, User.unapply)
  }
  
  val users = TableQuery[Users]
}

对于熟悉SQL语言的人来说,肯定与CREATE TABLE语句非常相似。 但是, Slick还可以使用*投影(直译为SELECT * FROM USERS )来定义由Scala类( User )表示的域实体到表行( Users )以及反之亦然的无缝转换。

我们尚未触及的一个细微细节是Db特性( this: Db =>参考this: Db =>构造)。 让我们来看看它是如何定义的:

trait Db {
  val config: DatabaseConfig[JdbcProfile]
  val db: JdbcProfile#Backend#Database = config.db
}

configDbConfigurationconfig ,而db是一个新的数据库实例。 稍后,在UsersTable特性中,使用import config.driver.api._声明将相关JDBC概要文件的各个类型引入范围。

ADDRESSES表的映射看起来非常相似,除了我们需要对USERS表的外键引用这一事实。

trait AddressesTable extends UsersTable { this: Db =>
  import config.driver.api._
  
  private class Addresses(tag: Tag) extends Table[Address](tag, "ADDRESSES") {
    // Columns
    def id = column[Int]("ADDRESS_ID", O.PrimaryKey, O.AutoInc)
    def addressLine = column[String]("ADDRESS_LINE")
    def city = column[String]("CITY") 
    def postalCode = column[String]("POSTAL_CODE")
    
    // ForeignKey
    def userId = column[Int]("USER_ID")
    def userFk = foreignKey("USER_FK", userId, users)
      (_.id, ForeignKeyAction.Restrict, ForeignKeyAction.Cascade)
    
    // Select
    def * = (id.?, userId, addressLine, city, postalCode) <> 
     (Address.tupled, Address.unapply)
  }
  
  val addresses = TableQuery[Addresses]
}

usersaddresses成员用作对相应表执行任何数据库访问操作的外观。

5.储存库

尽管存储库本身并不特定于Slick ,但定义专用层与数据库引擎进行通信始终是一个好的设计原则。 我们的应用程序中只有两个存储库, UsersRepositoryAddressesRepository

class UsersRepository(val config: DatabaseConfig[JdbcProfile])
    extends Db with UsersTable {
  
  import config.driver.api._
  import scala.concurrent.ExecutionContext.Implicits.global

  ...  
}

class AddressesRepository(val config: DatabaseConfig[JdbcProfile]) 
    extends Db with AddressesTable {

  import config.driver.api._
  import scala.concurrent.ExecutionContext.Implicits.global

  ...
}

我们稍后将展示的所有数据操作都将属于这些类之一。 另外,请注意继承链中Db特性的存在。

6.操作模式

一旦定义了表映射(或简化了数据库模式), Slick就可以将其投影到一系列DDL语句中,例如:

def init() = db.run(DBIOAction.seq(users.schema.create))
def drop() = db.run(DBIOAction.seq(users.schema.drop))

def init() = db.run(DBIOAction.seq(addresses.schema.create))
def drop() = db.run(DBIOAction.seq(addresses.schema.drop))

7.插入

在最简单的情况下,向表中添加新行就像向usersaddressesTableQuery的实例)中添加元素一样容易,例如:

def insert(user: User) = db.run(users += user)

当从应用程序代码中分配主键时,这很好用。 但是,如果在数据库端生成主键时(例如,使用auto-increments ),例如对于Users and Addresses表,我们必须要求将这些主标识符返回给我们:

def insert(user: User) = db
  .run(users returning users.map(_.id) += user)
  .map(id => user.copy(id = Some(id)))

8.查询

查询是Slick真正与众不同的特色之一。 正如我们已经提到的, Slick努力允许在数据库操作上使用Scala集合语义。 但是它出奇地好,请注意,您不是在使用标准的Scala类型,而是使用提升的类型:这种技术称为提升嵌入

让我们看一下这个快速示例,其中是通过表的主键从表中检索用户的一种可能方法:

def find(id: Int) = 
   db.run((for (user <- users if user.id === id) yield user).result.headOption)

另外,以for理解,我们可以只使用过滤操作,例如:

def find(id: Int) = db.run(users.filter(_.id === id).result.headOption)

结果(以及生成的SQL查询)完全相同。 万一我们需要获取用户及其地址,我们也可以在这里使用几个查询选项,从典型的连接开始:

def find(id: Int) = db.run(
  (for ((user, address) <- users join addresses if user.id === id) 
    yield (user, address)).result.headOption)

或者,或者:

def find(id: Int) = db.run(
  (for {
     user <- users if user.id === id
     address <- addresses if address.userId === id 
  } yield (user, address)).result.headOption)

光滑的查询功能确实非常强大,富有表现力和可读性。 我们只看了几个典型示例,但请浏览一下官方文档以查找更多信息。

9.更新

Slick中的更新表示为查询(基本上概述了应更新的内容)和更新本身的组合。 例如,让我们介绍一种更新用户的名字和姓氏的方法:

def update(id: Int, firstName: Option[String], lastName: Option[String]) = { 
def update(id: Int, firstName: Option[String], lastName: Option[String]) = { 
  val query = for (user <- users if user.id === id) 
    yield (user.firstName, user.lastName) 
  db.run(query.update(firstName, lastName)) map { _ > 0 }
}

10.删除

与更新类似,删除操作基本上只是一个查询,用于过滤出要删除的行,例如:

def delete(id: Int) = 
  db.run(users.filter(_.id === id).delete) map { _ > 0 }

11.流式

Slick提供了流式传输数据库查询结果的功能。 不仅如此,它的流实现完全支持反应流规范,并且可以立即与Akka Streams结合使用。

例如,让我们从users表流式传输结果,并使用Sink.fold处理阶段将它们作为序列收集。

def stream(implicit materializer: Materializer) = Source
  .fromPublisher(db.stream(users.result.withStatementParameters(fetchSize=10)))
  .to(Sink.fold[Seq[User], User](Seq())(_ :+ _))
  .run()

请注意, Slick的流功能对您正在使用的关系数据库JDBC驱动程序确实非常敏感,可能需要更多的探索和调整。 绝对要进行一些广泛的测试,以确保正确传输数据。

12. SQL

万一需要运行自定义SQL查询, Slick对此一无所获,并一如既往地尝试使其变得尽可能轻松,并提供有用的宏。 假设我们想使用普通的旧SELECT语句直接读取用户的名字和姓氏。

def getNames(id: Int) = db.run(
  sql"select user_first_name, user_last_name from users where user_id = #$id"
    .as[(String, String)].headOption)

就是这么简单。 如果无法提前知道SQL查询的形状,则Slick提供了自定义结果集提取的机制。 如果您有兴趣,官方文档中有专门针对普通旧SQL查询的非常好的部分。

13.测试

使用Slick库时,可以采用多种方法来测试数据库访问层。 传统的方法是使用内存数据库(例如H2 ),在我们的情况下,这转化为application.conf内部的较小配置更改:

db {
  driver = "slick.driver.H2Driver$"
  
  db {
  	url = "jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1"
  	driver=org.h2.Driver
  	connectionPool = disabled
  	keepAliveConnection = true
  }
}

请注意,如果在生产配置中我们打开了数据库连接池,则测试之一仅使用单个连接,并且显式禁用了池。 其他所有内容基本上保持不变。 我们唯一需要注意的是在测试运行之间创建和删除模式。 幸运的是,正如我们在“ 操纵模式”一节中看到的那样,使用Slick非常容易。

class UsersRepositoryTest extends Specification with DbConfiguration 
    with FutureMatchers with OptionMatchers with BeforeAfterEach {
  
  sequential
  
  val timeout = 500 milliseconds
  val users = new UsersRepository(config)
  
  def before = {
    Await.result(users.init(), timeout)
  }
  
  def after = {
    Await.result(users.drop(), timeout)
  }
  
  "User should be inserted successfully" >> { implicit ee: ExecutionEnv =>
    val user = User(None, "a@b.com", Some("Tom"), Some("Tommyknocker"))
    users.insert(user) must be_== (user.copy(id = Some(1))).awaitFor(timeout)
  }
}

非常基本的Specs2测试规范,具有单个测试步骤,以验证新用户是否已正确插入数据库表中。

如果出于任何原因要为Slick开发自己的数据库驱动程序,则可以使用有用的Slick TestKit模块以及示例驱动程序实现。

14.结论

Slick是极具表现力和强大功能的库,可以从Scala应用程序访问关系数据库 。 它非常灵活,并且在大多数情况下提供了多种同时完成任务的方法,力求在使开发人员具有较高的生产力和不掩盖他们使用关系模型SQL进行拨号这一事实之间保持平衡。

希望我们所有人现在都感染了Slick ,并渴望尝试一下。 官方文档是开始学习Slick并开始使用的绝佳场所。

15.下一步是什么

在本教程下一部分中,我们将讨论开发命令行(或简称为控制台) Scala应用程序。

翻译自: https://www.javacodegeeks.com/2016/08/developing-modern-applications-scala-database-access-slick.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值