Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exposed 1.1.1 소개

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Exposed 1.1.1 소개

Exposed 소개
- Exposed DSL
- Exposed DAO
- 다양한 예제 및 추가 확장 라이브러리 소개
- Anti patterns

Avatar for Sunghyouk Bae (Debop)

Sunghyouk Bae (Debop)

April 02, 2026
Tweet

More Decks by Sunghyouk Bae (Debop)

Other Decks in Programming

Transcript

  1. Agenda Introduction to Kotlin Exposed • Why Kotlin Exposed? •

    What is Kotlin Exposed • Exposed DSL - Typesafe SQL • Exposed DAO • Advanced Features • Implement JPA Features Generated by ChatGPT
  2. JPAܳ فҊ, ৵ ࢜۽਍ ߑधਸ ଺ইঠ ೞա? ҃೷੸ੋ ޙઁ ੄ध

    • High throughput ਃҳ ࢚ട੉ ݆ই૗ • ױࣽ Entity ী ؀ೠ ୊ܻо ؊ ݆ই૗ -> DTO݅ ੓যب غח ҃਋о ݆ই૗ (Projection) • ࠂ੟ೠ JPA Relation ੿੄ ߂ ҙܻ ߑधী ؀ೠ ޖ૑, ೙ਃࢿ хࣗ • ই૒ب N+1 Problem ਸ ٜ݅Ҋ ੓਺ • ా҅, Join, SubQuery ١ ࠂ੟ೠ ؘ੉ఠ ઁҕ੉ ೙ਃ -> SQL ҳޙਵ۽ ୊ܻೞח Ѫ੉ ࡅܴ • Convering Index, Hints , HASH Index ١ DB Vendor ౠച ӝמਸ ࢎਊೞӝ য۰਑ • NoSQL ١਷ ੉޷ Async/Non-Blocking ਸ ૑ਗ, য়۽૑ DB݅ زӝߑध੐ • CPU-Bounded ۄب غӝ ਤ೧ࢲח Coroutines ژח Virtual Threads ܳ ഝਊ೧ঠ ೣ • Spring Web fl ux ӝ߈ী ݏח ۄ੉࠳۞ܻ ೙ਃ (r2dbc, hibernate-reactive, vert.x sql client, exposed)
  3. ࢜۽਍ ో੄ ࢶఖ ઑѤ • Async/Non-Blocking ਸ ૑ਗ೧ঠ ೠ׮. (IO-Bounded/CPU-Bounded)

    • R2DBC, Vert.x | Coroutines, Virtual Threads • ؀۝ ؘ੉ఠ ੘স ߂ Vendor ౠച ੘স੉ ࣻਘ೧ঠ ೠ׮ (B2C ࢲ࠺झ ࠙ঠ) • JPA ӝמੋ Schema ࢤࢿ, Entity ੿੄ ߂ ҙ҅ ಴അ੉ оמ೧ঠ ೠ׮ (ORM) • Typesafe SQL DSL ૑ਗ • ౠࣻ ࣻഋ (Money, Encrypted, JSON) ਸ ૑ਗ೧ঠ ೠ׮
  4. ࢶ੿ ੉ਬ • Hibernate Reactive • QueryDSL ޷૑ਗ, JpaModelGen ݅

    ૑ਗ • Mutiny (reactive lib) ೟ण ೙ਃ (reactor, rxjava ৬ ਬࢎ) • MyBatis Dynamic SQL • Type-safe SQL Builder ۽ࢲ ಞܻೣ • ҕध੸ਵ۽ח Spring Template ݅ ૑ਗ, Vertx SQLTemplates ૑ਗ਷ ҳഅ ೙ਃ • Schema ࢤࢿ ӝמ ࠗ઒, ORM ӝࠄ ӝמ ࠗ੤ • Exposed • Typesafe DSL for SQL, Lightweight ORM • Coroutines ૑ਗ ژח Virtual Threads ܳ ഝਊೠ Async/Non-Blocking ૑ਗ • IO-Bounded Async/Non-Blocking ੉ ইצ CPU Bounded ੉׮.
  5. Introduction to Kotlin Exposed • Exposed is a Kotlin SQL

    database library with two fl avors • Typesafe SQL (using DSL) - Kotlin ӝ߈ Database ੘স੄ ୶࢚ചܳ ࣻ೯ • A Lightweight ORM (using DAO) - ORM ӝ߈੄ Database ੘সਸ ࣻ೯ • JDBC ಴ળ ળࣻ, Spring Framework Transaction ૑ਗ • Coroutines ૑ਗ - CPU bounded Async/Non-Blocking -> With Virtual Threads • Support Databases • H2, MariaDB, MySQL, Oracle, PostgreSQL, Microsoft SQL Server, SQLite
  6. Database Database ࢤࢿҗ ࢏ઁ @ParameterizedTest @MethodSource(ENABLE_DIALECTS_METHOD) fun `ؘ੉ఠ߬੉झ ࢤࢿҗ ࢏ઁ

    ੘স`(testDB: TestDB) { // MySQL਷ ҅੿ ӂೠ੉ ੓যঠ ೧ࢲ, పझ౟о ࠛоמ೤פ׮. Assumptions.assumeTrue { testDB in TestDB.ALL_H2 + TestDB.ALL_POSTGRES } withDb(testDB) { withAutoCommit(true) { val dbName = DB_NAME // ؘ੉ఠ߬੉झо ઓ੤ೞݶ ࢏ઁ (৘৻о ߊࢤ೧ب ޖद) runCatching { SchemaUtils.dropDatabase(dbName) } // ؘ੉ఠ߬੉झ ࢤࢿ SchemaUtils.createDatabase(dbName) // ؘ੉ఠ߬੉झ ࢏ઁ SchemaUtils.dropDatabase(dbName) } } } Path: 04-exposed-ddl/02-ddl Source: Ex01_CreateDatabase.kt
  7. Database Database ࢤࢿҗ ࢏ઁ @ParameterizedTest @MethodSource(ENABLE_DIALECTS_METHOD) fun `ؘ੉ఠ߬੉झ ࢤࢿҗ ࢏ઁ

    ੘স`(testDB: TestDB) { // MySQL਷ ҅੿ ӂೠ੉ ੓যঠ ೧ࢲ, పझ౟о ࠛоמ೤פ׮. Assumptions.assumeTrue { testDB in TestDB.ALL_H2 + TestDB.ALL_POSTGRES } withDb(testDB) { withAutoCommit(true) { val dbName = DB_NAME // ؘ੉ఠ߬੉झо ઓ੤ೞݶ ࢏ઁ (৘৻о ߊࢤ೧ب ޖद) runCatching { SchemaUtils.dropDatabase(dbName) } // ؘ੉ఠ߬੉झ ࢤࢿ SchemaUtils.createDatabase(dbName) // ؘ੉ఠ߬੉झ ࢏ઁ SchemaUtils.dropDatabase(dbName) } } } Path: 04-exposed-ddl/02-ddl Source: Ex01_CreateDatabase.kt CREATE DATABASE bluetape4k; DROP DATABASE bluetape4k;
  8. Schema Schema ࢤࢿ val currentSchema = getSchemaDefinition(tenant) SchemaUtils.createSchema(currentSchema) SchemaUtils.setSchema(currentSchema) transaction

    { MigrationUtils.statementsRequiredForDatabaseMigration(ActorTable, MovieTable, ActorInMovieTable) .forEach { exec(it) } } Path: 10-multitenant/01-multitenant-spring-web Source: DatabaseInitializer.kt
  9. Schema Schema ࢤࢿ val currentSchema = getSchemaDefinition(tenant) SchemaUtils.createSchema(currentSchema) SchemaUtils.setSchema(currentSchema) transaction

    { MigrationUtils.statementsRequiredForDatabaseMigration(ActorTable, MovieTable, ActorInMovieTable) .forEach { exec(it) } } Path: 10-multitenant/01-multitenant-spring-web Source: DatabaseInitializer.kt CREATE SCHEMA IF NOT EXISTS korean
  10. Schema Schema ࢤࢿ val currentSchema = getSchemaDefinition(tenant) SchemaUtils.createSchema(currentSchema) SchemaUtils.setSchema(currentSchema) transaction

    { MigrationUtils.statementsRequiredForDatabaseMigration(ActorTable, MovieTable, ActorInMovieTable) .forEach { exec(it) } } Path: 10-multitenant/01-multitenant-spring-web Source: DatabaseInitializer.kt CREATE SCHEMA IF NOT EXISTS korean CREATE SCHEMA IF NOT EXISTS English
  11. Tables object PersonTable: Table("person") { val id1 = integer("id1") val

    id2 = integer("id2") override val primaryKey = PrimaryKey(id1, id2, name = "PK_Person_ID") } Path: 04-exposed-ddl/02-ddl Source: Ex02_CreateTable.kt
  12. Tables object PersonTable: Table("person") { val id1 = integer("id1") val

    id2 = integer("id2") override val primaryKey = PrimaryKey(id1, id2, name = "PK_Person_ID") } @ParameterizedTest @MethodSource(ENABLE_DIALECTS_METHOD) fun `2ѐ੄ ஸۢ੉ PRIMARY KEY۽ ૑੿ػ ప੉࠶ਸ ࢤࢿ೤פ׮`(testDB: TestDB) { withDb(testDB) { val ddl = PersonTable.ddl.single() log.debug { "DDL: $ddl" } SchemaUtils.create(PersonTable) PersonTable.exists().shouldBeTrue() SchemaUtils.drop(PersonTable) } } Path: 04-exposed-ddl/02-ddl Source: Ex02_CreateTable.kt
  13. Tables object PersonTable: Table("person") { val id1 = integer("id1") val

    id2 = integer("id2") override val primaryKey = PrimaryKey(id1, id2, name = "PK_Person_ID") } @ParameterizedTest @MethodSource(ENABLE_DIALECTS_METHOD) fun `2ѐ੄ ஸۢ੉ PRIMARY KEY۽ ૑੿ػ ప੉࠶ਸ ࢤࢿ೤פ׮`(testDB: TestDB) { withDb(testDB) { val ddl = PersonTable.ddl.single() log.debug { "DDL: $ddl" } SchemaUtils.create(PersonTable) PersonTable.exists().shouldBeTrue() SchemaUtils.drop(PersonTable) } } CREATE TABLE IF NOT EXISTS person ( id1 INT, id2 INT, CONSTRAINT PK_Person_ID PRIMARY KEY (id1, id2) ) Path: 04-exposed-ddl/02-ddl Source: Ex02_CreateTable.kt
  14. ColumnDe fi nition Path: 04-exposed-ddl/02-ddl Source: Ex04_ColumnDefinition.kt val comment =

    "Amount of testers" val tester = object: Table("tester") { val amount = integer("amount") .withDefinition("COMMENT", stringLiteral(comment)) // ஸۢী ઱ࢳ ୶о }
  15. ColumnDe fi nition Path: 04-exposed-ddl/02-ddl Source: Ex04_ColumnDefinition.kt val comment =

    "Amount of testers" val tester = object: Table("tester") { val amount = integer("amount") .withDefinition("COMMENT", stringLiteral(comment)) // ஸۢী ઱ࢳ ୶о } -- MySQL 8 CREATE TABLE IF NOT EXISTS tester ( amount INT COMMENT 'Amount of testers' NOT NULL )
  16. ColumnDe fi nition Path: 04-exposed-ddl/02-ddl Source: Ex04_ColumnDefinition.kt val comment =

    "Amount of testers" val tester = object: Table("tester") { val amount = integer("amount") .withDefinition("COMMENT", stringLiteral(comment)) // ஸۢী ઱ࢳ ୶о } val tester = object: Table("tester") { val amount = integer("amount") val active = bool("active").nullable() .withDefinition("INVISIBLE") // Implicit ઑഥ द (select * from tester), ஸۢਸ ऀӣ } -- MySQL 8 CREATE TABLE IF NOT EXISTS tester ( amount INT COMMENT 'Amount of testers' NOT NULL )
  17. ColumnDe fi nition Path: 04-exposed-ddl/02-ddl Source: Ex04_ColumnDefinition.kt val comment =

    "Amount of testers" val tester = object: Table("tester") { val amount = integer("amount") .withDefinition("COMMENT", stringLiteral(comment)) // ஸۢী ઱ࢳ ୶о } val tester = object: Table("tester") { val amount = integer("amount") val active = bool("active").nullable() .withDefinition("INVISIBLE") // Implicit ઑഥ द (select * from tester), ஸۢਸ ऀӣ } -- MySQL 8 CREATE TABLE IF NOT EXISTS tester ( amount INT COMMENT 'Amount of testers' NOT NULL ) -- MySQL 8 CREATE TABLE IF NOT EXISTS tester ( amount INT NOT NULL, active BOOLEAN INVISIBLE NULL -- INVISIBLE ஸۢ )
  18. Index Path: 04-exposed-ddl/02-ddl Source: Ex05_CreateIndex.kt val tester = object: Table("tester")

    { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) // ੋؙझ ੿੄ val byName = index("tester_by_name", isUnique = false, name) }
  19. Index Path: 04-exposed-ddl/02-ddl Source: Ex05_CreateIndex.kt val tester = object: Table("tester")

    { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) // ੋؙझ ੿੄ val byName = index("tester_by_name", isUnique = false, name) } -- Postgres CREATE TABLE IF NOT EXISTS tester ( id SERIAL PRIMARY KEY, "name" VARCHAR(255) NOT NULL ); CREATE INDEX tester_by_name ON tester ("name");
  20. Index Path: 04-exposed-ddl/02-ddl Source: Ex05_CreateIndex.kt val tester = object: Table("tester")

    { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) // ੋؙझ ੿੄ val byName = index("tester_by_name", isUnique = false, name) } -- Postgres CREATE TABLE IF NOT EXISTS tester ( id SERIAL PRIMARY KEY, "name" VARCHAR(255) NOT NULL ); CREATE INDEX tester_by_name ON tester ("name"); val tester = object: Table("tester") { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) // Hash index ੿੄ val byName = index("tester_by_name", isUnique = false, name, indexType = "HASH") }
  21. Index Path: 04-exposed-ddl/02-ddl Source: Ex05_CreateIndex.kt val tester = object: Table("tester")

    { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) // ੋؙझ ੿੄ val byName = index("tester_by_name", isUnique = false, name) } -- Postgres CREATE TABLE IF NOT EXISTS tester ( id SERIAL PRIMARY KEY, "name" VARCHAR(255) NOT NULL ); CREATE INDEX tester_by_name ON tester ("name"); val tester = object: Table("tester") { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) // Hash index ੿੄ val byName = index("tester_by_name", isUnique = false, name, indexType = "HASH") } -- Postgres CREATE TABLE IF NOT EXISTS tester ( id SERIAL PRIMARY KEY, "name" VARCHAR(255) NOT NULL ); CREATE INDEX tester_by_name ON tester USING HASH ("name");
  22. Sequence Path: 04-exposed-ddl/02-ddl Source: Ex06_Sequence.kt private val myseq = org.jetbrains.exposed.v1.core.Sequence(

    name = "my_sequence", startWith = 4, incrementBy = 2, minValue = 1, maxValue = 100, cycle = true, cache = 20 ) // द௫झ ࢤࢿ SchemaUtils.createSequence(myseq)
  23. Sequence Path: 04-exposed-ddl/02-ddl Source: Ex06_Sequence.kt private val myseq = org.jetbrains.exposed.v1.core.Sequence(

    name = "my_sequence", startWith = 4, incrementBy = 2, minValue = 1, maxValue = 100, cycle = true, cache = 20 ) // द௫झ ࢤࢿ SchemaUtils.createSequence(myseq) -- Postgres CREATE SEQUENCE IF NOT EXISTS my_sequence START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20;
  24. Sequence Path: 04-exposed-ddl/02-ddl Source: Ex06_Sequence.kt private val myseq = org.jetbrains.exposed.v1.core.Sequence(

    name = "my_sequence", startWith = 4, incrementBy = 2, minValue = 1, maxValue = 100, cycle = true, cache = 20 ) // द௫झ ࢤࢿ SchemaUtils.createSequence(myseq) -- Postgres CREATE SEQUENCE IF NOT EXISTS my_sequence START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20; val tester = object: Table("tester") { val id = integer("id").autoIncrement(myseq) val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) }
  25. Sequence Path: 04-exposed-ddl/02-ddl Source: Ex06_Sequence.kt private val myseq = org.jetbrains.exposed.v1.core.Sequence(

    name = "my_sequence", startWith = 4, incrementBy = 2, minValue = 1, maxValue = 100, cycle = true, cache = 20 ) // द௫झ ࢤࢿ SchemaUtils.createSequence(myseq) -- Postgres CREATE SEQUENCE IF NOT EXISTS my_sequence START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20; val tester = object: Table("tester") { val id = integer("id").autoIncrement(myseq) val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) } -- Postgres CREATE SEQUENCE IF NOT EXISTS my_sequence START WITH 4 INCREMENT BY 2 MINVALUE 1 MAXVALUE 100 CYCLE CACHE 20; CREATE TABLE IF NOT EXISTS tester ( id INT PRIMARY KEY, "name" VARCHAR(255) NOT NULL ); INSERT INTO tester ("name", id) VALUES ('John Doe', NEXTVAL('my_sequence')); INSERT INTO tester ("name", id) VALUES ('Jane Doe', NEXTVAL('my_sequence'));
  26. Custom Enumeration Path: 04-exposed-ddl/02-ddl Source: Ex07_CustomEnumeration.kt internal enum class Status

    { ACTIVE, INACTIVE; override fun toString(): String = "Status: $name" } val tester = object: IntIdTable("tester") { var statusName = enumerationByName<Status>("status_name", 32).default(Status.ACTIVE) }
  27. Custom Enumeration Path: 04-exposed-ddl/02-ddl Source: Ex07_CustomEnumeration.kt internal enum class Status

    { ACTIVE, INACTIVE; override fun toString(): String = "Status: $name" } -- Postgres CREATE TABLE IF NOT EXISTS tester ( id SERIAL PRIMARY KEY, status_name VARCHAR(32) DEFAULT 'ACTIVE' NOT NULL ) val tester = object: IntIdTable("tester") { var statusName = enumerationByName<Status>("status_name", 32).default(Status.ACTIVE) }
  28. SELECT val row = users.selectAll() .where { users.id eq "andrey"

    } .andWhere { users.name.isNotNull() } .single() Path: 05-exposed-dml/01-dml Source: Ex01_Select.kt val r1 = users .selectAll() .where { users.id inList listOf("andrey", "alex") } .orderBy(users.name) .toList()
  29. SELECT val row = users.selectAll() .where { users.id eq "andrey"

    } .andWhere { users.name.isNotNull() } .single() Path: 05-exposed-dml/01-dml Source: Ex01_Select.kt -- Postgres SELECT users.id, users."name", users.city_id, users.flags FROM users WHERE (users.id = 'andrey') AND (users."name" IS NOT NULL) val r1 = users .selectAll() .where { users.id inList listOf("andrey", "alex") } .orderBy(users.name) .toList()
  30. SELECT val row = users.selectAll() .where { users.id eq "andrey"

    } .andWhere { users.name.isNotNull() } .single() Path: 05-exposed-dml/01-dml Source: Ex01_Select.kt -- Postgres SELECT users.id, users."name", users.city_id, users.flags FROM users WHERE (users.id = 'andrey') AND (users."name" IS NOT NULL) val r1 = users .selectAll() .where { users.id inList listOf("andrey", "alex") } .orderBy(users.name) .toList() -- Postgres SELECT users.id, users."name", users.city_id, users.flags FROM users WHERE users.id IN ('andrey', 'alex') ORDER BY users."name" ASC
  31. INSERT val id1 = idTable.insertAndGetId { it[name] = "name-1" }

    val id3 = idTable.insert { it[name] = "name-3" } get idTable.id Path: 05-exposed-dml/01-dml Source: Ex02_Insert.kt val cityNames = listOf("Paris", "Moscow", "Helsinki") val allCitiesIDs: List<ResultRow> = cities.batchInsert(cityNames) { name -> this[cities.name] = name } val idTable = object: IntIdTable("tmp") { val name = varchar("foo", 10).uniqueIndex() }
  32. INSERT val id1 = idTable.insertAndGetId { it[name] = "name-1" }

    val id3 = idTable.insert { it[name] = "name-3" } get idTable.id Path: 05-exposed-dml/01-dml Source: Ex02_Insert.kt -- Postgres INSERT INTO TMP (FOO) VALUES (‘name-1'); INSERT INTO TMP (FOO) VALUES (‘name-3’); val cityNames = listOf("Paris", "Moscow", "Helsinki") val allCitiesIDs: List<ResultRow> = cities.batchInsert(cityNames) { name -> this[cities.name] = name } val idTable = object: IntIdTable("tmp") { val name = varchar("foo", 10).uniqueIndex() }
  33. INSERT val id1 = idTable.insertAndGetId { it[name] = "name-1" }

    val id3 = idTable.insert { it[name] = "name-3" } get idTable.id Path: 05-exposed-dml/01-dml Source: Ex02_Insert.kt -- Postgres INSERT INTO TMP (FOO) VALUES (‘name-1'); INSERT INTO TMP (FOO) VALUES (‘name-3’); val cityNames = listOf("Paris", "Moscow", "Helsinki") val allCitiesIDs: List<ResultRow> = cities.batchInsert(cityNames) { name -> this[cities.name] = name } -- Postgres INSERT INTO cities ("name") VALUES ('Paris'); INSERT INTO cities ("name") VALUES ('Moscow'); INSERT INTO cities ("name") VALUES ('Helsinki'); val idTable = object: IntIdTable("tmp") { val name = varchar("foo", 10).uniqueIndex() }
  34. UPDATE val newName = "Alexey" users.update(where = { users.id eq

    alexId }) { it[users.name] = newName } Path: 05-exposed-dml/01-dml Source: Ex03_Update.kt val join = users.innerJoin(userData) join.update { it[userData.comment] = users.name it[userData.value] = 123 } users.update(where = { users.id like "a%" }, limit = 1) { it[users.id] = "NewName" }
  35. UPDATE val newName = "Alexey" users.update(where = { users.id eq

    alexId }) { it[users.name] = newName } Path: 05-exposed-dml/01-dml Source: Ex03_Update.kt -- Postgres UPDATE users SET "name"='Alexey' WHERE users.id = 'alex' val join = users.innerJoin(userData) join.update { it[userData.comment] = users.name it[userData.value] = 123 } users.update(where = { users.id like "a%" }, limit = 1) { it[users.id] = "NewName" }
  36. UPDATE val newName = "Alexey" users.update(where = { users.id eq

    alexId }) { it[users.name] = newName } Path: 05-exposed-dml/01-dml Source: Ex03_Update.kt -- Postgres UPDATE users SET "name"='Alexey' WHERE users.id = 'alex' val join = users.innerJoin(userData) join.update { it[userData.comment] = users.name it[userData.value] = 123 } users.update(where = { users.id like "a%" }, limit = 1) { it[users.id] = "NewName" } -- MySQL V8 UPDATE Users SET id='NewName' WHERE Users.id LIKE 'a%' LIMIT 1
  37. UPDATE val newName = "Alexey" users.update(where = { users.id eq

    alexId }) { it[users.name] = newName } Path: 05-exposed-dml/01-dml Source: Ex03_Update.kt -- Postgres UPDATE users SET "name"='Alexey' WHERE users.id = 'alex' val join = users.innerJoin(userData) join.update { it[userData.comment] = users.name it[userData.value] = 123 } -- Postgres UPDATE userdata SET "comment"=users."name", "value"=123 FROM users WHERE users.id = userdata.user_id users.update(where = { users.id like "a%" }, limit = 1) { it[users.id] = "NewName" } -- MySQL V8 UPDATE Users SET id='NewName' WHERE Users.id LIKE 'a%' LIMIT 1
  38. UPSERT private object Words: Table("words") { val word = varchar("name",

    64).uniqueIndex() val count = integer("count").default(1) } Path: 05-exposed-dml/01-dml Source: Ex04_Upsert.kt val wordA = Words.upsert { it[word] = "A" it[count] = 10 } get Words.word Words.upsert { it[word] = wordA // "A" it[count] = 9 }
  39. UPSERT private object Words: Table("words") { val word = varchar("name",

    64).uniqueIndex() val count = integer("count").default(1) } Path: 05-exposed-dml/01-dml Source: Ex04_Upsert.kt -- Postgres CREATE TABLE IF NOT EXISTS words ( "name" VARCHAR(64) NOT NULL, "count" INT DEFAULT 1 NOT NULL ); ALTER TABLE words ADD CONSTRAINT words_name_unique UNIQUE ("name"); val wordA = Words.upsert { it[word] = "A" it[count] = 10 } get Words.word Words.upsert { it[word] = wordA // "A" it[count] = 9 }
  40. UPSERT private object Words: Table("words") { val word = varchar("name",

    64).uniqueIndex() val count = integer("count").default(1) } Path: 05-exposed-dml/01-dml Source: Ex04_Upsert.kt -- Postgres CREATE TABLE IF NOT EXISTS words ( "name" VARCHAR(64) NOT NULL, "count" INT DEFAULT 1 NOT NULL ); ALTER TABLE words ADD CONSTRAINT words_name_unique UNIQUE ("name"); val wordA = Words.upsert { it[word] = "A" it[count] = 10 } get Words.word Words.upsert { it[word] = wordA // "A" it[count] = 9 } -- Postgres INSERT INTO words ("name", "count") VALUES ('A', 10) ON CONFLICT ("name") DO UPDATE SET "count"=EXCLUDED."count"
  41. UPSERT private object Words: Table("words") { val word = varchar("name",

    64).uniqueIndex() val count = integer("count").default(1) } Path: 05-exposed-dml/01-dml Source: Ex04_Upsert.kt -- Postgres CREATE TABLE IF NOT EXISTS words ( "name" VARCHAR(64) NOT NULL, "count" INT DEFAULT 1 NOT NULL ); ALTER TABLE words ADD CONSTRAINT words_name_unique UNIQUE ("name"); val wordA = Words.upsert { it[word] = "A" it[count] = 10 } get Words.word Words.upsert { it[word] = wordA // "A" it[count] = 9 } -- Postgres INSERT INTO words ("name", "count") VALUES ('A', 10) ON CONFLICT ("name") DO UPDATE SET "count"=EXCLUDED."count" -- Postgres INSERT INTO words ("name", "count") VALUES ('A', 9) ON CONFLICT ("name") DO UPDATE SET "count"=EXCLUDED."count"
  42. DELETE // DELETE FROM users WHERE users."name" LIKE '%thing' users.deleteWhere

    { users.name like "%thing" } Path: 05-exposed-dml/01-dml Source: Ex05_Delete.kt // ୭؀ 1ѐ݅ ࢏ઁೠ׮. userData.deleteWhere(limit = 1) { userData.value eq 20 }
  43. DELETE // DELETE FROM users WHERE users."name" LIKE '%thing' users.deleteWhere

    { users.name like "%thing" } Path: 05-exposed-dml/01-dml Source: Ex05_Delete.kt val join = users innerJoin userData val query1 = join.selectAll().where { userData.userId like "%ey" } query1.count() shouldBeGreaterThan 0L // ೯੉ ઓ੤ೠ׮. join.delete(userData) { userData.userId like "%ey" } shouldBeEqualTo 1 // ୭؀ 1ѐ݅ ࢏ઁೠ׮. userData.deleteWhere(limit = 1) { userData.value eq 20 }
  44. DELETE // DELETE FROM users WHERE users."name" LIKE '%thing' users.deleteWhere

    { users.name like "%thing" } Path: 05-exposed-dml/01-dml Source: Ex05_Delete.kt -- Postgres DELETE FROM userdata USING users WHERE users.id = userdata.user_id AND userdata.user_id LIKE '%ey' val join = users innerJoin userData val query1 = join.selectAll().where { userData.userId like "%ey" } query1.count() shouldBeGreaterThan 0L // ೯੉ ઓ੤ೠ׮. join.delete(userData) { userData.userId like "%ey" } shouldBeEqualTo 1 // ୭؀ 1ѐ݅ ࢏ઁೠ׮. userData.deleteWhere(limit = 1) { userData.value eq 20 }
  45. DELETE // DELETE FROM users WHERE users."name" LIKE '%thing' users.deleteWhere

    { users.name like "%thing" } Path: 05-exposed-dml/01-dml Source: Ex05_Delete.kt -- Postgres DELETE FROM userdata USING users WHERE users.id = userdata.user_id AND userdata.user_id LIKE '%ey' val join = users innerJoin userData val query1 = join.selectAll().where { userData.userId like "%ey" } query1.count() shouldBeGreaterThan 0L // ೯੉ ઓ੤ೠ׮. join.delete(userData) { userData.userId like "%ey" } shouldBeEqualTo 1 // ୭؀ 1ѐ݅ ࢏ઁೠ׮. userData.deleteWhere(limit = 1) { userData.value eq 20 } val towns = cities.alias("towns") val people = users.alias("people") val stats = userData.alias("stats") val aliasedJoin = Join(towns) .innerJoin(people, { towns[cities.id] }, { people[users.cityId] }) .innerJoin(stats, { people[users.id] }, { stats[userData.userId] }) val query = aliasedJoin .selectAll() .where { towns[cities.name] eq "Munich" } // ೯ਸ ࢏ઁೠ׮. aliasedJoin.delete(stats) { towns[cities.name] eq "Munich" }
  46. DELETE // DELETE FROM users WHERE users."name" LIKE '%thing' users.deleteWhere

    { users.name like "%thing" } Path: 05-exposed-dml/01-dml Source: Ex05_Delete.kt -- Postgres DELETE FROM userdata USING users WHERE users.id = userdata.user_id AND userdata.user_id LIKE '%ey' val join = users innerJoin userData val query1 = join.selectAll().where { userData.userId like "%ey" } query1.count() shouldBeGreaterThan 0L // ೯੉ ઓ੤ೠ׮. join.delete(userData) { userData.userId like "%ey" } shouldBeEqualTo 1 // ୭؀ 1ѐ݅ ࢏ઁೠ׮. userData.deleteWhere(limit = 1) { userData.value eq 20 } -- Postgres DELETE FROM userdata stats USING cities towns, users people WHERE towns.city_id = people.city_id AND people.id = stats.user_id AND towns."name" = 'Munich' val towns = cities.alias("towns") val people = users.alias("people") val stats = userData.alias("stats") val aliasedJoin = Join(towns) .innerJoin(people, { towns[cities.id] }, { people[users.cityId] }) .innerJoin(stats, { people[users.id] }, { stats[userData.userId] }) val query = aliasedJoin .selectAll() .where { towns[cities.name] eq "Munich" } // ೯ਸ ࢏ઁೠ׮. aliasedJoin.delete(stats) { towns[cities.name] eq "Munich" }
  47. Window Functions Window Functions (ebook) Path: 05-exposed-dml/03-functions Source: Ex05_WindowFunctions.kt ೣࣻݺ

    ࢸݺ ࠺Ҋ rowNumber() ਦب਋ ղ ࣽର ߣഐ ࠗৈ ઺ࠂ হযঠ ೡ ٸ ਬਊ rank() ҕز ࣽਤ ࠗৈ (઺ࠂ ࢤӝݶ Ѥցڭ) 1, 2, 2, 4 ࣽࢲ denseRank() ҕز ࣽਤ ࠗৈ, ߣഐ Ѥցڪ૑ ঋ਺ 1, 2, 2, 3 ࣽࢲ percentRank() ч੄ ࢚؀੸ ࣽਤ(%) (RANK - 1) / (N - 1) ҅࢑ cumeDist() ־੸ ࠺ਯ (Cumulative Distribution) i/N ഋध ࠺ਯ ntile(scale) ࠺ਯ ӝ߈ Ӓܛ ա־ӝ ؘ੉ఠܳ scale ѐ੄ Ӓܛ(bucket) ਵ۽ Ӑ١ೞѱ աׇࢲ п ೯੉ ࣘೠ Ӓܛਸ ߈ ജ lag(expr) അ੤ ೯ ӝળ ੉੹ ೯੄ ч ߈ജ ੉੹ ч ࠺Үী ࢎਊ lead(expr) അ੤ ೯ ӝળ ׮਺ ೯੄ ч ߈ജ ׮਺ ч ৘ஏ ١ firstValue() ਦب਋ ղ੄ ୐ߣ૩ ೯ ਦب਋੄ ୐ߣ૩ ೯੄ ч lastValue() ਦب਋ ղ੄ ૑Ә ೯ө૑ ઺ ݃૑݄ ೯ ਦب਋ ղ੄ അ੤ ೯ө૑੄ ݃૑݄ ೯੄ ч (૊ അ੤ ೯੄ ч)
  48. Window Functions Window Functions (ebook) Path: 05-exposed-dml/03-functions Source: Ex05_WindowFunctions.kt ೣࣻݺ

    ࢸݺ ࠺Ҋ rowNumber() ਦب਋ ղ ࣽର ߣഐ ࠗৈ ઺ࠂ হযঠ ೡ ٸ ਬਊ rank() ҕز ࣽਤ ࠗৈ (઺ࠂ ࢤӝݶ Ѥցڭ) 1, 2, 2, 4 ࣽࢲ denseRank() ҕز ࣽਤ ࠗৈ, ߣഐ Ѥցڪ૑ ঋ਺ 1, 2, 2, 3 ࣽࢲ percentRank() ч੄ ࢚؀੸ ࣽਤ(%) (RANK - 1) / (N - 1) ҅࢑ cumeDist() ־੸ ࠺ਯ (Cumulative Distribution) i/N ഋध ࠺ਯ ntile(scale) ࠺ਯ ӝ߈ Ӓܛ ա־ӝ ؘ੉ఠܳ scale ѐ੄ Ӓܛ(bucket) ਵ۽ Ӑ١ೞѱ աׇࢲ п ೯੉ ࣘೠ Ӓܛਸ ߈ ജ lag(expr) അ੤ ೯ ӝળ ੉੹ ೯੄ ч ߈ജ ੉੹ ч ࠺Үী ࢎਊ lead(expr) അ੤ ೯ ӝળ ׮਺ ೯੄ ч ߈ജ ׮਺ ч ৘ஏ ١ firstValue() ਦب਋ ղ੄ ୐ߣ૩ ೯ ਦب਋੄ ୐ߣ૩ ೯੄ ч lastValue() ਦب਋ ղ੄ ૑Ә ೯ө૑ ઺ ݃૑݄ ೯ ਦب਋ ղ੄ അ੤ ೯ө૑੄ ݃૑݄ ೯੄ ч (૊ അ੤ ೯੄ ч) ঱ઁ ࢎਊೡө? • ࣽਤ ݒӝӝ: ѱद౸ ੋӝӖ, ੼ࣻ ࣽਤ ١ → rowNumber, rank, denseRank • ߸ച ୶੸: ੉੹/׮਺ (೯੄) ч ࠺Ү → lag, lead • ా҅/࠺ਯ ࠙ࢳ: ࠙ನ ഛੋ → percentRank, cumeDist
  49. Window Functions Window Functions (ebook) Path: 05-exposed-dml/03-functions Source: Ex05_WindowFunctions.kt ೣࣻݺ

    Exposed DSL ৘द ࢎਊ ࢚ട / ݾ੸ rowNumber() rowNumber().over().orderBy(sales.amount) ࣽର ߣഐ ݒӝӝ (઺ࠂ হ਺) rank() rank().over().partitionBy(sales.year).orderBy(sales.amount.desc()) ҕز ࣽਤ ࠗৈ (1,2,2,4) denseRank() denseRank().over().partitionBy(sales.product).orderBy(sales.amount) ҕز ࣽਤ, ߣഐ Ѥցڪ૑ ঋ਺ (1,2,2,3) percentRank() percentRank().over().orderBy(users.score.desc()) ࢚؀੸ ࣽਤ ࠺ਯ (0~1) cumeDist() cumeDist().over().partitionBy(users.group).orderBy(users.score.desc()) ־੸ ࠙ನ ࠺ਯ (࢚ਤ ݻ ಌࣃ౟) lag(expr) lag(users.score).over().orderBy(users.date) ੉੹ ೯੄ ч ଵઑ (߸ച ୶੸) lead(expr) lead(users.score).over().partitionBy(users.group).orderBy(users.date) ׮਺ ೯੄ ч ଵઑ (৘ஏ, ࠺Ү)
  50. DSL vs DAO о ইפۄ DSL and DAO 항목 DSL

    DAO 주용도 검색/집계/조건 조합 관계 탐색/모델 표현 강점 SQL 가시성, 명시적 제어 객체 그래프 표현, 모델 가독성 주의점 쿼리 코드가 길어질 수 있음 로딩 전략/N+1 의식 필요 추천 상황 복잡한 조회, 벌크 연산 CRUD + 연관관계 탐색
  51. TABLE, Entity, EntityClass • Table - झః݃ ੿੄ • Entity

    - ё୓ ݽ؛ ઁҕ • EntityClass - ࢤࢿ/ઑഥ ೣࣻ ઁҕ
  52. JPA, Exposed DSL, DAO ೣࣻ ࠺Ү JPA DSL DAO INSERT

    em.persist(e) Table.insert { } Entity.new { } Find By Id em.find(E, id) Table.selectAll().where { id eq x } Entity.findById(id) UPDATE flush() Table.update({ઑѤ}) { } entity.prop = value DELETE em.remove(e) Table.deleteWhere { } entity.delete()
  53. Entity - referencedOn, referrersOn Path: 05-exposed-dml/05-entities Source: Ex04_LongIdTableEntity.kt class City(id:

    EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<City>(Cities) var name: String by Cities.name // one-to-many ҙ҅ val towns: SizedIterable<Town> by Town referrersOn Towns.cityId } class Town(id: EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<Town>(Towns) // many-to-one ҙ҅ var city: City by City referencedOn Towns.cityId } class Person(id: EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<Person>(People) var name: String by People.name // many-to-one ҙ҅ var city: City by City referencedOn People.cityId }
  54. Entity - referencedOn, referrersOn Path: 05-exposed-dml/05-entities Source: Ex04_LongIdTableEntity.kt class City(id:

    EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<City>(Cities) var name: String by Cities.name // one-to-many ҙ҅ val towns: SizedIterable<Town> by Town referrersOn Towns.cityId } class Town(id: EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<Town>(Towns) // many-to-one ҙ҅ var city: City by City referencedOn Towns.cityId } class Person(id: EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<Person>(People) var name: String by People.name // many-to-one ҙ҅ var city: City by City referencedOn People.cityId } @OneToMany -> ࠗݽী referrersOn @ManyToOne -> ੗धী referencedOn
  55. Entity - referencedOn, referrersOn UUIDEntity Path: 05-exposed-dml/05-entities Source: Ex04_UUIDTableEntity.kt class

    City(id: EntityID<UUID>): UUIDEntity(id) { companion object: UUIDEntityClass<City>(Cities) var name: String by Cities.name val towns: SizedIterable<Town> by Town referrersOn Towns.cityId // one-to-many } class Person(id: EntityID<UUID>): UUIDEntity(id) { companion object: UUIDEntityClass<Person>(People) var name: String by People.name var city: City by City referencedOn People.cityId } class Town(id: EntityID<UUID>): UUIDEntity(id) { companion object: UUIDEntityClass<Town>(Towns) var city: City by City referencedOn Towns.cityId // many-to-one } class Address(id: EntityID<UUID>): UUIDEntity(id) { companion object: UUIDEntityClass<Address>(Addresses) var person: Person by Person referencedOn Addresses.personId // many-to-one var city: City by City referencedOn Addresses.cityId // many-to-one var address: String by Addresses.address }
  56. Entity - fi eld with transform Path: 05-exposed-dml/05-entities Source: Ex08_EntityFieldWithTransform.kt

    object TableWithTransformss: IntIdTable() { // `transform` ೣࣻܳ ࢎਊೞৈ DB ஸۢҗ ূ౭౭ ೙٘੄ ࣻഋਸ ݏ୸׮. val value: Column<BigDecimal> = varchar("value", 50) .transform( wrap = { it.toBigDecimal() }, // colum -> field unwrap = { it.toString() }, // field -> column ) } class TableWithTransformEntity(id: EntityID<Int>): IntEntity(id) { companion object: IntEntityClass<TableWithTransformEntity>(TableWithTransforms) // `transform` ೣࣻܳ ࢎਊೞৈ DB ஸۢ (string) -> DSL ࣘࢿ (BigDecimal) -> ূ౭౭ ೙٘ (Int) ੄ ࣻഋਸ ߸ജೠ׮ var value: Int by TableWithTransforms.value .transform( wrap = { it.toInt() }, unwrap = { it.toBigDecimal() }, ) }
  57. Exposed extension library • exposed-crypt • exposed-javatime, expoed-kotlin—datetime • exposed-json

    (kotlinx-serialization) • JSON, JSONB Column • exposed-money • Java Money (Moneta) - ࣻ۝җ ױਤ۽ ಴അ
  58. Exposed custom library Bluetape4k Library • exposed-core, exposed-dao • exposed-tink

    • exposed-jackson2, exposed-jackson3, exposed-fastjson2 • exposed-measured • exposed-jdbc-lettuce, exposed-jdbc-redisson • exposed-postgresql, exposed-mysql8 • exposed-bigquery, exposed-duckdb
  59. Custom Columns private object T1: IntIdTable() { val name =

    varchar("name", 50) val lz4Fory = binarySerializedBlob<Embeddable>("lz4_fory", BinarySerializers.LZ4Fory).nullable() val lz4Kryo = binarySerializedBlob<Embeddable>("lz4_kryo", BinarySerializers.LZ4Kryo).nullable() val zstdFory = binarySerializedBlob<Embeddable2>("zstd_fory", BinarySerializers.ZstdFory).nullable() val zstdKryo = binarySerializedBlob<Embeddable2>("zstd_kryo", BinarySerializers.ZstdKryo).nullable() } class E1(id: EntityID<Int>): IntEntity(id) { companion object: IntEntityClass<E1>(T1) var name by T1.name var lz4Fory by T1.lz4Fory var lz4Kryo by T1.lz4Kryo var zstdFory by T1.zstdFory var zstdKryo by T1.zstdKryo override fun equals(other: Any?): Boolean = idEquals(other) override fun hashCode(): Int = idHashCode() override fun toString(): String = entityToStringBuilder() .add("name", name) .toString() } data class Embeddable( val name: String, val age: Int, val address: String, ): Serializable Path: 06-advanced/06-custom-columns Source: BinarySerializedBlobColumnTypeTest.kt data class Embeddable2( val name: String, val age: Int, val address: String, val zipcode: String, ): Serializable
  60. Custom Columns val embedded = Embeddable("Alice", 20, "Seoul") val embedded2

    = Embeddable2("John", 30, "Seoul", "12914") val id = T1.insertAndGetId { it[T1.name] = "Alice" it[T1.lz4Fory] = embedded it[T1.lz4Kryo] = embedded it[T1.zstdFory] = embedded2 it[T1.zstdKryo] = embedded2 } entityCache.clear() val row = T1.selectAll().where { T1.id eq id }.single() row[T1.id] shouldBeEqualTo id row[T1.lz4Fory] shouldBeEqualTo embedded row[T1.lz4Kryo] shouldBeEqualTo embedded row[T1.zstdFory] shouldBeEqualTo embedded2 row[T1.zstdKryo] shouldBeEqualTo embedded2 Path: 06-advanced/06-custom-columns Source: BinarySerializedBlobColumnTypeTest.kt val embedded = Embeddable("Alice", 20, "Seoul") val embedded2 = Embeddable2("John", 30, "Seoul", “12914") val e1 = E1.new { name = "Alice" lz4Fory = embedded lz4Kryo = embedded zstdFory = embedded2 zstdKryo = embedded2 } entityCache.clear() val loaded = E1.findById(e1.id).shouldNotBeNull() loaded shouldBeEqualTo e1 loaded.lz4Fory shouldBeEqualTo embedded loaded.lz4Kryo shouldBeEqualTo embedded loaded.zstdFory shouldBeEqualTo embedded2 loaded.zstdKryo shouldBeEqualTo embedded2
  61. Custom Sortable Id Table open class TimebasedUUIDTable(name: String = “",

    columnName: String = "id"): IdTable<UUID>(name) { /** * UUID v7 ਸ Client ীࢲ ࢤࢿ೤פ׮. */ final override val id: Column<EntityID<UUID>> = javaUUID(columnName).timebasedGenerated().entityId() /** ప੉࠶ ӝࠄః ੿੄ੑפ׮. */ final override val primaryKey = PrimaryKey(id) } Path: 06-advanced/07-custom-entities Source: TimebasedUUIDTableTest.kt
  62. Custom Sortable Id Table open class TimebasedUUIDTable(name: String = “",

    columnName: String = "id"): IdTable<UUID>(name) { /** * UUID v7 ਸ Client ীࢲ ࢤࢿ೤פ׮. */ final override val id: Column<EntityID<UUID>> = javaUUID(columnName).timebasedGenerated().entityId() /** ప੉࠶ ӝࠄః ੿੄ੑפ׮. */ final override val primaryKey = PrimaryKey(id) } Path: 06-advanced/07-custom-entities Source: TimebasedUUIDTableTest.kt fun Column<UUID>.timebasedGenerated(): Column<UUID> = clientDefault { Uuid.V7.nextId() }
  63. Custom Sortable Id Table open class TimebasedUUIDTable(name: String = “",

    columnName: String = "id"): IdTable<UUID>(name) { /** * UUID v7 ਸ Client ীࢲ ࢤࢿ೤פ׮. */ final override val id: Column<EntityID<UUID>> = javaUUID(columnName).timebasedGenerated().entityId() /** ప੉࠶ ӝࠄః ੿੄ੑפ׮. */ final override val primaryKey = PrimaryKey(id) } Path: 06-advanced/07-custom-entities Source: TimebasedUUIDTableTest.kt object T1: TimebasedUUIDTable("t_timebased_uuid") { val name = varchar("name", 255) val age = integer("age") } class E1(id: TimebasedUUIDEntityID): TimebasedUUIDEntity(id) { companion object: TimebasedUUIDEntityClass<E1>(T1) var name by T1.name var age by T1.age } fun Column<UUID>.timebasedGenerated(): Column<UUID> = clientDefault { Uuid.V7.nextId() }
  64. Custom Sortable Id Table @ParameterizedTest(name = "{0} - {1}ѐ ۨ௏٘")

    @MethodSource(GET_TESTDB_AND_ENTITY_COUNT) fun `TimebasedUUID idܳ о૓ ۨ௏٘ܳ ߓ஖۽ ࢤࢿೠ׮`(testDB: TestDB, recordCount: Int) { withTables(testDB, T1) { val records = List(recordCount) { Record( name = faker.name().fullName(), age = Random.nextInt(10, 80) ) } records.chunked(100).forEach { chunk -> T1.batchInsert(chunk, shouldReturnGeneratedValues = false) { this[T1.name] = it.name this[T1.age] = it.age } } T1.selectAll().count() shouldBeEqualTo recordCount.toLong() } } Path: 06-advanced/07-custom-entities Source: TimebasedUUIDTableTest.kt
  65. ஸۢ ঐഐച ߂ Ѩ࢝ ࠺Ѿ੿੸ ঐഐച ߑध (AEAD ߑध) val

    secretTable = object: IntIdTable("tink_aead_table") { val secret = tinkAeadVarChar("secret", 512, TinkAeads.AES256_GCM).nullable() val data = tinkAeadBinary("data", 512, TinkAeads.AES256_GCM).nullable() val blob = tinkAeadBlob("blob", TinkAeads.AES256_GCM).nullable() } Path: 06-advanced/12-exposed-tink Source: TinkColumnTypeTest.kt withTables(testDB, secretTable) { val insertedSecret = faker.lorem().sentence() val insertedData = faker.lorem().sentence() val insertedBlob = faker.lorem().sentence() val id = secretTable.insertAndGetId { it[secret] = insertedSecret it[data] = insertedData.toUtf8Bytes() it[blob] = insertedBlob.toUtf8Bytes() } secretTable.selectAll().count() shouldBeEqualTo 1L val row = secretTable.selectAll().where { secretTable.id eq id }.single() row[secretTable.secret] shouldBeEqualTo insertedSecret row[secretTable.data]!!.toUtf8String() shouldBeEqualTo insertedData row[secretTable.blob]!!.toUtf8String() shouldBeEqualTo insertedBlob }
  66. ஸۢ ঐഐച ߂ Ѩ࢝ Ѿ੿੸ ঐഐച ߑध (DAEAD ߑध) val

    searchableTable = object: IntIdTable("tink_daead_table") { val email = tinkDaeadVarChar("email", 512, TinkDaeads.AES256_SIV).nullable().index() val fingerprint = tinkDaeadBinary("fingerprint", 256, TinkDaeads.AES256_SIV).nullable() val blob = tinkDaeadBlob("blob", TinkDaeads.AES256_SIV).nullable() } Path: 06-advanced/12-exposed-tink Source: TinkColumnTypeTest.kt /** * DAEAD(Ѿ੿੸ ঐഐച)ח WHERE ੺۽ Ѩ࢝੉ оמ೤פ׮. * ```sql * SELECT COUNT(*) FROM tink_daead_table WHERE tink_daead_table.email = '<ঐഐޙ>' * ``` */ searchableTable.selectAll() .where { searchableTable.email eq insertedEmail } .count() shouldBeEqualTo 1L searchableTable.selectAll() .where { searchableTable.fingerprint eq insertedFingerprint.toUtf8Bytes() } .count() shouldBeEqualTo 1L searchableTable.selectAll() .where { searchableTable.blob eq insertedBlob.toUtf8Bytes() } .count() shouldBeEqualTo 1L
  67. Basic JPA Entities CompositeId Entity object Publishers: CompositeIdTable("publishers") { val

    pubId = integer("pub_id").autoIncrement().entityId() val isbn = javaUUID("isbn_code").autoGenerate().entityId() val name = varchar("publisher_name", 32) override val primaryKey = PrimaryKey(pubId, isbn) } Path: 07-jpa/01-convert-jpa-basic Source: ex04_compositeId/Ex01_CompositeId class Publisher(id: EntityID<CompositeID>): CompositeEntity(id) { companion object: CompositeEntityClass<Publisher>(Publishers) { fun new(isbn: UUID, init: Publisher.() -> Unit): Publisher { // pubId ח autoIncrement ੉޲۽, isbn ݅ਵ۽ CompositeID ܳ ࢤࢿ val compositeId = CompositeID { it[Publishers.isbn] = isbn } return Publisher.new(compositeId) { init() } } } var name: String by Publishers.name val authors: SizedIterable<Author> by Author referrersOn Authors // one-to-many val office: Office? by Office optionalBackReferencedOn Offices // one-to-one val allOffices: SizedIterable<Office> by Office optionalReferrersOn Offices // one-to-many }
  68. Basic JPA Entities Many-To-Many Relations object BankAccountTable: IntIdTable("bank_account") { val

    number = varchar("number", 255).uniqueIndex() } Path: 07-jpa/01-convert-jpa-basic Source: ex05_relations/BankSchema.kt object AccountOwnerTable: IntIdTable("account_owner") { val ssn = varchar("ssn", 255).uniqueIndex() } object OwnerAccountMapTable: Table("owner_account_map") { val ownerId = reference("owner_id", AccountOwnerTable) val accountId = reference("account_id", BankAccountTable) init { uniqueIndex(ownerId, accountId) } } class BankAccount(id: EntityID<Int>): IntEntity(id) { companion object: IntEntityClass<BankAccount>(BankAccountTable) var number: String by BankAccountTable.number // many to many with via val owners: SizedIterable<AccountOwner> by AccountOwner via OwnerAccountMapTable } class AccountOwner(id: EntityID<Int>): IntEntity(id) { companion object: IntEntityClass<AccountOwner>(AccountOwnerTable) var ssn: String by AccountOwnerTable.ssn // many to many with via val accounts: SizedIterable<BankAccount> by BankAccount via OwnerAccountMapTable }
  69. Advanced JPA Entities Tree - Self Reference Table Path: 07-jpa/02-convert-jpa-advanced

    Source: ex04_tree/TreeNodeSchema object TreeNodeTable: LongIdTable("tree_nodes") { val title = varchar("title", 255) val description = text("description").nullable() val depth = integer("depth").default(0) // ೐۽؋࣌ ӂ੢: optReference(..., onDelete = ReferenceOption.CASCADE) val parentId = optReference("parent_id", TreeNodeTable) } class TreeNode(id: EntityID<Long>): LongEntity(id) { companion object: LongEntityClass<TreeNode>(TreeNodeTable) { override fun new(init: TreeNode.() -> Unit): TreeNode { val node = super.new { } node.init() node.depth = (node.parent?.depth ?: 0) + 1 return node } } var title by TreeNodeTable.title var description by TreeNodeTable.description var depth by TreeNodeTable.depth var parent: TreeNode? by TreeNode optionalReferencedOn TreeNodeTable.parentId // ੗ध ֢٘ ઑഥ val children: SizedIterable<TreeNode> get() = TreeNode.find { TreeNodeTable.parentId eq id // ੗ध ֢٘ܳ ੤ӈ੸ਵ۽ ࢏ઁ೤פ׮. // ೐۽؋࣌ীࢲח parentId ஸۢী onDelete = ReferenceOption.CASCADE ࢸ੿ਸ ӂ੢೤פ׮ fun deleteDescendants() { children.forEach { it.deleteDescendants() } delete() } }
  70. Advanced JPA Entities Tree - Self Reference Table Path: 07-jpa/02-convert-jpa-advanced

    Source: ex04_tree/TreeNodeSchema buildTreeNodes() val parent = TreeNodeTable.alias("parent") val child = TreeNodeTable.alias("child") val join = parent.innerJoin(child) { parent[TreeNodeTable.id] eq child[TreeNodeTable.parentId] } val titles = join .select(parent[TreeNodeTable.title], child[TreeNodeTable.title]) .where { parent[TreeNodeTable.title] eq "child1" } .map { row -> row[parent[TreeNodeTable.title]] to row[child[TreeNodeTable.title]] } titles shouldHaveSize 2 titles.forEach { log.debug { "parent: ${it.first}, child: ${it.second}" } it.first shouldBeEqualTo "child1" it.second shouldStartWith "grand" }
  71. Advanced JPA Entities Tree - Self Reference Table Path: 07-jpa/02-convert-jpa-advanced

    Source: ex04_tree/TreeNodeSchema buildTreeNodes() val parent = TreeNodeTable.alias("parent") val child = TreeNodeTable.alias("child") val join = parent.innerJoin(child) { parent[TreeNodeTable.id] eq child[TreeNodeTable.parentId] } val titles = join .select(parent[TreeNodeTable.title], child[TreeNodeTable.title]) .where { parent[TreeNodeTable.title] eq "child1" } .map { row -> row[parent[TreeNodeTable.title]] to row[child[TreeNodeTable.title]] } titles shouldHaveSize 2 titles.forEach { log.debug { "parent: ${it.first}, child: ${it.second}" } it.first shouldBeEqualTo "child1" it.second shouldStartWith "grand" } SELECT parent.title, child.title FROM tree_nodes parent INNER JOIN tree_nodes child ON (parent.id = child.parent_id) WHERE parent.title = 'child1'
  72. Advanced JPA Entities Tree - Self Reference Table Path: 07-jpa/02-convert-jpa-advanced

    Source: ex04_tree/TreeNodeSchema buildTreeNodes() val sub = TreeNodeTable.alias("sub") val subQuery = sub .select(sub[TreeNodeTable.parentId]) .where { sub[TreeNodeTable.title] like "grand%" } val query = TreeNodeTable .selectAll() .where { TreeNodeTable.id inSubQuery subQuery } val nodes = TreeNode.wrapRows(query).toList() nodes shouldHaveSize 1 nodes.single().title shouldBeEqualTo "child1"
  73. Advanced JPA Entities Tree - Self Reference Table Path: 07-jpa/02-convert-jpa-advanced

    Source: ex04_tree/TreeNodeSchema buildTreeNodes() val sub = TreeNodeTable.alias("sub") val subQuery = sub .select(sub[TreeNodeTable.parentId]) .where { sub[TreeNodeTable.title] like "grand%" } val query = TreeNodeTable .selectAll() .where { TreeNodeTable.id inSubQuery subQuery } val nodes = TreeNode.wrapRows(query).toList() nodes shouldHaveSize 1 nodes.single().title shouldBeEqualTo "child1" SELECT tree_nodes.id, tree_nodes.title, tree_nodes.description, tree_nodes."depth", tree_nodes.parent_id FROM tree_nodes WHERE tree_nodes.id IN (SELECT sub.parent_id FROM tree_nodes sub WHERE sub.title LIKE 'grand%')
  74. Spring Boot AutoCon fi gure spring: datasource: url: jdbc:h2:mem:test driver-class-name:

    org.h2.Driver exposed: generate-ddl: false # true द DatabaseInitializerо SchemaUtils.create() प೯ show-sql: true # Exposed SQL ۽Ӓ ୹۱
  75. Spring @Transactional @Bean fun dataSource(): DataSource { val config =

    HikariConfig().apply { jdbcUrl = "jdbc:h2:mem:${Base58.randomString(8)};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL" driverClassName = "org.h2.Driver" username = "root" password = "" } return HikariDataSource(config) } /** * `@Transactional` ীࢲ ࢎਊೡ Exposed `SpringTransactionManager` ܳ ١۾೤פ׮. */ @Bean fun annotationDrivenTransactionManager(dataSource: DataSource): TransactionManager { log.info { "Create Exposed's SpringTransactionManager" } return SpringTransactionManager(dataSource, DatabaseConfig { useNestedTransactions = true }) }
  76. Exposed Repository pattern 09-spring/04-exposed-repository @Repository class ActorExposedRepository: JdbcRepository<Long, ActorRecord> {

    companion object: KLogging() override val table = ActorTable override fun extractId(entity: ActorRecord): Long = entity.id override fun ResultRow.toEntity(): ActorRecord = toActorRecord() @Transactional(readOnly = true) fun searchActors(params: Map<String, String?>): List<ActorRecord> { // build where } fun create(actor: ActorRecord): ActorRecord { val id = ActorTable.insertAndGetId { it[firstName] = actor.firstName it[lastName] = actor.lastName actor.birthday?.let { day -> it[birthday] = runCatching { LocalDate.parse(day) }.getOrNull() } } return actor.copy(id = id.value) } } JdbcRepository.kt
  77. JdbcRepository with Cache @Cacheable(key = "'country:' + #code") fun findByCode(code:

    String): CountryRecord? { log.debug { "----> Loading country with code[$code] and caching in redis ..." } // @Transactional ਸ ࢎਊೞ૑ ঋҊ, transaction {} ࠶۾ਸ ࢎਊೞৈ DBী ੽Ӕ೤פ׮. // நदী ੉޷ ч੉ ੓׮ݶ, Transactionਸ ࢎਊೞ૑ ঋҊ நदীࢲ чਸ ߈ജೞب۾ ೤פ׮. return transaction { val row = CountryTable.selectAll().where { CountryTable.code eq code }.singleOrNull() ?: return@transaction null CountryRecord( code = row[CountryTable.code], name = row[CountryTable.name], description = row[CountryTable.description] ) } } @Transactional @CacheEvict(key = "'country:' + #countryRecord.code") fun update(countryRecord: CountryRecord): Int { return CountryTable.update({ CountryTable.code eq countryRecord.code }) { it[name] = countryRecord.name it[description] = countryRecord.description } }
  78. Exposed Anti-Patterns • `@Transactional` җ `transaction { }` ઺୏ ޙઁ

    • `transaction {}` ഐ୹ࠁ׮ `TransactionManager.current()` ࢎਊ • TransactionManager ܳ ҕਬೞ૑ ޅ೧ ߊࢤೞח ழ޿ ־ۅ • JdbcTemplate җ Exposed о ࢲ۽ ׮ܲ ழ֏࣌ਸ ଵઑ • Spring DataSourceTransactionManager ৬ োѾػ DataSource ܳ Exposed ب ࢎਊ • Rollback ੉റ Transaction ੤ࢎਊ • Rollback ੉റ زੌೠ TransactionManager ۽ ౟ے੥࣌ ੤द੘द ৘৻ ߊࢤ • ৘৻ ୊ܻ റ ࢜۽਍ ౟ے੥࣌ਵ۽ ࠙ӝೞѢա, ౟ے੥࣌ਸ ৮੹൤ ઙܐ റ ੤द੘ • Connection Pool Ҋт • Exposed ղࠗ੸ਵ۽ ழ֏࣌ਸ ҅ࣘ ೡ׼ೞҊ ߈ജೞ૑ ঋח ޙઁ (ResultRow ё୓ ਬ૑, ౟ے੥࣌ ߧਤ ਤߓ) • ౟ے੥࣌ ղীࢲ ݽٚ ୊ܻܳ ՘ղҊ, ResultRow ח DTO ۽ ߸ജೞৈ ߈ജ • ؊ ੗ࣁೠ ղਊ਷ : Exposed ࢎਊ द ؀಴੸ पࣻ৬ ѐࢶ ߑߨ ଵҊ
  79. Resources Documents • Github Jetbrains/Exposed • Exposed Documentation • SQL

    and ORM alike Database Access with Kotlin Exposed • Guide to the Kotlin Exposed • Kotlin Exposed: From Setup to Advanced Usage • Kotlin Exposed Book (Exposed 1.1.1) • Github: Exposed Workshop
  80. Resources VIDEO • Exploring Exposed: A Kotlin Solution to Database

    Access • 7+/-2 thins you didn’t know about Exposed by Alexey Soshin • Using PostgreSQL as an Append-only Datastore with Kotlin and Exposed • PostgreSQL and Exposed - Kotlin database access