Type-safe SQL queries
Kotysa provides you a DSL to write type-safe SQL queries in pure Kotlin.
Table of content
Select
Select one or several columns or tables
- Single select returns either column's type or table's mapped Entity type
- 2 selects return a
Pair<T, U> - 3 selects return a
Triple<T, U, V> - 4 selects and more return a
List<Any?>
// select a Table, returns this table's mapped Entity (= will select all columns from this table)
fun selectUserById(id: Int) =
(sqlClient select Users
from Users
where Users.id eq id
).fetchOne()
// select a single column
fun selectFirstnameById(id: Int) =
(sqlClient select Users.firstname
from Users
where Users.id eq id
).fetchOne()
// returns Pair<String, String?>
fun selectFirstnameAndAliasById(id: Int) =
(sqlClient select Users.firstname and Users.alias
from Users
where Users.id eq id
).fetchOne()
// returns Triple<String, UUID, String?>
fun selectFirstnameAndRoleIdAndAliasById(id: Int) =
(sqlClient select Users.firstname and Users.roleId and Users.alias
from Users
where Users.id eq id
).fetchOne()
// returns List<Any?>
fun selectFirstnameAndLastnameAndAliasAndIsAdminById(id: Int) =
(sqlClient select Users.firstname and Users.lastname and Users.alias and Users.isAdmin
from Users
where Users.id eq id
).fetchOne()
// fetchAll = multiple results
// returns List<Pair<String, String?>>
fun selectAllFirstnameAndAlias() =
(sqlClient select Users.firstname and Users.alias
from Users
).fetchAll()Count
Counts the number of rows having a non-null value for the specified column
fun countWithAlias() =
(sqlClient selectCount Users.alias
from Users
).fetchOne()Distinct
Returns distinct values of the specified column
val distinctFirstnames =
(sqlClient selectDistinct Users.firstname
from Users
).fetchAll()Min, max and sum
fun selectUserMinId() =
(sqlClient selectMin Users.id
from Users
).fetchOne()
fun selectUserMaxId() =
(sqlClient selectMax Users.id
from Users
).fetchOne()
fun selectUserSumId() =
(sqlClient selectSum Users.id
from Users
).fetchOne()Select from
Shortcut to return rows from a table as table's mapped Entity (= will select all columns from this table)
fun selectFirstUserByFirstname(firstname: String) =
(sqlClient selectFrom Users
where Users.firstname eq firstname
// null String forbidden ^^^^^^^^
).fetchFirst()
fun selectAllUsersByAliases(alias1: String?) =
(sqlClient selectFrom Users
where Users.alias eq alias1
// null String accepted ^^
// if alias1==null, Kotysa will generate "WHERE user.alias IS NULL" SQL
).fetchAll()Select all from
Shortcut to return all rows from a table as table's mapped Entity (= will select all columns from this table)
fun selectAll() = sqlClient selectAllFrom Users- SqlClient returns a
List<User> - ReactorSqlCLient returns a
reactor.core.publisher.Flux<User> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<List<User>> - CoroutinesSqlCLient returns a
kotlinx.coroutines.flow.Flow<User>
Count all from
Shortcut to return the total number of rows of a table
fun countAll() = sqlClient selectCountAllFrom Users- SqlClient returns a
Long - ReactorSqlCLient returns a
reactor.core.publisher.Mono<Long> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<Long> - CoroutinesSqlCLient is a suspend function that returns a
Long
Map selected columns to a DTO
selectAndBuild gives access to a code block that will be executed for each returned row
Tip
Allow you to build a DTO from columns of several tables
data class UserDto(
val name: String,
val alias: String?
)
fun selectAllUsersMappedToDto() =
(sqlClient selectAndBuild { UserDto(it[Users.firstname]!!, it[Users.alias]) }
from Users
).fetchAll()- SqlClient returns a
List<UserDto> - ReactorSqlCLient returns a
reactor.core.publisher.Flux<UserDto> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<List<UserDto>> - CoroutinesSqlCLient returns a
kotlinx.coroutines.flow.Flow<UserDto>
Or
SQL OR clause
fun selectAllUsersByAliases(alias1: String?, alias2: String?) =
(sqlClient selectFrom Users
where Users.alias eq alias1
or Users.alias eq alias2
).fetchAll()Join
Join database tables with JOIN clause(s) :
- innerJoin
- leftJoin
- rightJoin
- fullJoin
JOIN clauses can be chained.
val admins =
(sqlClient selectFrom Users
innerJoin Roles on Users.roleId eq Roles.id
where Roles.label eq "admin"
).fetchAll() // returns all admin usersOf course, you can also join tables using equality clause between columns too, this query returns the same results
val admins =
(sqlClient select Users
from Users and Roles
where Users.roleId eq Roles.id
and Roles.label eq "admin"
).fetchAll() // returns all admin usersLimit and offset
For pagination, use LIMIT and OFFSET
val pagination =
(sqlClient selectFrom Users
limit 5 offset 1
).fetchAll()Group by
val countUsersGroupByCountry =
(sqlClient selectCount Users.id and Users.country
from Users
groupBy Users.country
).fetchAll()Order by
fun selectUserByIdAsc() =
(sqlClient selectFrom Users
orderByAsc Users.id
).fetchAll()Subqueries
Kotysa provides subquery support.
// subquery in select
fun selectUserById(id: Int) =
(sqlClient select Users.firstname
and {
(this select Roles.label
from Roles
where Roles.id eq Users.roleId)
}
from Users
where Users.id eq id
).fetchOne()
// Kotysa supports case when exists subquery in select
fun selectCaseWhenExistsSubQuery(userIds: List<Int>) =
(sqlClient selectDistinct Roles.label
andCaseWhenExists {
(this select Users.id
from Users
where Users.roleId eq Roles.id
and Users.id `in` userIds)
} then true `else` false
from Roles)
.fetchAll()
// subquery in where
fun selectRoleLabelWhereEqUserSubQuery(userId: Int) =
(sqlClient select Roles.label
from Roles
where Roles.id eq
{
(this select Users.roleId
from Users
where Users.id eq userId)
})
.fetchOne()
// where exists subquery
fun selectRoleLabelWhereExistsUserSubQuery(userIds: List<Int>) =
(sqlClient select Roles.label
from Roles
whereExists {
(this select Users.id
from Users
where Users.roleId eq Roles.id
and Users.id `in` userIds)
})
.fetchAll()
// Kotysa supports case when exists subquery in order by
fun selectOrderByCaseWhenExistsSubQuery(userIds: List<Int>) =
(sqlClient select Roles.label
from Roles
orderByDescCaseWhenExists {
(this select Users.id
from Users
where Users.roleId eq Roles.id
and Users.id `in` userIds)
} then true `else` false
andAsc Roles.label)
.fetchAll()Aliases
Kotysa provides aliases support for columns and tables.
// column alias. 2 syntaxes are available
fun selectAliasedFirstnameByFirstnameGet(firstname: String) =
(sqlClient select H2Users.firstname `as` "fna"
from H2Users
where H2Users.firstname["fna"] eq firstname
).fetchOne()
fun selectAliasedFirstnameByFirstnameAlias(firstname: String) =
(sqlClient select H2Users.firstname `as` "fna"
from H2Users
where QueryAlias<String>("fna") eq firstname
).fetchOne()
// table alias
fun selectFirstnameByFirstnameTableAlias(firstname: String) =
(sqlClient select H2Users["u"].firstname
from H2Users `as` "u"
where H2Users["u"].firstname eq firstname
).fetchOne()Conditional queries
Kotysa offers a specific query syntax for complex queries with conditional clauses.
Note : A query that uses query.selects() always returns a List<Any?>
Here is an example.
fun selectConditionalSyntax(params: Int = 0): List<List<Any?>> {
val selects = sqlClient.selects()
selects.select(tableUsers.firstname)
if (params > 0) {
selects.select(tableUsers.lastname)
}
if (params > 1) {
selects.select(tableRoles.label)
}
val froms = selects.froms()
froms.from(tableUsers)
if (params > 0) {
froms.from(tableUserRoles)
}
if (params > 1) {
froms.from(tableRoles)
}
val wheres = froms.wheres()
wheres.where(tableUsers.id).sup(0)
if (params > 0) {
wheres.where(tableUserRoles.userId).eq(tableUsers.id)
}
if (params > 1) {
wheres.where(tableRoles.id).eq(tableUserRoles.roleId)
}
val groupsBy = wheres.groupsBy()
groupsBy.groupBy(tableUsers.firstname)
if (params > 0) {
groupsBy.groupBy(tableUsers.lastname)
}
if (params > 1) {
groupsBy.groupBy(tableRoles.label)
}
val ordersBy = groupsBy.ordersBy()
if (params > 0) {
ordersBy.orderByDesc(tableUsers.lastname)
}
if (params > 1) {
ordersBy.orderByAsc(tableRoles.label)
}
ordersBy.orderByAsc(tableUsers.firstname)
return ordersBy.fetchAll()
}Fetch the database
Use the terminal operation that you need to fetch single or multiple results
With kotysa-jdbc, kotysa-spring-jdbc and kotysa-sqlite
fun fetchOne(): T?returns one result- @throws NoResultException if no results
- @throws NonUniqueResultException if more than one result
fun fetchOneOrNull(): T?returns one result, or null if no results- @throws NonUniqueResultException if more than one result
fun fetchFirst(): T?returns the first result- @throws NoResultException if no results
fun fetchFirstOrNull(): T?returns the first result, or null if no resultsfun fetchAll(): List<T>returns several results asList, can be empty if no resultsfun fetchAllStream(): Stream<T>returns several results asjava.util.stream.Stream, can be empty if no results
With kotysa-r2dbc or kotysa-spring-r2dbc using Coroutines syntax
suspend fun fetchOne(): T?returns one result- @throws NoResultException if no results
- @throws NonUniqueResultException if more than one result
suspend fun fetchOneOrNull(): T?returns one result, or null if no results- @throws NonUniqueResultException if more than one result
suspend fun fetchFirst(): T?returns the first result- @throws NoResultException if no results
suspend fun fetchFirstOrNull(): T?returns the first result, or null if no resultsfun fetchAll(): Flow<T>returns several results askotlinx.coroutines.flow.Flow, can be empty if no results
With kotysa-spring-r2dbc using Reactor syntax
fun fetchOne(): Mono<T>returns one result asreactor.core.publisher.Mono, or an empty Mono if no result- @throws NonUniqueResultException if more than one result
fun fetchFirst(): Mono<T>returns the first result asreactor.core.publisher.Mono, or an empty Mono if no resultfun fetchAll(): Flux<T>returns several results asreactor.core.publisher.Flux, or an empty Flux if no result
With kotysa-vertx-sqlclient using Mutiny syntax
fun fetchOne(): Uni<T>returns one result asio.smallrye.mutiny.Uni, or an empty Uni if no result- @throws NonUniqueResultException if more than one result
fun fetchFirst(): Uni<T>returns the first result asio.smallrye.mutiny.Uni, or an empty Uni if no resultfun fetchAll(): Uni<List<T>>returns several results asio.smallrye.mutiny.Unithat contains a List, or an empty Uni if no result
Create table
Use createTable or createTableIfNotExists
fun createTable() = sqlClient createTable Users
// or
fun createTable() = sqlClient createTableIfNotExists Users- SqlClient returns void
- CoroutinesSqlCLient is a suspend function that returns void
- ReactorSqlCLient returns a
reactor.core.publisher.Mono<Void> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<Void>
Insert
Insert one or several mapped objects in a database table
private val roleUser = Role("user")
private val roleAdmin = Role("admin")
private val userJdoe = User("John", roleUser.id, "USA")
private val userBboss = User("Big boss", roleAdmin.id, "France", "TheBoss")
fun insertRoles() = sqlClient.insert(roleUser, roleAdmin)
fun insertUsers() = sqlClient.insert(userJdoe, userBboss)- SqlClient returns void
- CoroutinesSqlCLient is a suspend function that returns void
- ReactorSqlCLient returns a
reactor.core.publisher.Mono<Void> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<Void>
InsertAndReturn
Insert one or several mapped objects in a database table, and return the inserted objects, useful for auto-incremented columns and/or columns with default values
private val userCharles = User("Charles", roleUser.id, "United Kingdom")
fun insertUserAndReturn() = sqlClient insertAndReturn userCharlesTIP
T corresponds to the inserted entity type
- SqlClient returns
Tor aList<T>if several entities passed - CoroutinesSqlCLient is a suspend function that returns
Tor akotlinx.coroutines.flow.Flow<T>if several entities passed - ReactorSqlCLient returns a
reactor.core.publisher.Mono<T>or areactor.core.publisher.Flux<T>if several entities passed - VertxSqlClient returns a
io.smallrye.mutiny.Uni<T>or aio.smallrye.mutiny.Multi<T>if several entities passed
Delete
Normal delete
Delete rows from a table, return the number of deleted rows
fun deleteById(id: Int) =
(sqlClient deleteFrom Users
where Users.id eq id
).execute()- SqlClient returns Int
- CoroutinesSqlCLient is a suspend function that returns Int
- ReactorSqlCLient returns a
reactor.core.publisher.Mono<Int> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<Int>
Delete all
Shortcut to delete all rows from a table, return the number of deleted rows
fun deleteAll() = sqlClient deleteAllFrom Users- SqlClient returns Int
- CoroutinesSqlCLient is a suspend function that returns Int
- ReactorSqlCLient returns a
reactor.core.publisher.Mono<Int> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<Int>
Update
Update rows from a table, return the number of updated rows
fun updateUserFirstname(id: Int, newFirstname: String) =
(sqlClient update Users
set Users.firstname eq newFirstname
where Users.id eq id
).execute()You can also set a column with a column's value, and for number you can add or subtract from this column's value
fun incrementUserMessageCount(id: Int) =
(sqlClient update Users
set Users.messageCount eq Users.messageCount plus 1
where Users.id eq id
).execute()- SqlClient returns Int
- CoroutinesSqlCLient is a suspend function that returns Int
- ReactorSqlCLient returns a
reactor.core.publisher.Mono<Int> - VertxSqlClient returns a
io.smallrye.mutiny.Uni<Int>
Transaction
Kotysa provides a functional Transaction support, all queries inside the transaction block will be transactional.
operator.transactional { transaction ->
// for example : transaction will rollback when exiting this code block
transaction.setRollbackOnly()
// execute your queries inside this transaction
}
UFOSS