Exposed
Exposed és una biblioteca SQL lleugera construïda sobre un controlador JDBC per al llenguatge de programació Kotlin. Admet tant controladors JDBC (bloquejants) com R2DBC (no bloquejants), donant-te flexibilitat per escollir entre models d’accés a bases de dades tradicionals o reactius.
Ofereix dues aproximacions per a l’accés a bases de dades: l’API de Domain-Specific Language (DSL) i l’API de Data Access Object (DAO).
L’API de Llenguatge Específic de Domini (DSL) d’Exposed proporciona una abstracció basada en Kotlin per interactuar amb bases de dades. Reflecteix de prop les sentències SQL reals, cosa que et permet treballar amb conceptes SQL familiars mentre et beneficies de la seguretat de tipus que ofereix Kotlin.
L’API d’Objecte d'Accés a Dades (DAO) d’Exposed proporciona una aproximació orientada a objectes per interactuar amb una base de dades, similar als marcs de treball tradicionals de Mapatge Objecte-Relacional (ORM) com Hibernate. Aquesta API és menys verbosa i proporciona una manera més intuïtiva i centrada en Kotlin d’interactuar amb la teva base de dades.
La flexibilitat d’Exposed et permet escollir l’aproximació que millor s’adapti a les necessitats del teu projecte, ja sigui que prefereixis el control directe de SQL amb l’API DSL o l’abstracció de més alt nivell de l’API DAO.
Entorn de treball
Crea un projecte amb Amper.
Modifica el fitxer module.yaml
:
product: jvm/app
dependencies:
- org.jetbrains.exposed:exposed-core:1.0.0-beta-5
- org.jetbrains.exposed:exposed-jdbc:1.0.0-beta-5
- com.h2database:h2:2.2.224
test-dependencies:
- io.kotest:kotest-assertions-core:5.9.0
-
El mòdul
exposed-core
proporciona els components fonamentals i les abstraccions necessàries per treballar amb bases de dades de manera segura quant a tipus i inclou l’API DSL. -
El mòdul
exposed-jdbc
és una extensió del mòdul exposed-core que afegeix suport per a Java Database Connectivity (JDBC).
Configurar una connexió a base de dades
Sempre que accedeixis a una base de dades utilitzant Exposed, comences obtenint una connexió i creant una transacció. Per configurar la connexió a la base de dades, utilitza la funció Database.connect()
.
Crea el fitxer test/TaslTest.kt
:
import org.jetbrains.exposed.v1.jdbc.Database
import kotlin.test.Test
class TaskTest {
@Test
fun testConnection() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
}
}
La funció Database.connect()
crea una instància d’una classe que representa la base de dades i pren dos o més paràmetres. En aquest cas, l’URL de connexió i el controlador.
-
jdbc:h2:mem:test
és l’URL de la base de dades per connectar-se:jdbc
especifica que aquesta és una connexió JDBC.h2
indica que la base de dades és una base de dades H2.mem
especifica que la base de dades està en memòria, el que significa que les dades només existiran en memòria i es perdran quan l’aplicació s’aturi.test
és el nom de la base de dades.
-
org.h2.Driver
especifica el controlador JDBC H2 que s’utilitzarà per establir la connexió.
DSL API
Definir una taula
A Exposed, una taula de base de dades està representada per un objecte heretat de la classe Table
.
Per definir l’objecte taula crea un nou fitxer Task.kt
:
import org.jetbrains.exposed.v1.core.Table
const val MAX_VARCHAR_LENGTH = 128
object Tasks : Table("tasks") {
val id = integer("id").autoIncrement()
val title = varchar("name", MAX_VARCHAR_LENGTH)
val description = varchar("description", MAX_VARCHAR_LENGTH)
val isCompleted = bool("completed").default(false)
}
En el constructor de Table
, passar el nom tasks
configura un nom personalitzat per a la taula.
Tingues en compte que si no s’especifica un nom personalitzat, Exposed en generarà un a partir del nom de la classe, que podria portar a resultats inesperats.
Dins de l’objecte Tasks, es defineixen les següents columnes:
-
id
de tipusInt
es defineix amb el mètodeinteger()
. La funcióautoIncrement()
indica que aquesta columna serà un enter autoincrementat, típicament utilitzat per a claus primàries. -
title
idescription
de tipusString
es defineixen amb el mètodevarchar()
. -
isCompleted
de tipusBoolean
es defineix amb el mètodebool()
. Utilitzant la funciódefault()
, configures el valor per defecte a fals.
En aquest punt, has definit una taula amb columnes, que essencialment crea el plànol per a la taula Tasks
.
Enable logging
Modifica el fitxer module.yaml
:
dependencies:
- io.github.oshai:kotlin-logging-jvm:7.0.3
- ch.qos.logback:logback-classic:1.5.18
Configura el “logging” tal com s’explica a Logging.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%r [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
Per defecte, les transaccions estan configurades amb Slf4jSqlDebugLogger
(el logging es fa a nivell de DEBUG
)
Crear i consultar una taula
Amb l’API DSL d’Exposed, pots interactuar amb una base de dades utilitzant una sintaxi segura quant a tipus similar a SQL.
Abans de començar a executar operacions de base de dades, has d’obrir una transacció.
Una transacció està representada per una instància de la classe Transaction
, dins de la qual pots definir i manipular dades utilitzant la seva funció lambda.
Exposed gestionarà automàticament l’obertura i tancament de la transacció en segon pla, assegurant una operació sense problemes.
import io.kotest.matchers.*
import kotlin.test.Test
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.*
class TaskTest {
private val logger = KotlinLogging.logger {}
@Test
fun testDSL() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(Tasks)
val task1 = Tasks.insert {
it[title] = "Learn Exposed"
it[description] = "Go through the Get started with Exposed tutorial"
} get Tasks.id
val task2 = Tasks.insert {
it[title] = "Read The Hobbit"
it[description] = "Read the first two chapters of The Hobbit"
it[isCompleted] = true
} get Tasks.id
task1 shouldBe 1
task2 shouldBe 2
logger.info { "Created new tasks with ids $task1 and $task2." }
Tasks.select(Tasks.id.count(), Tasks.isCompleted).groupBy(Tasks.isCompleted).forEach {
it[Tasks.id.count()] shouldBe 1
logger.info { "${it[Tasks.isCompleted]}: ${it[Tasks.id.count()]} " }
}
logger.info { "Remaining tasks: ${Tasks.selectAll().toList()}" }
}
}
}
Analitzem el codi i repassem cada secció.
Primer, crees la taula tasks utilitzant el mètode create()
de SchemaUtils
. L’objecte SchemaUtils
conté mètodes d’utilitat per crear, alterar i eliminar objectes de base de dades.
Un cop s’ha creat la taula, utilitzes el mètode d’extensió insert()
de Table
per afegir dos nous registres de Task.
val taskId = Tasks.insert {
it[title] = "Learn Exposed"
it[description] = "Go through the Get started with Exposed tutorial"
} get Tasks.id
val secondTaskId = Tasks.insert {
it[title] = "Read The Hobbit"
it[description] = "Read the first two chapters of The Hobbit"
it[isCompleted] = true
} get Tasks.id
Dins del bloc insert
, estableix els valors per a cada columna utilitzant el paràmetre it
. Exposed traduirà les funcions a les següents consultes SQL:
INSERT INTO TASKS (COMPLETED, DESCRIPTION, "name") VALUES (FALSE, 'Go through the Get started with Exposed tutorial', 'Learn Exposed')
INSERT INTO TASKS (COMPLETED, DESCRIPTION, "name") VALUES (TRUE, 'Read the first two chapters of The Hobbit', 'Read The Hobbit')
Com que la funció insert()
retorna un InsertStatement
, utilitzant el mètode get()
després de l’operació insert
recuperes el valor id
autoincrementat de la fila recentment afegida.
Amb la funció d’extensió select()
després crees una consulta per comptar el nombre de files i recuperar el valor isCompleted
per a cada fila de la taula.
Tasks.select(Tasks.id.count(), Tasks.isCompleted).groupBy(Tasks.isCompleted).forEach {
println("${it[Tasks.isCompleted]}: ${it[Tasks.id.count()]} ")
}
Utilitzant groupBy()
agrupa els resultats de la consulta per la columna isCompleted
, el que significa que agregarà les files basant-se en si estan completades o no.
La consulta SQL esperada es veu així:
SELECT COUNT(TASKS.ID), TASKS.COMPLETED FROM TASKS GROUP BY TASKS.COMPLETED
Using groupBy()
groups the results of the query by the isCompleted
column, which means it will aggregate the rows based on whether they are completed or not. The expected SQL query looks like this:
SELECT COUNT(TASKS.ID), TASKS.COMPLETED FROM TASKS GROUP BY TASKS.COMPLETED
És important tenir en compte que la consulta no s’executarà fins que cridis una funció que iteri a través del resultat, com forEach()
.
En aquest exemple, per a cada grup imprimim l’estat isCompleted
i el recompte corresponent de tasques.
Actualitzar i eliminar una tasca
Ampliem la funcionalitat de l’aplicació actualitzant i eliminant la mateixa tasca.
A la mateixa funció transaction()
, afegeix el següent codi a la teva implementació:
transaction {
// ...
// Update a task
Tasks.update({ Tasks.id eq task1 }) {
it[isCompleted] = true
}
val updatedTask = Tasks.select(Tasks.isCompleted).where(Tasks.id eq task1).single()
updatedTask[Tasks.isCompleted] shouldBe true
logger.info { "Updated task details: $updatedTask" }
// Delete a task
Tasks.deleteWhere { id eq task2 }
Tasks.selectAll().count() shouldBe 1
logger.info { "Remaining tasks: ${Tasks.selectAll().toList()}" }
}
Aquí tens l’explicació:
A la funció Tasks.update()
, especifiques la condició per trobar la tasca amb id igual a la de la tasca inserida anteriorment. Si es compleix la condició, el camp isCompleted
de la tasca trobada es configura com a true
.
Tasks.update({ Tasks.id eq tas1 }) {
it[isCompleted] = true
}
A diferència de la funció insert()
, update()
retorna el nombre de files actualitzades. Per recuperar després la tasca actualitzada, utilitzes la funció select()
amb la condició where per seleccionar només les tasques amb id
igual a task1
.
val updatedTask = Tasks.select(Tasks.isCompleted).where(Tasks.id eq task1).single()
Utilitzar la funció d’extensió single()
inicia la sentència i recupera el primer resultat trobat.
La funció deleteWhere()
, d’altra banda, elimina la tasca amb la condició especificada.
Tasks.deleteWhere { id eq task2 }
De manera similar a update()
, retorna el nombre de files que s’han eliminat.
DAO API
Afegeix una nova dependència:
dependencies:
- org.jetbrains.exposed:exposed-dao:1.0.0-beta-5
El mòdul exposed-dao
permet treballar amb l’API d’Objecte d’Accés a Dades (DAO).
Definir un objecte taula
L’API DAO d’Exposed proporciona la classe base IdTable
i les seves subclasses per definir taules que utilitzen una columna id
estàndard com a clau primària.
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
object Tasks : IntIdTable("tasks") {
val title = varchar("name", 128)
val description = varchar("description", 128)
val isCompleted = bool("completed").default(false)
}
En el constructor IntIdTable
, passar el nom “tasks” configura un nom personalitzat per a la taula. Si no proporciones un nom, Exposed el derivarà del nom de l’objecte, cosa que pot portar a resultats inesperats depenent de les convencions de nomenclatura.
La classe IntIdTable
afegeix automàticament una columna id
d’enter autoincrementable com a clau primària per a la taula.
Definir una entitat
Quan s’utilitza l’aproximació DAO, cada taula definida utilitzant IntIdTable
ha d’estar associada amb una classe d’entitat corresponent. La classe d’entitat representa registres individuals a la taula i s’identifica de manera única per una clau primària.
Per definir l’entitat, actualitza el teu fitxer Task.kt
amb el següent codi:
// ...
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.dao.IntEntity
import org.jetbrains.exposed.v1.dao.IntEntityClass
// ...
class Task(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Task>(TasksDAO)
var title by TasksDAO.title
var description by TasksDAO.description
var isCompleted by TasksDAO.isCompleted
override fun toString(): String {
return "Task(id=$id, title=$title, completed=$isCompleted)"
}
}
-
Task
esténIntEntity
, que és una classe base per a entitats amb una clau primària basada enInt
. -
El paràmetre
EntityID<Int>
representa la clau primària de la fila de base de dades a la qual aquesta entitat es mapeja. -
L’
companion object
esténIntEntityClass<Task>
, enllaçant la classe d’entitat amb la taulaTasksDAO
. -
Cada propietat (
title
,description
, iisCompleted
) es delega a la seva columna corresponent a la taulaTasksDAO
utilitzant la paraula clauby
de Kotlin. -
La funció
toString()
personalitza com es representa una instància deTask
com a cadena. Això és especialment útil per a la depuració o el registre. Quan s’imprimeixi, la sortida inclourà l’ID de l’entitat, el títol i l’estat de completat.
Crear i consultar una taula
Amb l’API DAO d’Exposed, pots interactuar amb la teva base de dades utilitzant una sintaxi orientada a objectes i segura quant a tipus, similar a treballar amb classes regulars de Kotlin. Quan executis qualsevol operació de base de dades, has d’executar-les dins d’una transacció.
Modifica el fitxer TaskTest.kt
:
@Test
fun testDAO() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(TasksDAO)
val task1 = Task.new {
title = "Learn Exposed DAO"
description = "Follow the DAO tutorial"
}
val task2 = Task.new {
title = "Read The Hobbit"
description = "Read chapter one"
isCompleted = true
}
task1.id.value shouldBe 1
task2.id.value shouldBe 2
logger.info { "Created new tasks with ids ${task1.id} and ${task2.id}" }
val completed = Task.find { Tasks.isCompleted eq true }.toList()
completed.count() shouldBe 1
logger.info { "Completed tasks: ${completed.count()}" }
}
}
Primer, crees la taula de tasques utilitzant el mètode SchemaUtils.create()
.
Un cop s’ha creat la taula, utilitzes el mètode d’extensió IntEntityClass.new()
per afegir dos nous registres de Task
:
val task1 = Task.new {
title = "Learn Exposed DAO"
description = "Follow the DAO tutorial"
}
val task2 = Task.new {
title = "Read The Hobbit"
description = "Read chapter one"
isCompleted = true
}
En aquest exemple, task1
i task2
són instàncies de l’entitat Task
, cadascuna representant una nova fila a la taula TasksDAO
. Dins del bloc new
, configures els valors per a cada columna.
Exposed traduirà les funcions a les següents consultes SQL:
INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Learn Exposed DAO', 'Follow the DAO tutorial', FALSE)
INSERT INTO TASKS ("name", DESCRIPTION, COMPLETED) VALUES ('Read The Hobbit', 'Read chapter one', TRUE)
Amb el mètode .find()
després realitzes una consulta filtrada, recuperant totes les tasques on isCompleted
és true
:
val completed = Task.find { Tasks.isCompleted eq true }.toList()
Actualitzar i eliminar una tasca
Ampliem la funcionalitat de l’aplicació actualitzant i eliminant una tasca.