如何编写与数据库无关的Play应用程序并执行首次数据库初始化?

62

我正在使用SlickPlay Framework 2.1,并且遇到了一些问题。

鉴于以下实体...

package models

import scala.slick.driver.PostgresDriver.simple._

case class Account(id: Option[Long], email: String, password: String)

object Accounts extends Table[Account]("account") {
  def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
  def email = column[String]("email")
  def password = column[String]("password")
  def * = id.? ~ email ~ password <> (Account, Account.unapply _)
}

我需要为一个特定的数据库驱动程序导入一个包,但我想在测试中使用H2,而在生产环境中使用PostgreSQL。我该怎么做?

我成功地通过覆盖我的单元测试中的驱动程序设置来解决了这个问题:

package test

import org.specs2.mutable._

import play.api.test._
import play.api.test.Helpers._

import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession

import models.{Accounts, Account}

class AccountSpec extends Specification {

  "An Account" should {
    "be creatable" in {
      Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
        Accounts.ddl.create                                                                                                                                          
        Accounts.insert(Account(None, "user@gmail.com", "Password"))
        val account = for (account <- Accounts) yield account
        account.first.id.get mustEqual 1
      }
    }
  }
}

我不喜欢这个解决方案,想知道是否有一种优雅的方法编写与数据库无关的代码,以便在测试和生产中使用两种不同的数据库引擎?

我也不想使用演化,而是希望让Slick为我创建数据库表:

import play.api.Application
import play.api.GlobalSettings
import play.api.Play.current
import play.api.db.DB

import scala.slick.driver.PostgresDriver.simple._
import Database.threadLocalSession

import models.Accounts

object Global extends GlobalSettings {

  override def onStart(app: Application) {
    lazy val database = Database.forDataSource(DB.getDataSource())

    database withSession {
      Accounts.ddl.create
    }
  }
}

第一次启动应用程序时,一切都正常...然后,当我第二次启动应用程序时,由于表已经存在于PostgreSQL数据库中,它会崩溃。

话虽如此,我的最后两个问题是:

  1. 如何确定数据库表是否已经存在?
  2. 如何使上述onStart方法与数据库无关,以便我可以使用FakeApplication测试我的应用程序?

1
截至2015年,最好的选择可能是play-slick插件:https://dev59.com/6mYr5IYBdhLWcg3wdp4C#28174261 - Sebastien Lorber
4个回答

39
你可以在这里找到如何使用蛋糕模式 / 依赖注入将 Slick 驱动程序与数据库访问层解耦的示例:https://github.com/slick/slick-examples

如何使用 FakeApplication 解耦 Slick 驱动程序和测试应用程序

几天前,我为 Play 编写了一个 Slick 集成库,该库将驱动程序依赖项移动到 Play 项目的 application.conf 文件中:https://github.com/danieldietrich/slick-integration

使用此库,您的示例将实现如下:

1) 将依赖项添加到 project/Build.scala 中

"net.danieldietrich" %% "slick-integration" % "1.0-SNAPSHOT"

添加快照仓库

resolvers += "Daniel's Repository" at "http://danieldietrich.net/repository/snapshots"

或者本地仓库,如果slick-integration是本地发布的

resolvers += Resolver.mavenLocal

2) 将Slick驱动程序添加到conf/application.conf中

slick.default.driver=scala.slick.driver.H2Driver

3) 实现app/models/Account.scala

在slick集成中,假定您使用自动递增的Long类型主键。pk名称为'id'。Table/Mapper实现具有默认方法(delete、findAll、findById、insert、update)。您的实体必须实现'withId',这是'insert'方法所需的。

package models

import scala.slick.integration._

case class Account(id: Option[Long], email: String, password: String)
    extends Entity[Account] {
  // currently needed by Mapper.create to set the auto generated id
  def withId(id: Long): Account = copy(id = Some(id))
}

// use cake pattern to 'inject' the Slick driver
trait AccountComponent extends _Component { self: Profile =>

  import profile.simple._

  object Accounts extends Mapper[Account]("account") {
    // def id is defined in Mapper
    def email = column[String]("email")
    def password = column[String]("password")
    def * = id.? ~ email ~ password <> (Account, Account.unapply _)
  }

}

4) 实现 app/models/DAL.scala

这是数据访问层(DAL),控制器通过它来访问数据库。事务由对应组件内的表/映射器实现处理。

package models

import scala.slick.integration.PlayProfile
import scala.slick.integration._DAL
import scala.slick.lifted.DDL

import play.api.Play.current

class DAL(dbName: String) extends _DAL with AccountComponent
    /* with FooBarBazComponent */ with PlayProfile {

  // trait Profile implementation
  val profile = loadProfile(dbName)
  def db = dbProvider(dbName)

  // _DAL.ddl implementation
  lazy val ddl: DDL = Accounts.ddl // ++ FooBarBazs.ddl

}

object DAL extends DAL("default")

5) Implement test/test/AccountSpec.scala

package test

import models._
import models.DAL._
import org.specs2.mutable.Specification
import play.api.test.FakeApplication
import play.api.test.Helpers._
import scala.slick.session.Session

class AccountSpec extends Specification {

  def fakeApp[T](block: => T): T =
    running(FakeApplication(additionalConfiguration = inMemoryDatabase() ++
        Map("slick.default.driver" -> "scala.slick.driver.H2Driver",
          "evolutionplugin" -> "disabled"))) {
      try {
        db.withSession { implicit s: Session =>
          try {
            create
            block
          } finally {
            drop
          }
        }
      }
    }

  "An Account" should {
    "be creatable" in fakeApp {
      val account = Accounts.insert(Account(None, "user@gmail.com", "Password"))
      val id = account.id
      id mustNotEqual None 
      Accounts.findById(id.get) mustEqual Some(account)
    }
  }

}

