How To Queries 🔗
How to Read One-Column Values? 🔗
We use the readValues[T]() to read single-column results:
import ba.sake.squery.{*, given}
def customersIds: List[Int] = ctx.run {
sql"SELECT id FROM customers".readValues[Int]()
}
There are also variations that return a single result, depending if you want an Option[T] or T:
sql"SELECT ...".readValueOpt[T]() : Option[T] // first result, if present
sql"SELECT ...".readValue[T]() : T // first result, or exception
How to Read Multi-Column Values? 🔗
When reading multi-column results, we care about the column names.
A natural fit for that are case classes.
We need to add a derives SqlReadRow, and then we can use it:
import ba.sake.squery.{*, given}
case class Customer(id: Int, name: String) derives SqlReadRow
def customers: List[Customer] = ctx.run {
sql"SELECT id, name FROM customers".readRows[Customer]()
}
Note that the case class' fields need to match the SELECT statement columns!
You can also read a multi-column row as a named tuple without declaring a case class:
def customers: Seq[(id: Int, name: String)] = ctx.run {
sql"SELECT id, name FROM customers"
.readRows[(id: Int, name: String)]()
}
Named-tuple field names follow the same rule: they must match the selected column names.
How to Configure Statement Execution? 🔗
Statement options are immutable and can be chained on any query:
import scala.concurrent.duration.*
sql"SELECT id, name FROM customers"
.withFetchSize(512)
.withTimeout(5.seconds)
.withMaxRows(1000)
.readRows[Customer]()
Scrollable or updatable result sets can be requested when the JDBC driver supports them:
sql"SELECT id, name FROM customers"
.withResultSet(ResultSetType.ScrollInsensitive, ResultSetConcurrency.ReadOnly)
.readRows[Customer]()
JDBC does not provide a statement-preparation overload that combines result-set settings with generated-key selection. When both fluent options are used, the last one replaces the earlier preparation option.
There are also variations that return a single result, depending if you want an Option[T] or T:
sql"SELECT ...".readRowOpt[T]() : Option[T] // first result, if present
sql"SELECT ...".readRow[T]() : T // first result, or exception
How to Read a Full Join? 🔗
Squery is using case class composition to read JOINed tables.
Let's say we have tables whose rows are represented by these 2 case classes:
case class Customer(id: Int, name: String) derives SqlReadRow
case class Phone(id: Int, number: String) derives SqlReadRow
Doing a FULL JOIN on them would look like this:
SELECT c.id, c.name,
p.id, p.number
FROM customers c
JOIN phones p ON p.customer_id = c.id
The result can be expressed as a composition of the 2 case classes above:
case class CustomerWithPhone(c: Customer, p: Phone) derives SqlReadRow
Variables like c: Customer are expected to have corresponding column names in the query: c.id and c.name.
The final query is a composition of Customer and Phone, so it maps nicely in your head, it is easier to read and manipulate.
You could have additional columns like a COUNT/SUM or whatever you need in CustomerWithPhone query result.
Full example:
import ba.sake.squery.{*, given}
case class Customer(id: Int, name: String) derives SqlReadRow
case class Phone(id: Int, number: String) derives SqlReadRow
case class CustomerWithPhone(c: Customer, p: Phone) derives SqlReadRow
def customerwithPhones: List[CustomerWithPhone] = ctx.run {
sql"""
SELECT c.id, c.name,
p.id, p.number
FROM customers c
JOIN phones p ON p.customer_id = c.id
""".readRows[CustomerWithPhone]()
}
How to Read an Outer Join? 🔗
The principle is the same as for FULL JOIN.
The only thing you need change is to make the JOIN-ed table an Option[T]:
case class CustomerWithPhoneOpt(c: Customer, p: Option[Phone]) derives SqlReadRow
def customerwithPhoneOpts: List[CustomerWithPhoneOpt] = ctx.run {
sql"SELECT ...".readRows[CustomerWithPhoneOpt]()
}
The p: Option[Phone] will be None when all its returned columns are NULL.
How to Map Flat Rows to Objects 🔗
Suppose you have these data class-es:
case class Customer(id: Int, name: String) derives SqlReadRow
case class Address(id: Int, name: String) derives SqlReadRow
case class CustomerWithAddressOpt(c: Customer, a: Option[Address]) derives SqlReadRow
Now, when you do a SELECT you'll get a Seq[CustomerWithPhoneOpt], a "flat" result, named tuple.
But, on your REST API you'd like to return a structured object:
case class CustomerDTO(id: Int, name: String, addresses: Seq[String])
Squery has utilities for exactly that.
Let's see how groupByOrderedOpt works:
import ba.sake.squery.utils.*
val groupedByCustomer: Map[Customer, Seq[Address]] = rowsLeftJoin.groupByOrderedOpt(_.c, _.a)
val dtos = groupedByCustomer.map { case (c, addresses) =>
CustomerDTO(c.id, c.name, addresses.map(_.name))
}.toSeq
We can see that it returns a Map[Customer, Seq[Address]], just as we wanted.
Then we just map over it and populate the DTO object, can't be simpler!
This does a few thing for us:
- keeps the list of results ordered, so you don't have to sort it twice (once in DB, and again in memory)
- extracts the value that we need from the raw row result
- handles the
Nonecase - handles the starting, empty Seq of results case
How To Do Dynamic Queries? 🔗
Of course, in the real world, you will need to compose queries dynamically at runtime.
Query.join 🔗
Combine any number of fragments with a separator:
def customers(): Seq[Customer] = ctx.run {
val filters = List(sql"id = 123", sql"name LIKE 'Bob%'")
val where = Query.when(filters.nonEmpty) {
sql"WHERE ${Query.join(filters, sql" AND ")}"
}
val query = sql"SELECT id, name FROM customers ${where}"
query.readRows[Customer]()
}
Query.join returns an empty fragment for an empty collection and preserves the separator exactly as supplied. Values interpolated into any fragment remain prepared statement parameters.
Query.when 🔗
Include a fragment only when a condition is true:
val orderBy = Query.when(sortByName)(sql"ORDER BY name")
sql"SELECT id, name FROM customers ${orderBy}"
Query.in 🔗
Create a dynamic IN list:
val ids = Seq(1, 2, 3)
sql"SELECT id, name FROM customers WHERE id IN ${Query.in(ids)}"
An empty collection produces (NULL), so an IN condition matches no rows. Do not use this empty-list behavior with NOT IN, because SQL handles NULL differently there.
Query.values 🔗
Join already-parenthesized rows for a multi-row insert:
val rows = customers.map(customer => sql"(${customer.name}, ${customer.street})")
sql"INSERT INTO customers(name, street) VALUES ${Query.values(rows)}"
Query.values requires at least one row and reports an IllegalArgumentException before executing SQL when the collection is empty.
Append with ++ 🔗
Directly append two fragments:
sql"name = 'Alice'" ++ sql"AND active = true"
concatenate 🔗
The ba.sake.squery.utils package also provides concatenate for combining optional fragments:
Seq(Option(sql"q1"), None, Option(sql"q2"))
.concatenate(sep = sql"AND", default = sql"true")
// same as this:
sql"q1 AND q2"