17
votes

In the Slick examples there are a few examples of joining where one of the resulting columns can be nulls, as it can be the case when doing left, right, or outer joins. For example:

val explicitLeftOuterJoin = for {
  (c, s) <- Coffees leftJoin Suppliers on (_.supID === _.id)
} yield (c.name, s.name.?)

But what if I want to return the entire mapped object? What I mean is:

val explicitLeftOuterJoin = for {
  (c, s) <- Coffees leftJoin Suppliers on (_.supID === _.id)
} yield (c, s.?)

This doesn't seem to work as it complains about "could not find implicit value for evidence parameter of type scala.slick.lifted.TypeMapper[Suppliers]". Basically I'd like it to return a list of tuple of (Coffee, Option[Supplier])

Why doesn't this work and what's the fix for it? Especially, since this works fine:

val q = for {
  c <- Coffees
  s <- Suppliers
} yield (c, s)

(I know that's an inner join)

4
related to stackoverflow.com/questions/14990365/cvogt

4 Answers

9
votes

UPDATE: This will be solved and simply work in Slick 3.0 coming end of 2014, no need anymore for the following workaround

This is a limitation of Slick at the moment. You have to call .? on every column individually. You can however place a function called ? in the table class that does this in a central place and thereby get .? on complete rows. This play-slick example code contains a generic solution involving some generated code. We also have a PR that adds auto-generation of a ? method lined up.

In the long run we will support a variant of outer joins in Slick where Slick is fully aware of the types involved and you do not need to specify .? anywhere. For now we have to live with workarounds involving code generation.

5
votes

Not the cleanest solution (uses scalaz 7.0.6 and shapeless 2.0.1), but this works for now (Slick 2.0.1):

Using the ? projection above, it's possible to create a Slick projection that converts the tuple of Option values => Option[TupleN] => Option[YourClass].

Add option Projection

Note: sequence is used to convert a tuple of Option values to Option[TupleN]. Code for sequence is defined at the bottom of this answer.

Add to Suppliers. Assumes Supplier is a case class.

  import scalaz._, Scalaz._
  import SequenceTupleOption._

  def option = (id.?, name.?, street.?) <> (optionApply, optionUnapply)
  def optionApply(t: (Option[Int], Option[String], Option[String])): Option[Comment] = {
    sequence(t).map(Supplier.tupled)
  }

  def optionUnapply(oc: Option[Supplier]): Option[(Option[Int], Option[String], Option[String])] = None

Using the option Projection

val explicitLeftOuterJoin = for {
  (c, s) <- Coffees leftJoin Suppliers on (_.supID === _.id)
} yield (c, s.option)

Advanced: sequence, Converts Tuple of Option Values to Option[TupleN]

This is the hard part that Travis Brown wrote. sequence converts from a tuple of Option values to a Option[TupleN] (using scalaz and shapeless).

import scalaz._, Scalaz._
import shapeless._, ops.hlist.{ RightFolder, Tupler }

object SequenceTupleOption {

  object applicativeFolder extends Poly2 {
    implicit def caseApplicative[A, B <: HList, F[_]](implicit
      app: Applicative[F]
    ) = at[F[A], F[B]] {
      (a, b) => app.ap(a)(app.map(b)(bb => (_: A) :: bb))
    }
  }

  def sequence[T, EL <: HList, L <: HList, OL <: HList, OT](t: T)(implicit
    gen: Generic.Aux[T, EL],
    eq: EL =:= L,
    folder: RightFolder.Aux[L, Option[HNil], applicativeFolder.type, Option[OL]],
    tupler: Tupler.Aux[OL, OT]
  ): Option[OT] =
    eq(gen.to(t)).foldRight(some(HNil: HNil))(applicativeFolder).map(tupler(_))

}

Usage for sequence:

import scalaz._, Scalaz._
import SequenceTupleOption._

case class Person(id: Int, name: String, age: Int)

val t = (Option(1), Option("Bob"), Option(40))

val person: Option[Person] = sequence(t).map(Person.tupled) // Some(Person(1,Bob,40))

High-level overview of what sequence does (not proper types):

  1. Converts a tuple of Option to an shapeless HList[Option[_]].
  2. sequence over the HList[Option[_]] to a Option[HList[_]]
  3. Convert the HList back to a tuple.
2
votes

In addition to the answer above: In case you have a class which extends Table and your * projection looks something like this:

def * = (col1, col2, col3)

than your ? function would look like:

def ? = (col1.?, col2.?, col3.?)

If you have defined such a function you can write:

for {
    (x,y) <- x leftJoin y on (...)
} yield (x, y.?)
0
votes

In Slick 3.1.1, the correct answer would be simply (as mentioned in some comments):

for {
  (c, s) <- coffees joinLeft suppliers on (_.supID === _.id)
} yield (c, s)