Как написать независимое от базы данных приложение Play и выполнить первоначальную инициализацию базы данных?

Я использую Slick с Play 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 метод выше DB-агностик, так что я могу проверить свое приложение с FakeApplication?

4 ответа

Решение

Вы можете найти пример того, как использовать шаблон торт / внедрение зависимостей для отделения драйвера Slick от слоя доступа к базе данных здесь: https://github.com/slick/slick-examples.

Как отделить драйвер Slick и протестировать приложение с FakeApplication

Несколько дней назад я написал библиотеку интеграции Slick для Play, которая переносит зависимость драйвера в application.conf проекта Play: https://github.com/danieldietrich/slick-integration.

С помощью этой библиотеки ваш пример будет реализован следующим образом:

1) Добавить зависимость к проекту /Build.scala

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

Добавить репозиторий снимков

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

Или локальный репозиторий, если локальная интеграция опубликована

resolvers += Resolver.mavenLocal

2) Добавьте драйвер Slick в файл conf / application.conf.

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

3) Реализовать приложение /models/Account.scala

В случае гладкой интеграции предполагается, что вы используете первичные ключи типа Long, которые автоматически увеличиваются. Имя ПК - это "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) Реализовать приложение /models/DAL.scala

Это уровень доступа к данным (DAL), который используется контроллерами для доступа к базе данных. Транзакции обрабатываются реализацией Table/Mapper в соответствующем Компоненте.

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) Реализуем 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? Если вы хотите сохранить данные, которые в настоящее время хранятся в ваших таблицах, тогда скрипт alter сделает эту работу. В настоящее время такой сценарий изменения должен быть написан от руки. DAL.ddl.createStatements может быть использован для извлечения операторов создания. Они должны быть отсортированы, чтобы быть лучше сопоставимыми с предыдущими версиями. Затем diff (с предыдущей версией) используется для ручного создания сценария изменения. Здесь эволюции используются для изменения схемы БД.

Вот пример того, как генерировать (первую) эволюцию:

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)

  }

}

Я также пытался решить эту проблему: возможность переключения баз данных между тестированием и производством. Идея обернуть каждый объект таблицы в черту не была привлекательной.

Я не пытаюсь обсудить плюсы и минусы схемы торта здесь, но я нашел другое решение для тех, кто заинтересован.

По сути, создайте объект, подобный этому:

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._

ОБНОВЛЕНИЕ: версия 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
    }
  }
}

Play-Slick делает то же самое, что предлагается в других ответах, и, похоже, находится под эгидой Play/Typesafe.

Вы просто можете импортировать import play.api.db.slick.Config.driver.simple._и он выберет подходящий драйвер в соответствии с conf/application.conf,

Он также предлагает еще несколько вещей, таких как пул соединений, генерация DDL...

Если, как и я, вы не используете Play! для проекта, решение предоставлено Nishruu здесь

Другие вопросы по тегам