agusgambina
agusgambina

Reputation: 6699

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

Upvotes: 1

Views: 1581

Answers (1)

Leo C
Leo C

Reputation: 22449

There are a couple of issues:

  1. With multiple conditions, the underscore placeholder used within your ON clause would not work the way intended
  2. _.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.

Upvotes: 3

Related Questions