当前位置: 首页 > 工具软件 > Scala Slick > 使用案例 >

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

东门晓博
2023-12-01

scala slick

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

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

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

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

1.简介

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

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

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

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

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

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

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

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 ),例如对于UsersAddresses表,我们必须要求将这些主标识符返回给我们:

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提供了流式传输数据库查询结果的功能。 不仅如此,它的流实现完全支持React流规范,并且可以立即与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

scala slick

 类似资料: