4.6. Programación con conectores#
Como se ha podido ver hasta aquí, el acceso de una aplicación a una base de datos relacional es relativamente sencillo y medianamente semejante sea cual sea el lenguaje de programación y el SGBD. Por tanto, el usar de modo básico conectores no entraña excesiva dificultad. Lo complicado, en realidad, es abstraer al resto del programa del acceso, de modo que logremos que manipule puramente objetos, aunque la información no esté almacenada según este modelo en la base de datos.
Así pues, el propósito a seguir cuando se codifica una aplicación es que todas las particularidades del acceso a datos estén reducidas a un paquete dentro de la aplicación (p.ej. llamado backend), fuera del cual no haya otra cosa que objetos.
Uno de los patrones más usados para lograr la abstracción es el patrón DAO, que se carga de tomar los objetos de la capa de negocio y transladarlos al soporte de persistencia o viceversa. Retomando el ya manido ejemplo de centros y alumnos:

Este patrón básicamente:
Define una interfaz para establecer las operaciones CRUD y, quizás, algunas consultas más específicas.
Define clases (
CentroDAO
,EstudianteDAO
) que implementan la interfaz anterior para el soporte de datos que utilice la aplicación, el cual forzosamente no tiene por qué ser una base de datos relacional. Un cambio en el soporte implica rehacer estas implementaciones, sin alterar el resto de la aplicación.El resto de la aplicación se encarga de utilizar la interfaz, por lo que es ajena a la implementación para un soporte particular.
Ver también
Todo el código que se describe y comenta en este apartado está en el repositorio de GitHub test_dao. Se recomienda descargarlo para repasar mejor las explicaciones.
Por lo general, aunque no forzosamente, cada clase del modelo tendrá asociada
una clase DAO. Recordemos las clases del modelo (Centro
y
Estudiante
), aunque en esta ocasión para intentar uniformizar la
implementación forzaremos a que ambas deriven de una interfaz que nos asegura que
ambas manejan de igual modo su identificador:
public interface Entity {
public Long getId();
public void setId(Long id);
}
La clase Centro
es esta:
public class Centro implements Entity {
public static enum Titularidad {
PUBLICA("público"),
PRIVADA("privado");
private String desc;
Titularidad(String desc) {
this.desc = desc;
}
public String getDescripcion() {
return desc;
}
/**
* Obtiene la titularidad a partir de la descripción.
* @param desc La descripción
* @return El elemento Titularidad o null, si no hay ninguno con esa descripción.
*/
public static Titularidad fromDesc(String desc) {
return Arrays.stream(Titularidad.values())
.filter(t -> t.getDescripcion().compareToIgnoreCase(desc) == 0).findFirst().orElse(null);
}
}
/**
* Código identificativo del centro.
*/
private Long id;
/**
* Nombre del centro.
*/
private String nombre;
/**
* Titularidad: pública o privada.
*/
private Titularidad titularidad;
public Centro() {
super();
}
/**
* Carga todos los datos en el objeto.
* @param id Código del centro.
* @param nombre Nombre del centro.
* @param titularidad Titularidad del centro.
* @return El propio objeto.
*/
public Centro cargarDatos(Long id, String nombre, Titularidad titularidad) {
setId(id);
setNombre(nombre);
setTitularidad(titularidad);
return this;
}
/**
* Constructor que admite todos los datos de definición del centro.
* @param id Código del centro.
* @param nombre Nombre del centro.
* @param titularidad Titularidad del centro (pública o privada)
*/
public Centro(Long id, String nombre, Titularidad titularidad) {
cargarDatos(id, nombre, titularidad);
}
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public Titularidad getTitularidad() {
return titularidad;
}
public void setTitularidad(Titularidad titularidad) {
this.titularidad = titularidad;
}
@Override
public String toString() {
return String.format("%s (%s)", getNombre(), getId());
}
}
que incluye la definición del enum Titularidad
. La del Estudiante
es esta
otra:
public class Estudiante implements Entity {
/**
* Identificador del estudiante.
*/
private Long id;
/**
* Nombre completo del estudiante.
*/
private String nombre;
/**
* Fecha de nacimiento del estudiante.
*/
private LocalDate nacimiento;
/**
* Centro al que está adscrito.
*/
private Centro centro;
public Estudiante() {
super();
}
/**
* Carga los datos del estudiante.
* @param id El identificador del estudiante.
* @param nombre El nombre del estudiante.
* @param nacimiento La fecha de nacimiento.
* @param centro El centro al que está adscrito.
* @return El propio objeto.
*/
public Estudiante cargarDatos(Long id, String nombre, LocalDate nacimiento, Centro centro) {
setId(id);
setNombre(nombre);
setNacimiento(nacimiento);
setCentro(centro);
return this;
}
/**
* Constructor que carga todos los datos.
* @param id El identificador del estudiante.
* @param nombre El nombre del estudiante.
* @param nacimiento La fecha de nacimiento.
* @param centro El centro al que está adscrito.
* @return El propio objeto.
*/
public Estudiante(Long id, String nombre, LocalDate nacimiento, Centro centro) {
this.cargarDatos(id, nombre, nacimiento, centro);
}
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public LocalDate getNacimiento() {
return nacimiento;
}
public void setNacimiento(LocalDate nacimiento) {
this.nacimiento = nacimiento;
}
public Centro getCentro() {
return centro;
}
public void setCentro(Centro centro) {
this.centro = centro;
}
@Override
public String toString() {
LocalDate hoy = LocalDate.now();
return String.format("%s (%d años)", getNombre(), ChronoUnit.YEARS.between(getNacimiento(), hoy));
}
}
Este es el modelo. Ahora necesitamos implementar el acceso a los datos. Eso
requiere definir la interfaz para las operaciones CRUD, que en un alarde de
originalidad llamaremos Crud
, y dos clases DAO, CentroSqlDAO
y
EstudianteSqlDAO
. La interfaz podemos establecerla como estimemos mejor,
mientras recoja todas las operaciones necesarias. Por ejemplo:
public interface Crud<T extends Entity> {
/**
* Obtiene una entidad a partir de su identificador.
* @param id Identificador de la entidad.
* @return La entidad requerida.
* @throws DataAccessException Si hubo algún problema en el acceso a los datos.
*/
public Optional<T> get(Long id) throws DataAccessException;
/**
* Obtiene la relación completa de entidades de un tipo.
* @return Una lista con todas las entidades.
* @throws DataAccessException Si hubo algún problema en el acceso a los datos.
*/
public List<T> get() throws DataAccessException;
/**
* Borra una entidad con un determinado identificador.
* @param id Identificador de la entidad.
* @return true, si se encontró la entidad y se borró.
* @throws DataAccessException Si hubo algún problema en el acceso a los datos.
*/
public boolean delete(Long id) throws DataAccessException;
/**
* Borra una entidad.
* @param obj La entidad que se quiere borrar.
* @return true, si la entidad existía y se borro.
* @throws DataAccessException Si hubo algún problema en el acceso a los datos.
*/
default boolean delete(T obj) throws DataAccessException {
return delete(obj.getId());
}
/**
* Agrega una entidad a la base de datos.
* @param obj La entidad que se quiere agregar.
* @throws DataAccessException Si hubo algún problema en el acceso o ya existía una entidad con ese identificador.
*/
public void insert(T obj) throws DataAccessException;
/**
* Agrega una multitud de entidades de un determinado tipo a la base de datos.
* @param objs Las entidades a agregar.
* @throws DataAccessException Si hubo algún problema en el acceso o ya existía alguna de las entidades.
*/
default void insert(Iterable<T> objs) throws DataAccessException {
for(T obj: objs) insert(obj);
}
/**
* Agrega un array de entidades a la base de datos.
* @param objs Las entidades a agregar.
* @throws DataAccessException Si hubo algún problema en el acceso o ya existía alguna de las entidades.
*/
default void insert(T[] objs) throws DataAccessException {
insert(Arrays.asList(objs));
}
/**
* Actualiza los campos de una entidad cuyo identificador no ha cambiado.
* @param obj La entidad con los valores actualizados.
* @return true, si la entidad existía y se pudo actualizar.
* @throws DataAccessException Si hubo algún problema en el acceso.
*/
public boolean update(T obj) throws DataAccessException;
/**
* Actualiza el identificador de una entidad.
* @param oldId El valor antiguo del identificador.
* @param newId El nuevo valor de identificador.
* @return true, si la entidad existía y se pudo actualizar.
* @throws DataAccessException Si hubo algún problema en el acceso.
*/
public boolean update(Long oldId, Long newId) throws DataAccessException;
/**
* Actualiza el identificador de una entidad.
* @param obj La entidad con el identificador sin actualizar.
* @param newId El nuevo valor de identificador.
* @return true, si la entidad existía y se pudo actualizar.
* @throws DataAccessException Si hubo algún problema en el acceso.
*/
default boolean update(T obj, Long newId) throws DataAccessException {
return update(obj.getId(), newId);
}
}
Nótese que la interfaz es genérica, porque tiene que servir para cualquier tipo de objeto que quiera almacenarse en la base de datos. En nuestro ejemplo, se particularizará para Centro y para Estudiante.
Aclaración
Esta interfaz no tiene por qué ser definida exactamente así: podría definirse otra que satisfaga también la necesidad de implementar las cuatro operaciones básicas. Por ejemplo, los métodos que obtienen todas las entidades de un tipo podrían devolverlas en forma de Stream en vez de List.
Como puede comprobarse, tanto las definiciones de las clases como la interfaz son independientes de cuál sea el soporte de almacenamiento y responden las primeras a la lógica de la aplicación y la segunda a la necesidad de obtención de datos almacenados.
Llegamos por fin a la parte en la que implementamos la lógica de la persistencia. Necesitamos, en principio, tres clases: una que abstraiga del establecimiento de la conexión, que bien podría ser ConnectionPool y otra por cada una de las clases del modelo:
Clase del modelo |
Clase del patrón |
Descripción |
---|---|---|
- |
ConnectionPool |
Se encarga de establecer la conexión. |
Centro |
CentroSqlDao |
Acceso a la tabla Centro. |
Estudiante |
EstudianteSqlDao |
Acceso a la tabla Estudiante. |
La implementación de ambas clases es muy semejante y ambas necesitarán que les pasemos en su construcción un objeto DataSource o un objeto Connection para que puedan llevar a cabo sus operaciones. ¿Qué les pasamos? Analicémoslo:
Si decidimos que se construyan pasando un objeto Connection, corremos el riesgo de que este objeto se cierre en algún momento, con lo que los objetos dejarán de poder realizar operaciones.
Si pasando un objeto DataSource, podemos crear una nueva conexión dedicada cada vez que hagamos una operación, pero nos quedamos sin la posibilidad de hacer transacciones que incluyan dos o más operaciones.
La mejor solución es permitir que se pase una u otra cosa, de manera que cuando tengamos que hacer una operación podamos obrar así:
@Override
public boolean delete(Long id) throws DataAccessException {
String sqlString = "DELETE FROM Centro WHERE id_centro = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection(); // ¡Ojo! No debe cerrarse directamente
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, id);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible borrar el centro", e);
}
}
Básicamente, tenemos un objeto cp
(ya veremos que de tipo ConnProvider
)
que nos permite crear un objeto envoltorio (cw
). A partir de este objeto
envoltorio podemos obtener la conexión en sí para hacer la operación. La
particularidad de este envoltorio es que al cerrar el objeto se cierra el objeto
Connection asociado, si se tomó para construir el objeto cp
un
DataSource; pero no se cerrará, si lo que se tomó para construir fue el propio
Connection.
Nota
Toda esta argucia es necesaria si queremos habilitar transacción que incluyan varias operaciones. Si no es así, podríamos construir los objetos DAO con el DataSource.
Por tanto, tenemos que implementar estos dos tipos de objetos:
public class ConnProvider {
private final DataSource ds;
private final Connection conn;
public static class ConnWrapper implements AutoCloseable {
private final Connection conn;
private final boolean closeable;
public ConnWrapper(DataSource ds) throws DataAccessException {
try {
conn = ds.getConnection();
}
catch(SQLException e) {
throw new DataAccessException(e);
}
closeable = true;
}
public ConnWrapper(Connection conn) {
this.conn = conn;
closeable = false;
}
public Connection getConnection() {
return conn;
}
@Override
public void close() throws SQLException {
if(closeable) conn.close();
}
}
public ConnProvider(DataSource ds) {
this.ds = ds;
conn = null;
}
public ConnProvider(Connection conn) {
ds = null;
this.conn = conn;
}
public ConnWrapper getConnWrapper() throws DataAccessException {
return ds == null?new ConnWrapper(conn):new ConnWrapper(ds);
}
}
Ya están listos los preliminares. Hechos, podemos implementar por completo las dos clases DAO:
public class CentroSqlDao implements Crud<Centro> {
private final ConnProvider cp;
public CentroSqlDao(DataSource ds) throws DataAccessException {
cp = new ConnProvider(ds);
}
public CentroSqlDao(Connection conn) {
cp = new ConnProvider(conn);
}
public static Centro resultSetToCentro(ResultSet rs) throws SQLException {
Long id = rs.getLong("id_centro");
String nombre = rs.getString("nombre");
Titularidad titularidad = Titularidad.fromNombre(rs.getString("titularidad"));
return new Centro(id, nombre, titularidad);
}
public static void centroToParams(PreparedStatement pstmt, Centro centro) throws SQLException {
pstmt.setString(1, centro.getNombre());
pstmt.setString(2, centro.getTitularidad().getNombre());
// En este caso el ID siempre tiene valor, con lo que puede usarse directamente setInt.
pstmt.setObject(3, centro.getId(), Types.BIGINT);
}
@Override
public Optional<Centro> get(Long id) throws DataAccessException {
String sqlString = "SELECT * FROM Centro WHERE id_centro = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, id);
try(ResultSet rs = pstmt.executeQuery()) {
return rs.next()?Optional.of(resultSetToCentro(rs)):Optional.empty();
}
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible obtener el centro", e);
}
}
@Override
public List<Centro> get() throws DataAccessException {
String sqlString = "SELECT * FROM Centro";
List<Centro> centros = new ArrayList<>();
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(Statement pstmt = conn.createStatement()) {
try(ResultSet rs = pstmt.executeQuery(sqlString)) {
while(rs.next()) {
centros.add(resultSetToCentro(rs));
}
return centros;
}
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible obtener el listado de centros", e);
}
}
@Override
public boolean delete(Long id) throws DataAccessException {
String sqlString = "DELETE FROM Centro WHERE id_centro = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, id);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible borrar el centro", e);
}
}
@Override
public void insert(Centro centro) throws DataAccessException {
String sqlString = "INSERT INTO Centro (nombre, titularidad, id_centro) VALUES (?, ?, ?)";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
centroToParams(pstmt, centro);
pstmt.executeUpdate();
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible agregar el centro", e);
}
}
@Override
public boolean update(Centro centro) throws DataAccessException {
String sqlString = "UPDATE Centro SET nombre = ?, titularidad = ? WHERE id_centro = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
centroToParams(pstmt, centro);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible actualizar el centro", e);
}
}
@Override
public boolean update(Long oldId, Long newId) throws DataAccessException {
String sqlString = "UPDATE Centro SET id_centro = ? WHERE id_centro = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, oldId);
pstmt.setLong(2, newId);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible actualizar el identificador del centro", e);
}
}
}
public class EstudianteSqlDao implements Crud<Estudiante> {
private final ConnProvider cp;
public EstudianteSqlDao(DataSource ds) throws DataAccessException {
cp = new ConnProvider(ds);
}
public EstudianteSqlDao(Connection conn) {
cp = new ConnProvider(conn);
}
private static Estudiante resultSetToEstudiante(ResultSet rs, Connection conn) throws SQLException {
Long id = rs.getLong("id_estudiante");
String nombre = rs.getString("nombre");
Date nac = rs.getDate("nacimiento");
LocalDate nacimiento = nac == null?null:nac.toLocalDate();
Long idCentro = rs.getLong("centro");
Centro centro = null;
// Carga inmediata.
if(idCentro != null) {
CentroSqlDao centroDao = new CentroSqlDao(conn);
try {
centro = centroDao.get(idCentro).orElse(null);
assert centro != null: "Identificador como clave foránea no existe";
}
catch(DataAccessException e) {
throw new SQLException(e);
}
}
return new Estudiante(id, nombre, nacimiento, centro);
}
private static void estudianteToParams(PreparedStatement pstmt, Estudiante estudiante) throws SQLException {
pstmt.setString(1, estudiante.getNombre());
LocalDate nacimiento = estudiante.getNacimiento();
pstmt.setDate(2, nacimiento == null?null:Date.valueOf(nacimiento));
Centro centro = estudiante.getCentro();
pstmt.setObject(3, centro == null?null:centro.getId(), Types.BIGINT);
pstmt.setObject(4, estudiante.getId() == null?null:estudiante.getId(), Types.BIGINT);
}
@Override
public Optional<Estudiante> get(Long id) throws DataAccessException {
String sqlString = "SELECT * FROM Estudiante WHERE id_estudiante = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, id);
try(ResultSet rs = pstmt.executeQuery()) {
return rs.next()?Optional.of(resultSetToEstudiante(rs, conn)):Optional.empty();
}
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible obtener el estudiante", e);
}
}
@Override
public List<Estudiante> get() throws DataAccessException {
String sqlString = "SELECT * FROM Estudiante";
List<Estudiante> estudiantes = new ArrayList<>();
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(Statement pstmt = conn.createStatement()) {
try(ResultSet rs = pstmt.executeQuery(sqlString)) {
while(rs.next()) {
estudiantes.add(resultSetToEstudiante(rs, conn));
}
return estudiantes;
}
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible obtener el listado de estudiantes", e);
}
}
@Override
public boolean delete(Long id) throws DataAccessException {
String sqlString = "DELETE FROM Estudiante WHERE id_estudiante = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, id);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible borrar el estudiante", e);
}
}
@Override
public void insert(Estudiante estudiante) throws DataAccessException {
String sqlString = "INSERT INTO Estudiante (nombre, nacimiento, centro, id_estudiante) VALUES (?, ?, ?, ?)";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString, Statement.RETURN_GENERATED_KEYS)) {
estudianteToParams(pstmt, estudiante);
pstmt.executeUpdate();
try(ResultSet rs = pstmt.getGeneratedKeys()) {
if(rs.next()) estudiante.setId(rs.getLong(1));
}
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible agregar el estudiante", e);
}
}
@Override
public boolean update(Estudiante estudiante) throws DataAccessException {
String sqlString = "UPDATE Estudiante Centro SET nombre = ?, nacimiento = ?, centro = ? WHERE id_estudiante = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
estudianteToParams(pstmt, estudiante);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible actualizar el estudiante", e);
}
}
@Override
public boolean update(Long oldId, Long newId) throws DataAccessException {
String sqlString = "UPDATE Estudiante SET id_estudiante = ? WHERE id_estudiante = ?";
try(ConnWrapper cw = cp.getConnWrapper()) {
Connection conn = cw.getConnection();
try(PreparedStatement pstmt = conn.prepareStatement(sqlString)) {
pstmt.setLong(1, oldId);
pstmt.setLong(2, newId);
return pstmt.executeUpdate() > 0;
}
}
catch(SQLException e) {
throw new DataAccessException("Imposible actualizar el identificador del estudiante", e);
}
}
}
Nótese que al crear estos objetos, podemos pasar un DataSource o un Connection. Lo primero será lo habitual, pero lo segundo lo podemos hacer cuando queremos que todas las operaciones pueden compartir conexión para formar parte de una misma transacción.
Otra particularidad de estas clases es que cómo son tan sencillas, usan SQL estándar y, por consiguiente, el código no depende del SGBD particular. De ahí que hayamos elegido nombres que hacen referencia a SQL y no a SQLite.
Sea como sea, estas clases nos permiten que el resto del código se abstraiga de cómo se hacen persistentes los datos. De este modo, hacer persistente un nuevo centro puede hacerse así:
// cpool es un objeto ConnectionPool y obtengo de él el DataSource.
DataSource ds = cpool.getDataSource();
Crud<Centro> centroDao = new CentroSqlDao(ds);
Centro astaroth = new Centro(11701164L, "IES Astaroth",Titularidad.PUBLICA);
centroDao.insert(astaroth);
Hay, por último, un detalle que no debe pasarse por alto: la relación entre
Estudiante
y Centro
. En el código se resuelve de la manera más sencilla
posible: al cargar un objeto Estudiante
también se carga el objeto Centro
al que está asociado, ya que una carga perezosa, esto es, que el centro se
cargue sólo cuando se requiere de forma efectiva con su getter
correspondiente, complica mucho el código.
Para terminar, si tuviéramos que crear una transacción, podríamos construir los objetos DAO con una conexión ya abierta:
// cpool es un objeto ConnectionPool y obtengo de él una conexión
try(Connection conn = cpool.getConnection()) {
// Ambos DAO se crean usando la misma conexión.
Crud<Centro> cdao = new CentroSqlDao(conn);
Crud<Estudiante> edao = new EstudianteSqlDao(conn);
// Transacción.
conn.setAutoCommit(false);
try {
cdao.delete(astaroth);
// Esta inserción falla.
cdao.insert(new Centro(11004866L, "IES Centro repetido", Titularidad.PUBLICA));
conn.commit();
}
catch(DataAccessException e) {
System.err.printf("Se malogra la transacción: %s\n", e.getMessage());
conn.rollback();
}
finally {
conn.setAutoCommit(true);
}
}
En el código anterior la eliminación del centro no llega a ser efectiva porque una operación posterior incluida en la misma transacción falla.
Ver también
El repositorio SqlUtils incluye otra implementación del patrón algo más elaborada, cuyo uso se ilustra en el código de prueba.