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.
4.6.1. Patrón DAO#
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 TestDAO. 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. |
Así pues, podemos codificar las dos clases DAO que implementa la interfaz CRUD:
public class CentroSqlDao implements Crud<Centro> {
/** Pool de conexiones */
private final DataSource cp;
/**
* Constructor que inicializa el proveedor de conexiones con un {@link DataSource}.
*
* @param ds Fuente de datos para obtener conexiones.
*/
public CentroSqlDao(DataSource ds) {
this.cp = ds;
}
/**
* Convierte un {@link ResultSet} en un objeto {@link Centro}.
*
* @param rs El {@link ResultSet} que contiene los datos del centro.
* @return Un objeto {@link Centro} con los datos del {@link ResultSet}.
* @throws SQLException Si ocurre un error al acceder a los datos del {@link ResultSet}.
*/
public static Centro resultSetToCentro(ResultSet rs) throws SQLException {
Long id = rs.getLong("id_centro");
String nombre = rs.getString("nombre");
Titularidad titularidad = Titularidad.fromString(rs.getString("titularidad"));
return new Centro(id, nombre, titularidad);
}
/**
* Establece los parámetros de un {@link PreparedStatement} con los datos de un {@link Centro}.
*
* @param pstmt El {@link PreparedStatement} donde se establecerán los parámetros.
* @param centro El objeto {@link Centro} cuyos datos se usarán para establecer los parámetros.
* @throws SQLException Si ocurre un error al establecer los parámetros en el {@link PreparedStatement}.
*/
public static void centroToParams(PreparedStatement pstmt, Centro centro) throws SQLException {
pstmt.setString(1, centro.getNombre());
pstmt.setString(2, centro.getTitularidad().toString());
// 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(Connection conn = cp.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(Connection conn = cp.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(Connection conn = cp.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(Connection conn = cp.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(Connection conn = cp.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(Connection conn = cp.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);
}
}
}
Nota
Como la clase es tan sencilla (también la relativa a Estudiante
),
usa 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.
Y EstudianteSqlDao
se implementará de modo análogo. Obsérvese que para
cualquier operación se opera del mismo modo:
@Override
public boolean delete(Long id) throws DataAccessException {
String sqlString = "DELETE FROM Centro WHERE id_centro = ?";
try(Connection conn = cp.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);
}
}
O sea, abrimos una conexión gracias al pool, realizamos la operación y cerramos.
Prudencia
Hay un grave carencia en la clase DAO que tendremos que resolver aún, pero es mejor no adelantarse.
Veamos cómo operar, por ejemplo, para hacer persistente un nuevo centro:
// 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, sin embargo, un grave problema: cada operación crea, usa y cierra su propia conexión, lo que provoca que dos o más conexiones no puedan pertenecer a una mismo conexión, es decir, no tenemos soporte para transacciones. Lo interesante sería que, además de poder pasar un DataSource para crear un objeto DAO y que este lo usara para crear sus propias construcciones, pudiéramos también poder pasar directamente una conexión y que este la usara sin llegar a cerrarla nunca. De este modo podríamos hacer:
// 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, las operaciones no deberían cerrar la conexión común, por lo que las dos pertenecerían a una misma conexión y la eliminación del centro nunca llegaría a producirse porque la inserción posterior incluida en la misma transacción falla. Además, el código de implementación de las clases DAO no debería complicarse para que el programador no tenga que estar atento a si cierra o no conexiones.
La solución es permitir que los objetos DAO se construyan tanto con un
DataSource como con un objeto Connection, para que se puedan usar como hemos
ilustrado arriba (transacción) o aún más arriba (sin transacción); y crear una clase auxiliar
ConnProvider
que abstraiga al escribir el DAO de la naturaleza del objeto
con se creó. De esta manera podremos seguir implementando las operaciones CRUD
tal como ilustramos la de delete, esto es, cerrando
aparentemente la conexión. En definitiva, CentroSqlDao
debe implementarse
exactamente como ilustramos antes, excepto la parte referente a la construcción
y el atributo cp
que ya no es un DataSource, sino de esta clase
ConnProvider
:
public class CentroSqlDao implements Crud<Centro> {
/** Proveedor de conexiones. */
private final ConnProvider cp;
/**
* Constructor que inicializa el proveedor de conexiones con un {@link DataSource}.
*
* @param ds Fuente de datos para obtener conexiones.
*/
public CentroSqlDao(DataSource ds) {
cp = new ConnProvider(ds);
}
/**
* Constructor que inicializa el proveedor de conexiones con una conexión existente.
* @param conn Conexión existente para el proveedor de conexiones.
*/
public CentroSqlDao(Connection conn) {
cp = new ConnProvider(conn);
}
/**
* Convierte un {@link ResultSet} en un objeto {@link Centro}.
*
* @param rs El {@link ResultSet} que contiene los datos del centro.
* @return Un objeto {@link Centro} con los datos del {@link ResultSet}.
* @throws SQLException Si ocurre un error al acceder a los datos del {@link ResultSet}.
*/
Por su parte, la clase auxiliar es la que técnicamente requiere que nos comamos más la cabeza:
public class ConnProvider {
/** DataSource utilizado para obtener conexiones. */
private final DataSource ds;
/** Conexión utilizada si se construye el proveedor con una conexión existente. */
private final Connection conn;
/**
* Wrapper para conexiones que evita que se cierren al invocar close().
*/
private static class ConnectionWrapper implements InvocationHandler {
/** Conexión original que se envuelve. */
private final Connection conn;
/**
* Constructor privado que crea un wrapper para una conexión.
* @param conn La conexión original a envolver.
*/
private ConnectionWrapper(Connection conn) {
this.conn = conn;
}
/**
* Crea un proxy de la conexión original que evita que se cierre al invocar close().
* @param conn La conexión original a envolver.
* @return Un objeto Connection que actúa como proxy de la conexión original.
*/
public static Connection createProxy(Connection conn) {
return (Connection) Proxy.newProxyInstance(
conn.getClass().getClassLoader(),
new Class<?>[]{Connection.class},
new ConnectionWrapper(conn)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch(method.getName()) {
// Evitamos cerrar la conexión al invocar close().
case "close":
return null;
// Cualquier otro método de la interfaz Connection
// se delega al objeto Connection original.
default:
return method.invoke(conn, args);
}
}
}
/**
* Constructor que inicializa el proveedor de conexiones con un {@link DataSource}.
* @param ds Fuente de datos para obtener conexiones.
*/
public ConnProvider(DataSource ds) {
this.ds = ds;
conn = null;
}
/**
* Constructor que inicializa el proveedor de conexiones con una conexión existente.
* @param conn Conexión existente a utilizar.
*/
public ConnProvider(Connection conn) {
ds = null;
this.conn = conn;
}
/**
* Obtiene una conexión a la base de datos.
* Si se construyó con un {@link DataSource}, devuelve una nueva conexión del pool.
* Si se construyó con una conexión existente, devuelve esa conexión envuelta en un proxy.
* @return Una conexión a la base de datos.
* @throws DataAccessException Si ocurre un error al obtener la conexión.
*/
public Connection getConnection() throws DataAccessException {
if(ds != null) {
try {
return ds.getConnection();
} catch (SQLException e) {
throw new DataAccessException(e);
}
}
else return ConnectionWrapper.createProxy(conn);
}
}
Básicamente este objeto se construye con un DataSource o un Connection y
tiene un método .getConnection()
. Cuando se construye con DataSource se
limita a generar una nueva conexión del pool y devolver el objeto; en cambio (y
esta es la magia), cuando se construye con un Connection no devuelve el propio
objeto sino un objeto envoltorio que se comporta del mismo modo, excepto por el
hecho de que tiene deshabilitado el método .close()
: cuando se invoca no
hace nada y, en consecuencia, no se cierra la conexión. De esta forma, podemos
implementar las operaciones en las clases DAO con la cláusula
try-with-resources,
y las conexiones se cerrarán cuando se construye el DAO con un DataSource y
no lo harán cuando se construye con un Connection.
Nota
La magia se logra gracias al uso del patrón Proxy que permite interceptar los métodos definidos en una interfaz (y Connection lo es).
Finalmente hay un último detalle para el cual conviene que mostremos la
implementación de EstudianteSqlDao
:
public class EstudianteSqlDao implements Crud<Estudiante> {
/** Proveedor de conexiones. */
private final ConnProvider cp;
/**
* Constructor que inicializa el proveedor de conexiones con un {@link DataSource}.
*
* @param ds Fuente de datos para obtener conexiones.
*/
public EstudianteSqlDao(DataSource ds) {
cp = new ConnProvider(ds);
}
/**
* Constructor que inicializa el proveedor de conexiones con una conexión existente.
* @param conn Conexión existente para el proveedor de conexiones.
*/
public EstudianteSqlDao(Connection conn) {
cp = new ConnProvider(conn);
}
/**
* Convierte un {@link ResultSet} en un objeto {@link Estudiante}.
*
* @param rs El {@link ResultSet} que contiene los datos del estudiante.
* @param conn Conexión para cargar el centro asociado al estudiante.
* @return Un objeto {@link Estudiante} con los datos del {@link ResultSet}.
* @throws SQLException Si ocurre un error al acceder a los datos del {@link ResultSet}.
*/
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);
}
/**
* Establece los parámetros de un {@link PreparedStatement} con los datos de un {@link Estudiante}.
*
* @param pstmt El {@link PreparedStatement} donde se establecerán los parámetros.
* @param estudiante El objeto {@link Estudiante} cuyos datos se usarán para establecer los parámetros.
* @throws SQLException Si ocurre un error al establecer los parámetros en el {@link PreparedStatement}.
*/
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(Connection conn = cp.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(Connection conn = cp.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);
}
}
public boolean delete(Long id) throws DataAccessException {
String sqlString = "DELETE FROM Estudiante WHERE id_estudiante = ?";
try(Connection conn = cp.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(Connection conn = cp.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(Connection conn = cp.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(Connection conn = cp.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);
}
}
}
El detalle por revisar es 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.
Ver también
El repositorio SqlUtils incluye otra implementación del patrón DAO bastante más elaborada que permite la carga perezosa y cuyo uso se ilustra en el código de prueba del propio paquete.
4.6.2. Conclusiones#
Toca para cerrar enumerar algunas conclusiones a las que hemos llegado:
Es indispensable usar un pool de conexiones para mejorar el rendimiento. De este modo, podemos cerrar las conexiones según la lógica de la aplicación sin preocuparnos de la penalización del rendimiento.
El soporte para transacciones es indispensable.
Debe separarse el acceso a la fuente de datos del resto de la aplicación. Un patrón muy socorrido, sobre todo en bases de datos, es el patrón DAO.
Gran parte de los retos que debemos resolver al programar con conectores, ya los resuelven las herramientas ORM, que trataremos a continuación, por lo que en en muchas ocasiones en las que o bien el programador no es avezado o bien, aunque lo sea, el rendimiento no es crítico, es más inteligente utilizarlas.