Slide 1

Slide 1 text

Hands-On Kotlin Web Development with Ktor

Slide 2

Slide 2 text

Leonid Stashevskii Ktor developer Anton Arhipov Developer Advocate @antonarhipov @_ _ _e5l

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Part 1. Introduction & overview Part 2. Persistence with Exposed Part 3. Non-functional requirements Part 4. Extending Ktor Hands-On Kotlin Web Development with Ktor

Slide 5

Slide 5 text

Part 1. Introduction & overview Part 2. Persistence with Exposed Part 3. Non-functional requirements Part 4. Extending Ktor Hands-On Kotlin Web Development with Ktor Demonstration kitchensink

Slide 6

Slide 6 text

For each part, we will: Rules of the game: hands-on 1. Provide you with a starter project 2. Give you some time to add functionality 3. Provide a sample solution and walk you through the code

Slide 7

Slide 7 text

IntelliJ IDEA with Kotlin (and Ktor) plugins Setup Docker HTTP client Database client

Slide 8

Slide 8 text

https://github.com/antonarhipov/ktor-workshop Setup % git branch * main branch01 branch02 branch03 branch04 branch05 branch06 branch07 branch08

Slide 9

Slide 9 text

https://github.com/antonarhipov/ktor-workshop Setup % git branch * main branch01 branch02 branch03 branch04 branch05 branch06 branch07 branch08 Initial state, generated application First tests - empty First tests - implementation CRUD implementation Adding structure and DI Database access with Exposed - basics Database access with Exposed - adding relations Database access with Exposed - adding entities Integration testing with Testcontainers

Slide 10

Slide 10 text

Part 1. Introduction to Ktor

Slide 11

Slide 11 text

What web framework should I use with Kotlin?

Slide 12

Slide 12 text

Hexagon * There is more, of course!

Slide 13

Slide 13 text

ktor.io

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

import com.example.plugins.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application :: module) .start(wait = true) } fun Application.module() { configureRouting() }

Slide 18

Slide 18 text

import com.example.plugins.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application :: module) .start(wait = true) } fun Application.module() { routing { get("/") { call.respondText("Hello World!") } } }

Slide 19

Slide 19 text

import com.example.plugins.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application :: module) .start(wait = true) } fun Application.module() { routing { get("/") { call.respondText("Hello World!") } } } Start an embedded server with Netty engine on port 8080

Slide 20

Slide 20 text

import com.example.plugins.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application :: module) .start(wait = true) } fun Application.module() { routing { get("/") { call.respondText("Hello World!") } } } Adding routes

Slide 21

Slide 21 text

import com.example.plugins.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application :: module) .start(wait = true) } fun Application.module() { routing { get("/") { call.respondText("Hello World!") } } } Request mapping

Slide 22

Slide 22 text

import com.example.plugins.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application :: module) .start(wait = true) } fun Application.module() { routing { get("/") { call.respondText("Hello World!") } } } Write response

Slide 23

Slide 23 text

