Scala Slick 2 join on multiple fields? - scala

how can do joins on multiple fields like in example beneath?
val ownerId = 1
val contactType = 1
...
val contact = for {
(t, c) <- ContactTypes leftJoin Contacts on (_.id === _.typeId && _.ownerId === ownerId)
if t.id === contactType
} yield (c.?, t)
How can I achieve this with Slick 2.0.1? Idelly I need slick to generate this kind of query
SELECT
x2."contact_id",
x2."type_id",
x2."owner_id",
x2."value",
x2."created_on",
x2."updated_on",
x3."id",
x3."type",
x3."model"
FROM
(
SELECT
x4."id" AS "id",
x4."type" AS "type",
x4."model" AS "model"
FROM
"contact_types" x4
)x3
LEFT OUTER JOIN(
SELECT
x5."created_on" AS "created_on",
x5."value" AS "value",
x5."contact_id" AS "contact_id",
x5."updated_on" AS "updated_on",
x5."type_id" AS "type_id",
x5."owner_id" AS "owner_id"
FROM
"contacts" x5
)x2 ON x3."id" = x2."type_id" AND x2.owner_id = 1
WHERE
(x3."id" = 3)
Please note ON x3."id" = x2."type_id" AND x2.owner_id = 16

Ok, so after digging through websites and source code I think I finally found the solution
leftJoin on() method accepts following parameter pred: (E1, E2) => T, so we simply can do like this
val contacts = for {
(t, c) <- ContactTypes leftJoin Contacts on ( (type, contact) => {
type.id === contact.typeId && contact.ownerId === ownerId
} )
} yield (c.?, t)
Which generated sql query as needed.

Related

Slick group by count

