How to group results with Slick - scala

Let's say I have those two models:
case class Department(id: Long, title: String)
case class Program(id: Long, departmentId: Long, title: String)
And two TableQuery, departments and programs, based on Table mapped to those case classes respectively.
I would like to make a query returning a Seq[(Department, Seq[Program])], where I have a list of departments with their corresponding programs.
I started like this:
val query =
(departments join programs on ((d, p) => d.id === p.departmentId))
.groupBy {...}
But what ever I put in the group by clause just doesn't make sense.
please help.

#osehyum, thanks for your answer. Your query is returning this:
Map[Long, Seq[(Department, Program)]]
The Long here is for the Department Id.
I managed to turn it with this:
val query = departments.joinLeft(programs).on(_.id === _.departmentId).result
.map(_.groupBy(_._1).toSeq)
.map(items => items.map { case (dep, rows) =>
(dep, rows.map(_._2).filter(_.isDefined).map(_.get))
})
Note that I used joinLeft this time, so Program became Option[Program], so I had to filter and map. It is tested.

How about this?
val query = departments.join(programs).on(_.id === _.departmentId)
.result
.map(_.groupBy(_._1.id))
db.run(query)

Related

Multiple left joins using slick?

I have the following slick entities:
class Person(personId, houseId, carId)
class House(houseId)
class Car(carId)
I want to select a Person and their optional house and car, my query is:
val query = personTable
.filter(_.personId === personId)
.joinLeft(houseTable)
.on(_.houseId === _.houseId)
.joinLeft(carTable)
.on(_._1.carId === _.carId)
.result
.headOption
However, the return type of the query looks a little funny, I'd expect it to be a tuple(person, house, car):
Option[(Person, Option[House], Option[Car])]
But it's actually a tuple(tuple(person, house), car):
Option[((Person, Option[House]), Option[Car])]
The data that comes back does seem correct, its just in an unusual structure, maybe I'm not performing the multiple joins correctly above?
Things look normal to me.
If you don't like the current datatype, you can map in your query to change it, like this e.g:
val query = personTable
.filter(_.personId === personId)
.joinLeft(houseTable)
.on(_.houseId === _.houseId)
.joinLeft(carTable)
.on(_._1.carId === _.carId)
.map{case((person, houseOpt), carOpt) => (person, houseOpt, carOpt)}
.result
.headOption

Group by in many-to-many join with Quill

I am trying to achieve with Quill what the following PostgreSQL query does:
select books.*, array_agg(authors.name) from books
join authors_books on(books.id = authors_books.book_id)
join authors on(authors.id = authors_books.author_id)
group by books.id
For now I have this in my Quill version:
val books = quote(querySchema[Book]("books"))
val authorsBooks = quote(querySchema[AuthorBook]("authors_books"))
val authors = quote(querySchema[Author]("authors"))
val q: db.Quoted[db.Query[(db.Query[Book], Seq[String])]] = quote{
books
.join(authorsBooks).on(_.id == _.book_id)
.join(authors).on(_._2.author_id == _.id)
.groupBy(_._1._1.id)
.map {
case (bId, q) => {
(q.map(_._1._1), unquote(q.map(_._2.name).arrayAgg))
}
}
}
How can I get rid of the nested query in the result (db.Query[Book]) and get a Book instead?
I might be a little bit rusty with SQL but are you sure that your query is valid? Particularly I find suspicious that you do select books.* while group by books.id i.e. you directly return fields that you didn't group by. And attempt to translate that wrong query directly is what makes things go wrong
One way to fix it is to do group by by all fields. Assuming Book is declared as:
case class Book(id: Int, name: String)
you can do
val qq: db.Quoted[db.Query[((Index, String), Seq[String])]] = quote {
books
.join(authorsBooks).on(_.id == _.book_id)
.join(authors).on(_._2.author_id == _.id)
.groupBy(r => (r._1._1.id, r._1._1.name))
.map {
case (bId, q) => {
// (Book.tupled(bId), unquote(q.map(_._2.name).arrayAgg)) // doesn't work
(bId, unquote(q.map(_._2.name).arrayAgg))
}
}
}
val res = db.run(qq).map(r => (Book.tupled(r._1), r._2))
Unfortunately it seems that you can't apply Book.tupled inside quote because you get error
The monad composition can't be expressed using applicative joins
but you can easily do it after db.run call to get back your Book.
Another option is to do group by just Book.id and then join the Book table again to get all the fields back. This might be actually cleaner and faster if there are many fields inside Book