dependencies { implementation("io.ktor:ktor-server-core-jvm") implementation("io.ktor:ktor-server-netty-jvm") implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("io.ktor:ktor-server-tests-jvm") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") }

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

Routing HTTP

Slide 26

Slide 26 text

Service Routing HTTP

Slide 27

Slide 27 text

Database Persistence Service Routing HTTP

Slide 28

Slide 28 text

Database Persistence Service Routing Model DAO Model DTO JSON HTTP

Slide 29

Slide 29 text

Database Persistence Service Routing Model DAO Model DTO HTTP JSON

Slide 30

Slide 30 text

Database Persistence Service Routing Model DAO Model DTO HTTP JSON

Slide 31

Slide 31 text

Database Persistence Service Routing Model DAO Model DTO HTTP JSON Libraries, DI, plugins, patterns, best practices, etc Grey area:

Slide 32

Slide 32 text

Database Persistence Service Routing Model DAO Model DTO HTTP JSON Coroutines Libraries, DI, plugins, patterns, best practices, etc Grey area:

Slide 33

Slide 33 text

Ktor's approach - explicitness

Slide 34

Slide 34 text

Ktor's approach - explicitness Fine-grained approach to dependencies "Dependency per plugin" Compatible with JPMS

Slide 35

Slide 35 text

TODO: more intro? No "magic". No plugin starts automatically

Slide 36

Slide 36 text

TODO: more intro? No "magic". No plugin starts automatically Use the install() function to con fi gure plugins

Slide 37

Slide 37 text

Hands-on time! Use the project generator to create new project. Add Routing, Content Negotiation, and kotlinx.serialization plugins. Run it!!! What do you see? Warmup

Slide 38

Slide 38 text

Hands-on time! Use the project generator to create new project. Add Routing, Content Negotiation, and kotlinx.serialization plugins. Run it!!! What do you see? Warmup Hint: if you select kotlinx.serialization at the plugins stage of the wizard, the other two will be selected automatically

Slide 39

Slide 39 text

Hands-on time! Use the project generator to create new project. Add Routing, Content Negotiation, and kotlinx.serialization plugins. * Run it!!! What do you see? * - Add more plugins if you like, of course!

Slide 40

Slide 40 text

Hands-on time! Use the project generator to create new project. Add Routing, Content Negotiation, and kotlinx.serialization plugins. Run it!!! What do you see? get("/") { call.respondText("Hello World!") } get("/json/kotlinx-serialization") { call.respond(mapOf("hello" to "world")) }

Slide 41

Slide 41 text

Hands-on time! Use the project generator to create new project. Add Routing, Content Negotiation, and kotlinx.serialization plugins. Run it!!! What do you see? https: // github.com/antonarhipov/ktor-workshop % git branch * main branch01 branch02 branch03

Slide 42

Slide 42 text

Hands-on time! Use the project generator to create new project. Add Routing, Content Negotiation, and kotlinx.serialization plugins. Run it!!! What do you see? https: // github.com/antonarhipov/ktor-workshop % git branch * main branch01 branch02 branch03 This is our starting point

Slide 43

Slide 43 text

Hands-on time! Implement endpoints for CRUD operations for the User data class. @Serializable data class User( val userId: Int, val userType: UserType, val displayName: String, val link: String, val aboutMe: String? = null ) enum class UserType { REGISTERED, MODERATOR, }

Slide 44

Slide 44 text

Hands-on time! Implement endpoints for CRUD operations for the User data class. @Serializable data class User( val userId: Int, val userType: UserType, val displayName: String, val link: String, val aboutMe: String? = null ) enum class UserType { REGISTERED, MODERATOR, } Tests

Slide 45

Slide 45 text

import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { routing { get("/") { call.respondText("Hello World!") } } } client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } } Creates a TestApplication

Slide 46

Slide 46 text

import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { routing { get("/") { call.respondText("Hello World!") } } } client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } } De fi ne functionality to be tested

Slide 47

Slide 47 text

import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { routing { get("/") { call.respondText("Hello World!") } } } client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } } Use client instance interaction with the TestApplication

Slide 48

Slide 48 text

import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { routing { get("/") { call.respondText("Hello World!") } } } client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } }

Slide 49

Slide 49 text

import kotlin.test.* class ApplicationTest { @Test fun testRoot() = testApplication { application { routing { get("/") { call.respondText("Hello World!") } } } client.get("/").apply { assertEquals(HttpStatusCode.OK, status) assertEquals("Hello World!", bodyAsText()) } } } It creates an application instance for each test. What if we want to reuse the application instance for several tests?

Slide 50

Slide 50 text

