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 Querycom a projeção (String, Int). Quando chamado, ele retorna um Listde (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 yieldcláusula da forcompreensã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
forcompreensão como no exemplo anterior, ou pela *projeção padrão ). É por isso que field1 ~ field2retorna uma projeção de Projection2[A, B]onde
Aé o tipo de field1e 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, TupleNmas 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 unapplyvem? Este é um método Scala padrão disponível para qualquer classe de caso comum - apenas definir Barfornece Bar.unapplyum extrator que pode ser usado para recuperar o ide com o namequal
Barfoi 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 Querycom uma projeção mapeada para Baz. Quando chamado, ele retorna um Listdos Bazobjetos:
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 forcompreensõ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
forcompreensão no lugar de ns. Um bom exemplo é a Mônada Opção . Aqui está o exemplo anterior em que a mesma forafirmação funciona tanto
Listem Optionmô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, flatMape filter. Portanto, a forcompreensã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, mape filtersão usados para gerar uma Querypela transformação repetida Query(Bars)
da cada invocação de filtere 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?