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 Querycon la proiezione (String, Int). Quando viene richiamato, restituisce una Listdi (String, Int)tuple secondo la proiezione.
val result: List[(String, Int)] = q.list
In questo caso, hai definito la proiezione che desideri nella yieldclausola della forcomprensione.
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
forcomprensione come nell'esempio precedente, o dalla *proiezione predefinita ). Questo è il motivo per cui field1 ~ field2restituisce una proiezione di Projection2[A, B]dove
Aè il tipo di field1ed 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 TupleNun 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 unapplyviene? Questo è un metodo Scala standard disponibile per qualsiasi classe case ordinaria - la semplice definizione Barti dà Bar.unapplyun estrattore che può essere usato per recuperare ide nameche è
Barstato 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 Querycon una proiezione mappata su Baz. Quando viene richiamato, restituisce un Listof Bazobjects:
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 forcomprensione:
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
forcomprensione al posto di ns. Un buon esempio è la monade Option . Ecco l'esempio precedente in cui la stessa foristruzione funziona sia sulle
monadi Listche sulle Optionmonadi:
// (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, flatMape filtermetodi. Quindi la forcomprensione (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, mape filtervengono utilizzati per generare un Querydalla trasformazione ripetuta di Query(Bars)
uno con l'invocazione filtere 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?