La *
méthode:
Cela renvoie la projection par défaut - ce que vous décrivez:
«toutes les colonnes (ou valeurs calculées) qui m'intéressent habituellement ».
Votre table peut avoir plusieurs champs; vous n'avez besoin que d'un sous-ensemble pour votre projection par défaut. La projection par défaut doit correspondre aux paramètres de type de la table.
Prenons-le un à la fois. Sans le <>
truc, juste le *
:
// First take: Only the Table Defintion, no case class:
object Bars extends Table[(Int, String)]("bar") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = id ~ name // Note: Just a simple projection, not using .? etc
}
// Note that the case class 'Bar' is not to be found. This is
// an example without it (with only the table definition)
Juste une définition de table comme celle-ci vous permettra de faire des requêtes comme:
implicit val session: Session = // ... a db session obtained from somewhere
// A simple select-all:
val result = Query(Bars).list // result is a List[(Int, String)]
la projection par défaut de (Int, String)
conduit à un List[(Int, String)]
pour des requêtes simples comme celles-ci.
// SELECT b.name, 1 FROM bars b WHERE b.id = 42;
val q =
for (b <- Bars if b.id === 42)
yield (b.name ~ 1)
// yield (b.name, 1) // this is also allowed:
// tuples are lifted to the equivalent projection.
Quel est le type de q
? C'est un Query
à la projection (String, Int)
. Lorsqu'il est invoqué, il retourne un List
de (String, Int)
tuples selon la projection.
val result: List[(String, Int)] = q.list
Dans ce cas, vous avez défini la projection que vous souhaitez dans la yield
clause de la for
compréhension.
Maintenant environ <>
et Bar.unapply
.
Cela fournit ce que l'on appelle des projections mappées .
Jusqu'à présent, nous avons vu comment slick vous permet d'exprimer des requêtes dans Scala qui renvoient une projection de colonnes (ou de valeurs calculées); Ainsi, lors de l'exécution de ces requêtes, vous devez considérer la ligne de résultat d'une requête comme un tuple Scala . Le type du tuple correspondra à la projection qui est définie (par votre
for
compréhension comme dans l'exemple précédent, ou par la *
projection par défaut ). C'est pourquoi field1 ~ field2
renvoie une projection de Projection2[A, B]
où
A
est le type de field1
et B
est le type de field2
.
q.list.map {
case (name, n) => // do something with name:String and n:Int
}
Queury(Bars).list.map {
case (id, name) => // do something with id:Int and name:String
}
Nous avons affaire à des tuples, qui peuvent être encombrants si nous avons trop de colonnes. Nous aimerions penser aux résultats non pas comme TupleN
mais plutôt comme un objet avec des champs nommés.
(id ~ name) // A projection
// Assuming you have a Bar case class:
case class Bar(id: Int, name: String) // For now, using a plain Int instead
// of Option[Int] - for simplicity
(id ~ name <> (Bar, Bar.unapply _)) // A MAPPED projection
// Which lets you do:
Query(Bars).list.map ( b.name )
// instead of
// Query(Bars).list.map { case (_, name) => name }
// Note that I use list.map instead of mapResult just for explanation's sake.
Comment cela marche-t-il? <>
prend une projection Projection2[Int, String]
et renvoie une projection mappée sur le type Bar
. Les deux arguments Bar, Bar.unapply _
indiquent clairement comment cette (Int, String)
projection doit être mappée à une classe de cas.
Il s'agit d'une cartographie bidirectionnelle; Bar
est le constructeur de la classe case, c'est donc les informations nécessaires pour passer de (id: Int, name: String)
à a Bar
. Et unapply
si vous l'avez deviné, c'est l'inverse.
D'où unapply
vient-il? Il s'agit d'une méthode Scala standard disponible pour toute classe de cas ordinaire - il suffit de définir Bar
un Bar.unapply
qui est un extracteur qui peut être utilisé pour récupérer le id
et avec name
lequel le a
Bar
été construit:
val bar1 = Bar(1, "one")
// later
val Bar(id, name) = bar1 // id will be an Int bound to 1,
// name a String bound to "one"
// Or in pattern matching
val bars: List[Bar] = // gotten from somewhere
val barNames = bars.map {
case Bar(_, name) => name
}
val x = Bar.unapply(bar1) // x is an Option[(String, Int)]
Ainsi, votre projection par défaut peut être mappée à la classe de cas que vous vous attendez le plus à utiliser:
object Bars extends Table[Bar]("bar") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = id ~ name <>(Bar, Bar.unapply _)
}
Ou vous pouvez même l'avoir par requête:
case class Baz(name: String, num: Int)
// SELECT b.name, 1 FROM bars b WHERE b.id = 42;
val q1 =
for (b <- Bars if b.id === 42)
yield (b.name ~ 1 <> (Baz, Baz.unapply _))
Ici, le type de q1
est un Query
avec une projection mappée vers Baz
. Lorsqu'elle est appelée, elle renvoie un List
des Baz
objets:
val result: List[Baz] = q1.list
Enfin, en passant , les .?
offres Option Lifting - la manière Scala de traiter des valeurs qui peuvent ne pas l'être.
(id ~ name) // Projection2[Int, String] // this is just for illustration
(id.? ~ name) // Projection2[Option[Int], String]
Ce qui, en conclusion, fonctionnera bien avec votre définition originale de Bar
:
case class Bar(id: Option[Int] = None, name: String)
// SELECT b.id, b.name FROM bars b WHERE b.id = 42;
val q0 =
for (b <- Bars if b.id === 42)
yield (b.id.? ~ b.name <> (Bar, Bar.unapply _))
q0.list // returns a List[Bar]
En réponse au commentaire sur la façon dont Slick utilise les for
compréhensions:
D'une manière ou d'une autre, les monades parviennent toujours à apparaître et demandent à faire partie de l'explication ...
Car les compréhensions ne sont pas spécifiques aux collections uniquement. Ils peuvent être utilisés sur n'importe quel type de Monad , et les collections ne sont que l'un des nombreux types de types de monades disponibles dans Scala.
Mais comme les collections sont familières, elles constituent un bon point de départ pour une explication:
val ns = 1 to 100 toList; // Lists for familiarity
val result =
for { i <- ns if i*i % 2 == 0 }
yield (i*i)
// result is a List[Int], List(4, 16, 36, ...)
Dans Scala, un pour la compréhension est un sucre syntaxique pour les appels de méthode (éventuellement imbriqués): Le code ci-dessus est (plus ou moins) équivalent à:
ns.filter(i => i*i % 2 == 0).map(i => i*i)
En gros, quoi que ce soit avec filter
, map
, des flatMap
méthodes (autrement dit, un Monad ) peut être utilisé dans une
for
compréhension à la place de ns
. Un bon exemple est la monade Option . Voici l'exemple précédent où la même for
instruction fonctionne à la fois sur les
monades List
et sur les Option
monades:
// (1)
val result =
for {
i <- ns // ns is a List monad
i2 <- Some(i*i) // Some(i*i) is Option
if i2 % 2 == 0 // filter
} yield i2
// Slightly more contrived example:
def evenSqr(n: Int) = { // return the square of a number
val sqr = n*n // only when the square is even
if (sqr % 2 == 0) Some (sqr)
else None
}
// (2)
result =
for {
i <- ns
i2 <- evenSqr(i) // i2 may/maynot be defined for i!
} yield i2
Dans le dernier exemple, la transformation ressemblerait peut-être à ceci:
// 1st example
val result =
ns.flatMap(i => Some(i*i)).filter(i2 => i2 %2 ==0)
// Or for the 2nd example
result =
ns.flatMap(i => evenSqr(i))
Dans Slick, les requêtes sont monadiques - ce ne sont que des objets avec les méthodes map
, flatMap
et filter
. Donc, la for
compréhension (montrée dans l'explication de la *
méthode) se traduit simplement par:
val q =
Query(Bars).filter(b => b.id === 42).map(b => b.name ~ 1)
// Type of q is Query[(String, Int)]
val r: List[(String, Int)] = q.list // Actually run the query
Comme vous pouvez le voir, flatMap
, map
et filter
sont utilisés pour générer une Query
par la transformation répétée Query(Bars)
avec chaque appel filter
et map
. Dans le cas des collections, ces méthodes itèrent et filtrent la collection, mais dans Slick, elles sont utilisées pour générer du SQL. Plus de détails ici:
Comment Scala Slick traduit-il le code Scala en JDBC?