9.8 KiB
Overview
The DAO (Data Access Object) API of Exposed, is similar to ORM frameworks like Hibernate with a Kotlin-specific API.
A DB table is represented by an object inherited from org.jetbrains.exposed.sql.Table like this:
object StarWarsFilms : Table() {
val id: Column<Int> = integer("id").autoIncrement().primaryKey()
val sequelId: Column<Int> = integer("sequel_id").uniqueIndex()
val name: Column<String> = varchar("name", 50)
val director: Column<String> = varchar("director", 50)
}
Tables that contain an Int id with the name id can be declared like this:
object StarWarsFilms : IntIdTable() {
val sequelId: Column<Int> = integer("sequel_id").uniqueIndex()
val name: Column<String> = varchar("name", 50)
val director: Column<String> = varchar("director", 50)
}
Note that these Column types will be defined automatically, so you can also just leave them out. This would produce the same result as the example above:
object StarWarsFilms : IntIdTable() {
val sequelId = integer("sequel_id").uniqueIndex()
val name = varchar("name", 50)
val director = varchar("director", 50)
}
An entity instance or a row in the table is defined as a class instance:
class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms)
var sequelId by StarWarsFilms.sequelId
var name by StarWarsFilms.name
var director by StarWarsFilms.director
}
Basic CRUD operations
Create
val movie = StarWarsFilm.new {
name = "The Last Jedi"
sequelId = 8
director = "Rian Johnson"
}
Read
To get entities use one of the following
val movies = StarWarsFilm.all()
val movies = StarWarsFilm.find { StarWarsFilms.sequelId eq 8 }
val movie = StarWarsFilm.findById(5)
- For a list of available predicates see DSL Where expression.
Read a value from a property similar to any property in a Kotlin class:
val name = movie.name
Sort (Order-by)
Ascending order:
val movies = StarWarsFilm.all().sortedBy { it.sequelId }
Descending order:
val movies = StarWarsFilm.all().sortedByDescending{ it.sequelId }
Update
Update a value of a property similar to any property in a Kotlin class:
movie.name = "Episode VIII – The Last Jedi"
- Note: Exposed doesn't make an immediate update when you set a new value for Entity, it just stores it on the inner map. "Flushing" values to the database occurs at the end of the transaction or before next
select *from the database.
Delete
movie.delete()
Referencing
many-to-one reference
Let's say you have this table:
object Users: IntIdTable() {
val name = varchar("name", 50)
}
class User(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<User>(Users)
var name by Users.name
}
And now you want to add a table referencing this table (and other tables!):
object UserRatings: IntIdTable() {
val value = long("value")
val film = reference("film", StarWarsFilms)
val user = reference("user", Users)
}
class UserRating(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<UserRating>(UserRatings)
var value by UserRatings.value
var film by StarWarsFilm referencedOn UserRatings.film // use referencedOn for normal references
var user by User referencedOn UserRatings.user
}
Now you can get the film for a rating in the same way you would get any other field:
filmRating.film // returns a StarWarsFilm object
Now if you wanted to get all the ratings for a film, you could do that by using the FilmRating.find function, but what is much easier is to just add a referrersOn field to the StarWarsFilm class:
class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms)
...
val ratings by UserRating referrersOn UserRatings.film // make sure to use val and referrersOn
...
}
You can call:
movie.ratings // returns all UserRating objects with this movie as film
Optional reference
You can also add an optional reference:
object UserRatings: IntIdTable() {
...
val secondUser = reference("second_user", Users).nullable() // this reference is nullable!
...
}
class UserRating(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<UserRating>(UserRatings)
...
var secondUser by User optionalReferencedOn UserRatings.secondUser // use optionalReferencedOn for nullable references
...
}
Now secondUser will be a nullable field.
Of course, you can still use referrersOn.
many-to-many reference
In some cases, a many-to-many reference may be required. Let's assume you want to add a reference to the following Actors table to the StarWarsFilm class:
object Actors: IntIdTable() {
val firstname = varchar("firstname", 50)
val lastname = varchar("lastname", 50)
}
class Actor(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<Actor>(Actors)
var firstname by Actors.firstname
var lastname by Actors.lastname
}
Create an additional intermediate table to store the references:
object StarWarsFilmActors : Table() {
val starWarsFilm = reference("starWarsFilm", StarWarsFilms).primaryKey(0)
val actor = reference("actor", Actors).primaryKey(1)
}
Add a reference to StarWarsFilm:
class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms)
...
var actors by Actor via StarWarsFilmActors
...
}
Note: Creating the entity and the reference in the same transaction does only work if you set the
id column manually. If you're using UUIDTables and UUIDEntity you can do it like this:
transaction {
//only works with UUIDTable and UUIDEntity
StarWarsFilm.new (UUID.randomUUID()){
...
actors = SizedCollection(listOf(actor))
}
}
If you don't want to set the ID column manually, you have to create the entity in it's own transaction and set the relation afterward in another transaction:
// create film
val film = transaction {
StarWarsFilm.new {
name = "The Last Jedi"
sequelId = 8
director = "Rian Johnson"
}
}
//create actor
val actor = transaction {
Actor.new {
firstname = "Daisy"
lastname = "Ridley"
}
}
//add reference
transaction {
film.actors = SizedCollection(listOf(actor))
}
Parent-Child reference
Parent-child reference is very similar to many-to-many version, but an intermediate table contains both references to the same table. Let's assume you want to build a hierarchical entity which could have parents and children. Our tables and an entity mapping will look like
object NodeTable : IntIdTable() {
val name = varchar("name", 50)
}
object NodeToNodes : Table() {
val parent = reference("parent_node_id", NodeTable)
val child = reference("child_user_id", NodeTable)
}
class Node(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Node>(NodeTable)
var name by NodeTable.name
var parents by Node.via(NodeToNodes.child, NodeToNodes.parent)
var children by Node.via(NodeToNodes.parent, NodeToNodes.child)
}
As you can see NodeToNodes columns target only NodeTable and another version of via function were used.
Now you can create a hierarchy of nodes.
val root = Node.new { name = "root" }
val child1 = Node.new {
name = "child1"
}
child1.parents = SizedCollection(root) // assign parent
val child2 = Node.new { name = "child2" }
root.children = SizedCollection(listOf(child1, child2)) // assign children
Beware that you can't setup references inside a new block as an entity is not created yet and id is not defined to be referenced to.
Advanced CRUD operations
Read entity with a join to another table
Let's imagine that you want to find all users who rated second SW film with more than 5. First of all, we should write that query using Exposed DSL.
val query = Users.innerJoin(UserRatings).innerJoin(StarWarsFilm)
.slice(Users.columns)
.select {
StarWarsFilms.sequelId eq 2 and (UserRatings.value gt 5)
}.withDistinct()
After that all we have to do is to "wrap" a result with User entity:
val users = User.wrapRows(query).toList()
Auto-fill created and updated columns on entity change
See example by @PaulMuriithi here.
Use queries as expressions
Imagine that you want to sort cities by how many users each city has. In order to do so, you can write a sub-query which counts users in each city and order by that number. Though in order to do so you'll have to convert Query to Expression. This can be done using wrapAsExpression function:
val expression = wrapAsExpression<Int>(Users
.slice(Users.id.count())
.select {
Cities.id eq Users.cityId
})
val cities = Cities
.selectAll()
.orderBy(expression, SortOrder.DESC)
.toList()