Used libraries: Exposed, h2database
Learn how to add persistence to a website using the Exposed ORM framework.
In this series of tutorials, we'll show you how to create a simple blog application in Ktor:
- In the first tutorial, we showed how to host static content like images and HTML pages.
- In the second tutorial, we added interactivity to our application using the FreeMarker template engine.
- In this tutorial, we'll add persistence to our website using the Exposed framework. We'll use the H2 local database to store articles.
First, you need to add dependencies for the Exposed and H2 libraries. Open the gradle.properties
file and specify library versions:
{src="gradle.properties" lines="17-18"}
Then, open build.gradle.kts
and add the following dependencies:
{src="snippets/tutorial-website-interactive-persistence/build.gradle.kts" lines="3-4,20-21,25-28,32"}
Click the Load Gradle Changes icon in the top right corner of the build.gradle.kts
file to install newly added dependencies.
Exposed uses the org.jetbrains.exposed.sql.Table
class as a database table. To update the Article
model, open the models/Article.kt
file and replace the existing code with the following:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/models/Article.kt"}
The id
, title
, and body
columns will store information about our articles. The id
column will act as a primary key.
If you examine the types of the properties in the
Articles
object, you'll see that they have theColumn
type with the necessary type argument:id
has the typeColumn<Int>
, and bothtitle
andbody
have the typeColumn<String>
.
{type="tip"}
A data access object (DAO) is a pattern that provides an interface to a database without exposing the details of the specific database. We'll define a DAOFacade
interface later to abstract our specific requests to the database.
Every database access using Exposed is started by obtaining a connection to the database. For that, you pass JDBC URL and the driver class name to the Database.connect
function. Create the dao
package inside com.example
and add a new DatabaseFactory.kt
file. Then, insert this code:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DatabaseFactory.kt" lines="1-13,17,21"}
Note that
driverClassName
andjdbcURL
are hardcoded here. Ktor allows you to extract such settings to a custom configuration group.
After obtaining the connection, all SQL statements should be placed inside a transaction:
fun init() {
// ...
val database = Database.connect(jdbcURL, driverClassName)
transaction(database) {
// Statements here
}
}
In this code sample, the default database is passed explicitly to the transaction
function. If you have only one database, you can omit it. In this case, Exposed automatically uses the last connected database for transactions.
Note that the
Database.connect
function doesn't establish a real database connection until you call the transaction - it only creates a descriptor for future connections.
Given that the Articles
table is already declared, we can call SchemaUtils.create(Articles)
wrapped in transaction
call at the bottom of the init
function to instruct the database to create this table if it doesn't yet exist:
fun init() {
// ...
val database = Database.connect(jdbcURL, driverClassName)
transaction(database) {
SchemaUtils.create(Articles)
}
}
For our convenience, let's create a utility function dbQuery
inside the DatabaseFactory
object, which we'll be using for all future requests to the database. Instead of using the transaction to access it in a blocking way, let's take advantage of coroutines and start each query in its own coroutine:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DatabaseFactory.kt" lines="19-20"}
The resulting DatabaseFactory.kt
file should look as follows:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DatabaseFactory.kt"}
Finally, we need to load the created configuration at the application startup. Open Application.kt
and call DatabaseFactory.init
from the Application.module
body:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/Application.kt" lines="9-13"}
Now let's create an interface to abstract the necessary operations for updating articles. Create the DAOFacade.kt
file inside the dao
package and fill it with the following code:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacade.kt"}
We need to list all articles, view an article by its ID, add a new article, edit, or delete it. Since all these functions perform database queries under the hood, they are defined as suspending functions.
To implement the DAOFacade
interface, place the caret at its name, click a yellow bulb icon next to this interface and select Implement interface. In the invoked dialog, leave the default settings and click OK.
In the Implement Members dialog, select all the functions and click OK.
IntelliJ IDEA creates the DAOFacadeImpl.kt
file inside the dao
package. Let's implement all functions using Exposed DSL.
Let's start with a function returning all entries. Our request is wrapped into a dbQuery
call. We call the Table.selectAll
extension function to get all the data from the database. The Articles
object is a subclass of Table
, so we use Exposed DSL methods to work with it.
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacadeImpl.kt" lines="1-17,44"}
Table.selectAll
returns an instance of Query
, so to get the list of Article
instances, we need to manually extract data for each row and convert it to our data class. We accomplish that using the helper function resultRowToArticle
that builds an Article
from the ResultRow
.
The ResultRow
provides a way to get the data stored in the specified Column
by using a concise get
operator, allowing us to use the bracket syntax, similar to an array or a map.
The type of
Articles.id
isColumn<Int>
, which implements theExpression
interface. That is why we can pass any column as an expression.
Now let's implement a function returning one article:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacadeImpl.kt" lines="19-24"}
The select
function takes an extension lambda as an argument. The implicit receiver inside this lambda is of type SqlExpressionBuilder
. You don't use this type explicitly, but it defines a bunch of useful operations on columns, which you use to build your queries. You can use comparisons (eq
, less
, greater
), arithmetic operations (plus
, times
), check whether value belongs or doesn't belong to a provided list of values (inList
, notInList
), check whether the value is null or non-null, and many more.
select
returns a list of Query
values. As before, we convert them to articles. In our case, it should be one article, so we return it as a result.
To insert a new article into the table, use the Table.insert
function, which takes a lambda argument:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacadeImpl.kt" lines="26-32"}
Inside this lambda, we specify which value is supposed to be set for which column. The it
argument has a type InsertStatement
on which we can call the set
operator taking column and value as arguments.
To update the existing article, the Table.update
is used:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacadeImpl.kt" lines="34-39"}
Finally, use Table.deleteWhere
to remove an article from the database:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacadeImpl.kt" lines="41-43"}
Let's create an instance of DAOFacade
and add a sample article to be inserted to the database before the application is started.
Add the following code at the bottom of DAOFacadeImpl.kt
:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/dao/DAOFacadeImpl.kt" lines="46-52"}
Now we are ready to use implemented database operations inside route handlers.
Open the plugins/Routing.kt
file.
To show all articles, call dao.allArticles
inside the get
handler:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/plugins/Routing.kt" lines="22-24"}
To post a new article, call the dao.addNewArticle
function inside post
:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/plugins/Routing.kt" lines="28-34"}
To get an article for showing and editing, use dao.article
inside get("{id}")
and get("{id}/edit")
, respectively:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/plugins/Routing.kt" lines="35-42"}
Finally, go to the post("{id}")
handler and use dao.editArticle
to update an article and dao.deleteArticle
to delete it:
{src="snippets/tutorial-website-interactive-persistence/src/main/kotlin/com/example/plugins/Routing.kt" lines="43-58"}
You can find the resulting project for this tutorial here: tutorial-website-interactive-persistence.
Let's see if our journal application is performing as expected. We can run our application by pressing the Run button next to fun main(...)
in our Application.kt
:
IntelliJ IDEA will start the application, and after a few seconds, we should see the confirmation that the app is running:
[main] INFO Application - Responding at http://0.0.0.0:8080
Open http://localhost:8080/
in a browser and try to create, edit, and delete articles. Articles will be saved in the build/db.mv.db
file. In IntelliJ IDEA, you can see the content of this file in a Database tool window.