How does Slick translate code such as:
val q2 = for {
c <- Coffees if c.price < 9.0
s <- Suppliers if s.id === c.supID
} yield (c.name, s.name)
for(
In Scala the for
"loop" isn't actually a special language construct, but rather syntactic sugar. Your first example
val q2 = for {
c <- Coffees if c.price < 9.0
s <- Suppliers if s.id === c.supID
} yield (c.name, s.name)
translates to something in the lines of:
val q2 = Coffees.withFilter(_.price < 9.0).flatMap(c =>
Suppliers.withFilter(_.id === c.supID).map(s =>
(c.name, s.name)
)
)
Now, the flatMap
, map
, withFilter
(and foreach
) do not actually filter the collection, but rather collect what's hapening in an AST (Abstract Syntax Tree), which is then handled to Slick to translate to SQL.
Also, c.price
, c.supID
are actually Slick column
s, whose <
, >
, ===
(and so on) methods don't return bool, but collect the comparison as well, which
is later passed down to be converted to SQL.
This is a talk by the creators, where most of this is described (correctly).
Slick's stable API achieves this via what it calls lifted embedding. Your example is clearly using the stable API (as you use ===
for equality and not ==
).
The beauty of Slick (and in turn Scala) is that - this much is achieved without using macros or Scala-Virtualized. (Side Note: Slick's experimental API does use macros - and this will allow you to use ==
instead of ===
or is
)
The translation to SQL is achieved using:
Scala's for
comprehension syntax, which is translated to method calls.
Tables defined in Slick are Monads - they have the magic foreach
, map
,
flatMap
, and
filter
methods which allow them to be expressed in for
'loops' while Scala
translates them to method calls (as correctly illustrated in the code
provided by the other answer by @emil-ivanov).
As with regular Scala collections, the for
is syntactic sugar for nested
method calls to flatMap
/map
and filter
; unlike regular collections,
the Slick Table
objects' versions of map
and filter
return representations
of a query, building it along with every filter condition (if
) or join
(as in s <- Suppliers if s.id is c.supID
)
So the type of q2
is not your usual collection (as a for comprehension in Scala
is typically used to return), but rather a representation of a query.
(Just as the Scala Option Monad also works with for
comprehensions despite
not being a 'collection' (in the way that List
or Map
is))
You can see the underlying query with q2.selectStatement
.
Scala's implicit lifting - c.price
is not an Int
but rather a representation of
a column value - so the expression c.price < 9.0
becomes c.price.<(Const(9.0))
(an Int
is lifted to the desired type), and <
is a just a method of the
class that represents c.price
, a Column
. The <
method
does not do what <
usually does (in the case of plain Int
s) - it simply returns
a representation of the SQL AST corresponding to price < 9
that becomes part of
the SQL that is generated and sent off to JDBC to execute.
There's a whole lot else going on, in terms of details, but I think the query monad and the implicit lifting are the chief ingredients.