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

Я использую 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 в производство. Как мне продолжить?

Мне удалось обойти это, переопределив параметры драйвера в моем 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
      }
    }
  }
}

Мне не нравится это решение, и мне интересно, есть ли элегантный способ написания DB-агностического кода, поэтому используются два разных ядра базы данных: один в тестировании и другой в производстве?

Я тоже не хочу использовать эволюцию и предпочитаю, чтобы 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.

Тем не менее, мои последние два вопроса:

  • Как определить, существуют ли уже таблицы базы данных?
  • Как я могу сделать метод onStart выше DB-agnostic, чтобы я мог проверить свое приложение с помощью FakeApplication?

Ответ 1

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

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

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

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

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

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

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

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

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

resolvers += Resolver.mavenLocal

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

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

3) Внедрить приложение/модели/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) Внедрить приложение/модели/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) Внедрить тест/тест/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)
    }
  }

}

Как определить, существуют ли уже таблицы базы данных

Я не могу дать вам достаточный ответ на этот вопрос...

... но, возможно, это не так, как вы хотите. Что делать, если вы добавляете атрибут в таблицу, скажем Account.active? Если вы хотите сохранить данные, хранящиеся в настоящее время в ваших таблицах, тогда будет выполнен запрос script. В настоящее время такой alter script должен быть написан вручную. DAL.ddl.createStatements можно использовать для извлечения операторов create. Их следует сортировать, чтобы они были лучше сопоставимы с предыдущими версиями. Затем diff (с предыдущей версией) используется для ручного создания alter script. Здесь эволюции используются для изменения схемы db.

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

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)

  }

}

Ответ 2

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

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

В принципе, создайте такой объект:

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 com.lolboxen.scala.slick.driver

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

Ответ 3

play-slick делает то же самое, что и в других ответах, и, похоже, он находится под зонтиком Play/типизированный.

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

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

Ответ 4

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