This document summarizes 7 things you may not know about Exposed, a SQL query builder and ORM for Kotlin. It covers topics like logging queries, date support, many-to-many relationships, coroutine support, representing NULL values, mapping between SQL DSL and DAO entities, and executing arbitrary SQL statements. It also provides information on where to find help or contribute to the Exposed project.
5. UsersTable
.select { UsersTable.firstName eq firstName }
.toList()
Selecting using criteria: SQL DSL
val count: Int = UsersTable
.deleteWhere { UsersTable.lastName eq lastName }
Deleting: SQL DSL
6. val users = User.find {
UsersTable.firstName eq firstName
}.toList()
Selecting using criteria: DAO API
users.forEach {
it.delete()
}
Deleting: DAO API
9. Logging
val sql = UsersTable
.select { UsersTable.firstName eq firstName }
.prepareSQL(this)
println(sql)
SELECT users.id, users.first_name, users.last_name
FROM users WHERE users.first_name = ?
10. Date support
object UsersTable : IntIdTable() {
val firstName = varchar("first_name", 20)
val lastName = varchar("last_name", 20)
val createdAt = datetime("created_at")
}
11. Date support
// JDK 7, legacy
"org.jetbrains.exposed:exposed-jodatime"
// JDK 8+
"org.jetbrains.exposed:exposed-java-time"
// Kotlin
"org.jetbrains.exposed:exposed-kotlin-datetime"
13. Many–to-many relationship
object Members : UUIDTable() {
val user = reference("user_id", Users, onDelete =
ReferenceOption.CASCADE)
val group = reference("group_id", Groups, onDelete =
ReferenceOption.CASCADE)
val createdAt =
datetime("created_at").defaultExpression(CurrentDateTime)
}
14. Many–to-many relationship
class Group(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Group>(Groups)
var name by Groups.name
var description by Groups.description
var owner by User referencedOn Groups.owner
val members by User.via(Members.group, Members.user)
}
15. Many–to-many relationship
class User(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<User>(Users)
var username by Users.username
val groups by Group.via(Members.user, Members.group)
}
20. Null expression
SELECT COUNT(CASE
WHEN messages.seen = 'NO' THEN NULL
ELSE 1 END)
FROM messages
Messages.slice(
Count(
case()
.When(seen eq "NO", "NULL"/null)
.Else(intLiteral(1))
)
).selectAll().toList()
21. Null expression
SELECT COUNT(CASE
WHEN messages.seen = 'NO' THEN NULL
ELSE 1 END)
FROM messages
Messages.slice(
Count(
case()
.When(seen eq "NO", Op.nullOp<Int>())
.Else(intLiteral(1))
)
).selectAll().toList()
22. Null expression
SELECT COUNT(CASE
WHEN messages.seen = 'YES' THEN 1
ELSE NULL END)
FROM messages
Messages.slice(
Count(
case()
.When(seen eq "YES", intLiteral(1))
.Else(Op.nullOp())
)
).selectAll().toList()
23. From SQL DSL to DAO Entities
val query: Query = Groups
.innerJoin(Users)
.innerJoin(Members)
.slice(Groups.columns)
.selectAll()
.withDistinct()
🤔
24. From SQL DSL to DAO Entities
val groups: SizedIterable<Group> =
Group.wrapRows(query)
val query: Query = Groups
.innerJoin(Users)
.innerJoin(Members)
.slice(Groups.columns)
.selectAll()
.withDistinct()
26. Summary
1. Log a single query
2. Choose what date library to work with
3. Flexible support for many-to-many relationship
4. Coroutine support
5. Expression that represents SQL NULL
6. Easy mapping from SQL DSL to DAO Entities
7. Arbitrary statements
35s
Hi,
My name is Alexey Soshin.
I’m a contributor to the Exposed framework, and I’m a big fan of it.
Today I wanted to talk about a few less known features of that framework.
Most of the examples here come from questions asked on GitHub issues of the Exposed project, or on StackOverflow.
About the title of my talk, for those that are unfamiliar with Seven Plus Minus Two.
It’s the number of things we can hold in our short term memory.
In this case, it’s just an escape hatch for me.
So I don’t need to commit exactly how many topic I’m going to cover.
https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two
40s
Now, let’s start with a quick raise of hands please.
How many here are using Hibernate or some other form of JPA in production?
Like SpringData or something like that?
…
And how many of you use Exposed in production?
…
Ok, great, thank you. That’s just for me to understand how much time I need to spend on discussing what Exposed framework is.
And also to check who is either came to the wrong talk, or just didn’t have coffee in the morning.
40s
To set the scene, this is how I usually worked with databases before discovering the Exposed framework.
This is from official SpringData GitHub example.
To be clear, my intention is not to mock JPA or Hibernate in any way.
This is just so I could compare apples to apples, so to say.
The top example uses an annotation on an interface and a special language that describes what data should we fetch at runtime.
40s
And the bottom example uses some conventions, like starting the method name with an action, like “remove”.
And specifying the columns used in the name of the method.
In both cases, you have zero support from your language.
If you have an active database connection, and your IDE is very smart, then you may get some support.
But not much.
50s
Now, when JetBrains were working on Kotlin, one of their goals was to create a language that would be great for creating DSLs.
Funny fact, you can look at the code of the Exposed library, and you will see a lot of commits from their CTO.
That was how he tested the limits of the new language his company was creating.
What do you get as a result when you use the Exposed library?
It provides you with a SQL DSL.
So there’s no need to write your SQLs in Strings.
No need to remember what the function naming conventions are.
You get typesafety provided by the compiler.
And autocompletion as a bonus.
If you prefer to work with objects instead of building queries, Exposed framework also provides a DAO API.
Here you can see that we invoke methods on the entity, and instead of select they are called find.
Instead of writing a delete query, with DAO API you would just invoke a delete method on each object.
Whether to use SQL DSL or the DAO API has a lot to do with preferences of your team.
DAO API is simpler, and SQL DSL is more poferwul.
???
But this talk is not the “Convince my company to use Exposed” talk.
This is a talk about things you may not known about.
So, let’s start with the first point: logging queries.
The usual way you log queries in Exposed is by adding a logger to the transaction.
That’s how you open a transaction in Exposed, by using a transaction block.
And then inside it you can define a logger.
15s
Then each query that you run gets printed to the console like this.
???
There’s another way to inspect just a single query that you may now know about, though.
You can invoke `prepareSQL` method on the query, and it will return you your query as a string.
This is useful if you want to run this query yourself.
Or do EXPLAIN PLAN on it, for example.
There’s a subtle difference between this and using a logger.
This method won’t execute the query at all.
It will return the SQL, but won’t run it.
While the logger prints the query as you try to run it.
20s
If you worked with Exposed, you may have noticed that it supports different column types out of the box, like integer, varchar and all that.
But not dates.
For dates you need to pick one of the three libraries.
Why is that, and which library is the correct one?
The reason that you need to specify the date library explicitly is that Exposed was one of the first Kotlin libraries.
And JetBrains started developing it when Java 7 was around, which didn’t have a good Date support.
Back in the day, everyone was using JodaTime library for dates.So did JetBrains in their projects. It was natural for them to use JodaTime for representing dates.
When Java 8 came out, the Exposed team added support for the Date it came with.
And then a few years ago Kotlin has added it’s own DateTime library mainly for the sake of Kotlin Multiplatform.
So now there are three date libraries to pick from.
If your project is pure Kotlin, you should use DateTime.
In other cases, use the Java Time library.
I don’t think there’s a good reason to use JodaTime nowadays.
30s
Next topic.
When you work with relational databases, you often need to represent a many-to-many relationship.
For example here we have users, that can be assigned to groups.
A single user can be in multiple groups.
And groups of course have multiple users, otherwise those aren’t really groups.
First let’s look at the connection table, that connects users to groups.
It doesn’t have anything special defined on it, and the only thing I want to show you here is that this table is called “Members”, because we’ll use that later.
Some may not know that in Exposed, many-to-many relationship is defined using a `via` function.
Now, let’s take a look at the Group entity, and focus on the definition on the last line.
We say that we’ll fetch users via Members table, using the group_id column as source, and user_id column as target
And just to drive that point home, here’s how it looks from the user perspective.
So again we say that we fetch Groups via the members table, and now the source is the user, and the target is the groups that user belongs to.
The next feature is in my opinion pretty well documented, but we still get a lot of questions about, so I decided to cover it anyway.
Coroutines and suspending functions as you all know are one of the Kotlin killer features.
They allow you to be efficient about IO, and you can run thousands of coroutines concurrently.
For those reasons, a lot of functions in Kotlin are suspending functions.
But you can’t invoke suspending functions from the Exposed transaction block, because that block is not a suspending function itself.
So here we have a simple suspending function, that just waits 10 milliseconds and doesn’t return anything.
And if we try to invoke it from a transaction, we get a compilation error.
The way to overcome that is to use “newSuspendedTransaction” provided by Exposed library.
This is a suspending function, so from that block you can invoke other suspending functions.
You can also nest suspended transactions. For that we have the suspendedTransaction function.
So, if you need some fine grained control over your transactions, you can have it with Exposed.
You can open one transaction, and inside open an inner transaction, and if any part of that fails, everything will be rolled back.
One subtle detail is that if you decide to use newSuspendedTransaction inside another newSuspendedTransaction, it will do exactly as you tell it to.
It will open a completely new transaction. So if the outer transaction rolls back, the inner transaction won’t.
Sometimes you want that behavior, sometimes you don’t.
So be aware of it, and pick carefully between those two blocks.
Next, let’s look at the following SQL query someone tried to rewrite with Exposed.
The SQL part should look pretty straightforward.
There’s a varchar type column that says if a message was seen or not.
If it wasn’t seen, we map it to null, otherwise, we map it to 1.
And then we count how many see messages there are.
And count function ignores the nulls.
There may be some SQL experts that would say that it could be rewritten is such a way that the NULL is made redundant, but let’s assume that person absolutely had to have that null there.
So, the way you would try to write it is by using the case() builder we have in Exposed.
And for those that don’t work with Exposed, slice() function is just Exposed way of specifying which columns you want to query.
As you can see, this is pretty similar to the SQL query, and you get autompletion, so it isn’t hard to write.
The problem we have here is that you cannot pass “NULL” as a string, and you also can’t pass Kotlin’s null.
So it’s not very intuitive what should you do there.
The way that problem can be solved is by using a NullOp Exposed provides.
It’s an expression, and the value it returns is SQL NULL.
Now remember, case is also an expression, and its result is the number of read messages user has or something like that.
So we need to specify the type of the null, and its type in this case is integer.
That’s why we have that nullOp<Int> there
If we were allowed to rewrite that query slightly, and have the NullOp in the Else case, for example, then Kotlin generic type inference would kick in, and we wouldn’t have to specify the type of the null.
I mentioned earlier that Exposed has two syntaxes: the SQL DSL syntax and the DAO syntax.
Sometimes you have a complex query that you’ve written with Exposed SQL DSL.
And now you want to convert its results to DAO entities.
You can do a mapping yourself, but there’s a better way to do it.
There’s a method called `wrapRows` in Exposed, that is available on each entity.
It takes the query, and attempts to map it.
So there’s no need to do that work manually. Exposed can do it for you.
There’s also a method that is called wrapRow, that works on a single row.
I think that’s my final point for today.
Now, I talked a lot about how the aim of Exposed framework is to provide a type safe and ergonomic way to work with databases.
And I hope that I showed a few tricks how to do that efficiently.
Maybe I even convinced some of you to give Exposed a try.
But sometimes, a database feature is too specific or too new for us to implement it.
So, there is an exec method available to you inside a transaction, and that method will execute an arbitrary prepared statement.
This method exposes a low level Java API, so you have an awkward iterator and indexes that start with 1.
Not recommended, but that option is left to you.
Well, let’s quickly recap the things you may have not known.
In Exposed, you can log a single query, no need to log all of them.
We discussed why Exposed requires you to pick the date library you work with, and that by default, you should pick Kotlin DateTime
Exposed provides you with a flexible way to define many-to-many relationships using via fucntion
There’s coroutine support, so if your code uses suspending functions, it’s not a problem for Exposed. Use newSuspendedTransaction
If you need to use NULL in your query, there’s a special expression for that.
You also don’t have to map SQL DSL to entities manually, Exposed can do this for you.
And finally, you can break Exposed and use arbitrary statements if you really have to.
That’s all the features I wanted to talk about today.
If you have questions on how to use some of the Exposed features, me and other contributors will be happy to answer them on StackOverflow.
If you find any bugs related to Exposed library, you can open a GitHub issue, and the team will look into it.
And if you have an idea for improving the library, Pull Requests are more than welcome!
If you’re interested to learn more about the Exposed framework, I just finished recording the first course on Exposed for LinkedIn Learning.
It should be out beginning of 2023.
Thank you for coming to my talk.
If you enjoyed it, you can follow me on LinkedIn, Medium or Twitter.
I’m an author of a video course on System Design that is published on Udemy, so if you’re interested in Software Architecture, there’s a coupon that will provide you this course for free.
Also, I’m an author of Kotlin Design Patterns and Best Practices book. Check it out on Amazon.
And last thing I wanted to say as someone who was born in Ukraine.
Thank you for all your support.
Glory to Ukraine!
Enjoy the rest of this great conference.
Thank you.