sql >> Base de Datos >  >> RDS >> Mysql

Grupo dinámico elegante

Aquí hay una solución para Slick 3.2.3 (y algunos antecedentes sobre mi enfoque):

Es posible que haya notado dinámicamente seleccionando Las columnas son fáciles siempre que pueda asumir un tipo fijo, por ejemplo: columnNames = List("col1", "col2") tableQuery.map( r => columnNames.map(name => r.column[String](name)) )

Pero si prueba un enfoque similar con un groupBy operación, Slick se quejará de que "does not know how to map the given types" .

Entonces, si bien esta no es una solución elegante, al menos puede satisfacer la seguridad de tipos de Slick definiendo estáticamente ambos:

  1. groupby tipo de columna
  2. Límite superior/inferior de la cantidad de groupBy columnas

Una forma simple de implementar estas dos restricciones es asumir nuevamente un tipo fijo y bifurcar el código para todas las cantidades posibles de groupBy columnas.

Aquí está la sesión completa de Scala REPL para darle una idea:

import java.io.File

import akka.actor.ActorSystem
import com.typesafe.config.ConfigFactory
import slick.jdbc.H2Profile.api._

import scala.concurrent.{Await, Future}
import scala.concurrent.duration._


val confPath = getClass.getResource("/application.conf")
val config = ConfigFactory.parseFile(new File(confPath.getPath)).resolve()
val db = Database.forConfig("slick.db", config)

implicit val system = ActorSystem("testSystem")
implicit val executionContext = system.dispatcher

case class AnyData(a: String, b: String)
case class GroupByFields(a: Option[String], b: Option[String])

class AnyTable(tag: Tag) extends Table[AnyData](tag, "macro"){
  def a = column[String]("a")
  def b = column[String]("b")
  def * = (a, b) <> ((AnyData.apply _).tupled, AnyData.unapply)
}

val table = TableQuery[AnyTable]

def groupByDynamically(groupBys: Seq[String]): DBIO[Seq[GroupByFields]] = {
  // ensures columns are returned in the right order
  def selectGroups(g: Map[String, Rep[Option[String]]]) = {
    (g.getOrElse("a", Rep.None[String]), g.getOrElse("b", Rep.None[String])).mapTo[GroupByFields]
  }

  val grouped = if (groupBys.lengthCompare(2) == 0) {
    table
      .groupBy( cols => (cols.column[String](groupBys(0)), cols.column[String](groupBys(1))) )
      .map{ case (groups, _) => selectGroups(Map(groupBys(0) -> Rep.Some(groups._1), groupBys(1) -> Rep.Some(groups._2))) }
  }
  else {
    // there should always be at least one group by specified
    table
      .groupBy(cols => cols.column[String](groupBys.head))
      .map{ case (groups, _) => selectGroups(Map(groupBys.head -> Rep.Some(groups))) }
  }

  grouped.result
}

val actions = for {
  _ <- table.schema.create
  _ <- table.map(a => (a.column[String]("a"), a.column[String]("b"))) += ("a1", "b1")
  _ <- table.map(a => (a.column[String]("a"), a.column[String]("b"))) += ("a2", "b2")
  _ <- table.map(a => (a.column[String]("a"), a.column[String]("b"))) += ("a2", "b3")
  queryResult <- groupByDynamically(Seq("b", "a"))
} yield queryResult

val result: Future[Seq[GroupByFields]] = db.run(actions.transactionally)
result.foreach(println)

Await.ready(result, Duration.Inf)

Donde esto se pone feo es cuando puedes tener más de unos pocos groupBy columnas (es decir, tener un if separado rama para más de 10 casos se volvería monótono). Con suerte, alguien se abalanzará y editará esta respuesta sobre cómo ocultar ese texto modelo detrás de una capa de azúcar sintáctica o de abstracción.