Let's say I have organizations, each organization has different groups, and users subscribe to groups.
case class OrganizationEntity(id: Option[Int], name: String)
case class GroupEntity(id: Option[Int], organizationId: Int, name: String)
case class GroupUserEntity(groupId: Int, userId: Int)
I need to get all groups of an organization, with the organizationName, and the quantity of users subscribed to that group.
In SQL, this can be easily done with this query:
SELECT g.*, o.organizationname, COUNT(DISTINCT gu.userid) FROM `group` g
LEFT JOIN organization o ON g.orgid = o.organizationid
LEFT JOIN group_user gu ON g.groupid = gu.groupid
WHERE g.orgid = 1234
GROUP BY g.groupid;
But I am struggling to replicate that in slick,
I've started writing this, but I am stuck now:
def findByOrganizationId(organizationId: Int) = {
(for {
g <- groups if g.organizationId === organizationId
o <- organizations if o.id === organizationId
gu <- groupUsers if g.id === gu.groupid
} yield (g, o.name, gu)).groupBy(_._3.groupid).map { case (_, values) => (values.map { case (g, orgname, users) => (g, orgname, users.) } }.result
}
You can just add .length to do the count in your code.
I think should also work directly in the yield so you don't need the groupBy:
def findByOrganizationId(organizationId: Int) = {
(for {
g <- groups if g.organizationId === organizationId
o <- organizations if o.id === organizationId
gu <- groupUsers if g.id === gu.groupid
} yield (g, o.name, gu.length)).result
}

Scala Slick joinLeft and combined conditions

I want to be able to create a query with Slick that let me filter left joins in a dynamic way
case class Player(
id: Long,
createdAt: DateTime,
lastModificationDate: DateTime,
name: String
)
class PlayerTable(tag: Tag) extends Table[Player](tag, "players") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[DateTime]("createdAt")
def lastModificationDate = column[DateTime]("lastModificationDate")
def name = column[String]("name")
override def * : ProvenShape[Player] = (
id,
createdAt,
lastModificationDate,
updatedAt,
name
) <> (Player.tupled, Player.unapply)
}
case class PlayerGame(
id: Long,
createdAt: DateTime,
lastModificationDate: DateTime,
playerId: Long,
level: Int,
status: String
)
class PlayerGameTable(tag: Tag) extends Table[PlayerGame](tag, "player_games") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[DateTime]("createdAt")
def lastModificationDate = column[DateTime]("lastModificationDate")
def playerId = column[Long]("playerId")
def level = column[Int]("level")
def status = column[String]("status")
override def * : ProvenShape[PlayerGame] = (
id,
createdAt,
lastModificationDate,
playerId,
level,
status
) <> (PlayerGame.tupled, PlayerGame.unapply)
}
I want to write a query like this with Slick, where the WHERE CLAUSE is dynamic. I wrote two examples
SELECT *
FROM players
LEFT JOIN player_games AS playerGamesOne ON players.id = playerGamesOne.playerId AND playerGamesOne.level = 1
LEFT JOIN player_games AS playerGamesTwo ON players.id = playerGamesTwo.playerId AND playerGamesTwo.level = 2
WHERE playerGamesOne.status LIKE 'gameOver'
OR playerGamesTWO.status LIKE 'gameOver'
SELECT *
FROM players
LEFT JOIN player_games AS playerGamesOne ON players.id = playerGamesOne.playerId AND playerGamesOne.level = 1
LEFT JOIN player_games AS playerGamesTwo ON players.id = playerGamesTwo.playerId AND playerGamesTwo.level = 2
WHERE playerGamesOne.status LIKE 'playing'
OR playerGamesTwo.status NOT LIKE 'gameOver'
I was trying something like this, but I get Rep[Option[PlayerGameTable]] as the parameter. Maybe there is a different way of doing something like this
val baseQuery = for {
((p, g1), g2) <- PlayerTable.playerQuery joinLeft
PlayerGameTable.playerGameQuery ON ((x, y) => x.id === y.playerId && y.level === 1) joinLeft
PlayerGameTable.playerGameQuery ON ((x, y) => x._1.id === y.playerId && y.level === 2)
} yield (p, g1, g2)
private def filterPlayerGames(gameStatus: String, playerGamesOneOpt: Option[PlayerGameTable], playerGamesTwoOpt: Option[PlayerGameTable]) = {
(gameStatus, playerGamesOneOpt, playerGamesOneOpt) match {
case (gameStatus: String, Some(playerGamesOne: PlayerGameTable), Some(playerGamesOne: PlayerGameTable)) if gameStatus == "gameOver" => playerGamesOne.status === "gameOver" || playerGamesTwo.status === "gameOver"
}
}
It is a complex question, if soemthing is not clear please let me know and I will try to clarify it
There are a couple of issues:
With multiple conditions, the underscore placeholder used within your ON clause would not work the way intended
_.level = something is an assignment, not a condition
Assuming PlayerTable.playerQuery is TableQuery[PlayerTable] and PlayerGameTable.playerGameQuery is TableQuery[PlayerGameTable], your baseQuery should look like this:
val baseQuery = for {
((p, g1), g2) <- PlayerTable.playerQuery joinLeft
PlayerGameTable.playerGameQuery on ((x, y) => x.id === y.playerId && y.level === 1) joinLeft
PlayerGameTable.playerGameQuery on ((x, y) => x._1.id === y.playerId && y.level === 2)
} yield (p, g1, g2)
It's not entirely clear to me how your filterPlayerGames method is going to handle dynamic conditions. Nor do I think any filtering wrapper method will be flexible enough to cover multiple conditions with arbitrary and/or/negation operators. I would suggest that you use the baseQuery for the necessary joins and build filtering queries on top of it, similar to something like below:
val query1 = baseQuery.filter{ case (_, g1, g2) =>
g1.filter(_.status === "gameOver").isDefined || g2.filter(_.status === "gameOver").isDefined
}
val query2 = baseQuery.filter{ case (_, g1, g2) =>
g1.filter(_.status === "playing").isDefined || g2.filter(_.status =!= "gameOver").isDefined
}
Note that with the left joins, g1 and g2 are of Option type, thus isDefined is applied for the or operation.
On a separate note, given that your filtering conditions are only on PlayerGameTable, it would probably be more efficient to perform filtering before the joins.

Slick: Weird case statement when creating a query with left join

