Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scala slick one-to-many collections

Tags:

scala

slick

I have a database that contain activities with a one-to-many registrations relation. The goal is to get all activities, with a list of their registrations.

By creating a cartesian product of activities with registrations, all necessary data to get that data is out is there. But I can't seem to find a nice way to get it into a scala collection properly; let's of type: Seq[(Activity, Seq[Registration])]

case class Registration(
  id: Option[Int],
  user: Int,
  activity: Int
)

case class Activity(
  id: Option[Int],
  what: String,
  when: DateTime,
  where: String,
  description: String,
  price: Double
)

Assuming the appropriate slick tables and tablequeries exist, I would write:

val acts_regs = (for {
   a <- Activities
   r <- Registrations if r.activityId === a.id
} yield (a, r))
  .groupBy(_._1.id)
  .map { case (actid, acts) => ??? }
}

But I cannot seem to make the appropriate mapping. What is the idiomatic way of doing this? I hope it's better than working with a raw cartesian product...

In Scala

In scala code it's easy enough, and would look something like this:

  val activities = db withSession { implicit sess =>
    (for {
      a <- Activities leftJoin Registrations on (_.id === _.activityId)
    } yield a).list
  }

  activities
    .groupBy(_._1.id)
    .map { case (id, set) => (set(0)._1, set.map(_._2)) }

But this seems rather inefficient due to the unnecessary instantiations of Activity which the table mapper will create for you. Neither does it look really elegant...

Getting a count of registrations

The in scala method is even worse when only interested in a count of registrations like so:

val result: Seq[Activity, Int] = ???

In Slick

My best attempt in slick would look like this:

  val activities = db withSession { implicit sess =>
    (for {
      a <- Activities leftJoin Registrations on (_.id === _.activityId)
    } yield a)
      .groupBy(_._1.id)
      .map { case (id, results) => (results.map(_._1), results.length) }
  }

But this results in an error that slick cannot map the given types in the "map"-line.

like image 808
A.J.Rouvoet Avatar asked Aug 28 '14 11:08

A.J.Rouvoet


2 Answers

I would suggest:

  val activities = db withSession { implicit sess =>
    (for {
      a <- Activities leftJoin Registrations on (_.id === _.activityId)
    } yield a)
      .groupBy(_._1)
      .map { case (activity, results) => (activity, results.length) }
  }

The problem with

  val activities = db withSession { implicit sess =>
    (for {
      a <- Activities leftJoin Registrations on (_.id === _.activityId)
    } yield a)
      .groupBy(_._1.id)
      .map { case (id, results) => (results.map(_._1), results.length) }
  }

is that you can't produce nested results in group by. results.map(_._1) is a collection of items. SQL does implicit conversions from collections to single rows in some cases, but Slick being type-safe doesn't. What you would like to do in Slick is something like results.map(_._1).head, but that is currently not supported. The closest you could get is something like (results.map(_.id).max, results.map(_.what).max, ...), which is pretty tedious. So grouping by the whole activities row is probably the most feasible workaround right now.

like image 115
cvogt Avatar answered Oct 02 '22 05:10

cvogt


A solution for getting all registrations per activity:

    // list of all activities
    val activities = Activities
    // map of registrations belonging to those activities
    val registrations = Registrations
      .filter(_.activityId in activities.map(_.id))
      .list
      .groupBy(_.activityId)
      .map { case (aid, group) => (aid, group.map(_._2)) }
      .toMap

    // combine them
    activities
      .list
      .map { a => (a, registrations.getOrElse(a.id.get, List()))

Which gets the job done in 2 queries. It should be doable to abstract this type of "grouping" function into a scala function.

like image 23
A.J.Rouvoet Avatar answered Oct 02 '22 05:10

A.J.Rouvoet