Slick Codegen и таблицы с> 22 столбцами
Я новичок в слике. Я создаю набор тестов для приложения Java с Scala, ScalaTest и Slick. Я использую пятно, чтобы подготовить данные перед тестом и сделать утверждения на данных после теста. Используемая база данных содержит несколько таблиц с более чем 22 столбцами. Я использую slick-codegen для генерации кода моей схемы.
Для таблиц с более чем 22 столбцами slick-codegen генерирует не класс case, а пользовательский тип на основе HList и сопутствующий метод конструктора. Насколько я понимаю, это потому, что ограничение на то, что кортежи и классы дел могут иметь только 22 поля. При создании кода поля Row-объекта доступны только по индексу.
У меня есть пара вопросов по этому поводу:
- Насколько я понимаю, ограничение в 22 поля для классов дел уже исправлено в Scala 2.11, верно?
- Если это так, можно ли будет настроить slick-codegen для генерации классов дел для всех таблиц? Я посмотрел на это: мне удалось установить
override def hlistEnabled = false
в переопределенномSourceCodeGenerator
, Но это приводит кCannot generate tuple for > 22 columns, please set hlistEnable=true or override compound.
Так что я не вижу смысла удалять HList. Может быть, подвох в части "или переопределить соединение", но я не понимаю, что это значит. - В Интернете и поиске по 22 столбцам я наткнулся на несколько решений, основанных на вложенных кортежах. Можно ли настроить кодоген для использования этого подхода?
- Если генерирование кода с классами case с> 22 полями не является жизнеспособным вариантом, я думаю, что было бы возможно сгенерировать обычный класс, который имеет функцию "accessor" для каждого столбца, таким образом обеспечивая "отображение" из доступа на основе индекса на основе имени доступа. Я был бы счастлив реализовать поколение для этого сам, но я думаю, что мне нужно несколько указателей, с чего начать. Я думаю, что это должно быть в состоянии переопределить стандартный Codegen для этого. Я уже использую переопределенный
SourceCodeGenerator
для некоторых пользовательских типов данных. Но кроме этого случая использования, документация генератора кода не очень мне помогает.
Я был бы очень признателен за помощь здесь. Заранее спасибо!
4 ответа
Я закончил дальнейшую настройку Slick-Codegen. Сначала я отвечу на свои вопросы, затем опубликую свое решение.
Ответы на вопросы
- Ограничение 22 арности может быть снято для классов case, это не для кортежей. И slick-codegen также генерирует некоторые кортежи, о которых я не был полностью осведомлен, когда задавал вопрос.
- Не релевантно, см. Ответ 1. (Это может стать уместным, если лимит 22 арности будет снят и для кортежей.)
- Я решил не исследовать это дальше, поэтому этот вопрос пока остается без ответа.
- Это подход, который я выбрал, в конце концов.
Решение: сгенерированный код
Итак, я закончил генерировать "обычные" классы для таблиц с более чем 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 довольно поучительным.
Я создал BigAssTableRow
object
использовать 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
}
}
}