ODBC es una API (Application Programming Interface) de bajo nivel, independiente del lenguaje y del SGBD, que define una interfaz estándar en C para acceder a bases de datos relacionales. Fue desarrollada por Microsoft en 1992 y adoptada posteriormente como estándar por el grupo SQL Access Group (SAG) de X/Open.
El objetivo fundamental de ODBC es conseguir la independencia entre la aplicación y el sistema gestor de bases de datos. Sin ODBC, cada aplicación necesitaría una implementación específica para cada SGBD (Oracle, SQL Server, MySQL…). Con ODBC, la aplicación habla con una API genérica y el driver ODBC (proporcionado por el fabricante del SGBD) se encarga de traducir las llamadas al dialecto nativo del motor de base de datos.
ODBC utiliza el concepto de DSN (Data Source Name), un identificador de configuración que agrupa los parámetros de conexión (driver, servidor, base de datos, credenciales) y que la aplicación usa para establecer la conexión sin necesidad de codificar esos parámetros en el código fuente.
Aunque ODBC es un estándar de los años 90, sigue siendo ampliamente utilizado en entornos empresariales para conectar herramientas de reporting y hojas de cálculo (como Excel o Power BI) a bases de datos corporativas, y es la base sobre la que se construyó JDBC.
La misma aplicación puede conectar a Oracle, SQL Server, MySQL o PostgreSQL cambiando únicamente el DSN y el driver instalado, sin recompilar el código.
Fichero de configuración o entrada en el registro del sistema que almacena parámetros de conexión. Puede ser de usuario (solo accesible por el usuario actual), de sistema (accesible por todos) o de fichero.
API de bajo nivel en C; verbosa y propensa a errores de gestión de memoria. No orientada a objetos. Requiere instalación y configuración de drivers en cada máquina cliente.
JDBC es la API estándar de Java (parte del Java SE) para acceder a bases de datos relacionales desde aplicaciones Java. Publicada por Sun Microsystems en 1997, define un conjunto de interfaces Java que los fabricantes de SGBD implementan en forma de drivers JDBC. Es, conceptualmente, el equivalente Java de ODBC.
JDBC sigue el mismo principio de independencia que ODBC: la aplicación llama a la API estándar de JDBC (interfaces como Connection, Statement, ResultSet), y el driver JDBC del SGBD proporciona la implementación concreta. Cambiar de base de datos es, en teoría, tan sencillo como cambiar la cadena de conexión y el driver.
JDBC define cuatro tipos de driver según cómo realizan la comunicación con el SGBD: Type 1 (puente JDBC-ODBC, obsoleto), Type 2 (native-API, usa librerías nativas del cliente), Type 3 (network protocol, traducción en un servidor intermedio) y Type 4 (pure Java driver), el más común hoy en día: implementado completamente en Java, se comunica directamente con el SGBD usando su protocolo de red nativo sin necesidad de software adicional en el cliente.
Una mejora fundamental respecto a ODBC es el soporte para PreparedStatement, que precompila la consulta SQL y permite reutilizarla con distintos parámetros. Además de mejorar el rendimiento, los PreparedStatements son la principal defensa contra los ataques de SQL Injection, ya que los parámetros se envían separados del código SQL y nunca se interpolan directamente en la cadena.
Statement construye y envía la consulta SQL como texto plano. PreparedStatement precompila la consulta con marcadores de posición (?). Ventajas: previene SQL Injection, mejor rendimiento en consultas repetidas y manejo automático del escapado de tipos.
JDBC permite agrupar múltiples operaciones en un lote (addBatch() / executeBatch()). En lugar de enviar 1000 INSERTs individuales, se envían en un solo mensaje de red, reduciendo la latencia en un orden de magnitud.
Type 4 (Pure Java) es el estándar actual: se descarga como un JAR, no requiere instalación nativa en el cliente y se comunica directamente con el protocolo del SGBD. Ejemplos: postgresql-42.x.jar, mysql-connector-java.jar.
Por defecto, JDBC opera en modo auto-commit (cada sentencia es una transacción). Para operaciones que deben ser atómicas, se desactiva con setAutoCommit(false) y se controla manualmente con commit() / rollback().
JPA (Java Persistence API) es la especificación estándar de Java EE / Jakarta EE para el mapeo objeto-relacional (ORM). Define cómo se mapean las clases Java a tablas de base de datos y cómo se gestionan las operaciones de persistencia (consulta, inserción, actualización, eliminación) de forma transparente, sin que el desarrollador tenga que escribir SQL directamente en la mayoría de los casos.
JPA introduce un nivel de abstracción superior al de JDBC. El desarrollador trabaja con objetos Java (Entidades) anotados con metadatos JPA, y el proveedor JPA (Hibernate, EclipseLink, OpenJPA) se encarga de generar y ejecutar el SQL necesario sobre la base de datos. Este enfoque se denomina ORM (Object-Relational Mapping).
El objeto central de JPA es el EntityManager: gestiona el ciclo de vida de las entidades (nuevo, gestionado, desacoplado, eliminado), sincroniza el estado en memoria con la base de datos y coordina las transacciones. Actúa como una caché de primer nivel: las entidades recuperadas se almacenan en el contexto de persistencia y, si se solicita la misma entidad dos veces en la misma transacción, JPA devuelve el objeto en memoria sin ir a la base de datos.
Para las consultas, JPA define JPQL (Java Persistence Query Language), un lenguaje similar a SQL pero orientado a objetos: se opera sobre entidades y sus propiedades, no sobre tablas y columnas. Esto permite que las consultas sean independientes del SGBD subyacente. Para consultas complejas también se puede usar la Criteria API (consultas programáticas tipadas) o SQL nativo cuando sea necesario.
@EntityGraph o cláusulas JOIN FETCH en JPQL para cargar las asociaciones en una única consulta.
Cuando múltiples transacciones acceden y modifican los mismos datos de forma simultánea, pueden aparecer anomalías de concurrencia: situaciones en las que el resultado de las operaciones es incorrecto o inconsistente respecto a lo que se obtendría si las transacciones se ejecutasen de forma estrictamente secuencial. El estándar SQL define cuatro problemas clásicos.
Una transacción lee datos que han sido modificados por otra transacción que aún no ha confirmado (commit). Si la segunda transacción hace rollback, la primera habrá operado con datos que nunca existieron de forma definitiva.
Una transacción lee el mismo registro dos veces y obtiene valores distintos, porque otra transacción lo modificó y confirmó entre las dos lecturas.
Una transacción ejecuta dos veces la misma consulta con condición de rango y obtiene filas adicionales (o menos filas) en la segunda ejecución, porque otra transacción insertó o eliminó filas que satisfacen esa condición.
Dos transacciones leen el mismo valor, lo modifican de forma independiente y la segunda escritura sobreescribe la primera sin haber tenido en cuenta el cambio anterior. La actualización de la primera transacción se pierde.
El control de concurrencia pesimista asume que los conflictos entre transacciones son frecuentes y los previene adquiriendo bloqueos antes de acceder a los datos. Una transacción que no puede obtener un bloqueo queda en espera hasta que la transacción que lo retiene lo libera.
El sistema de bloqueos es el mecanismo de control de concurrencia más antiguo y extendido en los SGBDs relacionales. Se basa en el principio de que una transacción debe declarar su intención sobre un recurso antes de acceder a él, y el gestor de bloqueos garantiza que los accesos incompatibles no se ejecuten simultáneamente.
Existen dos tipos fundamentales de bloqueo. El bloqueo compartido (S — Shared) se adquiere para operaciones de lectura: múltiples transacciones pueden tener bloqueos compartidos sobre el mismo recurso simultáneamente, ya que las lecturas no se interfieren entre sí. El bloqueo exclusivo (X — Exclusive) se adquiere para operaciones de escritura: solo una transacción puede tener un bloqueo exclusivo, y es incompatible con cualquier otro bloqueo (compartido o exclusivo) sobre el mismo recurso.
La granularidad del bloqueo es la decisión más importante en el diseño del control de concurrencia: se puede bloquear a nivel de base de datos completa, tabla, página de disco, fila individual o incluso celda. Mayor granularidad (fila) implica mayor concurrencia pero mayor overhead; menor granularidad (tabla) implica menor overhead pero más contención.
| Tipo de bloqueo | Lectura simultánea | Escritura simultánea | Cuándo se usa |
|---|---|---|---|
| 🔵 Compartido (S) | ✅ Permitida | ❌ Bloqueada | SELECT en modo serializable / LOCK IN SHARE MODE |
| 🔴 Exclusivo (X) | ❌ Bloqueada | ❌ Bloqueada | UPDATE, DELETE, INSERT; SELECT FOR UPDATE |
| 🟡 Intención (IS/IX) | A nivel tabla | A nivel tabla | Protocolo interno para bloqueos jerárquicos |
| 🟣 Actualización (U) | ✅ con bloqueos S | ❌ con otros U o X | Patrón READ-THEN-MODIFY para evitar deadlock |
Protocolo de Bloqueo en Dos Fases (2PL — Two-Phase Locking): para garantizar la serializabilidad, una transacción debe seguir dos fases estrictas: (1) fase de crecimiento: solo puede adquirir bloqueos, nunca liberarlos; (2) fase de contracción: solo puede liberar bloqueos, nunca adquirir nuevos. El 2PL garantiza schedules serializables pero no previene los deadlocks.
El control de concurrencia optimista asume que los conflictos entre transacciones son infrecuentes. En lugar de bloquear los recursos preventivamente, permite que las transacciones procedan sin bloqueos y verifica al final si ha habido conflicto. Si lo hay, la transacción que pierde debe reintentarse.
La implementación más extendida de la concurrencia optimista en los SGBDs modernos es MVCC (Multi-Version Concurrency Control). En lugar de bloquear las filas modificadas, el SGBD mantiene múltiples versiones de cada fila. Cuando una transacción modifica una fila, no sobreescribe la versión existente, sino que crea una versión nueva marcada con el timestamp o número de transacción. Las transacciones que leen ven la versión que era vigente en el momento en que comenzaron, independientemente de las modificaciones posteriores.
MVCC es el mecanismo de concurrencia de PostgreSQL, Oracle y MySQL/InnoDB. Sus ventajas son notables: las lecturas nunca bloquean las escrituras y viceversa, lo que elimina la contención de bloqueos en la mayoría de los escenarios y permite un rendimiento mucho mayor en cargas de trabajo de lectura intensiva.
En el contexto de aplicaciones Java/JPA, la concurrencia optimista se implementa con versioning: la entidad tiene un campo @Version que JPA incrementa en cada actualización. Antes de confirmar, JPA verifica que la versión en la base de datos coincide con la versión que se leyó. Si no coincide (otro cliente actualizó el registro mientras tanto), lanza una OptimisticLockException.
Lecturas sin bloqueo: los SELECT no compiten con los UPDATE. Alta concurrencia en aplicaciones de lectura intensiva. Sin deadlocks por bloqueos de lectura. Cada transacción ve una instantánea consistente de los datos en el momento de su inicio (snapshot isolation).
El mantenimiento de múltiples versiones consume espacio en disco. Los SGBDs necesitan procesos de garbage collection periódicos para eliminar versiones antiguas que ya no son necesarias para ninguna transacción activa. En PostgreSQL este proceso se llama VACUUM.
Adecuado cuando los conflictos son raros (baja contención), las transacciones son cortas, y el coste de reintentar es bajo. Ideal para aplicaciones web con muchos usuarios leyendo y pocas escrituras sobre los mismos registros simultáneamente.
Adecuado cuando los conflictos son frecuentes (alta contención), el coste de reintentar es alto (procesamiento intensivo), o se requiere una garantía inmediata de exclusividad (reservas de asientos, transferencias bancarias en tiempo real).
El estándar SQL-92 (ANSI) define cuatro niveles de aislamiento que permiten a los diseñadores de sistemas elegir el equilibrio deseado entre consistencia de los datos y rendimiento / concurrencia. A mayor nivel de aislamiento, mayor consistencia pero menor rendimiento (más bloqueos, menor throughput); a menor nivel, mayor rendimiento pero mayor riesgo de anomalías.
La elección del nivel de aislamiento es una decisión arquitectónica crítica que debe tomarse a nivel de transacción o de sesión, dependiendo de los requisitos de negocio de cada operación. No existe un nivel universalmente correcto: una operación de lectura informativa puede tolerrar un nivel bajo, mientras que una transferencia bancaria requiere el nivel máximo.
| Nivel de aislamiento | Dirty Read | Non-Repeatable Read | Phantom Read | Rendimiento | Cuándo usar |
|---|---|---|---|---|---|
| READ UNCOMMITTED | ✗ Posible | ✗ Posible | ✗ Posible | ⚡⚡⚡⚡ Máximo | Estadísticas aproximadas; nunca en transacciones de negocio |
| READ COMMITTED (predeterminado en Oracle, PostgreSQL, SQL Server) |
✓ Prevenido | ✗ Posible | ✗ Posible | ⚡⚡⚡ Alto | La mayoría de aplicaciones web; buen equilibrio general |
| REPEATABLE READ (predeterminado en MySQL InnoDB) |
✓ Prevenido | ✓ Prevenido | ✗ Posible | ⚡⚡ Medio | Informes que leen los mismos datos varias veces |
| SERIALIZABLE | ✓ Prevenido | ✓ Prevenido | ✓ Prevenido | ⚡ Bajo | Transacciones financieras críticas; máxima consistencia |
Las transferencias bancarias deben usar SERIALIZABLE. El riesgo de cualquier anomalía es inaceptable. El menor rendimiento se justifica por la criticidad de la operación.
READ COMMITTED para la mayoría de operaciones. REPEATABLE READ o bloqueo optimista para la reserva de stock. Balance entre consistencia y escalabilidad.
READ COMMITTED o incluso snapshots de solo lectura (réplica). Los informes analíticos toleran cierta inconsistencia temporal. El rendimiento es prioritario.