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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13661339/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 04:45:21  来源:igfitidea点击:

How to write database-agnostic Play application and perform first-time database initialization?

scalaplayframework-2.0slick

提问by j3d

I'm using Slickwith a Play Framework2.1 and I have some troubles.

我将SlickPlay Framework2.1一起使用,但遇到了一些麻烦。

Given the following entity...

鉴于以下实体...

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 _)
}

...I have to import a package for a specificdatabase driver, but I want to use H2for testingand PostgreSQLin production. How should I proceed?

...我必须为特定的数据库驱动程序导入一个包,但我想在生产中使用H2进行测试PostgreSQL。我应该如何进行?

I was able to workaround this by overridingthe driver settings in my unit test:

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

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, "[email protected]", "Password"))
        val account = for (account <- Accounts) yield account
        account.first.id.get mustEqual 1
      }
    }
  }
}

I don't like this solution and I'm wondering if there is an elegant way to write DB-agnostic code so there are two different database engines used - one in testing and another in production?

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

I don't want to use evolution, either, and prefer to let Slick create the database tables for me:

我也不想使用进化,更喜欢让 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
    }
  }
}

The first time I start the application, everything works fine... then, of course, the second time I start the application it crashes because the tables already exist in the PostgreSQL database.

我第一次启动应用程序时,一切正常……然后,当然,第二次启动应用程序时它崩溃了,因为这些表已经存在于 PostgreSQL 数据库中。

That said, my last two questions are:

也就是说,我的最后两个问题是:

  1. How can I determine whether or not the database tables already exist?
  2. How can I make the onStartmethod above DB-agnostic so that I can test my application with FakeApplication?
  1. 如何确定数据库表是否已经存在?
  2. 我怎样才能使onStart上面的方法与数据库无关,以便我可以用 测试我的应用程序FakeApplication

采纳答案by Daniel Dietrich

You find an example on how to use the cake pattern / dependency injection to decouple the Slick driver from the database access layer here: https://github.com/slick/slick-examples.

您可以在此处找到有关如何使用蛋糕模式/依赖项注入将 Slick 驱动程序与数据库访问层分离的示例:https: //github.com/slick/slick-examples

How to decouple the Slick driver and test application with FakeApplication

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

A few days ago I wrote a Slick integration library for play, which moves the driver dependency to the application.conf of the Play project: https://github.com/danieldietrich/slick-integration.

前几天写了一个Slick的play集成库,把驱动依赖移到了Play项目的application.conf中:https: //github.com/danieldietrich/slick-integration

With the help of this library your example would be implemented as follows:

在此库的帮助下,您的示例将按如下方式实现:

1) Add the dependency to project/Build.scala

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

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

Add snapshot repository

添加快照存储库

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

Or local repository, if slick-integration is published locally

或本地存储库,如果 slick-integration 在本地发布

resolvers += Resolver.mavenLocal

2) Add the Slick driver to conf/application.conf

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

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

3) Implement app/models/Account.scala

3) 实现 app/models/Account.scala

In the case of slick-integration, it is assumed that you use primary keys of type Long which are auto incremented. The pk name is 'id'. The Table/Mapper implementation has default methods (delete, findAll, findById, insert, update). Your entities have to implement 'withId' which is needed by the 'insert' method.

在 slick-integration 的情况下,假设您使用自动递增的 Long 类型的主键。pk 名称是“id”。Table/Mapper 实现具有默认方法(delete、findAll、findById、insert、update)。您的实体必须实现“插入”方法所需的“withId”。

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) Implement app/models/DAL.scala

4) 实现 app/models/DAL.scala

This is the Data Access Layer (DAL) which is used by the controllers to access the database. Transactions are handled by the Table/Mapper implementation within the corresponding Component.

这是控制器用来访问数据库的数据访问层 (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

5) 实现 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, "[email protected]", "Password"))
      val id = account.id
      id mustNotEqual None 
      Accounts.findById(id.get) mustEqual Some(account)
    }
  }

}

How to determine whether or not the database tables already exist

如何判断数据库表是否已经存在

I cannot give you a sufficient answer to this question...

对于这个问题,我无法给你足够的答案......

... but perhaps this is not really s.th you want to do. What if you add an attribute to an table, say Account.active? If you want to safe the data currently stored within your tables, then an alter script would do the job. Currently, such an alter script has to be written by hand. The DAL.ddl.createStatementscould be used to retrieve the create statements. They should be sorted to be better comparable with previous versions. Then a diff (with previous version) is used to manually create the alter script. Here, evolutions are used to alter the db schema.

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

Here's an example on how to generate (the first) evolution:

这是关于如何生成(第一个)进化的示例:

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)

  }

}

回答by triggerNZ

I was also trying to address this problem: the ability to switch databases between test and production. The idea of wrapping each table object in a trait was unappealing.

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

I am not trying to discuss the pros and cons of the cake pattern here, but I found another solution, for those who are interested.

我不想在这里讨论蛋糕图案的优缺点,但我找到了另一种解决方案,供感兴趣的人使用。

Basically, make an object like this:

基本上,制作一个这样的对象:

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
    }
  }
}

Obviously, you can do any decision logic you like here. It does not have to be based on system properties.

显然,您可以在此处执行任何您喜欢的决策逻辑。它不必基于系统属性。

Now, instead of:

现在,而不是:

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

You can say

你可以说

import mypackage.MovableDriver.simple._

UPDATE: A Slick 3.0 Version, courtesy of trent-ahrens:

更新:一个 Slick 3.0 版本,由 trent-ahrens 提供:

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
    }
  }
}

回答by Sebastien Lorber

The play-slickdoes exactly the same as what is proposed in the other answers, and it seems to be under the umbrella of Play/Typesafe.

播放光滑不完全一样,什么是在其他的答案提出的,它似乎是玩的/伞下的类型安全。

You just can import import play.api.db.slick.Config.driver.simple._and it will choose the appropriate driver according to conf/application.conf.

您只需导入import play.api.db.slick.Config.driver.simple._,它就会根据conf/application.conf.

It also offers some more things like connection pooling, DDL generation...

它还提供了更多功能,例如连接池、DDL 生成...

回答by Oleg Efrem

If, like me, you're not using Play! for the project, a solution is provided by Nishruu here

如果你和我一样,没有使用 Play!对于该项目,Nishruu在此处提供了解决方案