class ApplicationTest { val testApp = TestApplication { application { ... } } val client = testApp.createClient {} @Test fun testRoot() = runBlocking { val response = client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello World!", response.bodyAsText()) } }

Slide 51

Slide 51 text

class ApplicationTest { val testApp = TestApplication { application { ... } } val client = testApp.createClient {} @Test fun testRoot() = runBlocking { val response = client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello World!", response.bodyAsText()) } } Alternatively, create TestApplication instance

Slide 52

Slide 52 text

class ApplicationTest { val testApp = TestApplication { application { ... } } val client = testApp.createClient {} @Test fun testRoot() = runBlocking { val response = client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello World!", response.bodyAsText()) } } Con fi gure HttpClient for the TestApplication

Slide 53

Slide 53 text

class ApplicationTest { val testApp = TestApplication { application { ... } } val client = testApp.createClient {} @Test fun testRoot() = runBlocking { val response = client.get("/") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello World!", response.bodyAsText()) } } Need to use runBlocking to run in a coroutine scope

Slide 54

Slide 54 text

class ApplicationTest { val testApp = TestApplication { application { ... } } val client = testApp.createClient { install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { json() } } @Test fun testRoot() = runBlocking { . .. ContentNegotiation needs to be con fi gured for the HttpClient

Slide 55

Slide 55 text

class ApplicationTest { val testApp = TestApplication { application { ... } } val client = testApp.createClient { install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { json() } } @Test fun testRoot() = runBlocking { . .. ContentNegotiation needs to be con fi gured for the HttpClient

Slide 56

Slide 56 text

HttpClient val response: HttpResponse = client.get("/data") assertEquals(HttpStatusCode.OK, response.status) assertEquals( ... , response.bodyAsText()) private val testApp = TestApplication { .. . } private val client = testApp.createClient { ... } Make request, check response https://ktor.io/docs/client-requests.html

Slide 57

Slide 57 text

HttpClient private val testApp = TestApplication { .. . } private val client = testApp.createClient { ... } val data = Json.decodeFromString> (bodyAsText()) assertEquals( ... , data.size) Deserialize response body into objects val response: HttpResponse = client.get("/data") assertEquals(HttpStatusCode.OK, response.status) assertEquals( ... , response.bodyAsText()) Make request, check response https://ktor.io/docs/client-requests.html

Slide 58

Slide 58 text

HttpClient private val testApp = TestApplication { .. . } private val client = testApp.createClient { ... } val data = Json.decodeFromString> (bodyAsText()) assertEquals( ... , data.size) Deserialize response body into objects val response: HttpResponse = client.get("/data") assertEquals(HttpStatusCode.OK, response.status) assertEquals( ... , response.bodyAsText()) Make request, check response val response = client.post("/data") { contentType(ContentType.Application.Json) setBody( ... ) } POST with HttpRequestBuilder https://ktor.io/docs/client-requests.html

Slide 59

Slide 59 text

Hands-on time! Implement endpoints for CRUD operations @Serializable data class User( val userId: Int, val userType: UserType, val displayName: String, val link: String, val aboutMe: String? = null ) enum class UserType { REGISTERED, MODERATOR, }

Slide 60

Slide 60 text

Hands-on time! Implement endpoints for CRUD operations @Test fun `get all data`(): Unit = runBlocking { ... } @Test fun `post data instance`(): Unit = runBlocking { ... } @Test fun `put data instance`(): Unit = runBlocking { ... } @Test fun `delete data instance`(): Unit = runBlocking { ... } @Serializable data class User( val userId: Int, val userType: UserType, val displayName: String, val link: String, val aboutMe: String? = null ) enum class UserType { REGISTERED, MODERATOR, } Implement these tests git branch * branch01

Slide 61

Slide 61 text

git branch * branch02

Slide 62

Slide 62 text

Let's take a look at the solution

Slide 63

Slide 63 text

https://ktor.io/docs/server-requests.html Receiving requests & sending responses https://ktor.io/docs/server-responses.html

Slide 64

Slide 64 text

Sending responses get("/") { call.respondText("Hello World!") } Respond something

Slide 65

Slide 65 text

Sending responses get("/") { call.respondText("Hello World!") } post("/") { val user = call.receive() ... call.respond(status = HttpStatusCode.Created, message = "Data added successfully") } Respond with HTTP code Add response content

Slide 66

Slide 66 text

Receiving requests get("/") { call.respondText("Hello World!") } post("/") { val user = call.receive() ... call.respond(status = HttpStatusCode.Created, message = "Data added successfully") } Deserialize request content

Slide 67

Slide 67 text

Receiving requests get("/") { call.respondText("Hello World!") } post("/") { val user = call.receive() ... call.respond(status = HttpStatusCode.Created, message = "Data added successfully") } get("/{userId}") { val userId = call.parameters["userId"] ... } Access request parameters

Slide 68

Slide 68 text

Hands-on time! Implement endpoints for CRUD operations for the User data class. @Serializable data class User( val userId: Int, val userType: UserType, val displayName: String, val link: String, val aboutMe: String? = null ) enum class UserType { REGISTERED, MODERATOR, } routing { route("/users") { get { ... } post { .. . } get("/{userId}") { . .. } put("/{userId}") { . .. } delete("/{userId}") { ... } } } Hint

Slide 69

Slide 69 text

git branch * branch03

Slide 70

Slide 70 text

Let's take a look at the solution git branch * branch03

Slide 71

Slide 71 text

Structuring the project https://ktor.io/docs/server-application-structure.html

Slide 72

Slide 72 text

Dependency injection

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version") Adding Koin build.gradle.kts

Slide 76

Slide 76 text

implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version") Adding Koin build.gradle.kts In Ktor app: install(Koin) { slf4jLogger() modules(usersDataModule) }

Slide 77

Slide 77 text

implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version") Adding Koin build.gradle.kts In Ktor app: install(Koin) { slf4jLogger() modules(usersDataModule) } val usersDataModule = module { single { RepositoryImpl() } }

Slide 78

Slide 78 text

implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version") Adding Koin build.gradle.kts In Ktor app: install(Koin) { slf4jLogger() modules(usersDataModule) } val usersDataModule = module { single { RepositoryImpl() } } fun Application.configureRouting() { val repository by inject()

Slide 79

Slide 79 text

Hands-on time! Add Koin (or KodeinDI) dependencies to the project: implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")

Slide 80

Slide 80 text

Hands-on time! Abstract the repository implementation with an interface (UserRepository) Inject the repository implementation (UserRepositoryImpl) using Koin Don't forget to adjust the tests code! Add Koin (or KodeinDI) dependencies to the project: implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")

Slide 81

Slide 81 text

Hands-on time! Abstract the repository implementation with an interface (UserRepository) Inject the repository implementation (UserRepositoryImpl) using Koin Don't forget to adjust the tests code! class UsersRepositoryImpl : UsersRepository { override fun findAll(): List { TODO("Not yet implemented") } ... git branch * branch04 Add Koin (or KodeinDI) dependencies to the project: implementation("io.insert-koin:koin-ktor:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")

Slide 82

Slide 82 text

Let's take a look at the solution

Slide 83

Slide 83 text

Part 2. Persistence with Exposed

Slide 84

Slide 84 text

What database framework should I use with Kotlin (and Ktor)?

Slide 85

Slide 85 text

SQLDelight

Slide 86

Slide 86 text

https://github.com/JetBrains/Exposed

Slide 87

Slide 87 text

Adding Exposed: dependencies build.gradle.kts val exposed_version = "0.50.1" implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version")

Slide 88

Slide 88 text

Using Exposed: connecting to DB val database = Database.connect( url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", user = "root", driver = "org.h2.Driver", password = "" )

Slide 89

Slide 89 text

val database = Database.connect( HikariDataSource(HikariConfig().apply { jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" username = "root" driverClassName = "org.h2.Driver" password = "" }) ) Using Exposed: connecting to DB

Slide 90

Slide 90 text

val database = Database.connect( HikariDataSource(HikariConfig().apply { jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" username = "root" driverClassName = "org.h2.Driver" password = "" }) ) Using Exposed: connecting to DB transaction(database) { SchemaUtils.createMissingTablesAndColumns(Datum) }

Slide 91

Slide 91 text

val database = Database.connect( HikariDataSource(HikariConfig().apply { if (embedded) { username = "root" password = "" driverClassName = "org.h2.Driver" jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" } else { username = "test" password = "test" driverClassName = "org.postgresql.Driver" jdbcUrl = "jdbc:postgresql: // localhost:5432/test" } } )) Using Exposed: connecting to DB Check branch05, Database.kt

Slide 92

Slide 92 text

@Serializable data class Data(val id: Int, val text: String) object Datum : Table("data") { val id = integer("id").autoIncrement() val text = varchar("text", 255) }

Slide 93

Slide 93 text

@Serializable data class Data(val id: Int, val text: String) object Datum : Table("data") { val id = integer("id").autoIncrement() val text = varchar("text", 255) } Datum.selectAll().map { Data(it[Datum.id], it[Datum.text]) } Select and map

Slide 94

Slide 94 text

@Serializable data class Data(val id: Int, val text: String) object Datum : Table("data") { val id = integer("id").autoIncrement() val text = varchar("text", 255) } Datum.selectAll().map { Data(it[Datum.id], it[Datum.text]) } Datum.selectAll().where { Datum.id eq id }.singleOrNull() ? . let { Data(it[Datum.id], it[Datum.text]) } Select and map

Slide 95

Slide 95 text

@Serializable data class Data(val id: Int, val text: String) object Datum : Table("data") { val id = integer("id").autoIncrement() val text = varchar("text", 255) } Datum.selectAll().map { Data(it[Datum.id], it[Datum.text]) } Datum.selectAll().where { Datum.id eq id }.singleOrNull() ? . let { Data(it[Datum.id], it[Datum.text]) } Datum.insert { it[Datum.id] = data.id it[Datum.text] = data.text } Datum.update({ Datum.id eq data.id }) { it[Datum.text] = data.text } update / insert

Slide 96

Slide 96 text

Hands-on time! 1. Add Exposed dependencies to the project val exposed_version = "0.50.1" implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version") https://github.com/JetBrains/exposed

Slide 97

Slide 97 text

Hands-on time! 2. De fi ne table objects for the data classes 1. Add Exposed dependencies to the project val exposed_version = "0.50.1" implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version") https://github.com/JetBrains/exposed

Slide 98

Slide 98 text

Hands-on time! 2. De fi ne table objects for the data classes 3. Implement UserRepository using Exposed with the real database 1. Add Exposed dependencies to the project val exposed_version = "0.50.1" implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version") https://github.com/JetBrains/exposed

Slide 99

Slide 99 text

Hands-on time! 2. De fi ne table objects for the data classes 3. Implement UserRepository using Exposed with the real database 1. Add Exposed dependencies to the project 4. Integrate the new implementation into Ktor application using DI val exposed_version = "0.50.1" implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:$exposed_version") https://github.com/JetBrains/exposed

Slide 100

Slide 100 text

Let's take a look at the solution git branch * branch05

Slide 101

Slide 101 text

Table relations 1 N

Slide 102

Slide 102 text

id: integer text: varchar id: integer name: varchar data_ref: integer Data Item

Slide 103

Slide 103 text

object Datum : Table("data_table") { val id = integer("id").autoIncrement() val text = varchar("text", 255) override val primaryKey = PrimaryKey(id) } object Items : Table("items_table") { val id = integer("id").autoIncrement() val name = varchar("name", 255) val datum_ref = reference("datum_ref", Datum.id) override val primaryKey = PrimaryKey(id) }

Slide 104

Slide 104 text

object Datum : Table("data_table") { val id = integer("id").autoIncrement() val text = varchar("text", 255) override val primaryKey = PrimaryKey(id) } object Items : Table("items_table") { val id = integer("id").autoIncrement() val name = varchar("name", 255) val datum_ref = reference("datum_ref", Datum.id) override val primaryKey = PrimaryKey(id) }

Slide 105

Slide 105 text

val dataId = Datum.insert { it[Datum.text] = data.text } get Datum.id Items.insert { it[Items.name] = item.name it[Items.datum_ref] = dataId }

Slide 106

Slide 106 text

val dataId = Datum.insert { it[Datum.text] = data.text } get Datum.id Items.insert { it[Items.name] = item.name it[Items.datum_ref] = dataId } val item = Item("Some item") val data = Data("Some data")

Slide 107

Slide 107 text

Hands-on time! Update the UserRepository implementation to handle the relation object ContentTable : Table("content") { ... val author = reference("author_id", UserTable.id) Update data classes and application code

Slide 108

Slide 108 text

Let's take a look at the solution git branch * branch06

Slide 109

Slide 109 text

Exposed entities

Slide 110

Slide 110 text

object Datum : Table("data_table") { val id = integer("id").autoIncrement() val text = varchar("text", 255) override val primaryKey = PrimaryKey(id) } object Items : Table("items_table") { val id = integer("id").autoIncrement() val name = varchar("name", 255) val datum_ref = reference("datum_ref", Datum.id) override val primaryKey = PrimaryKey(id) }

Slide 111

Slide 111 text

object Datum : LongIdTable("data_table") { val id = integer("id").autoIncrement() val text = varchar("text", 255) override val primaryKey = PrimaryKey(id) } object Items : Table("items_table") { val id = integer("id").autoIncrement() val name = varchar("name", 255) val datum_ref = reference("datum_ref", Datum.id) override val primaryKey = PrimaryKey(id) }

Slide 112

Slide 112 text

object Datum : LongIdTable("data_table") { val id = integer("id").autoIncrement() val text = varchar("text", 255) override val primaryKey = PrimaryKey(id) } object Items : IdTable("items_table") { override val id = integer("id").autoIncrement().entityId() val name = varchar("name", 255) val datum_ref = reference("datum_ref", Datum.id) override val primaryKey = PrimaryKey(id) }

Slide 113

Slide 113 text

class DataEntity(id: EntityID) : LongEntity(id) { var text by DatumEntityTable.text companion object : LongEntityClass(DatumEntityTable) } Defining entities

Slide 114

Slide 114 text

class DataEntity(id: EntityID) : LongEntity(id) { var text by DatumEntityTable.text companion object : LongEntityClass(DatumEntityTable) } class ItemsEntity(id: EntityID) : IntEntity(id) { var name by ItemsEntityTable.name companion object : IntEntityClass(ItemsEntityTable) } Defining entities

Slide 115

Slide 115 text

class DataEntity(id: EntityID) : LongEntity(id) { var text by DatumEntityTable.text companion object : LongEntityClass(DatumEntityTable) } class ItemsEntity(id: EntityID) : IntEntity(id) { var name by ItemsEntityTable.name var data_ref by DataEntity referencedOn ItemsEntityTable.datum_ref companion object : IntEntityClass(ItemsEntityTable) } Defining entities

Slide 116

Slide 116 text

val date = DataEntity.new { text = "Some text" } ItemsEntity.new { name = "Some item" data_ref = date } Operations with entities

Slide 117

Slide 117 text

val date = DataEntity.new { text = "Some text" } ItemsEntity.new { name = "Some item" data_ref = date } Operations with entities DataEntity.find { DatumEntityTable.text like "A%" }.toList()

Slide 118

Slide 118 text

class DataEntity(id: EntityID) : LongEntity(id) { var text by DatumEntityTable.text companion object : LongEntityClass(DatumEntityTable) } class ItemsEntity(id: EntityID) : IntEntity(id) { var name by ItemsEntityTable.name var data_ref by DataEntity referencedOn ItemsEntityTable.datum_ref companion object : IntEntityClass(ItemsEntityTable) } Defining entities

Slide 119

Slide 119 text

class DataEntity(id: EntityID) : LongEntity(id) { var text by DatumEntityTable.text val items by ItemsEntity referrersOn ItemsEntityTable.datum_ref companion object : LongEntityClass(DatumEntityTable) } class ItemsEntity(id: EntityID) : IntEntity(id) { var name by ItemsEntityTable.name var data_ref by DataEntity referencedOn ItemsEntityTable.datum_ref companion object : IntEntityClass(ItemsEntityTable) } Defining entities

Slide 120

Slide 120 text

DataEntity.find { DatumEntityTable.text like "A%" } .singleOrNull() ?. let { data -> DataInfo( text = data.text, items = data.items.map { ItemInfo(it.name) } ) } Operations with entities

Slide 121

Slide 121 text

DataEntity.find { DatumEntityTable.text like "A%" } .singleOrNull() ?. let { data -> DataInfo( text = data.text, items = data.items.map { ItemInfo(it.name) } ) } Operations with entities val selectedData = Datum.selectAll() .where { Datum.text like "A%" } .singleOrNull() ?. let { Data( text = it[Datum.text], items = Items.selectAll().where { Items.datum_ref eq it[Datum.id] }.map { Item(it[Items.name]) } ) } DAO vs DSL

Slide 122

Slide 122 text

Hands-on time! Implement a new repository with Exposed entities Integrate into Ktor app

Slide 123

Slide 123 text

Let's take a look at the solution git branch * branch07

Slide 124

Slide 124 text

Testcontainers

Slide 125

Slide 125 text

Hands-on time! Implement Testcontainers tests

Slide 126

Slide 126 text

Let's take a look at the solution git branch * branch08

Slide 127

Slide 127 text

Part 3. Non-functional requirements

Slide 128

Slide 128 text

Authentication, JWT git branch * branch09

Slide 129

Slide 129 text

WebSockets

Slide 130

Slide 130 text

More features & plugins status pages request validation static content type safe routing default headers, CORS CallLogging, CallId, MDC Logging & monitoring Docker image GraalVM native image

Slide 131

Slide 131 text

if when try Part 4. Extending Ktor

Slide 132

Slide 132 text

https://ktor.io/docs/server-custom-plugins.html

Slide 133

Slide 133 text

@JetBrainsKtor https://github.com/ktorio