El *
método:
Esto devuelve la proyección predeterminada , que es como la describe:
'todas las columnas (o valores calculados) que normalmente me interesan'.
Su tabla puede tener varios campos; solo necesita un subconjunto para su proyección predeterminada. La proyección predeterminada debe coincidir con los parámetros de tipo de la tabla.
Tomémoslo uno a la vez. Sin las <>
cosas, solo el *
:
object Bars extends Table[(Int, String)]("bar") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = id ~ name
}
Solo una definición de tabla como esa le permitirá realizar consultas como:
implicit val session: Session =
val result = Query(Bars).list
la proyección predeterminada de (Int, String)
conduce a una List[(Int, String)]
para consultas simples como estas.
val q =
for (b <- Bars if b.id === 42)
yield (b.name ~ 1)
¿Qué tipo de q
? Es una Query
con la proyección (String, Int)
. Cuando se invoca, devuelve un número List
de (String, Int)
tuplas según la proyección.
val result: List[(String, Int)] = q.list
En este caso, ha definido la proyección que desea en la yield
cláusula de la for
comprensión.
Ahora sobre <>
y Bar.unapply
.
Esto proporciona lo que se llama proyecciones mapeadas .
Hasta ahora hemos visto cómo slick le permite expresar consultas en Scala que devuelven una proyección de columnas (o valores calculados); Entonces, al ejecutar estas consultas , debe pensar en la fila de resultados de una consulta como una tupla de Scala . El tipo de la tupla coincidirá con la proyección definida (según su
for
comprensión como en el ejemplo anterior, o según la *
proyección predeterminada ). Esta es la razón por la que field1 ~ field2
devuelve una proyección de Projection2[A, B]
dónde
A
es el tipo de field1
y B
es el tipo de field2
.
q.list.map {
case (name, n) =>
}
Queury(Bars).list.map {
case (id, name) =>
}
Estamos tratando con tuplas, que pueden resultar engorrosas si tenemos demasiadas columnas. Nos gustaría pensar en los resultados no como TupleN
un objeto con campos con nombre, sino más bien.
(id ~ name)
case class Bar(id: Int, name: String) // For now, using a plain Int instead
(id ~ name <> (Bar, Bar.unapply _))
Query(Bars).list.map ( b.name )
¿Como funciona esto? <>
toma una proyección Projection2[Int, String]
y devuelve una proyección mapeada en el tipo Bar
. Los dos argumentos Bar, Bar.unapply _
dicen hábilmente cómo esta (Int, String)
proyección debe mapearse a una clase de caso.
Este es un mapeo de dos vías; Bar
es el constructor de la clase de caso, por lo que esa es la información necesaria para pasar de (id: Int, name: String)
a Bar
. Y unapply
si lo has adivinado, es al revés.
De donde unapply
viene Este es un método estándar de Scala disponible para cualquier clase de caso ordinario; simplemente definir Bar
le da Bar.unapply
cuál es un extractor que se puede usar para recuperar el id
y con el name
que
Bar
se construyó:
val bar1 = Bar(1, "one")
val Bar(id, name) = bar1
val bars: List[Bar] =
val barNames = bars.map {
case Bar(_, name) => name
}
val x = Bar.unapply(bar1)
Por lo tanto, su proyección predeterminada se puede asignar a la clase de caso que más espera usar:
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 _)
}
O incluso puede tenerlo por consulta:
case class Baz(name: String, num: Int)
val q1 =
for (b <- Bars if b.id === 42)
yield (b.name ~ 1 <> (Baz, Baz.unapply _))
Aquí el tipo de q1
es un Query
con una proyección mapeada a Baz
. Cuando se invoca, devuelve una List
de Baz
objetos:
val result: List[Baz] = q1.list
Finalmente, como acotación al margen, .?
ofrece Option Lifting : la forma de Scala de lidiar con valores que pueden no serlo.
(id ~ name)
(id.? ~ name)
Lo cual, para terminar, funcionará muy bien con su definición original de Bar
:
case class Bar(id: Option[Int] = None, name: String)
val q0 =
for (b <- Bars if b.id === 42)
yield (b.id.? ~ b.name <> (Bar, Bar.unapply _))
q0.list
En respuesta al comentario sobre cómo Slick usa for
comprensiones:
De alguna manera, las mónadas siempre logran aparecer y exigen ser parte de la explicación ...
Porque las comprensiones no son específicas de colecciones solamente. Pueden utilizarse en cualquier tipo de Mónada. , y las colecciones son solo uno de los muchos tipos de mónadas disponibles en Scala.
Pero como las colecciones son familiares, son un buen punto de partida para una explicación:
val ns = 1 to 100 toList;
val result =
for { i <- ns if i*i % 2 == 0 }
yield (i*i)
En Scala, un para comprensión es azúcar sintáctico para llamadas de método (posiblemente anidadas): el código anterior es (más o menos) equivalente a:
ns.filter(i => i*i % 2 == 0).map(i => i*i)
Básicamente, cualquier cosa con filter
, map
, flatMap
métodos (en otras palabras, una mónada ) se puede utilizar en una
for
comprensión en lugar de ns
. Un buen ejemplo es la mónada Option . Aquí está el ejemplo anterior donde la misma for
declaración funciona tanto en las
mónadas List
como en las Option
mónadas:
val result =
for {
i <- ns
i2 <- Some(i*i)
if i2 % 2 == 0
} yield i2
def evenSqr(n: Int) = {
val sqr = n*n
if (sqr % 2 == 0) Some (sqr)
else None
}
result =
for {
i <- ns
i2 <- evenSqr(i)
} yield i2
En el último ejemplo, la transformación quizás se vería así:
val result =
ns.flatMap(i => Some(i*i)).filter(i2 => i2 %2 ==0)
result =
ns.flatMap(i => evenSqr(i))
En Slick, las consultas son monádicas: son solo objetos con los métodos map
, flatMap
y filter
. Entonces, la for
comprensión (que se muestra en la explicación del *
método) simplemente se traduce en:
val q =
Query(Bars).filter(b => b.id === 42).map(b => b.name ~ 1)
val r: List[(String, Int)] = q.list
Como se puede ver, flatMap
, map
y filter
se utilizan para generar una Query
por la transformación repetida de Query(Bars)
cada invocación de filter
y map
. En el caso de las colecciones, estos métodos en realidad iteran y filtran la colección, pero en Slick se utilizan para generar SQL. Más detalles aquí:
¿Cómo traduce Scala Slick el código Scala a JDBC?