Tips para usar Spring JdbcTemplate

Ejecutar SQL directamente via JDBC es una de las tantas formas que existe para interactuar en la base datos desde la capa de persistencia. Si vienen del mundo de Java y Spring una de las alternativas más populares que ofrece es usar el famosísimo JdbcTemplate.

Ahora es obvio que el usar Jdbc en vez JPA, que es otra alternativa de persistencia SQL, tienes sus pros y contras en donde como un pro importante es el performance que ganamos en el aplicación al ejecutar sentencies SQL directos y el contra más sobresaliente es el boilerplate que genera y el mantenimiento que esto implica.

OJO: Este no es un post para debatir ampliamente sobre el uso de JPA o Spring JDBC, solo describo los contras más resaltantes para los cuales explicare algunos utilitarios y tips para contrarestarlos en cierto modo.

Si Spring Boot forma parte de tu stack de desarrollo, el Spring JdbcTemplate puedes encontrarlo en los starters como spring-boot-starter-jdbc o spring-boot-starter-jpa que ya incluye el paqute de core de jdbc.

Por otro lado, para la configuración del datasource y JdbcTemplate dejo estos blogs en donde explican muy bien este tópico:


He divido los escenarios en tres secciones: transacciones, consulta de información y ejecución de store procedures (Si es que alguien necesitará usarlo en estos días).

Transacciones

SimpleJdbcInsert, simplifica el código con solo especificar el nombre de la tabla o el procedure y le pasas los parámetros a través de un Mapa.

Al interno este componente usa JdbcTemplate para manejo de las operaciones, eso quiere decir que no le ganará en performance al uso directo del template, además optimiza la metadata de la base de datos solo si la base datos la provee correctamente.

Insertar una entidad y obtener el id generado

Iniciamos con el insert de un registro donde se necesita que devuelva el id generado, operación clásica definiendo la operación SQL en una constante y el GeneratedKeyHolder para obtener el key generado.

public static String INSERT_SQL = "INSERT INTO SURVEY_RESPONSE(SURVEY_ID, USER_ID) VALUES (?, ?)";
public SurveyResponse save(SurveyResponse response) {
KeyHolder holder = new GeneratedKeyHolder();
this.jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, response.getSurveyId());
ps.setString(2, response.getUserId());
return ps;
}
}, holder);
response.setResponseId(holder.getKey().longValue())
return response;
}

Con el SimpleJdbcInsert solo se define la tabla y se ejecuta el método donde retorna como Number, la llave generada. En SimpleJdbcInsert puedes definir otras características de la operación como las columnas que vas usar o como en este caso la columna que va generar la llave (usingGeneratedKeyColumns).

public SurveyResponse save(SurveyResponse response) throws DataAccessException {
SimpleJdbcInsert insert = new
SimpleJdbcInsert(this.jdbcTemplate)
.withTableName("SURVEY_RESPONSE")
.usingGeneratedKeyColumns("RESPONSE_ID");
    Map<String, Object> parameters = new HashMap<>();
parameters.put("SURVEY_ID", response.getSurveyId());
parameters.put("USER_ID", response.getUserId());
Number id = insert.executeAndReturnKey(parameters);
response.setResponseId(id.longValue());
return response;
}

Actualización masiva (batch)

Si necesitas actualizar e insertar en cantidades industriales, recomiendo ejecutar el batch via JdbcTemplate para un mejor desempeño pero también puedes hacerlo con el SimpleJdbcInsert. A continuación se presenta ambos escenarios:

public List<Answer> saveAllAnswers(List<Answer> entities) {
this.jdbcTemplate.batchUpdate(
"INSERT INTO ANSWER(RESPONSE_ID, QUESTION_ID, OPTION_ID, TEXT) VALUES (?,?,?,?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i)
throws SQLException {
Answer answer = entities.get(i);
ps.setLong(1, answer.getResponseId());
ps.setString(2, answer.getQuestionId());
ps.setString(3, answer.getOptionId());
ps.setString(4, answer.getText());
}
                @Override
public int getBatchSize() {
return entities.size();
}
});
return entities;
}

Usando SimpleJdbcInsert y el utilitario SqlParameterSourceUtils para crear el batch SQL con solo pasarle el arreglo de objetos.

