Introducció
En les activitats anteriors has utilitzat alguns operadors de Ktorm.
A continuació els anem a veure en detall.
Built-in Operators
Any operator in Ktorm is a Kotlin function that returns a SqlExpression
.
Here is a list of built-in operators that Ktorm supports now:
TODO canviar ordre columnes (primer sql)
Kotlin Function Name | SQL Keyword/Operator | Usage |
---|---|---|
isNull | is null | Ktorm: Employees.name.isNull() SQL: employee.name is null |
isNotNull | is not null | Ktorm: Employees.name.isNotNull() |
unaryMinus (-) | - | Ktorm: -Employees.salary SQL: -t_employee.salary |
unaryPlus (+) | + | Ktorm: +Employees.salary SQL: +t_employee.salary |
not (!) | not | Ktorm: !Employees.name.isNull() SQL: not (t_employee.name is null) |
plus (+) | + | Ktorm: Employees.salary + Employees.salary SQL: t_employee.salary + t_employee.salary |
minus (-) | - | Ktorm: Employees.salary - Employees.salary SQL: t_employee.salary - t_employee.salary |
times (*) | * | Ktorm: Employees.salary * 2 SQL: t_employee.salary * 2 |
div (/) | / | Ktorm: Employees.salary / 2 SQL: t_employee.salary / 2 |
rem (%) | % | Ktorm: Employees.id % 2 SQL: t_employee.id % 2 |
like | like | Ktorm: Employees.name like “vince” SQL: t_employee.name like ‘vince’ |
notLike | not like | Ktorm: Employees.name notLike “vince” SQL: t_employee.name not like ‘vince’ |
and | and | Ktorm: Employees.name.isNotNull() and (Employees.name like “vince”) SQL: t_employee.name is not null and t_employee.name like ‘vince’ |
or | or | Ktorm: Employees.name.isNull() or (Employees.name notLike “vince”) SQL: t_employee.name is null or t_employee.name not like ‘vince’ |
xor | xor | Ktorm: Employees.name.isNotNull() xor (Employees.name notLike “vince”) SQL: t_employee.name is not null xor t_employee.name not like ‘vince’ |
lt / less | < | Ktorm: Employees.salary lt 1000 SQL: t_employee.salary < 1000 |
lte / lessEq | <= | Ktorm: Employees.salary lte 1000 SQL: t_employee.salary <= 1000 |
gt / greater | > | Ktorm: Employees.salary gt 1000 SQL: t_employee.salary > 1000 |
gte / greaterEq | >= | Ktorm: Employees.salary gte 1000 SQL: t_employee.salary >= 1000 |
eq | = | Ktorm: Employees.id eq 1 SQL: t_employee.id = 1 |
neq / notEq | <> | Ktorm: Employees.id neq 1 SQL: t_employee.id <> 1 |
notBetween | not between | Ktorm: Employees.id notBetween 1..3 SQL: t_employee.id not between 1 and 3 |
inList | in | Ktorm: Employees.departmentId.inList(1, 2, 3) SQL: t_employee.department_id in (1, 2, 3) |
notInList | not in | Ktorm: Employees.departmentId notInList db.from(Departments).selectDistinct(Departments.id) SQL: t_employee.department_id not in (select distinct t_department.id from t_department) |
exists | exists | Ktorm: exists(db.from(Employees).select()) SQL: exists (select * from t_employee) |
notExists | not exists | Ktorm: notExists(db.from(Employees).select()) SQL: not exists (select * from t_employee) |
These operators can be divided into two groups by the implementation way: Overloaded Kotlin built-in operators and normal operator functions.
Overloaded Kotlin built-in operators:
This group of operators are generally used to implement basic arithmetic operators (such as plus, minus, times, etc).
Because of operator overloading, they are used just like real arithmetic performs, for example, Employees.salary + 1000
.
But actually, they just create SQL expressions instead, those expressions will be translated into the corresponding operators in SQL by SqlFormatter
.
Here is the implementation of the plus operator, we can see that it just creates a BinaryExpression<T>
:
// TODO imports
infix operator fun <T : Number> ColumnDeclaring<T>.plus(expr: ColumnDeclaring<T>): BinaryExpression<T> {
return BinaryExpression(BinaryExpressionType.PLUS, asExpression(), expr.asExpression(), sqlType)
}
Normal operator functions
There are many limits overloading Kotlin’s built-in operators.
For example, the equals
function is restricted to return Boolean values only, but Ktorm’s operator functions need to return SQL expressions, so Ktorm provides another function eq
for us to implement equality comparisons.
Additionally, there are also many operators that don’t exist in Kotlin, such as like
, Ktorm provides a like
function for string matching in SQL.
Here is the implementation of the like
function, and this kind of functions are generally marked with an infix keyword:
infix fun ColumnDeclaring<*>.like(argument: String): BinaryExpression<Boolean> {
return BinaryExpression(
type = BinaryExpressionType.LIKE,
left = asExpression(),
right = ArgumentExpression(argument, VarcharSqlType),
sqlType = BooleanSqlType
)
}
Operator Precedence
Operators can be used continuously, but if we use different operators together, we will meet the problem of their precedence. There can be many operators in an expression, different combination order of operators can lead to different results and even errors. Only if the operators are combined in a certain order, the expression’s result can be correct and unique.
For instance, in the expression 1 + 2 * 3, the multiplication’s precedence is higher than plus, so 2 * 3 is combined first, the result is 7; If we ignore the precedence of operators, then 1 + 2 is combined first, the result will be 9, which is absolutely wrong. Normally, the precedence of multiplicative operators is higher than additive operators’, the precedence of conjunctions are higher than disjunctions’. But there are a little difference in Ktorm.
For overloaded Kotlin built-in operators, their precedence follows the specification of Kotlin language. Such as the expression Employees.salary + 1000 * 2
, the multiplication’s precedence is higher, so the final translated SQL is employee.salary + 2000
.
However, for normal operator functions, there is no such thing as precedence. In the level of Kotlin language, they are all normal function callings, so they just need to be combined sequentially, and that is quite counterintuitive for us.
For example, in the expression a or b and c
, the or
and and
are both operator functions. Intuitively, the precedence of and
is higher, so it should be combined first, but actually, they are both normal functions, so our intuition is wrong. If we don’t have a clear understanding on this, some unexpected bugs may occur, to solve the problem, we can use brackets if needed, eg. a or (b and c)
.
Custom Operators
We’ve talked about the built-in operators provided by Ktorm’s core module, they provided supports for operators in standard SQL, but what if we want to use some special operators provided by a special database?
Let’s take PostgreSQL’s ilike
operator as an example, learning how to extend our custom operators with Ktorm.
ilike
is a special operator in PostgreSQL. Similar to like
, it also matches strings, but ignoring cases.
El primer que has de fer es crear una classe IlikeExpression
a partir de ScalarExpression<Boolean>
:
data class ILikeExpression(
val left: ScalarExpression<*>,
val right: ScalarExpression<*>,
override val sqlType: SqlType<Boolean> = BooleanSqlType,
override val isLeafNode: Boolean = false
) : ScalarExpression<Boolean>()
Having the expression type, we also need an extension function to create expression instances conveniently, that’s the operator function.
We mark this function with an infix keyword, so it can be used just like a real operator in SQL:
infix fun ColumnDeclaring<*>.ilike(argument: String): ILikeExpression {
return ILikeExpression(asExpression(), ArgumentExpression(argument, VarcharSqlType)
}
Now we can use this operator function, just like other operators. But Ktorm cannot recognize our custom expression type ILikeExpression
by default and are not able to generate SQLs correctly.
So we can extend the SqlFormatter
class, override the visitUnknown
function, detect our custom expression types and generate proper SQLs:
class CustomSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int)
: PostgreSqlFormatter(database, beautifySql, indentSize) {
override fun visitUnknown(expr: SqlExpression): SqlExpression {
if (expr is ILikeExpression) {
if (expr.left.removeBrackets) {
visit(expr.left)
} else {
write("(")
visit(expr.left)
removeLastBlank()
write(") ")
}
write("ilike ")
if (expr.right.removeBrackets) {
visit(expr.right)
} else {
write("(")
visit(expr.right)
removeLastBlank()
write(") ")
}
return expr
} else {
super.visitUnknown(expr)
}
}
}
Finally, register this custom SQL formatter into the Database
object by dialect support.
val database = Database.connect(
url = "jdbc:postgresql://localhost:5432/ktorm",
dialect = object : SqlDialect {
override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int): SqlFormatter {
return CustomSqlFormatter(database, beautifySql, indentSize)
}
}
)
All done! The usage of ilike:
val query = database.from(Employees).select().where
In this way, Ktorm supports ilike operator now. Actually, this is one of the features of ktorm-support-postgresql module, if you really need to use ilike, you don’t have to repeat the code above, please add the dependency to your project.
Maven dependency:
Or Gradle:
compile "org.ktorm:ktorm-support-postgresql:${ktorm.version}"