web-dev-qa-db-fra.com

Comment écrire une application Play indépendante de la base de données et effectuer la première initialisation de la base de données?

J'utilise Slick avec un Play Framework 2.1 et j'ai quelques problèmes.

Étant donné l'entité suivante ...

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

... Je dois importer un package pour un pilote de base de données spécifique, mais je veux utiliser H2 pour test et PostgreSQL dans production . Comment dois-je procéder?

J'ai pu contourner cela en remplaçant les paramètres du pilote dans mon test unitaire:

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

Je n'aime pas cette solution et je me demande s'il existe une façon élégante d'écrire du code DB-agnostique, donc il y a deux moteurs de base de données différents utilisés - un en test et un autre en production?

Je ne veux pas non plus utiliser l'évolution, et je préfère laisser Slick créer les tables de base de données pour moi:

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

La première fois que je démarre l'application, tout fonctionne bien ... puis, bien sûr, la deuxième fois que je démarre l'application, elle se bloque car les tables existent déjà dans la base de données PostgreSQL.

Cela dit, mes deux dernières questions sont les suivantes:

  1. Comment puis-je déterminer si les tables de base de données existent déjà?
  2. Comment puis-je faire la méthode onStart ci-dessus agnostique DB afin de pouvoir tester mon application avec FakeApplication?
62
j3d

Vous trouverez un exemple sur la façon d'utiliser l'injection de modèle/dépendance de gâteau pour découpler le pilote Slick de la couche d'accès à la base de données ici: https://github.com/slick/slick-examples .

Comment découpler le pilote Slick et tester l'application avec FakeApplication

Il y a quelques jours, j'ai écrit une bibliothèque d'intégration Slick pour play, qui déplace la dépendance du pilote vers le fichier application.conf du projet Play: https://github.com/danieldietrich/slick-integration .

Avec l'aide de cette bibliothèque, votre exemple serait implémenté comme suit:

1) Ajoutez la dépendance à project/Build.scala

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

Ajouter un référentiel d'instantanés

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

Ou référentiel local, si l'intégration simplifiée est publiée localement

resolvers += Resolver.mavenLocal

2) Ajoutez le pilote Slick à conf/application.conf

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

3) Implémenter app/models/Account.scala

Dans le cas d'une intégration simplifiée, on suppose que vous utilisez des clés primaires de type Long qui sont incrémentées automatiquement. Le nom du pk est 'id'. L'implémentation de Table/Mapper a des méthodes par défaut (delete, findAll, findById, insert, update). Vos entités doivent implémenter 'withId' qui est nécessaire à la méthode '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) Implémenter app/models/DAL.scala

Il s'agit de la couche d'accès aux données (DAL) qui est utilisée par les contrôleurs pour accéder à la base de données. Les transactions sont gérées par l'implémentation Table/Mapper dans le composant correspondant.

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) Implémentez 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)
    }
  }

}

Comment déterminer si les tables de base de données existent déjà

Je ne peux pas vous donner une réponse suffisante à cette question ...

... mais peut-être que ce n'est pas vraiment ce que vous voulez faire. Et si vous ajoutez un attribut à une table, dites Account.active? Si vous souhaitez sécuriser les données actuellement stockées dans vos tables, un script alter fera le travail. Actuellement, un tel script de remplacement doit être écrit à la main. Le DAL.ddl.createStatements pourrait être utilisé pour récupérer les instructions create. Ils doivent être triés pour être mieux comparables aux versions précédentes. Ensuite, un diff (avec la version précédente) est utilisé pour créer manuellement le script alter. Ici, les évolutions sont utilisées pour modifier le schéma db.

Voici un exemple sur la façon de générer (la première) évolution:

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)

  }

}
39
Daniel Dietrich

J'essayais également de résoudre ce problème: la possibilité de basculer les bases de données entre le test et la production. L'idée d'envelopper chaque objet de table dans un trait n'était pas attrayante.

Je n'essaie pas de discuter des avantages et des inconvénients du modèle de gâteau ici, mais j'ai trouvé une autre solution, pour ceux qui sont intéressés.

Fondamentalement, créez un objet comme celui-ci:

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

De toute évidence, vous pouvez faire la logique de décision que vous aimez ici. Il ne doit pas être basé sur les propriétés du système.

Maintenant, au lieu de:

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

Tu peux dire

import mypackage.MovableDriver.simple._

MISE À JOUR: Une version Slick 3.0, gracieuseté de 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
    }
  }
}
28
triggerNZ

Le play-slick fait exactement la même chose que ce qui est proposé dans les autres réponses, et il semble être sous l'égide de Play/Typesafe.

Vous pouvez simplement importer import play.api.db.slick.Config.driver.simple._ et il choisira le pilote approprié en fonction de conf/application.conf.

Il offre également plus de choses comme le pool de connexions, la génération DDL ...

2
Sebastien Lorber