Slick Codegen и таблицы с> 22 столбцами

Я новичок в слике. Я создаю набор тестов для приложения Java с Scala, ScalaTest и Slick. Я использую пятно, чтобы подготовить данные перед тестом и сделать утверждения на данных после теста. Используемая база данных содержит несколько таблиц с более чем 22 столбцами. Я использую slick-codegen для генерации кода моей схемы.

Для таблиц с более чем 22 столбцами slick-codegen генерирует не класс case, а пользовательский тип на основе 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 столбцам я наткнулся на несколько решений, основанных на вложенных кортежах. Можно ли настроить кодоген для использования этого подхода?
  4. Если генерирование кода с классами case с> 22 полями не является жизнеспособным вариантом, я думаю, что было бы возможно сгенерировать обычный класс, который имеет функцию "accessor" для каждого столбца, таким образом обеспечивая "отображение" из доступа на основе индекса на основе имени доступа. Я был бы счастлив реализовать поколение для этого сам, но я думаю, что мне нужно несколько указателей, с чего начать. Я думаю, что это должно быть в состоянии переопределить стандартный Codegen для этого. Я уже использую переопределенный SourceCodeGenerator для некоторых пользовательских типов данных. Но кроме этого случая использования, документация генератора кода не очень мне помогает.

Я был бы очень признателен за помощь здесь. Заранее спасибо!

4 ответа

Решение

Я закончил дальнейшую настройку Slick-Codegen. Сначала я отвечу на свои вопросы, затем опубликую свое решение.

Ответы на вопросы

  1. Ограничение 22 арности может быть снято для классов case, это не для кортежей. И 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))

Самое сложное было выяснить, как * картографирование работает в слике. Там не так много документации, но я нашел этот ответ Stackru довольно поучительным.

Я создал BigAssTableRowobject использовать HList прозрачный для клиентского кода. Обратите внимание, что apply функция в объекте перегружает apply из класса дела. Так что я все еще могу создавать сущности, вызывая 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)

Для этого кода используются полностью прозрачные кортежи или списки HList.

Решение: как это генерируется

Я просто выложу весь свой код генератора здесь. Это не идеально; Пожалуйста, дайте мне знать, если у вас есть предложения по улучшению! Огромные части просто скопированы с 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()
  }
}

Начиная с 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
}

РЕДАКТИРОВАТЬ

Итак, если вы хотите, чтобы 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(", ")})"""
    }
  }
}

Затем вы структурируете код codegen в отдельном проекте, как описано, чтобы он генерировал исходный код во время компиляции. В отличие от того, что задокументировано, вам не нужно писать свой основной метод. Вместо этого вы можете передать свое имя класса в качестве аргумента 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")
}

Эта проблема решена в Slick 3.3: https://github.com/slick/slick/pull/1889/

Это решение обеспечивает def * а также def ? а также поддерживает простой SQL.

Как вы уже узнали, есть несколько доступных опций - вложенные кортежи, преобразование из Slick HList в Shapeless HList, а затем в case-классы и так далее.

Я обнаружил, что все эти опции слишком сложны для этой задачи, и пошел с настроенным Slick Codegen, чтобы создать простой класс-оболочку с аксессорами.

Посмотрите на эту суть.

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
    }


}

}

Другие вопросы по тегам