Search code examples
scalaplayframeworkebeanplayframework-2.6

Play Framework and autogenerated evolutions


I love bootstrapping projects with Play! Ebean, in memory database : evolutions generates automatically when I need a new model, this is awesome.

I am learning Play Scala, and there is a lack of support between Ebean and Scala

It can work, with .enablePlugins(PlayScala, PlayEbean) But case classes are not supported as an Ebean model.

I was wondering, do you know a ORM that :

  • autogenerates mysql or postgresql schema given model classes
  • is scala friendly (esp. with case classes)
  • minimalistic boilerplate ( this is a Ebean model without need of other file like any repository)

I dont think slick or JPA generates evolutions ? I tried and did not get it working.

PS : with case classes, you get implicit readers and writers from/to json and this is also awesome.


Solution

  • I think Slick ORM as mentioned in the answers is definitely the way to go here.

    Slick ORM works great in scala, as you can use operations such as filter, map and other functional paradigms in this particular ORM. On another hand, slick has awesome documentation as you can see from this link:

    http://slick.lightbend.com/doc/3.1.0/

    Assuming that you are open to the ORM, we can easily go ahead and use slick-codegen library that automatically reflects on your database schema and creates a file containing all the models in your database.

    The documentation is here specifically on slick-codegen : http://slick.lightbend.com/doc/3.1.0/code-generation.html

    But I'll break it down for you to make it even easier. The way to do this is as follows for postgres :

    1. Add slick-codegen to your library dependencies by adding this line in your build.sbt : libraryDependencies += "com.typesafe.slick" %% "slick-codegen" % "3.1.0"
    2. Make sure your database is running on whatever port (let's say 5432) for postgres and include the appropriate postgresql driver in your build.sbt
    3. Create the following scala file in your project (which you can right click to run in IntelliJ or may have to change it to an executable Scala file if you're not using IntelliJ. People have also figured out a way to run this via sbt itself during compile time, but I won't be getting into that) :

      object SlickCodeGen { def main(args: Array[String]): Unit = { slick.codegen.SourceCodeGenerator.main( Array("slick.jdbc.PostgresProfile", "org.postgresql.Driver", DATABASE_URL, DIRECTORY_TO_PLACE_FILE, PACKAGE, USERNAME, PASSWORD) ) } }

    4. After you have run the scala file, you'll see a new file called Tables.scala in the directory and package that you previously specified.

    5. The file contains minimal and only necessary components for you, so for example, for a table such as Computer that you've shown in your link, the implicit conversions from database to case classes will be generated and may look as follows(just for demo purposes, but the length of the file will be about the same, if this is too much boilerplate):

      package
      // AUTO-GENERATED Slick data model
      /** Stand-alone Slick data model for immediate use */
      object Tables extends {
        val profile = slick.jdbc.PostgresProfile
      } with Tables
      
      /** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
      trait Tables {
        val profile: slick.jdbc.JdbcProfile
        import profile.api._
        import slick.model.ForeignKeyAction
        // NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
        import slick.jdbc.{GetResult => GR}
      
        /** DDL for all tables. Call .create to execute. */
        lazy val schema
          : profile.SchemaDescription = Computer.schema 
        @deprecated("Use .schema instead of .ddl", "3.0")
        def ddl = schema
      
        case class ComputerRow(name: String,
                               introduced: Date,
                               discontinued: Date,
                               company: Company)
      
        /** GetResult implicit for fetching ComputerRow objects using plain SQL queries */
        implicit def GetResultComputerRow(implicit e0: GR[String],
                                         e1: GR[Date],
                                         e2: GR[Company]): GR[ComputerRow] =
          GR { prs =>
            import prs._
            ComputerRow.tupled(
              (<<[String],
               <<[Date],
               <<[Date],
               <<[Company]))
          }
      
        /** Table description of table computer. Objects of this class serve as prototypes for rows in queries. */
        class Computers(_tableTag: Tag)
            extends profile.api.Table[ComputerRow](_tableTag,
                                                  None,
                                                  "computer") {
          def * =
            (name, introduced, discontinued, company) <> (ComputerRow.tupled, ComputerRow.unapply)
      
          /** Maps whole row to an option. Useful for outer joins. */
          def ? =
            (Rep.Some(name),
             Rep.Some(introduced),
             Rep.Some(discontinued),
             Rep.Some(company).shaped.<>(
              { r =>
                import r._;
                _1.map(
                  _ =>
                    ComputerRow.tupled(
                      (_1.get, _2.get, _3.get, _4.get)))
              },
              (_: Any) =>
                throw new Exception("Inserting into ? projection not supported.")
            )
      
          /** Database column name SqlType(text) */
          val name: Rep[String] = column[String]("name", O.PrimaryKey)
      
          /** Database column introduced SqlType(date) */
          val firstName: Rep[Date] = column[Date]("introduced")
      
          /** Database column discontinued SqlType(date) */
          val lastName: Rep[Date] = column[Date]("discontinued")
      
          /** Database column company SqlType(text) */
          val gender: Rep[Company] = column[Company]("company")
        }
      
        /** Collection-like TableQuery object for table Computer */
        lazy val Computer = new TableQuery(tag => new Computer(tag))
      }
      
      1. Once this file is created, then you can easily leverage the use of case classes (like you wanted), one example is when you need to add a new row to the computer table, you can simply do Computer += ComputerRow(...)

    Note that ComputerRow here is a case class.

    So to sum it up,

    1. slick-codegen can generate your scala classes automatically from the database, or the other way around by running Computer.schema in this case.
    2. slick is extremely scala friendly (case classes, monad operations)
    3. decent boilerplate but really use to use in your application, also can be customized and all the tables can be created in one file or separated depending on your needs.

    I think you can't go wrong with Slick here.