Slick代码生成器和拥有超过22列的表

5

我对Slick不太熟悉。我正在使用Scala、ScalaTest和Slick为Java应用程序创建测试套件。在测试之前,我使用Slick准备数据,并在测试后对数据进行断言。所使用的数据库有一些表格具有超过22列。我使用slick-codegen生成我的模式代码。

对于超过22列的表格,slick-codegen不会生成一个case class,而是生成基于HList的自定义类型和一个伴随的“构造”方法。据我所知,这是因为元组和case class只能有22个字段的限制。代码生成的方式是,Row对象的字段只能通过索引访问。

我有几个关于此的问题:

  1. 据我所知,case classes的22个字段限制已经在Scala 2.11中得到解决,对吗?
  2. 如果是这样,是否可以自定义slick-codegen以为所有表格生成case classes?我查看了一下:我成功地在重写的SourceCodeGenerator中设置了override def hlistEnabled = false。但是这将导致Cannot generate tuple for > 22 columns, please set hlistEnable=true or override compound.。所以我不明白能够禁用HList的意义。也许问题在“或覆盖复合”部分,但我不理解它的含义。
  3. 在互联网上搜索slick和22列,我发现了一些基于嵌套元组的解决方案。是否可以自定义代码生成器使用这种方法?
  4. 如果生成具有> 22个字段的case classes的代码不可行,我认为可以生成一个普通类,该类具有每个列的“访问器”函数,从而提供从基于索引的访问到基于名称的访问的“映射”。我很乐意自己实现此生成,但我认为我需要一些指针来开始。我认为它应该能够覆盖此标准代码生成器。我已经为一些自定义数据类型使用了重写的SourceCodeGenerator。但是除了这个用例之外,代码生成器的文档对我没有太大帮助。

我真的很感激你的帮助。提前致谢!

4个回答

6

更新于2019-02-15:*随着Slick 3.3.0的发布,如@Marcus所回答的那样,现在内置支持生成包含多于22列的表。

自Slick 3.2.0起,对于大于22个参数的case class,最简单的解决方案是在*方法中使用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
}

编辑

因此,如果您想让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(", ")})"""
    }
  }
}

然后,您需要将代码生成代码结构化为一个单独的项目,如文档所述,以便在编译时生成源代码。您可以将您的类名作为参数传递给您正在扩展的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")
}

mapTo在slick-codegen中没有被使用,是吗?有任何想法为什么不用? - Marcus
1
@mbee 你说得对。我已经更新了我的答案,展示了如何通过最小限度地覆盖现有的代码生成器来实现它的工作。 - ecoe

6

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

问题的答案

  1. 22个元素的限制可能会被取消,但对于元组不会。而且,slick-codegen也会生成一些元组,这是我在提问时没有完全意识到的。
  2. 与答案1无关。(如果22个元素的限制也被取消,则可能变得相关。)
  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))

最困难的部分是找出Slick中*映射的工作原理。文档不多,但我发现这个Stackoverflow答案非常有启发性。
我创建了BigAssTableRowobject,以使客户端代码对HList的使用更加透明。请注意,对象中的apply函数重载了案例类中的apply。因此,我仍然可以通过调用BigAssTableRow(id: 1L, name: "Foo")来创建实体,而*投影仍然可以使用接受HListapply函数。
所以,现在我可以做这样的事情:
// 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)

对于这段代码,无论是元组还是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()
  }
}

对我来说有效。但是那个 .tail.tail.tail.tail...head 的东西可能会成为一个问题,或者它被优化了吗? - Wesley Schleumer de Góes
1
我用HList和模式匹配修改了你的解决方案:https://gist.github.com/schleumer/bbce61cce86bbf398b5624bd36658a68 - Wesley Schleumer de Góes

1

1
你知道 Slick 3.3 什么时候会发布吗? - PJ Fanning
@Marcus 我刚试了新的Slick 3.3.0代码生成,它确实可以处理具有超过22列的表格的生成,但是视图仍然不会生成(如预期)。此外,尽管我的上述解决方案可以生成视图,但不幸的是它与slick 3.3.0不兼容。 - ecoe
@ecoe很抱歉,我对slick中的视图一窍不通。你的代码中哪部分是关于视图的呢? - Marcus

1

正如你已经发现的那样,可用的选项很少——嵌套元组、从 Slick HList 转换到 Shapeless HList,然后再转换为 case classes 等等。

我发现所有这些选项对于这个任务来说都太复杂了,因此选择了自定义的 Slick Codegen 来生成带有访问器的简单包装类。

看看这个 gist

class MyCodegenCustomisations(model: Model) extends slick.codegen.SourceCodeGenerator(model){
import ColumnDetection._


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

    val columnIndexByName = columns.map(_.name).zipWithIndex.toMap
    def getColumnIndex(columnName: String): Option[Int] = {
        columnIndexByName.get(columnName)

    }

    private def getWrapperCode: Seq[String] = {
        if (columns.length <= 22) {
            //do not generate wrapper for tables which get case class generated by Slick
            Seq.empty[String]
        } else {
            val lines =
                columns.map{c =>
                    getColumnIndex(c.name) match {
                        case Some(colIndex) =>
                            //lazy val firstname: Option[String] = row.productElement(1).asInstanceOf[Option[String]]
                            val colType = c.exposedType
                            val line = s"lazy val ${c.name}: $colType = values($colIndex).asInstanceOf[$colType]"
                            line
                        case None => ""
                    }
                }
            Seq("",
                "/*",
                "case class Wrapper(private val row: Row) {",
                "// addressing HList by index is very slow, let's convert it to vector",
                "private lazy val values = row.toList.toVector",
                ""

            ) ++ lines ++ Seq("}", "*/", "")

        }
    }


    override def code: Seq[String] = {
        val originalCode = super.code
        originalCode ++ this.getWrapperCode
    }


}

}


感谢您的建议,@gaiser。它让我朝着正确的方向前进。然而,您的解决方案使用了一个包装器,这导致客户端代码需要知道是否使用包装器。受到您的方法的启发,我创建了一个完全透明的解决方案,供客户端使用,请查看我的答案。 - Bart Kummel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接