如何确定数据库表是否已存在

我无法为您提供一个充分的答案...

...但也许这并不是您真正想做的事情。如果您想向表中添加一个属性,比如Account.active,如果您想保留当前存储在表中的数据,则可以使用修改脚本。目前,这样的修改脚本必须手动编写。可以使用DAL.ddl.createStatements来检索创建语句。它们应该按照版本排序,以便更好地与之前的版本进行比较。然后使用差异(与之前的版本)手动创建修改脚本。在这里,演化用于修改数据库模式。

以下是生成(第一个)演化的示例:

object EvolutionGenerator extends App {

  import models.DAL

  import play.api.test._
  import play.api.test.Helpers._

    running(FakeApplication(additionalConfiguration = inMemoryDatabase() ++
        Map("slick.default.driver" -> "scala.slick.driver.PostgresDriver",
          "evolutionplugin" -> "disabled"))) {


    val evolution = (
      """|# --- !Ups
         |""" + DAL.ddl.createStatements.mkString("\n", ";\n\n", ";\n") +
      """|
         |# --- !Downs
         |""" + DAL.ddl.dropStatements.mkString("\n", ";\n\n", ";\n")).stripMargin

    println(evolution)

  }

}

只是另一个问题... 我正在尝试您的代码,我想知道如何修改Mapper类,以便它可以交替地接受调用者提供的Id。例如,账户实体的ID应该是从电子邮件地址生成的哈希码... 数据库中与此帐户相关的任何其他数据都应由此哈希码引用。另外,您如何处理外键? - j3d
j3d,在这个最近的 Stefan Zeiger 的讲座中有一个关于外键的很好的解释:http://skillsmatter.com/podcast/scala/slick-database-access-with-scala(跳到32:00分钟)。 - Daniel Dietrich
关于id,当您使用类型为Entity和Mapper trait的case类时,它始终是Long类型的技术主键。如果您想使用功能键(例如电子邮件的哈希码),您只需要实现自己的Component,它与_Component相同,但包装了Table类型的对象。 - Daniel Dietrich
非常抱歉,我的问题表述不够清晰。我知道如何定义外键(FK)…我只是想知道在_Component中实现了强制、自动生成ID的一对多关系的管理方式...无论如何,我不想再继续讲下去了。非常感谢您的支持和宝贵的帮助。非常感谢。 - j3d
好的,抱歉 :) 我还没有尝试过这个,因为Slick对我来说也是新的。 一般来说,有三种(不一定不同)情况: 1)您的ID是特定类型的自动生成 - 这就是_Component目前所做的。对它的外键如视频中所述是直截了当的(请参见上面的评论)。 2)您的ID在其他地方生成。在将实体插入数据库之前,您必须手动设置它。对它的外键应该像1)中描述的那样直截了当。 3)您有一个“复合”pk,由几个属性组成,例如名称电子邮件...(请参见下一个评论) - Daniel Dietrich
如果您引用了具有这样的复合主键的实体,我会尝试创建到主键每个属性的外键,这里是两个外键(到名称和电子邮件)。--因为这不太可扩展(您的表中有许多额外的外键字段),所以我选择在Slick集成中仅使用生成的ID作为PK。如果您有一个功能唯一的属性(例如电子邮件哈希),只需将唯一约束添加到该属性 - 如果要搜索它,则添加索引...但我还没有尝试过在Slick中使用它... - Daniel Dietrich

28

我也在试图解决这个问题:在测试和生产之间切换数据库的能力。将每个表对象都包装在一个特质中的想法并不吸引人。

我并不想在这里讨论蛋糕模式的利弊,但我找到了另一个解决方案,对于那些感兴趣的人来说。

基本上,创建这样一个对象:

package mypackage
import scala.slick.driver.H2Driver
import scala.slick.driver.ExtendedProfile
import scala.slick.driver.PostgresDriver

object MovableDriver {
  val simple = profile.simple
  lazy val profile: ExtendedProfile = {
    sys.env.get("database") match {
      case Some("postgres") => PostgresDriver
      case _ => H2Driver
    }
  }
}

显然,你可以在这里使用任何决策逻辑。它不必基于系统属性。

现在,与其:

import scala.slick.driver.H2Driver.simple._

你可以说

import mypackage.MovableDriver.simple._

更新: Trent-ahrens 做出了一个漂亮的 3.0 版本:

package mypackage

import com.typesafe.config.ConfigFactory

import scala.slick.driver.{H2Driver, JdbcDriver, MySQLDriver}

object AgnosticDriver {
  val simple = profile.simple
  lazy val profile: JdbcDriver = {
    sys.env.get("DB_ENVIRONMENT") match {
      case Some(e) => ConfigFactory.load().getString(s"$e.slickDriver") match {
        case "scala.slick.driver.H2Driver" => H2Driver
        case "scala.slick.driver.MySQLDriver" => MySQLDriver
      }
      case _ => H2Driver
    }
  }
}

这是一个简单易行的解决方案。我确实需要进行一些调整才能与Slick 3.0兼容,代码片段在这里:https://gist.github.com/lolboxen/24f90773621497c412f8 - Trent Ahrens
谢谢。我一直想着要做这件事。我会粘贴你的代码来完成答案。 - triggerNZ

2

play-slick与其他答案中提到的内容完全相同,而且似乎是在Play/Typesafe的保护下。

你只需要导入import play.api.db.slick.Config.driver.simple._,它会根据conf/application.conf选择适当的驱动程序。

它还提供了一些其他功能,如连接池、DDL生成等。


-1
如果像我一样,你不是在项目中使用Play!,那么Nishruu提供了一个解决方案这里

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接