Il *
metodo:
Questo restituisce la proiezione predefinita , che è come descrivi:
"tutte le colonne (o valori calcolati) che di solito mi interessano".
La tua tabella potrebbe avere diversi campi; hai solo bisogno di un sottoinsieme per la tua proiezione predefinita. La proiezione predefinita deve corrispondere ai parametri di tipo della tabella.
Prendiamolo uno alla volta. Senza il <>
materiale, solo il *
:
// 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)
Solo una definizione di tabella come questa ti consentirà di eseguire query come:
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 proiezione predefinita di (Int, String)
porta a List[(Int, String)]
per query semplici come queste.
// 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.
Qual è il tipo di q
? È una Query
con la proiezione (String, Int)
. Quando viene richiamato, restituisce una List
di (String, Int)
tuple secondo la proiezione.
val result: List[(String, Int)] = q.list
In questo caso, hai definito la proiezione che desideri nella yield
clausola della for
comprensione.
Ora circa <>
e Bar.unapply
.
Ciò fornisce le cosiddette proiezioni mappate .
Finora abbiamo visto come slick ti permetta di esprimere query in Scala che restituiscono una proiezione di colonne (o valori calcolati); Quindi, quando si eseguono queste query, è necessario pensare alla riga del risultato di una query come a una tupla Scala . Il tipo di tupla corrisponderà alla proiezione definita (dalla tua
for
comprensione come nell'esempio precedente, o dalla *
proiezione predefinita ). Questo è il motivo per cui field1 ~ field2
restituisce una proiezione di Projection2[A, B]
dove
A
è il tipo di field1
ed B
è il tipo di 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
}
Abbiamo a che fare con tuple, che potrebbero essere ingombranti se abbiamo troppe colonne. Vorremmo pensare ai risultati non come a TupleN
un oggetto con campi denominati.
(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.
Come funziona? <>
prende una proiezione Projection2[Int, String]
e restituisce una proiezione mappata sul tipo Bar
. I due argomenti Bar, Bar.unapply _
dicono a slick come questa (Int, String)
proiezione deve essere mappata a una classe case.
Questa è una mappatura a due vie; Bar
è il costruttore della classe case, quindi queste sono le informazioni necessarie per passare da (id: Int, name: String)
a Bar
. E unapply
se l'hai indovinato, è per il contrario.
Da dove unapply
viene? Questo è un metodo Scala standard disponibile per qualsiasi classe case ordinaria - la semplice definizione Bar
ti dà Bar.unapply
un estrattore che può essere usato per recuperare id
e name
che è
Bar
stato costruito con:
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)]
Quindi la tua proiezione predefinita può essere mappata alla classe case che più ti aspetti di usare:
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 _)
}
Oppure puoi anche averlo per query:
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 _))
Qui il tipo di q1
è a Query
con una proiezione mappata su Baz
. Quando viene richiamato, restituisce un List
of Baz
objects:
val result: List[Baz] = q1.list
Infine, per inciso , le .?
offerte Option Lifting - il modo Scala di trattare valori che potrebbero non esserlo.
(id ~ name) // Projection2[Int, String] // this is just for illustration
(id.? ~ name) // Projection2[Option[Int], String]
Che, concludendo, funzionerà bene con la tua definizione originale di 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]
In risposta al commento su come Slick usa la for
comprensione:
In qualche modo, le monadi riescono sempre a presentarsi e richiedono di far parte della spiegazione ...
Perché le comprensioni non sono specifiche solo per le raccolte. Possono essere utilizzati su qualsiasi tipo di monade e le raccolte sono solo uno dei tanti tipi di monade disponibili in Scala.
Ma poiché le raccolte sono familiari, rappresentano un buon punto di partenza per una spiegazione:
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, ...)
In Scala, una comprensione for è zucchero sintattico per chiamate di metodi (possibilmente annidate): Il codice sopra è (più o meno) equivalente a:
ns.filter(i => i*i % 2 == 0).map(i => i*i)
In sostanza, qualsiasi cosa con filter
, map
, flatMap
metodi (in altre parole, una Monade ) può essere utilizzato in una
for
comprensione al posto di ns
. Un buon esempio è la monade Option . Ecco l'esempio precedente in cui la stessa for
istruzione funziona sia sulle
monadi List
che sulle Option
monadi:
// (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
Nell'ultimo esempio, la trasformazione sarebbe forse simile a questa:
// 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))
In Slick, le query sono monadica - sono solo oggetti con il map
, flatMap
e filter
metodi. Quindi la for
comprensione (mostrata nella spiegazione del *
metodo) si traduce semplicemente in:
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
Come si può vedere, flatMap
, map
e filter
vengono utilizzati per generare un Query
dalla trasformazione ripetuta di Query(Bars)
uno con l'invocazione filter
e map
. Nel caso delle raccolte, questi metodi effettivamente iterano e filtrano la raccolta, ma in Slick vengono utilizzati per generare SQL. Maggiori dettagli qui: In che
modo Scala Slick traduce il codice Scala in JDBC?