Voici une solution de contournement pour Slick 3.2.3 (et quelques informations sur mon approche) :
Vous avez peut-être remarqué la sélection dynamique les colonnes sont faciles tant que vous pouvez supposer un type fixe, par exemple :
columnNames = List("col1", "col2")
tableQuery.map( r => columnNames.map(name => r.column[String](name)) )
Mais si vous essayez une approche similaire
avec un groupBy
opération, Slick se plaindra qu'il "does not know how to map the given types"
.
Ainsi, bien que ce ne soit pas une solution élégante, vous pouvez au moins satisfaire la sécurité de type de Slick en définissant statiquement les deux :
groupby
type de colonne- Limite supérieure/inférieure de la quantité de
groupBy
colonnes
Une façon simple d'implémenter ces deux contraintes est de supposer à nouveau un type fixe et de brancher le code pour toutes les quantités possibles de groupBy
colonnes.
Voici la session de travail complète de Scala REPL pour vous donner une idée :
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)
Là où cela devient moche, c'est quand vous pouvez avoir plus de quelques groupBy
colonnes (c'est-à-dire avoir un if
séparé branche pour plus de 10 cas deviendrait monotone). J'espère que quelqu'un interviendra et modifiera cette réponse pour savoir comment masquer ce passe-partout derrière un sucre syntaxique ou une couche d'abstraction.