Skip to content

Instantly share code, notes, and snippets.

@gchaldu
Created August 1, 2025 16:10
Show Gist options
  • Select an option

  • Save gchaldu/d5b9128f149cd98324785a3c7b911a90 to your computer and use it in GitHub Desktop.

Select an option

Save gchaldu/d5b9128f149cd98324785a3c7b911a90 to your computer and use it in GitHub Desktop.
Implementación de Repositorios y Servicios con Spring JdbcTemplate Sección 5

Ejercicio Práctico: Implementación de Repositorios y Servicios con Spring JDBC

Este Gist presenta una implementación base de las capas de Repositorio y Servicio para los módulos de Educación y Experiencia en una aplicación Spring Boot, utilizando JdbcTemplate. Este código es un punto de partida para comprender cómo interactuar con una base de datos relacional y cómo estructurar las capas de una aplicación típica.


📂 Estructura del Código

El código está organizado en interfaces y sus implementaciones para la capa de persistencia (Repositories) y la capa de lógica de negocio (Services). Puedes encontrar cada componente en su archivo .java correspondiente dentro de este Gist.

1. Interfaces del Repositorio

Definen el contrato para el acceso a datos, especificando las operaciones CRUD (Crear, Leer, Actualizar, Borrar) y búsquedas específicas.

2. Implementaciones del Repositorio

Aquí se encuentra el código concreto para interactuar con la base de datos utilizando JdbcTemplate. Se encargan de la traducción entre objetos Java y filas de la base de datos.

3. Interfaces del Servicio

Definen las operaciones de negocio que la aplicación puede realizar, utilizando los repositorios para la interacción con los datos.

4. Implementaciones del Servicio

Contienen la lógica de negocio y coordinan las operaciones de los repositorios, a menudo incluyendo validaciones de negocio.


💡 Puntos a Considerar para los Estudiantes

  • JdbcTemplate: Observen cómo simplifica las operaciones de base de datos comparado con JDBC puro.
  • RowMapper: Entiendan cómo se mapean las filas de SQL a objetos Java.
  • Optional<T>: Noten su uso para manejar la posible ausencia de resultados de forma segura.
  • Anotaciones de Spring: Reconozcan @Repository y @Service para la gestión de componentes por parte de Spring.
  • Lógica de Negocio: Identifiquen las validaciones de negocio implementadas directamente en los métodos save de los servicios.

¡Esperamos que este Gist les sea de gran ayuda para comprender la interacción entre las capas de persistencia y servicio en Spring Boot!

