光滑的codegen和表> 22列

Bar*_*mel 5 scala slick slick-codegen

我是Slick的新手.我正在使用Scala,ScalaTest和Slick为Java应用程序创建测试套件.我在测试之前使用slick来准备数据,并在测试之后对数据进行断言.使用的数据库有一些超过22列的表.我使用slick-codegen生成我的架构代码.

对于列数超过22的表,slick-codegen不会生成案例类,而是基于HList的自定义类型和伴随"构造函数"方法.据我了解,这是因为元组和案例类只能有22个字段的限制.生成代码的方式,Row对象的字段只能通过索引访问.

我有几个问题:

  1. 根据我的理解,案例类的22个字段限制已在Scala 2.11中修复,对吧?
  2. 如果是这种情况,是否可以自定义slick-codegen来为所有表生成案例类?我调查了这个:我设法进行override def hlistEnabled = false了覆盖SourceCodeGenerator.但这导致了Cannot generate tuple for > 22 columns, please set hlistEnable=true or override compound.所以我没有能够解除HList的意义.可能是'或覆盖复合'部分,但我不明白这意味着什么.
  3. 在光滑的22列上搜索互联网,我发现了一些基于嵌套元组的解决方案.是否可以自定义codegen以使用此方法?
  4. 如果生成包含> 22字段的案例类的代码不是一个可行的选项,我认为可以生成一个普通的类,它为每列提供了"访问器"功能,从而提供了基于索引的访问的"映射"基于名称的访问.我很乐意为自己实现这一代,但我想我需要一些指针从哪里开始.我认为它应该能够覆盖标准的codegen.我已经使用重写SourceCodeGenerator了一些自定义数据类型.但是,除了这种使用情况下,代码生成的文档并不能帮助我太多.

我真的很感激这里的一些帮助.提前致谢!

Bar*_*mel 6

我最终进一步定制了slick-codegen.首先,我会回答我自己的问题,然后我会发布我的解决方案.

问题的答案

  1. 案例类可以解除22个arity限制,而不是元组.而且slick-codegen也会生成一些元组,当我提出这个问题时,我并不完全清楚这些元组.
  2. 不相关,请参阅答案1.(如果元组的22个arity限制也被提升,这可能会变得相关.)
  3. 我选择不再对此进行调查,所以现在这个问题仍然没有答案.
  4. 这是我最终采取的方法.

解决方案:生成的代码

所以,我最终为超过22列的表生成"普通"类.让我举一个我现在生成的例子.(生成器代码如下所示.)(出于简洁和可读性的原因,此示例少于22列.)

case class BigAssTableRow(val id: Long, val name: String, val age: Option[Int] = None)

type BigAssTableRowList = HCons[Long,HCons[String,HCons[Option[Int]]], HNil]

object BigAssTableRow {
  def apply(hList: BigAssTableRowList) = new BigAssTableRow(hlist.head, hList.tail.head, hList.tail.tail.head)
  def unapply(row: BigAssTableRow) = Some(row.id :: row.name :: row.age)
}

implicit def GetResultBoekingenRow(implicit e0: GR[Long], e1: GR[String], e2: GR[Optional[Int]]) = GR{
  prs => import prs._
  BigAssTableRow.apply(<<[Long] :: <<[String] :: <<?[Int] :: HNil)
}

class BigAssTable(_tableTag: Tag) extends Table[BigAssTableRow](_tableTag, "big_ass") {
  def * = id :: name :: age :: :: HNil <> (BigAssTableRow.apply, BigAssTableRow.unapply)

  val id: Rep[Long] = column[Long]("id", O.PrimaryKey)
  val name: Rep[String] = column[String]("name", O.Length(255,varying=true))
  val age: Rep[Option[Int]] = column[Option[Int]]("age", O.Default(None))
}

lazy val BigAssTable = new TableQuery(tag => new BigAssTable(tag))
Run Code Online (Sandbox Code Playgroud)

最困难的部分是找出*Slick中的映射是如何工作的.没有太多的文档,但我发现这个Stackoverflow的答案相当有启发性.

我创建了BigAssTableRow object使用HList透明的客户端代码.请注意,apply对象中的函数会重写applycase类.所以我仍然可以通过调用创建实体BigAssTableRow(id: 1L, name: "Foo"),而*投影仍然可以使用apply带有的函数HList.

所以,我现在可以做这样的事情:

// I left out the driver import as well as the scala.concurrent imports 
// for the Execution context.

val collection = TableQuery[BigAssTable]
val row = BigAssTableRow(id: 1L, name: "Qwerty") // Note that I leave out the optional age

Await.result(db.run(collection += row), Duration.Inf)

Await.result(db.run(collection.filter(_.id === 1L).result), Duration.Inf)
Run Code Online (Sandbox Code Playgroud)

对于这个代码,它是完全透明的元组或HLists在引擎盖下使用.

解决方案:如何生成

我将在这里发布我的整个生成器代码.它并不完美; 如果您有改进建议,请告诉我!只需从slick.codegen.AbstractSourceCodeGenerator相关类中复制大量的部分,然后稍微更改一下.还有一些与此问题没有直接关系的事情,例如添加java.time.*数据类型和过滤特定表.我把它们留了下来,因为它们可能有用.另请注意,此示例适用于Postgres数据库.

import slick.codegen.SourceCodeGenerator
import slick.driver.{JdbcProfile, PostgresDriver}
import slick.jdbc.meta.MTable
import slick.model.Column

import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration

object MySlickCodeGenerator {
  val slickDriver = "slick.driver.PostgresDriver"
  val jdbcDriver = "org.postgresql.Driver"
  val url = "jdbc:postgresql://localhost:5432/dbname"
  val outputFolder = "/path/to/project/src/test/scala"
  val pkg = "my.package"
  val user = "user"
  val password = "password"

  val driver: JdbcProfile = Class.forName(slickDriver + "$").getField("MODULE$").get(null).asInstanceOf[JdbcProfile]
  val dbFactory = driver.api.Database
  val db = dbFactory.forURL(url, driver = jdbcDriver, user = user, password = password, keepAliveConnection = true)

  // The schema is generated using Liquibase, which creates these tables that I don't want to use
  def excludedTables = Array("databasechangelog", "databasechangeloglock")

  def tableFilter(table: MTable): Boolean = {
    !excludedTables.contains(table.name.name) && schemaFilter(table.name.schema)
  }

  // There's also an 'audit' schema in the database, I don't want to use that one
  def schemaFilter(schema: Option[String]): Boolean = {
    schema match {
      case Some("public") => true
      case None => true
      case _ => false
    }
  }

  // Fetch data model
  val modelAction = PostgresDriver.defaultTables
    .map(_.filter(tableFilter))
    .flatMap(PostgresDriver.createModelBuilder(_, ignoreInvalidDefaults = false).buildModel)

  val modelFuture = db.run(modelAction)

  // customize code generator
  val codegenFuture = modelFuture.map(model => new SourceCodeGenerator(model) {

    // add custom import for added data types
    override def code = "import my.package.Java8DateTypes._" + "\n" + super.code

    override def Table = new Table(_) {
      table =>

      // Use different factory and extractor functions for tables with > 22 columns
      override def factory   = if(columns.size == 1) TableClass.elementType else if(columns.size <= 22) s"${TableClass.elementType}.tupled" else s"${EntityType.name}.apply"
      override def extractor = if(columns.size <= 22) s"${TableClass.elementType}.unapply" else s"${EntityType.name}.unapply"

      override def EntityType = new EntityTypeDef {
        override def code = {
          val args = columns.map(c =>
            c.default.map( v =>
              s"${c.name}: ${c.exposedType} = $v"
            ).getOrElse(
              s"${c.name}: ${c.exposedType}"
            )
          )
          val callArgs = columns.map(c => s"${c.name}")
          val types = columns.map(c => c.exposedType)

          if(classEnabled){
            val prns = (parents.take(1).map(" extends "+_) ++ parents.drop(1).map(" with "+_)).mkString("")
            s"""case class $name(${args.mkString(", ")})$prns"""
          } else {
            s"""
/** Constructor for $name providing default values if available in the database schema. */
case class $name(${args.map(arg => {s"val $arg"}).mkString(", ")})
type ${name}List = ${compoundType(types)}
object $name {
  def apply(hList: ${name}List): $name = new $name(${callArgs.zipWithIndex.map(pair => s"hList${tails(pair._2)}.head").mkString(", ")})
  def unapply(row: $name) = Some(${compoundValue(callArgs.map(a => s"row.$a"))})
}
          """.trim
          }
        }
      }

      override def PlainSqlMapper = new PlainSqlMapperDef {
        override def code = {
          val positional = compoundValue(columnsPositional.map(c => if (c.fakeNullable || c.model.nullable) s"<<?[${c.rawType}]" else s"<<[${c.rawType}]"))
          val dependencies = columns.map(_.exposedType).distinct.zipWithIndex.map{ case (t,i) => s"""e$i: GR[$t]"""}.mkString(", ")
          val rearranged = compoundValue(desiredColumnOrder.map(i => if(columns.size > 22) s"r($i)" else tuple(i)))
          def result(args: String) = s"$factory($args)"
          val body =
            if(autoIncLastAsOption && columns.size > 1){
              s"""
val r = $positional
import r._
${result(rearranged)} // putting AutoInc last
              """.trim
            } else {
              result(positional)
            }

              s"""
implicit def $name(implicit $dependencies): GR[${TableClass.elementType}] = GR{
  prs => import prs._
  ${indent(body)}
}
          """.trim
        }
      }

      override def TableClass = new TableClassDef {
        override def star = {
          val struct = compoundValue(columns.map(c=>if(c.fakeNullable)s"Rep.Some(${c.name})" else s"${c.name}"))
          val rhs = s"$struct <> ($factory, $extractor)"
          s"def * = $rhs"
        }
      }

      def tails(n: Int) = {
        List.fill(n)(".tail").mkString("")
      }

      // override column generator to add additional types
      override def Column = new Column(_) {
        override def rawType = {
          typeMapper(model).getOrElse(super.rawType)
        }
      }
    }
  })

  def typeMapper(column: Column): Option[String] = {
    column.tpe match {
      case "java.sql.Date" => Some("java.time.LocalDate")
      case "java.sql.Timestamp" => Some("java.time.LocalDateTime")
      case _ => None
    }
  }

  def doCodeGen() = {
    def generator = Await.result(codegenFuture, Duration.Inf)
    generator.writeToFile(slickDriver, outputFolder, pkg, "Tables", "Tables.scala")
  }

  def main(args: Array[String]) {
    doCodeGen()
    db.close()
  }
}
Run Code Online (Sandbox Code Playgroud)


eco*_*coe 5

2019年2月15日更新:*随着Slick 3.3.0的发布,@ Marcus 回答说,内置支持生成超过22列的表的代码。

从Slick 3.2.0开始,对于> 22个参数案例类,最简单的解决方案是使用*方法mapTo而不是<>运算符(根据记录的单元测试)在*方法中定义默认投影:

case class BigCase(id: Int,
                   p1i1: Int, p1i2: Int, p1i3: Int, p1i4: Int, p1i5: Int, p1i6: Int,
                   p2i1: Int, p2i2: Int, p2i3: Int, p2i4: Int, p2i5: Int, p2i6: Int,
                   p3i1: Int, p3i2: Int, p3i3: Int, p3i4: Int, p3i5: Int, p3i6: Int,
                   p4i1: Int, p4i2: Int, p4i3: Int, p4i4: Int, p4i5: Int, p4i6: Int)

class bigCaseTable(tag: Tag) extends Table[BigCase](tag, "t_wide") {
      def id = column[Int]("id", O.PrimaryKey)
      def p1i1 = column[Int]("p1i1")
      def p1i2 = column[Int]("p1i2")
      def p1i3 = column[Int]("p1i3")
      def p1i4 = column[Int]("p1i4")
      def p1i5 = column[Int]("p1i5")
      def p1i6 = column[Int]("p1i6")
      def p2i1 = column[Int]("p2i1")
      def p2i2 = column[Int]("p2i2")
      def p2i3 = column[Int]("p2i3")
      def p2i4 = column[Int]("p2i4")
      def p2i5 = column[Int]("p2i5")
      def p2i6 = column[Int]("p2i6")
      def p3i1 = column[Int]("p3i1")
      def p3i2 = column[Int]("p3i2")
      def p3i3 = column[Int]("p3i3")
      def p3i4 = column[Int]("p3i4")
      def p3i5 = column[Int]("p3i5")
      def p3i6 = column[Int]("p3i6")
      def p4i1 = column[Int]("p4i1")
      def p4i2 = column[Int]("p4i2")
      def p4i3 = column[Int]("p4i3")
      def p4i4 = column[Int]("p4i4")
      def p4i5 = column[Int]("p4i5")
      def p4i6 = column[Int]("p4i6")

      // HList-based wide case class mapping
      def m3 = (
        id ::
        p1i1 :: p1i2 :: p1i3 :: p1i4 :: p1i5 :: p1i6 ::
        p2i1 :: p2i2 :: p2i3 :: p2i4 :: p2i5 :: p2i6 ::
        p3i1 :: p3i2 :: p3i3 :: p3i4 :: p3i5 :: p3i6 ::
        p4i1 :: p4i2 :: p4i3 :: p4i4 :: p4i5 :: p4i6 :: HNil
      ).mapTo[BigCase]

      def * = m3
}
Run Code Online (Sandbox Code Playgroud)

编辑

因此,如果您随后希望slick-codegen使用上述mapTo方法生成巨大的表,则可以将相关部分覆盖到代码生成器中并添加一条mapTo语句:

package your.package
import slick.codegen.SourceCodeGenerator
import slick.{model => m}


class HugeTableCodegen(model: m.Model) extends SourceCodeGenerator(model) with GeneratorHelpers[String, String, String]{


  override def Table = new Table(_) {
    table =>

    // always defines types using case classes
    override def EntityType = new EntityTypeDef{
      override def classEnabled = true
    }

    // allow compound statements using HNil, but not for when "def *()" is being defined, instead use mapTo statement
    override def compoundValue(values: Seq[String]): String = {
      // values.size>22 assumes that this must be for the "*" operator and NOT a primary/foreign key
      if(hlistEnabled && values.size > 22) values.mkString("(", " :: ", s" :: HNil).mapTo[${StringExtensions(model.name.table).toCamelCase}Row]")
      else if(hlistEnabled) values.mkString(" :: ") + " :: HNil"
      else if (values.size == 1) values.head
      else s"""(${values.mkString(", ")})"""
    }

    // should always be case classes, so no need to handle hlistEnabled here any longer
    override def compoundType(types: Seq[String]): String = {
      if (types.size == 1) types.head
      else s"""(${types.mkString(", ")})"""
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,按文档所述在单独的项目中构造代码生成代码,以便它在编译时生成源。您可以将您的类名作为参数传递给SourceCodeGenerator您要扩展的类:

lazy val generateSlickSchema = taskKey[Seq[File]]("Generates Schema definitions for SQL tables")
generateSlickSchema := {

  val managedSourceFolder = sourceManaged.value / "main" / "scala"
  val packagePath = "your.sql.table.package"

  (runner in Compile).value.run(
    "slick.codegen.SourceCodeGenerator", (dependencyClasspath in Compile).value.files,
    Array(
      "env.db.connectorProfile",
      "slick.db.driver",
      "slick.db.url",
      managedSourceFolder.getPath,
      packagePath,
      "slick.db.user",
      "slick.db.password",
      "true",
      "your.package.HugeTableCodegen"
    ),
    streams.value.log
  )
  Seq(managedSourceFolder / s"${packagePath.replace(".","/")}/Tables.scala")
}
Run Code Online (Sandbox Code Playgroud)