Slick/Scala - how do I access fields of the mapped projection/projected table part of a join in a where query - scala

I have a number of basic queries define, and am using query composition to add stuff such as ordering, paging, where clauses and so on...
But I have a problem accessing the fields of the joined 2nd table in the where clause...
Here's my table queries and my table. All tables are mapped to case classes.
val basicCars = TableQuery[CarTable]
val basicCarValues = TableQuery[CarValueTable]
val carsWithValues = for {
(c, v) <- basicCars leftJoin basicCarValues on (_.id === _.carId)
} yield (c, v.?)
Now I reuse/compose queries by doing stuff such as
carsWithValues.where(_._1.id === someId)
which works perfectly...
But if I want to access any value of the 2nd table... and I try
carsWithValues.where(_._2.latestPrice === somePrice)
It tells me that somePrice is not a member of MappedProjection......
error: value somePrice is not a member of scala.slick.lifted.MappedProjection[Option[com......datastore.slick.generated.Tables.CarValue],(Option[Long], Option[Long], Option[String],.....
I understand that this kind of can't work, cause _._2 is a MappedProjection and not just a CarValue sitting in the tuple..
But I can't figure out how to use any field of the table that is in the MappedProjection in a where clause?

The .? from the Slick code generator is implemented using a MappedProjection, which doesn't have the members anymore. If you postpone the call to .? it works:
val carsWithValues = for {
(c, v) <- basicCars leftJoin basicCarValues on (_.id === _.carId)
} yield (c, v)
carsWithValues.where(_._2.latestPrice === somePrice).map{ case (c,v) => (c,v.?) }

Related

How can I run inter-dependent queries alongside a non-DB operation in the same transaction using slick

Given the data model (<- indicating a foreign key dependency)
TableA <- TableB <- TableC
^ v
-----------------
I need to execute an api DELETE operation that soft-deletes a row in TableC. This delete must also trigger a call to another service (requiring values from TableA and TableB) if there are no more undeleted TableC entries that reference that row's parent in TableB. If the external call fails, I want to rollback the soft-delete. I want to do all of this in an idiomatic fashion (I'm effectively brand new to scala/slick), and use transactions for the rollback
Based on what I've read, I need to be using for comprehension to assemble the queries, but I'm having issues getting the database operations to gel nicely with the external service call. My original plan was:
val select = for {
tableCRow <- tableBDao.TableQueryC.filter(_.id === idParam)
tableBRow <- tableBDao.TableQueryB if tableCRow.tableBForeignKey === tableBRow.id
tableARow <- TableADao.TableQueryA if tableCRow.tableAForeignKey === tableARow.id
count <- tableBDao.TableQueryC.filter(_.tableBForeignKey === tableBRow.id).map(_.id).countDefined
_ <- tableBDao.softDeleteRow(idParam)
_ <- if (count > 1) DBIO.successful(httpRequestService.deleteOtherResource(tableARow.someValue, tableBRow.someValue))
} yield ()
db.run(select.result)
But this had problems because I couldn't pass Slick's Rep[T] values to my httpRequestService method. I then tried to break it down into two portions - SELECT first, then DELETE, like so:
val select = for {
tableCRow <- tableBDao.TableQueryC.filter(_.id === idParam)
tableBRow <- tableBDao.TableQueryB if tableCRow.tableBForeignKey === tableBRow.id
tableARow <- TableADao.TableQueryA if tableCRow.tableAForeignKey === tableARow.id
count <- tableBDao.TableQueryC.filter(_.tableBForeignKey === tableBRow.id).map(_.id).countDefined
} yield (tableBRow.date.formatted("yyyy-MM-DD"), tableARow.externalServiceId, count)
val result: Future[Option[(String, Long, Integer)]] = db.run(select.result.headOption)
result.map {
case None => throw new IllegalArgumentException("exception message")
case Some(data) =>
val delete = for {
_ <- tableBDao.softDeleteRow(idParam)
_ <- if (data._3 > 1) DBIO.successful(httpRequestService.cancelSchedulerJob(data._2, data._1))
} yield numRows
db.run(delete.transactionally)
}
But, despite this actually passing IntelliJ IDEA checks, it won't compile as my count query returns a Rep[Int], which lacks a map function. Additionally, each of my table(A|B|C)Row maps raises an error because they're expecting slick.lifted.Query[Nothing,Nothing,Seq] and they're getting slick.lifted.Query[Nothing,T,Seq]. Finally, the db.run statement doesn't want to use headOption, and apparently returns Any which doesn't support map
halp
Solved this by finally understanding how slick puts things together in a for-comprehension. I had to pull the count out of the query and into a followup groupby->map function, which accumulated my list of things I wanted to count and THEN counted them as opposed to counting as part of the query. This fixed all the rest of the problems too, as the count query was throwing off the expected return types of everything else.
basically, the solution looked like (thing1 was for a join):
val select = (for {
thing1 <- query1
thing2 <- query2
thing3 <- query3
listToCount <- query4 selecting everything I wanted to count
} yield (thing2, thing3, listToCount))
.groupBy({
case (thing2, thing3, listToCount) =>
(thing2, thing3)
})
.map({
case ((thing2, thing3), list) =>
(thing2.deliveryDate, thing3.schedulerJobId, list.map(_._3).length)
})

Slick 3.0.0: How to query one-to-many / many-to-many relations

Basically the same question has been asked about a year ago for slick 2.x (scala slick one-to-many collections). I'm wondering if there has any progression been made with the release of reactive slick.
Let's say for example we have three tables. library, book and library_to_book where a library has many books. What I want is a list of libraries with their books. In scala this would be something like Seq[(Library, Seq[Book])]. The query I have is as follows:
val q = (for {
l <- libraries
ltb <- libraryToBooks if l.id === ltb.libraryId
b <- books if ltb.bookId === b.id
} yield (l, b)
db.run(q.result).map( result => ??? )
results in this case is of type Seq[(Library, Book)]. How do I have to change my query to get a result of type Seq[(Library, Seq[Book])] instead? What is the "slick way" of writing such queries?
IMO your code looks fine. It really depends on what feels more readable to you. Alternatively, you can use join as well:
val findBooksQuery = libraries
.join(libraryToBooks).on(_.id === _.libraryId)
.join(books).on(_.id === _._2.bookId)
.result
val action = (for {
booksResult <- findBooksQuery
} yield {
booksResult.map { row =>
val (libraryTableRow, libraryToBooksTableRow) = row._1
val booksTableRow = row._2
// TODO: Access all data from the rows and construct desired DS
}
}
db.run(action)
You can then do a groupBy on a particular key to get the kind of data structure you are looking for. In this case, it would be more evolved as it is join across three tables. Example, add following to your query:
val findBooksQuery = libraries
.join(libraryToBooks).on(_.id === _.libraryId)
.join(books).on(_.id === _._2.bookId)
// To group by libraries.id
.groupBy(_._1.id)
.result
To what you want to map to, db.run returns a Future(of something), a Future[Seq[(Library, Seq[Book])]] in your case. When mapping over a future you have access to the Seq and you can transform it to something else to get a new Future.

In Slick 3.0, how to simplify nested `db.run`?

I'm using Slick 3.0, and following is my codes:
def registerMember(newMember: TeamMember): Future[Long] = {
db.run(
teamProfileTable.filter(u => u.ID === newMember.ID).result.headOption
).flatMap {
case None => Future(-1)
case _ => db.run(
(teamProfileTable returning teamProfileTable.map(_.staffID)) += newMember.toTeamRecord
)
}
}
This may look ok. But when there are more layers of callback, the codes may become hard to read. I tried to simplify the codes using for-expression or andThen.. But due to the pattern matching part, I can only use flatMap to implement this..
Does anyone have ideas about how to refactor this?
I think a for comprehension should be okay here, you just need conditional handling of the Option in the result of the first Future. Something like this should work (note I did not compile check this):
def registerMember(newMember: TeamMember): Future[Long] = {
for{
r1Opt <- db.run(teamProfileTable.filter(u => u.ID === newMember.ID).result.headOption
r2 <- r1Opt.fold(Future.successful(-1L))(r1 => db.run((teamProfileTable returning teamProfileTable.map(_.staffID)) += newMember.toTeamRecord)
} yield r2
}
You can see on the right side of the fold that I have access to the result of the first Future if it was a Some (as r1).
I would even take this a step further and create separate methods for the steps of the for comprehension to clean things up, like so:
def registerMember(newMember: TeamMember): Future[Long] = {
def findMember =
db.run(teamProfileTable.filter(u => u.ID === newMember.ID).result.headOption
def addMember(r1Opt:Option[TeamMember]) = {
r1Opt.fold(Future.successful(-1L)){r1 =>
db.run((teamProfileTable returning teamProfileTable.map(_.staffID)) +=
newMember.toTeamRecord)
}
}
for{
r1Opt <- findMember
r2 <- addMember(r1Opt)
} yield r2
}
Another approach to simplify nested db.runs in Slick 3.0 when the query spans two tables could be to join the queries into a single query. Joining and Zipping. However, the OP seems to have the somewhat rarer case of nested queries on the same table so this approach may not be helpful in that particular case.
val query = slickLoginInfos join slickUserLoginInfos on
((l,ul) => l.id === ul.loginInfoId)
db.run((for { (l, ul) <- query } yield (ul)).result.headOption)

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.

How can I use yield with Slick Query?

How can I use yield with Slick Query ? So imagine I have code like this
def rolesQuery(owner_id: UUID) = for {
(role, rel) <- Roles innerJoin AbilitiesMapping on (_.id === _.obj_id) if rel.owner_id === owner_id.bind
} yield role
which I use like this
val rolesQuery: lifted.Query[Roles.type, Role] = DBManager.rolesQuery(user_id)
rolesQuery.foreach((role: Role) => {
println(modifiedRole(role))
})
but what if I want to produce another container with modified Roles ? I tried this
val lst = for (role <- rolesQuery)
yield modifiedRole(role)
But it returns WrappingQuery in lst instead of actually case class object. I see that there is mapResults method but it returns object of type UnitInvoker instead of container of Role-s
What should I do ? Shall I just call .list() on Query object ?
Yes, you if you want to process the results in a for comprehension, you will need to fetch the data and list will accomplish that. As it stands, your for-comp seems to be just mapping from one query to another query:
val lst = for (role <- rolesQuery.list)
yield modifiedRole(role)
Just make sure you have an implicit Session in scope.