Services en la capa de negocio¶
Los Service (clases anotadas con @Service en Spring) contienen la lógica de negocio de la aplicación y actúan como la capa intermedia entre los Controller (capa de presentación) y los Repository (capa de persistencia). Sus responsabilidades principales son:
- Orquestar operaciones que involucran varias entidades o repositorios.
- Mantener límites transaccionales (
@Transactional). - Aplicar validaciones y reglas de negocio antes de persistir datos.
- Gestionar excepciones y adaptar errores para la capa superior.
- Mapear entidades a DTOs y viceversa (p. ej. con
ModelMapper).
Separar esta lógica en Service mejora la testabilidad, la reutilización y mantiene los Controller ligeros.
Buenas prácticas¶
- Marcar métodos que modifican datos con
@Transactional. - Devolver/aceptar DTOs en las fronteras del
Servicepara evitar exponer entidades JPA. - Mantener servicios cohesionados (cada
Servicedebería tener responsabilidad clara). - Documentar efectos secundarios (persistencia, eventos publicados, llamadas externas).
Ejemplo: CRUD completo con ModelMapper¶
Supongamos una entidad Form y su FormDto. El Service manejará operaciones básicas: crear, leer, actualizar y borrar, usando ModelMapper para convertir entre entidad y DTO.
1) Entidad (resumen)
@Entity
public class Form {
@Id
private UUID id;
private String title;
private String description;
private Status status;
// getters/setters
}
2) DTO (resumen)
public class FormDto {
private UUID id;
private String title;
private String description;
// getters/setters
}
3) FormService — implementación CRUD
@Service
public class FormService {
private final FormRepository formRepository;
private final ModelMapper modelMapper;
public FormService(FormRepository formRepository, ModelMapper modelMapper) {
this.formRepository = formRepository;
this.modelMapper = modelMapper;
}
// Leer por id
@Transactional(readOnly = true)
public FormDto get(UUID id) {
Form form = formRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Form not found"));
return modelMapper.map(form, FormDto.class);
}
// Leer todos
@Transactional(readOnly = true)
public List<FormDto> getAll() {
List<Form> forms = formRepository.findAll();
Type listType = new TypeToken<List<FormDto>>() {}.getType();
return modelMapper.map(forms, listType);
}
// Crear
@Transactional
public FormDto create(FormDto dto) {
Form form = modelMapper.map(dto, Form.class);
if (form.getId() == null) {
form.setId(UUID.randomUUID());
}
form.setStatus(Status.ACTIVE);
Form saved = formRepository.save(form);
return modelMapper.map(saved, FormDto.class);
}
// Actualizar
@Transactional
public FormDto update(UUID id, FormDto dto) {
Form existing = formRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Form not found"));
// Mapear sobre la entidad existente para conservar campos no presentes en el DTO
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
modelMapper.map(dto, existing);
Form saved = formRepository.save(existing);
return modelMapper.map(saved, FormDto.class);
}
// Borrar
@Transactional
public void delete(UUID id) {
if (!formRepository.existsById(id)) {
throw new EntityNotFoundException("Form not found");
}
formRepository.deleteById(id);
}
}