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
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
}
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.
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,
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.
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)