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 Exposed - 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’Exposed - 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/appdependencies: - 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.224test-dependencies: - io.kotest:kotest-assertions-core:5.9.0-
El mòdul
exposed-coreproporciona 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.Databaseimport 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:jdbcespecifica que aquesta és una connexió JDBC.h2indica que la base de dades és una base de dades H2.memespecifica 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.Driverespecifica el controlador JDBC H2 que s’utilitzarà per establir la connexió.
Tingues en compte que invocar Database.connect() només configura els paràmetres de connexió, però no estableix immediatament una connexió amb la base de dades. La connexió real amb la base de dades s’establirà més tard quan es realitzi una operació de base de dades.
Per defecte, Exposed registra automàticament la connexió a la base de dades. Pots canviar aquest comportament establint el paràmetre connectionAutoRegistration quan crides Database.connect().
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:
-
idde tipusIntes defineix amb el mètodeinteger(). La funcióautoIncrement()indica que aquesta columna serà un enter autoincrementat, típicament utilitzat per a claus primàries. -
titleidescriptionde tipusStringes defineixen amb el mètodevarchar(). -
isCompletedde tipusBooleanes 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.18Configura el “logging” tal com s’explica a Kotlin - 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)
Si només vols una solució senzilla només afegeix una dependència amb slf4j-nop:
I configura la transacció per tal que utilitzi StdOutSqlLogger:
transaction { // print sql to std-out addLogger(StdOutSqlLogger)
// ...}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.idDins 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.COMPLETEDUsing 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.
Pot ser útil saber que si s’obre una segona transacció després de la primera, trobaràs que la taula i les seves dades s’han perdut encara que l’aplicació no s’hagi aturat. Aquest és el comportament esperat en bases de dades H2 quan es gestionen connexions i transaccions.
Per mantenir la base de dades oberta, afegeix ;DB_CLOSE_DELAY=-1 a l’URL de la base de dades:
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")DAO API
Afegeix una nova dependència:
dependencies: - org.jetbrains.exposed:exposed-dao:1.0.0-beta-5El 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.EntityIDimport org.jetbrains.exposed.v1.dao.IntEntityimport 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)" }}-
Taskesté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 objectesté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 taulaTasksDAOutilitzant la paraula claubyde Kotlin. -
La funció
toString()personalitza com es representa una instància deTaskcom 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.