Slick one to many and grouping

I'm trying to model the following with Slick 3.1.0;
case class Review(txt: String, userId: Long, id: Long)
case class User(name: String, id: Long)
case class ReviewEvent(event: String, reviewId: Long)
I need to populate a class called a FullReview, which looks like;
case class FullReview(r: Review, user: User, evts: Seq[ReviewEvent])
Assuming I have the right tables for each of the models, I'm trying to fetch a FullReview using a combination of join and group by, like so:
val withUser = for {
(r, u) <- RTable join UTable on (_.userId === _.id)
}
val withUAndEvts = (for {
((r, user), evts) <- withUser joinLeft ETable on {
case ((r, _), ev) => r.id === ev.reviewId
}
} yield (r, user, events)).groupBy(_._1._id)
This seems to yield, when a nested Query type, from what I can see. What am I doing wrong here?
If I understand you correctly, you can use following example:
val users = TableQuery[Users]
val reviews = TableQuery[Reviews]
val events = TableQuery[ReviewEvents]
override def findAllReviews(): Future[Seq[FullReview]] = {
val query = reviews
.join(users).on(_.userId === _.id)
.joinLeft(events).on(_._1.id === _.reviewId)
db.run(query.result).map { a =>
a.groupBy(_._1._1.id).map { case (_, tuples) =>
val ((review, user), _) = tuples.head
val reviewEvents = tuples.flatMap(_._2)
FullReview(review, user, reviewEvents)
}.toSeq
}
}
If you want to add pagination to this request, I've already answered here and here is full example.
From some tinkering around, I figured it would just be better to do the aggregation on the client. What that would mean, indirectly, is that if 100 rows on the table ETable would match a single row on the RTable, you would get multiple rows on the client. The client then has to implement its own aggregation to group all the ReviewEvent by Review.
As far as pagination is concerned, you may do something like;
def withUser(page: Int, pageSize: Int) = for {
(r, u) <- RTable.drop(page * pageSize).take(pageSize) join UTable on (_.userId === _.id)
}
I guess this is elegant enough for now. If someone has a better answer, I'd be happy to hear it.

Dynamically created slick queries

I have three tables: users, groups and users_groups. There is a many to many relation from groups and users because one user can belong in multiple groups and a group is constituted by multiple users.
I have a GET query like /group?name=X&user=Y
From that I'm searching from groups with name like X, but the tricky part is searching for the groups which the Y user doesnt belong.
def findUserGroups(id: Long) = {
users_groups.filter(ug => ug.userID === id)
}
From that I get all the groups which the user belongs, then I do this
var queries : List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]= List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]()
userGroups map { userGroup =>
val query : Query[GroupsTable, GroupsTable#TableElementType, Seq] = groups.filter(_.id =!= userGroup.group.get)
queries = query :: queries
}
If I println userGroup it gives me the correct groups.
Finally I've been trying a union
def findGroupByNameSynthFunction(name: String, queries: List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]) = {
val query1 = groups.filter(g => g.name like ("%" + name + "%"))
val unionQuery: Query[GroupsTable, GroupsTable#TableElementType, Seq] = query1
queries map { query =>
unionQuery ++ query
}
unionQuery
}
I execute it
val found = GroupsTable.findGroupByNameSynthFunction(name, queries).run
But I get all groups anyway.
Can Someone explained me what I'm doing very wrong!? :)
I am not sure where exactly you had the wrong expectation, but it probably revolves around the expression
queries map { query =>
unionQuery ++ query
}
This doesn't have any side-effect. Unless you are doing something with the resulting value (and you are not) this doesn't do anything. You probably want something like
def findGroupByNameSynthFunction(
name: String,
queries: List[Query[GroupsTable, GroupsTable#TableElementType, Seq]]
) = queries.map(_.filter(_.name like ("%" + name + "%")))
.reduce(_ union _)

scala slick one-to-many collections

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.
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.
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.