9.5.-Otros aspectos
9.5. Otros aspectos a tener en cuenta¶
Resumen
En este tema se revisan decisiones de diseño que aparecen cuando una aplicación con base de datos empieza a crecer: desfase objeto-relacional, integridad, identificadores, cierre de recursos, transacciones, pool de conexiones y herramientas como SQLDelight.
Supongamos que estás desarrollando una aplicación de comercio electrónico. La aplicación necesita almacenar productos, pedidos, usuarios, pagos y envíos. Además, debe mostrar productos, permitir añadirlos al carrito, registrar pagos y gestionar pedidos.
En una aplicación de este tipo no basta con saber ejecutar un SELECT o un INSERT. Hay que tomar decisiones de diseño:
- Cómo se relacionan los objetos de Kotlin con las tablas.
- Dónde se valida la integridad de los datos.
- Qué tipo de identificadores se usan.
- Cómo se cierran conexiones y resultados.
- Cuándo se necesita una transacción.
- Cómo se evita abrir conexiones de forma ineficiente.
Directamente relacionado con el RA9 del módulo de Programación, este tema refuerza la gestión de información almacenada en bases de datos manteniendo integridad y consistencia.
| Código | Descripción |
|---|---|
| RA9 | Gestiona información almacenada en bases de datos manteniendo la integridad y consistencia de los datos. |
| CE a | Se han identificado las características y métodos de acceso a sistemas gestores de bases de datos. |
| CE g | Se han creado aplicaciones para gestionar la información presente en bases de datos. |
1. Desfase objeto-relacional¶
El desfase objeto-relacional aparece porque la programación orientada a objetos y las bases de datos relacionales no modelan la información exactamente igual.
En Kotlin podemos tener clases como estas:
data class Usuario(
val id: Int,
val nombre: String,
val pedidos: List<Pedido>
)
data class Pedido(
val id: Int,
val total: Double
)
En una base de datos relacional, esa misma información suele repartirse en tablas:
CREATE TABLE usuarios (
id INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL
);
CREATE TABLE pedidos (
id INT PRIMARY KEY,
usuario_id INT NOT NULL,
total DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
Lectura del ejemplo:
- En Kotlin,
Usuariopuede contener una lista de pedidos. - En SQL, los pedidos viven en otra tabla.
- La relación se expresa con
usuario_id. - El código debe transformar filas relacionadas en objetos conectados.
Los ORM, como Hibernate o Exposed, ayudan a gestionar este mapeo. Aun así, no eliminan la necesidad de entender cómo están diseñadas las tablas ni qué consultas se ejecutan por debajo.
Busca los ejemplos MapeoFilaAObjeto y MapeoUnoAMuchos y estúdialos o ejecútalos.
2. Integridad: código o base de datos¶
La integridad referencial y las reglas de negocio pueden gestionarse en el código, en la base de datos o en ambos lugares.
Ejemplo de integridad delegada a la base de datos:
CREATE TABLE productos (
id INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10, 2) NOT NULL CHECK (precio >= 0),
sku VARCHAR(30) UNIQUE NOT NULL
);
La base de datos impide:
- Productos sin nombre.
- Precios negativos.
- Dos productos con el mismo
sku.
Ejemplo de validación previa en Kotlin:
fun validarProducto(nombre: String, precio: Double) {
require(nombre.isNotBlank()) {
"El nombre del producto no puede estar vacío."
}
require(precio >= 0) {
"El precio no puede ser negativo."
}
}
Lectura del ejemplo:
- La validación en Kotlin da mensajes más cercanos a la persona usuaria.
- La restricción en base de datos protege la información aunque falle la aplicación.
- Ambos enfoques se complementan.
Criterio práctico
Las reglas críticas de integridad deben estar en la base de datos. La aplicación puede validar antes para mejorar la experiencia de uso, pero no debería ser la única barrera.
3. Identificadores: autonuméricos y UUID¶
Los identificadores permiten localizar de forma única cada fila. Dos opciones habituales son los identificadores autonuméricos y los UUID.
Ejemplo con identificador autonumérico:
Ventajas:
- Son sencillos de leer.
- Ocupan poco espacio.
- Funcionan bien en bases de datos centralizadas.
Inconvenientes:
- Revelan orden de creación.
- Pueden complicar fusiones entre bases de datos.
- Dependen de la generación del SGBDR.
Ejemplo con UUID:
Ventajas:
- Se pueden generar desde la aplicación.
- Son útiles en sistemas distribuidos.
- Reducen colisiones entre fuentes distintas.
Inconvenientes:
- Son menos legibles.
- Ocupan más espacio.
- Pueden afectar al rendimiento de índices si no se usan con criterio.
La elección depende del contexto. En una práctica sencilla, un id autonumérico suele ser suficiente. En sistemas distribuidos, móviles o con sincronización entre nodos, un UUID puede ser más adecuado.
4. Cierre de objetos de base de datos¶
En JDBC hay tres recursos que aparecen constantemente:
Connection: conexión con la base de datos.PreparedStatement: sentencia preparada.ResultSet: resultados de una consulta.
Si no se cierran correctamente, pueden aparecer fugas de recursos, bloqueos o agotamiento del pool de conexiones.
En Kotlin, la forma recomendada es usar use:
dataSource.connection.use { connection ->
val sql = "SELECT id, nombre FROM clientes"
connection.prepareStatement(sql).use { statement ->
statement.executeQuery().use { resultSet ->
while (resultSet.next()) {
println(resultSet.getString("nombre"))
}
}
}
}
Lectura del ejemplo:
- La conexión se cierra al salir del primer
use. - La sentencia se cierra al salir del segundo
use. - El resultado se cierra al salir del tercer
use. - Si ocurre una excepción, los recursos también se cierran.
No confiar en el recolector de basura
No conviene esperar a que el recolector de basura libere recursos de base de datos. Las conexiones y cursores deben cerrarse explícitamente.
Busca el ejemplo CierreRecursosUse y estúdialo o ejecútalo.
5. Gestión de transacciones¶
Una transacción agrupa varias operaciones para que se ejecuten como una unidad. Si todas funcionan, se confirma con commit. Si una falla, se deshace con rollback.
Ejemplo típico: registrar un pedido y descontar stock. Ambas operaciones deben completarse juntas.
connection.autoCommit = false
try {
insertarPedido(connection, pedido)
descontarStock(connection, productoId, unidades)
connection.commit()
} catch (e: SQLException) {
connection.rollback()
throw e
} finally {
connection.autoCommit = true
}
Lectura del ejemplo:
autoCommit = falseevita confirmar cada sentencia por separado.insertarPedidoregistra el pedido.descontarStockactualiza existencias.commit()confirma ambas operaciones.rollback()deshace los cambios si algo falla.finallyrestaura el modo habitual de la conexión.
Situación real
Si se registra el pedido pero falla el descuento de stock, el sistema queda incoherente. Una transacción evita ese estado intermedio: o se hacen ambas operaciones o no se hace ninguna.
Busca los ejemplos TransaccionCommit y TransaccionRollback y estúdialos o ejecútalos.
6. Pool de conexiones¶
Abrir una conexión a la base de datos es costoso. En una aplicación con muchas peticiones, abrir y cerrar conexiones físicas continuamente puede perjudicar mucho el rendimiento.
Un pool de conexiones mantiene un conjunto de conexiones listas para reutilizarse. La aplicación pide una conexión al pool, la usa y la devuelve al terminar.
val dataSource = HikariDataSource().apply {
jdbcUrl = "jdbc:postgresql://localhost:5432/tienda"
username = "postgres"
password = "postgres"
maximumPoolSize = 10
}
dataSource.connection.use { connection ->
println("Conexión obtenida del pool: ${connection.isValid(2)}")
}
Lectura del ejemplo:
HikariDataSourcerepresenta el pool de conexiones.jdbcUrl,usernameypasswordconfiguran el acceso.maximumPoolSizelimita cuántas conexiones se mantienen.dataSource.connectionobtiene una conexión disponible.usedevuelve la conexión al pool cuando termina el bloque.
La conexión no se destruye necesariamente al cerrar el bloque; queda disponible para futuras operaciones. Esa es la diferencia clave respecto a abrir conexiones manualmente sin pool.
Busca el ejemplo PoolHikariBasico y estúdialo o ejecútalo.
7. SQLDelight¶
SQLDelight es una librería que permite escribir SQL y generar código Kotlin tipado para ejecutar consultas. Puede ser útil cuando se quiere mantener control sobre SQL, pero con ayuda del compilador y modelos generados.
La idea general es:
A partir de consultas como esa, SQLDelight genera código Kotlin para invocarlas de forma segura. No sustituye el aprendizaje de SQL, pero reduce errores de escritura y mejora la integración con Kotlin.
8. Conclusión¶
Los aspectos tratados en este tema suelen aparecer cuando se pasa de ejemplos pequeños a aplicaciones reales. No basta con conectar y ejecutar consultas: hay que decidir cómo se modelan los datos, quién valida la integridad, cómo se identifican las filas, cómo se cierran recursos, cuándo usar transacciones y cómo reutilizar conexiones.
La idea clave es que una aplicación con base de datos debe diseñarse pensando en consistencia, mantenimiento y rendimiento desde el principio.
Fuentes y bibliografía¶
- Ejemplos U9
- Programación - 08 Programación con Bases de Datos - José Luis González
- SQLDelight
- HikariCP