public List<Answer> saveAllAnswers(List<Answer> entities) {
SimpleJdbcInsert simpleJdbcInsert =
new SimpleJdbcInsert(this.jdbcTemplate)
.withTableName("ANSWER")
.usingGeneratedKeyColumns("ID");
  int[] ints = simpleJdbcInsert.executeBatch(
SqlParameterSourceUtils.createBatch(entities.toArray()));
return entities;
}

Repositorio genérico

Gracias a SimpleJdbcInsert podemos abstraer la ejecución de los registros en un método donde tenemos como parámetros mandatorios el nombre de la tabla y el mapa de datos.

public void insert(Map<String, Object> map, String tableName) throws DataAccessException {
if (logger.isDebugEnabled()) {
logger.debug("Inserting data into table : " + tableName);
map.forEach((key, value) ->
logger.debug(key + ":" + value));
}
SimpleJdbcInsert insert= new SimpleJdbcInsert(this.jdbcTemplate)
.withTableName(tableName);
insert.execute(map);
}

Esto puede ayudar para registro de eventos, tabla “catálogos” o tablas temporales. Registro de data que no requiera mucho cuidado.

Queries

RowMapper vs BeanPropertyRowMapper

En mi opinión, tal como lo describe Spring en su documentación, por temas de performance casi en el 99% de los casos usaría RowMapper.

Fuente: Documentación de Spring

Pero habrá casos excepcionales donde quieras usar el componente BeanPropertyRowMapper, como por ejemplo: obtener catálogos de varios atributos con mediana cantidad de data, entre otros casos.

Solo para quedar claro, para los demás casos usar RowMapper, RowMapper FTW!

Aca un ejemplo de un query de listado de personas implementado por diferentes Mappers.

private static final String PERSON_SQL = "SELECT ID, NAME,"
"EMAIL, PHONE, GENRE, AGE, BIRTHDAY, OCCUPATION, " +
"ADDRESS, STREET, CITY, STATE, COUNTRY, ZIP_CODE " +
"FROM PERSON";
public List<Person> find(String id) throws DataAccessException {
List<Person> people = this.jdbcTemplate
.queryForObject(PERSON_SQL,
new Object[] { id },
new PersonMapper());
return people;
}
private static class PersonMapper implements RowMapper<Person> {
@Override
public Person mapRow(ResultSet rs, int rowNum) throws SQLException {
Person person = new Person();
person.setId(rs.getInt("ID"));
person.setFirstName(rs.getString("NAME"));
// TODO: set other attributes
.
.
.
person.setZipCode(rs.getInt("ZIP_CODE"))
return person;
}
}

Con el BeanPropertyRowMapper te ahorras el seteo por atributo siempre y cuando el bean tenga el constructor por defecto.

public List<Person> find() throws DataAccessException {
List<Person> people = this.jdbcTemplate
.query(PERSON_SQL, new BeanPropertyRowMapper(Person.class));
return people;
}

Stored Procedures

SimpleJdbcCall al igual que el SimpleJdbcInsert te permite definir características para realizar la ejecución del store procedure o de una función, la compilación se realiza en la primera llamada luego Spring lo almacena en memoria para las siguientes operaciones.

A continuación, un ejemplo de un store que retorna un listado de personas según el segmento donde se usa el método .returnResultSet() para definir el resultado esperado con el mapper especifico.

private SimpleJdbcCall personSp;

public PersonRepository(JdbcTemplate jdbcTemplate) {
this.personSp = new SimpleJdbcCall(jdbcTemplate)
.withSchemaName("SCHEMA")
.withProcedureName("SP_GET_PERSON_BY_SEGMENT")
.declareParameters(
new SqlParameter("P_SEGMENT_ID", Types.VARCHAR))
.returningResultSet("RESULT", new PersonRowMapper());
}
public List<Person> getPeopleFromSegment(String segmentId) {
Map<String, Object> out = personSp.execute(segmentId);
return (List<Person>) out.get("RESULT");
}

Como se puede apreciar en el SimpleJdbcCall se puede definir más características tanto básicas como avanzadas en la ejecución de un store procedure de una forma más declarativa.

Tengo como // TODO agregar microbenchmarks en comparación de algunos componentes, para convencer a los escépticos que no crean en mi palabra :)