public interface IEducationRepository {
Education save(Education education);
Optional<Education> findById(Long id);
List<Education> findAll();
void deleteById(Long id);
List<Education> findByPersonalInfoId(Long personalInfoId);
}
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.dao.EmptyResultDataAccessException;
import java.sql.PreparedStatement;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Repository
public class EducationRepositoryImpl implements IEducationRepository {
private final JdbcTemplate jdbcTemplate;
public EducationRepositoryImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
private final RowMapper<Education> educationRowMapper = (rs, rowNum) -> {
Education education = new Education();
education.setId(rs.getLong("id"));
education.setDegree(rs.getString("degree"));
education.setInstitution(rs.getString("institution"));
education.setStartDate(rs.getObject("start_date", LocalDate.class)); // Mapear a LocalDate
education.setEndDate(rs.getObject("end_date", LocalDate.class)); // Mapear a LocalDate (puede ser nulo)
education.setDescription(rs.getString("description"));
education.setPersonalInfoId(rs.getLong("personal_info_id"));
return education;
};
@Override
public Education save(Education education) {
if (education.getId() == null) {
String sql = "INSERT INTO educations (degree, institution, start_date, end_date, description, personal_info_id) VALUES (?, ?, ?, ?, ?, ?) RETURNING id";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, education.getDegree());
ps.setString(2, education.getInstitution());
ps.setObject(3, education.getStartDate()); // Usar setObject para LocalDate
ps.setObject(4, education.getEndDate()); // Usar setObject para LocalDate (admite null)
ps.setString(5, education.getDescription());
ps.setLong(6, education.getPersonalInfoId());
return ps;
}, keyHolder);
education.setId(Objects.requireNonNull(keyHolder.getKey()).longValue());
} else {
String sql = "UPDATE educations SET degree = ?, institution = ?, start_date = ?, end_date = ?, " +
"description = ?, personal_info_id = ? WHERE id = ?";
jdbcTemplate.update(sql,
education.getDegree(),
education.getInstitution(),
education.getStartDate(),
education.getEndDate(),
education.getDescription(),
education.getPersonalInfoId(),
education.getId());
}
return education;
}
@Override
public Optional<Education> findById(Long id) {
String sql = "SELECT id, degree, institution, start_date, end_date, description, " +
"personal_info_id FROM educations WHERE id = ?";
try {
return Optional.of(jdbcTemplate.queryForObject(sql, educationRowMapper, id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<Education> findAll() {
String sql = "SELECT id, degree, institution, start_date, end_date, description, personal_info_id FROM educations";
return jdbcTemplate.query(sql, educationRowMapper);
}
@Override
public void deleteById(Long id) {
String sql = "DELETE FROM educations WHERE id = ?";
jdbcTemplate.update(sql, id);
}
@Override
public List<Education> findByPersonalInfoId(Long personalInfoId) {
String sql = "SELECT id, degree, institution, start_date, end_date, description, personal_info_id FROM educations WHERE personal_info_id = ?";
return jdbcTemplate.query(sql, educationRowMapper, personalInfoId);
}
}
public interface IExperienceRepository {
List<Experience> findAll();
Optional<Experience> findById(Long id);
Experience save(Experience experience);
void deleteById(Long id);
List<Experience> findByPersonalInfoId(Long personalInfoId);
}
import lombok.RequiredArgsConstructor; // Añadir si se usa Lombok para el constructor
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import java.sql.PreparedStatement;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Repository
@RequiredArgsConstructor // Sustituye al constructor explícito para JdbcTemplate si se usa Lombok
public class ExperienceRepositoryImpl implements IExperienceRepository {
private final JdbcTemplate jdbcTemplate;
private final RowMapper<Experience> experienceRowMapper = (rs, rowNum) -> {
Experience experience = new Experience();
experience.setId(rs.getLong("id"));
experience.setJobTitle(rs.getString("job_title"));
experience.setCompanyName(rs.getString("company_name"));
experience.setStartDate(rs.getObject("start_date", LocalDate.class));
experience.setEndDate(rs.getObject("end_date", LocalDate.class));
experience.setDescription(rs.getString("description"));
experience.setPersonalInfoId(rs.getLong("personal_info_id"));
return experience;
};
@Override
public List<Experience> findAll() {
String sql = "SELECT id, job_title, company_name, start_date, end_date, description, personal_info_id FROM experiences";
return jdbcTemplate.query(sql, experienceRowMapper);
}
@Override
public Optional<Experience> findById(Long id) {
String sql = "SELECT id, job_title, company_name, start_date, end_date, description, " +
"personal_info_id FROM experiences WHERE id = ?";
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, experienceRowMapper, id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public Experience save(Experience experience) {
if (experience.getId() == null) {
// INSERT
String sql = "INSERT INTO experiences (job_title, company_name, start_date, end_date, description, " +
"personal_info_id) VALUES (?, ?, ?, ?, ?, ?) RETURNING id";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql,
new String[]{"id"});
ps.setString(1, experience.getJobTitle());
ps.setString(2, experience.getCompanyName());
ps.setObject(3, experience.getStartDate());
ps.setObject(4, experience.getEndDate());
ps.setString(5, experience.getDescription());
ps.setLong(6, experience.getPersonalInfoId());
return ps;
}, keyHolder);
experience.setId(Objects.requireNonNull(keyHolder.getKey()).longValue());
} else {
// UPDATE
String sql = "UPDATE experiences SET job_title = ?, company_name = ?, start_date = ?, end_date = ?, " +
"description = ?, personal_info_id = ? WHERE id = ?";
jdbcTemplate.update(sql,
experience.getJobTitle(),
experience.getCompanyName(),
experience.getStartDate(),
experience.getEndDate(),
experience.getDescription(),
experience.getPersonalInfoId(),
experience.getId());
}
return experience;
}
@Override
public void deleteById(Long id) {
String sql = "DELETE FROM experiences WHERE id = ?";
jdbcTemplate.update(sql, id);
}
public List<Experience> findByPersonalInfoId(Long personalInfoId) {
String sql = "SELECT id, job_title, company_name, start_date, end_date, description, personal_info_id FROM experiences WHERE personal_info_id = ?";
return jdbcTemplate.query(sql, experienceRowMapper, personalInfoId);
}
}
public interface IEducationService {
List<Education> findAll();
Optional<Education> findById(Long id);
Education save(Education education);
void deleteById(Long id);
List<Education> findEducationByPersonalInfoId(Long personalInfoId);
}
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Service
public class EducationServiceImpl implements IEducationService {
private final IEducationRepository educationRepository;
public EducationServiceImpl(IEducationRepository educationRepository) {
this.educationRepository = educationRepository;
}
@Override
public List<Education> findAll() {
return educationRepository.findAll();
}
@Override
public Optional<Education> findById(Long id) {
return educationRepository.findById(id);
}
@Override
public Education save(Education education) {
// Validación 1: Asegurar que la fecha de inicio no sea nula, como exige la DB
if (education.getStartDate() == null) {
throw new IllegalArgumentException("La fecha de inicio de la educación no puede estar vacía.");
}
// Validación 2: La fecha de inicio no puede ser posterior a la de fin
if(education.getEndDate() != null && education.getStartDate().isAfter(education.getEndDate())) {
throw new IllegalArgumentException("La fecha de inicio de la educación no puede ser posterior a la fecha de fin.");
}
return educationRepository.save(education);
}
@Override
public void deleteById(Long id) {
System.out.println("Eliminando educación por ID: " + id + " en el servicio...");
educationRepository.deleteById(id);
}
@Override
public List<Education> findEducationByPersonalInfoId(Long personalInfoId) {
return educationRepository.findByPersonalInfoId(personalInfoId);
}
}
public interface IExperienceService {
List<Experience> findAll();
Optional<Experience> findById(Long id);
Experience save(Experience experience);
void deleteById(Long id);
List<Experience> findExperienceByPersonalInfoId(Long personalInfoId);
}
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Service
public class ExperienceServiceImpl implements IExperienceService {
private final IExperienceRepository experienceRepository;
public ExperienceServiceImpl(IExperienceRepository experienceRepository) {
this.experienceRepository = experienceRepository;
}
@Override
public List<Experience> findAll() {
return experienceRepository.findAll();
}
@Override
public Optional<Experience> findById(Long id) {
return experienceRepository.findById(id);
}
@Override
public Experience save(Experience experience) {
// Validación 1: Asegurar que la fecha de inicio no sea nula
if(experience.getStartDate() == null){
throw new IllegalArgumentException("La fecha de inicio de la experiencia no puede estar vacía.");
}
// Validación 2: La fecha de inicio no puede ser posterior a la de fin (solo si end_date no es nula)
if(experience.getEndDate()!=null
&& experience.getStartDate().isAfter(experience.getEndDate())
){
throw new IllegalArgumentException("La fecha de inicio de la experiencia no puede ser posterior a la fecha de fin.");
}
// Validaciones 3 y 4 (ya estaban bien):
if(experience.getJobTitle()==null || experience.getJobTitle().trim().isEmpty()){
throw new IllegalArgumentException("El título del trabajo no puede estar vacío.");
}
if(experience.getCompanyName()==null || experience.getCompanyName().trim().isEmpty()){
throw new IllegalArgumentException("El nombre de la compañía no puede estar vacío.");
}
return experienceRepository.save(experience);
}
@Override
public void deleteById(Long id) {
System.out.println("Eliminando experiencia por ID: " + id + " en el servicio...");
experienceRepository.deleteById(id);
}
@Override
public List<Experience> findExperienceByPersonalInfoId(Long personalInfoId) {
return experienceRepository.findByPersonalInfoId(personalInfoId);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment