62 votes

Slick: Comment écrire DB-agnostique applications et d'effectuer la première fois DB initialisation

Je suis en train d'utiliser Lisse avec un Jeu 2.1 et j'ai quelques problèmes mineurs. Compte tenu de l'entité ci-dessous...

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

... J'ai importer un package pour un spécifique pilote de base de données... mais je veux l'utiliser H2 pour les tests et PostgreSQL dans la production. J'ai été en mesure de contourner ceci en substituant les paramètres du pilote dans mon unité de 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, "user@gmail.com", "Password"))
        val account = for (account <- Accounts) yield account
        account.first.id.get mustEqual 1
      }
    }
  }
}

Personnellement je n'aime pas cette solution, et je me demandais si il est une manière élégante d'écrire DB-agnostique code.

Ensuite, un autre problème que j'ai est que je ne veux pas utiliser de l'évolution et préfère laisser la Nappe de créer les tables de la base 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 lance l'application, tout fonctionne bien... alors, bien sûr, la deuxième fois que je lance l'application, il se bloque parce que 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 oui ou non les tables de base de données existe déjà?
  2. Comment puis-je faire de l' onStart méthode ci-dessus DB-agnostique, afin que je puisse tester mon application avec FakeApplication?

Toute aide serait vraiment appréciée ;-)

39voto

Daniel Dietrich Points 501

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

Comment dissocier la Nappe de pilote et de l'application de test avec FakeApplication

Il y A quelques jours, j'ai écrit une Nappe intégration de la bibliothèque pour le jeu, ce qui déplace le pilote de la dépendance à l'application.conf de la pièce de projet: https://github.com/danieldietrich/slick-integration.

Avec l'aide de cette bibliothèque de votre exemple serait mis en œuvre comme suit:

1) Ajouter la dépendance de projet/Construire.scala

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

Ajouter le référentiel

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

Ou référentiel local, si lisses d'intégration est publié localement

resolvers += Resolver.mavenLocal

2) Ajouter la Nappe pilote de conf/application.conf

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

3) mettre en Œuvre app/models/Compte.scala

Dans le cas de la nappe intégration, il est supposé que vous utilisez des clés primaires de type Long qui s'auto-incrémenté. Le pk nom est "id". La Table/Mappeur de mise en œuvre a défaut de méthodes (supprimer, findAll, findById, d'insertion, de mise à jour). Votre entités ont à mettre en œuvre withId' qui est nécessaire par le "insertion" de la méthode.

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) mettre en Œuvre app/models/DAL.scala

C'est la Couche d'Accès aux Données (DAL), qui est utilisé par les contrôleurs pour accéder à la base de données. Les Transactions sont gérées par la Table/Mappeur de mise en œuvre au sein de la Composante correspondante.

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) mettre en Œuvre le 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)
    }
  }

}

Comment faire pour déterminer si oui ou non les tables de base de données existe déjà

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

... mais peut-être que ce n'est pas vraiment s.th que vous voulez faire. Si vous ajoutez un attribut d'une table, dire Account.active? Si vous voulez en sécurité les données actuellement stockées dans vos tables, un alter script pour faire le travail. Actuellement, une telle modifier le script doit être écrit à la main. L' DAL.ddl.createStatements pourrait être utilisé pour récupérer les instructions de création. Ils doivent être triés pour être mieux comparables avec les précédentes versions. Puis 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 la db schéma.

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)

  }

}

28voto

triggerNZ Points 545

J'ai essayé de résoudre ce problème: la possibilité de basculer entre les bases de données de test et de production. L'idée d'envelopper chaque objet de la table dans un trait a été désagréable.

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

En gros, faire un objet comme ceci:

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

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

Maintenant, au lieu de:

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

Vous pouvez dire

import mypackage.MovableDriver.simple._

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X