In one of my queries in the application, I just realized that Slick adds this add case statement in select.
Here is how I build the query:
def search(searchCriteria: SearchCriteria,
drop: Long = 0,
take: Long = 100): Future[Seq[(School, Option[Address])]] = {
val query = Schools.schools joinLeft
Addresses.addresses on ((s, a) => s.addressId === a.id && a.deletedAt.isEmpty)
val q = for {
(school, address) <- query.filter{ case (s, a) =>
List(
Some(s.deletedAt.isEmpty),
searchCriteria.school.name.map(n => s.name.toLowerCase like s"%${n.toLowerCase}%"),
searchCriteria.school.ready.map(r => s.ready === r)
).collect({ case Some(criteria) => criteria }).reduceLeftOption(_ && _).getOrElse(true: Rep[Boolean])
}
} yield (school, address)
db.run(q.drop(drop).take(take).result)
}
It is a really simple join with some filtering. And in fact, everything works fine. But, when I inspect the executed query I see this:
SELECT
x2."id",
x2."address_id",
x2."name",
x2."about",
x2."number_of_students",
x2."website_url",
x2."media_id",
x2."slug",
x2."short_description",
x2."ready",
x2."classrooms",
x2."year_established",
x2."display_copyright",
x2."createdat",
x2."updatedat",
x2."deletedat",
x2."createdby",
x2."updatedby",
x2."dli_number",
(CASE WHEN (x3."id" IS NULL)
THEN NULL
ELSE 1 END),
x3."id",
x3."country_id",
x3."continent_id",
x3."state_id",
x3."address1",
x3."address2",
x3."city",
x3."zip",
x3."createdat",
x3."updatedat",
x3."deletedat",
x3."createdby",
x3."updatedby"
FROM "school" x2 LEFT OUTER JOIN "address" x3 ON (x2."address_id" = x3."id") AND (x3."deletedat" IS NULL)
WHERE ((x2."deletedat" IS NULL) AND (lower(x2."name") LIKE '%a%')) AND (x2."ready" = TRUE)
LIMIT 100
OFFSET 0
Why Slick adds this:
(CASE WHEN (x3."id" IS NULL) THEN NULL ELSE 1 END)
I can't see how this is useful in any way.
Thanks,

Update dynamic number of columns

I have an update function in my UserDAO class that takes a few optional values:
def update(id: Int, name: Option[String], password: Option[String], age: Option[Int])
I know how to update all of the values:
val query = for {
u <- users if u.id === id
} yield (u.name, u.password, u.age)
db.run(query.update(name.get, password.get, age.get))
But want to do it conditionally update the different columns, depending on if their Option value is defined. Something like this perhaps:
val query = for {
u <- users if u.id === id
} yield (u.name if name.isDefined, u.password if password.isDefined, u.age if age.isDefined) // Pseudo code
db.run(query(update(...)) // Unpack arguments here
For slick 3, you can try like this,
val query = for {
u <- db.run(users.filter(_.id === id).result)
u1 = if(u.nonEmpty && u.head.name.isDefined){
u.head.copy(name = u.head.name) //add more if needed
}
else
{
u
}
res <- db.run(users.update(u1))
} yield res
for slick 2 no need of for-yield
val u = db.run(users.filter(_.id === id).result)
val u1 = if(u.nonEmpty && u.head.name.isDefined){
u.head.copy(name = u.head.name) //add more if needed
}
else
{
u
}
val res = db.run(users.update(u1))
Hope my answer was helpful.

Slick 3.0.0 - How to sortBy on a query with joinLeft

This question is related to another. I'm also trying to sort on a query with a joinLeft but in slick 3.0.0. And as the Option Rep are automatically lifted how would I do the exact same thing ?:
def list(filter: String, orderBy: Int):Future[Seq[(Computer, Option[Company])]] = {
val initialQuery = for {
(computer, company) <- Computer.filter(_.name like filter) leftJoin
Company on (_.companyId === _.id)
} yield (computer, company)
val sortedQuery = orderBy match {
case 2 => initialQuery.sortBy(_._1.name) //Works ok, column from a primary table
case 3 => initialQuery.sortBy(_._2.map(_.name)) //could not find implicit value for parameter ol: slick.lifted.OptionLift[slick.lifted.ColumnOrdered[String],slick.lifted.Rep[Option[QO]]]
}
db.run(sortedQuery.result)
}
Thanks,
I suppose that missing parenthesis is just a typo. I had this problem recently when I was specifying the sort direction in the wrong place, using your example:
case 3 => initialQuery.sortBy(_._2.map(_.name.asc))
It should be:
case 3 => initialQuery.sortBy(_._2.map(_.name).asc)
I had this problem too. I had a joinLeft and I want to order on a boolean column. You must be decide when Its join is empty what value you want replace it.
for example:
initialQuery.sortBy(_._2.map(_.name).getOrElse(""))
Are you sure you copied the correct code?
case 3 => data.sortBy(_._2.map(_.name) //could not find implicit value for parameter
Missing a ")"
Should be
case 3 => data.sortBy(_._2.map(_.name))
You could put the fields into the result set.
For example:
val initialQuery = for {
(computer, company) <- Computer.filter(_.name like filter) leftJoin Company on (_.companyId === _.id)
} yield (computer, company, company.map(_.name))
val sortedQuery = orderBy match {
case 2 => initialQuery.sortBy(_._1.name)
case 3 => initialQuery.sortBy(_._3)
}
db.run(sortedQuery.map(v => (v._1, v._2).result)