JdbcTemplate: Consulta la base de datos World de MySQL
Índice
- 1. Contexto y objetivo
- 2. Configuración del proyecto
- 3. El record City
- 4. CityRepository con JdbcTemplate
- 5. RowMapper inline con lambda
- 6. Test de integración con JUnit
- 7. Conclusión
1. Contexto y objetivo
La base de datos world es una base de datos de ejemplo distribuida por MySQL. Contiene tres tablas: city, country y countrylanguage. Es un punto de partida conveniente para practicar consultas JDBC sin tener que diseñar un esquema propio.
El objetivo de este tutorial es conectar una aplicación Spring Boot a esa base de datos y consultar la tabla city usando JdbcTemplate, el componente central del módulo spring-jdbc para ejecutar operaciones JDBC sin manejar conexiones ni excepciones de bajo nivel de forma manual.
No se usa Spring Data JPA. La interacción con la base de datos es directa vía SQL, lo cual hace explícito exactamente qué se consulta y cómo se mapea el resultado.
2. Configuración del proyecto
Dependencias
El proyecto requiere dos dependencias en pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
spring-boot-starter-jdbc incluye spring-jdbc y configura automáticamente un bean JdbcTemplate si hay un DataSource disponible en el contexto. mysql-connector-j es el driver JDBC oficial de MySQL.
application.properties
spring.application.name=spring-world-api
spring.datasource.url=${DB_WORLD_URL}
spring.datasource.username=${DB_WORLD_USER}
spring.datasource.password=${DB_WORLD_PASS}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Las credenciales se inyectan desde variables de entorno. Un valor típico para DB_WORLD_URL sería:
jdbc:mysql://localhost:3306/world
La propiedad driver-class-name es opcional cuando se usa mysql-connector-j, ya que Spring Boot puede inferir el driver desde la URL. Se incluye acá por claridad explícita.
3. El record City
package com.springwriter;
public record City(
int id,
String name,
String countryCode,
String district,
int population
) {}
City es un Java record. Los records son clases inmutables con constructor canónico, equals, hashCode y toString generados automáticamente por el compilador. Son adecuados como objetos de transferencia de datos cuando no se requiere mutabilidad.
La tabla city en la base de datos world tiene la siguiente estructura relevante:
CREATE TABLE city (
ID int NOT NULL AUTO_INCREMENT,
Name char(35) NOT NULL DEFAULT '',
CountryCode char(3) NOT NULL DEFAULT '',
District char(20) NOT NULL DEFAULT '',
Population int NOT NULL DEFAULT 0,
PRIMARY KEY (ID)
);
Los campos del record se corresponden directamente con las columnas de esa tabla.
4. CityRepository con JdbcTemplate
package com.springwriter;
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class CityRepository {
private final JdbcTemplate jdbcTemplate;
public CityRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<City> getAllCities() {
return jdbcTemplate.query(
"SELECT ID, Name, CountryCode, District, Population FROM city",
(rs, rowNum) -> new City(
rs.getInt("ID"),
rs.getString("Name"),
rs.getString("CountryCode"),
rs.getString("District"),
rs.getInt("Population")
)
);
}
}
@Repository marca la clase como componente de acceso a datos. Spring la detecta en el escaneo de componentes y además aplica traducción de excepciones JDBC a la jerarquía de org.springframework.dao, lo que permite manejar errores de persistencia con excepciones no verificadas.
El bean JdbcTemplate se inyecta por constructor. Spring Boot lo configura automáticamente a partir del DataSource definido en application.properties. JdbcTemplate es thread-safe una vez configurado, por lo que puede compartirse entre múltiples beans sin sincronización adicional.
El método query ejecuta la consulta SQL y mapea cada fila del ResultSet mediante el RowMapper provisto.
5. RowMapper inline con lambda
RowMapper<T> es una interfaz funcional del módulo spring-jdbc:
@FunctionalInterface
public interface RowMapper<T extends @Nullable Object> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
Al ser funcional, puede implementarse con una expresión lambda. En getAllCities, el lambda recibe el ResultSet posicionado en la fila actual y el número de fila, y retorna una instancia de City:
(rs, rowNum) -> new City(
rs.getInt("ID"),
rs.getString("Name"),
rs.getString("CountryCode"),
rs.getString("District"),
rs.getInt("Population")
)
JdbcTemplate itera el ResultSet internamente y llama al RowMapper por cada fila. Las SQLException que puedan lanzarse dentro del lambda son capturadas y traducidas por JdbcTemplate.
Alternativas al RowMapper inline
| Opción | Cuándo usarla |
|---|---|
| Lambda inline | Lógica de mapeo simple y usada en un solo lugar |
Clase que implementa RowMapper<T> |
Lógica reutilizable en varios métodos o repositorios |
BeanPropertyRowMapper<T> |
Mapeo por convención nombre-columna/nombre-campo (requiere setter o convención de nombres compatible) |
DataClassRowMapper<T> |
Mapeo a records o clases sin setters, disponible desde Spring 6.1 |
Para este caso, el lambda inline es suficiente. DataClassRowMapper<City> sería una alternativa válida si los nombres de las columnas coincidieran con los del record tras normalización, pero en este caso hay diferencias (CountryCode vs countryCode) que requerirían configuración adicional.
6. Test de integración con JUnit
package com.springwriter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringWorldApiApplicationTests {
CityRepository cityRepository;
public SpringWorldApiApplicationTests(@Autowired CityRepository cityRepository) {
this.cityRepository = cityRepository;
}
@Test
void testCityRepository_getAllCities() {
List<City> cities = cityRepository.getAllCities();
assertThat(cities).isNotEmpty();
assertThatList(cities).hasSize(4079);
}
}
@SpringBootTest levanta el contexto completo de Spring, incluyendo la conexión real a la base de datos. Este es un test de integración: verifica que el repositorio funciona contra la base de datos world instalada localmente o en el entorno de CI.
La segunda assertion assertThatList(cities).hasSize(4079) verifica que la tabla city de la base de datos world contiene exactamente 4079 filas, que es el número de registros en la distribución estándar de esa base de datos.
Precondición del test
Este test depende de que la base de datos world esté disponible con sus datos originales al momento de la ejecución. Las variables de entorno DB_WORLD_URL, DB_WORLD_USER y DB_WORLD_PASS deben estar configuradas en el entorno donde corre el test.
7. Conclusión
El flujo completo del tutorial cubre la configuración de spring-boot-starter-jdbc, la definición de un record como modelo de datos, la implementación de un repositorio con JdbcTemplate y un RowMapper inline, y la verificación del comportamiento con un test de integración contra la base de datos real.
JdbcTemplate mantiene el control explícito sobre el SQL ejecutado sin delegar esa responsabilidad a un framework de mapeo objeto-relacional. Eso lo hace útil en contextos donde se necesita precisión sobre las consultas o donde no se quiere introducir la complejidad de JPA.
Un paso natural desde este punto es agregar consultas parametrizadas, por ejemplo para filtrar ciudades por CountryCode o por rango de Population, usando los métodos query de JdbcTemplate que aceptan Object... o PreparedStatementSetter como argumentos.
Comentarios (0)
Todavía no hay comentarios.
Dejá un comentario