O *
método:
Isso retorna a projeção padrão - que é como você descreve:
'todas as colunas (ou valores computados) que geralmente estou interessado' em.
Sua tabela pode ter vários campos; você só precisa de um subconjunto para sua projeção padrão. A projeção padrão deve corresponder aos parâmetros de tipo da tabela.
Vamos fazer um de cada vez. Sem as <>
coisas, apenas *
:
// 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)
Apenas uma definição de tabela como essa permitirá que você faça consultas como:
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)]
a projeção padrão de (Int, String)
leva a um List[(Int, String)]
para consultas simples como essas.
// 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 é o tipo de q
? É um Query
com a projeção (String, Int)
. Quando chamado, ele retorna um List
de (String, Int)
tuplas de acordo com a projeção.
val result: List[(String, Int)] = q.list
Neste caso, você definiu a projeção que deseja na yield
cláusula da for
compreensão.
Agora sobre <>
e Bar.unapply
.
Isso fornece o que é chamado de projeções mapeadas .
Até agora vimos como o slick permite expressar consultas no Scala que retornam uma projeção de colunas (ou valores calculados); Portanto, ao executar essas consultas, você deve pensar na linha de resultado de uma consulta como uma tupla Scala . O tipo de tupla corresponderá à projeção definida (por sua
for
compreensão como no exemplo anterior, ou pela *
projeção padrão ). É por isso que field1 ~ field2
retorna uma projeção de Projection2[A, B]
onde
A
é o tipo de field1
e B
é o tipo 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
}
Estamos lidando com tuplas, o que pode ser complicado se tivermos muitas colunas. Gostaríamos de pensar nos resultados não como, TupleN
mas como um objeto com campos nomeados.
(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.
Como é que isso funciona? <>
obtém uma projeção Projection2[Int, String]
e retorna uma projeção mapeada no tipo Bar
. Os dois argumentos Bar, Bar.unapply _
mostram como essa (Int, String)
projeção deve ser mapeada para uma classe de caso.
Este é um mapeamento bidirecional; Bar
é o construtor da classe de caso, então essa é a informação necessária para ir de (id: Int, name: String)
para a Bar
. E unapply
se você adivinhou, é o contrário.
De onde unapply
vem? Este é um método Scala padrão disponível para qualquer classe de caso comum - apenas definir Bar
fornece Bar.unapply
um extrator que pode ser usado para recuperar o id
e com o name
qual
Bar
foi construído:
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)]
Portanto, sua projeção padrão pode ser mapeada para a classe de caso que você mais 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 _)
}
Ou você pode até mesmo tê-lo por consulta:
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 _))
Aqui, o tipo de q1
é a Query
com uma projeção mapeada para Baz
. Quando chamado, ele retorna um List
dos Baz
objetos:
val result: List[Baz] = q1.list
Finalmente, como um aparte, o .?
ofertas de elevação Opção - a maneira Scala de lidar com valores que podem não ser.
(id ~ name) // Projection2[Int, String] // this is just for illustration
(id.? ~ name) // Projection2[Option[Int], String]
O que, resumindo, funcionará muito bem com sua definição original 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]
Em resposta ao comentário sobre como o Slick usa as for
compreensões:
De alguma forma, as mônadas sempre conseguem aparecer e exigem ser parte da explicação ...
Pois as compreensões não são específicas apenas para coleções. Eles podem ser usados em qualquer tipo de Mônada , e as coleções são apenas um dos muitos tipos de mônadas disponíveis no Scala.
Mas como as coleções são familiares, elas são um bom ponto de partida para uma explicação:
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, ...)
Em Scala, um para compreensão é um açúcar sintático para chamadas de método (possivelmente aninhadas): O código acima é (mais ou menos) equivalente a:
ns.filter(i => i*i % 2 == 0).map(i => i*i)
Basicamente, qualquer coisa com filter
, map
, flatMap
métodos (em outras palavras, uma Mônada ) pode ser usado em uma
for
compreensão no lugar de ns
. Um bom exemplo é a Mônada Opção . Aqui está o exemplo anterior em que a mesma for
afirmação funciona tanto
List
em Option
mônadas quanto em :
// (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
No último exemplo, a transformação talvez fosse assim:
// 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))
No Slick, as consultas são monádicas - são apenas objetos com os métodos map
, flatMap
e filter
. Portanto, a for
compreensão (mostrada na explicação do *
método) se traduz apenas em:
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
Como você pode ver, flatMap
, map
e filter
são usados para gerar uma Query
pela transformação repetida Query(Bars)
da cada invocação de filter
e map
. No caso de coleções, esses métodos realmente iteram e filtram a coleção, mas no Slick eles são usados para gerar SQL. Mais detalhes aqui:
Como o Scala Slick traduz o código do Scala em JDBC?