Fundamentos de Spring Boot¶
Objetivos específicos¶
- Comprender los conceptos básicos de Spring Boot como framework para aplicaciones Java.
- Configurar un entorno de desarrollo para proyectos Spring Boot.
- Desarrollar una aplicación sencilla utilizando las características principales de Spring Boot.
- Familiarizarse con las herramientas necesarias para el desarrollo moderno en Java.
!!! TIP "💁♂️ Uno de los principales objetivos es:" - Fomentar la practica del aprendisaje autonomo y el uso de recursos en línea es decir aprender a aprender siguiendo el concepto de aprendizaje autodidacta.
Recursos para la elaboración de la práctica¶
- Documentación oficial de Spring Boot
- Guía de inicio rápido de Spring Boot
- Tutoriales de Spring Boot en Baeldung
- Videos de Spring Boot en YouTube
Requisitos previos¶
- Conocimientos básicos de Java y programación orientada a objetos.
- Instalación de Java Development Kit (JDK) de java 21 o superior.
- Instalación de un IDE compatible con Java (Eclipse, IntelliJ IDEA, VSCode).
- Instalación de Maven o Gradle para la gestión de dependencias.
- Instalación de Git para control de versiones.
- Sistema operativo: macOS, Windows (incluyendo WSL) o Linux (se recomienda WSL para usuarios de Windows).
Dinámica de la práctica¶
- Configuración del entorno de desarrollo para Spring Boot.
- Creación de un proyecto Spring Boot utilizando Spring Initializr.
- Desarrollo de una aplicación sencilla que incluya controladores, servicios y repositorios.
Entregables¶
- Esta tarea debe ser realizada de forma individual.
- La práctica debe ser entregada en un repositorio de GitHub.
- La practica consta de dos ejercicios:
- Ejercicio 1: Mini-ORM en Java (sin Spring Boot)
- Ejercicio 2: Mini-ORM con Spring Boot
- Código fuente del proyecto en un repositorio de GitHub, para ello se recomienda trabajar en 2 ramas diferentes, una para cada parte de la practica.
- Rama
mainpara es donde debe ir un README.md indicando como le parecio la practica y los conceptos aprendidos. - Rama
ejercicio-1para el Ejercicio 1: Mini-ORM en Java (sin Spring Boot) es decir la primera parte de la practica. - Rama
ejercicio-2para el Ejercicio 2: Mini-ORM con Spring Boot es decir la segunda parte de la practica.
Criterios de evaluación¶
- Primer ejercicio (Mini-ORM en Java sin Spring Boot): 60%
- Estructura del proyecto: 5%
- Implementación de las anotaciones personalizadas: 5%
- Toda la implementación: 10%
- Evaluación primera parte del servicio: 40%
- Segundo ejercicio (Mini-ORM con Spring Boot): 40%
💁♂️ Tome en cuneta lo siguiente:
POR CADA VEZ QUE NO CUPLE LAS INTRUCCIONES DE LA PRACTICA, SE LE RESTARAN PUNTOS EXACTAMENTE 5 PUNTOS.
Ejercicios¶
Para esta practica, iniciaremos desde los conceptos basicos es decir un antes de Spring Boot y luego avanzaremos a Spring Boot. Para ello estaremos creando una seria de aplicaciones sencillas que nos permitiran entender los conceptos basicos de Spring Boot.
Parte 1: Aplicación Java Simple¶
En esta seccion crearemos una aplicacion Java simple en la cual estaremos revisando los siguientes conceptos:
- Estructura de un proyecto Java
- Clases y objetos
- Métodos y atributos
- Manejo de excepciones
- Programacion orientada a objetos
- Programacion funcional en Java
- Uso de librerias externas:
Lombok - Uso de
Mavenpara la gestion de dependencias
Ejercicio 1: 🧭 Mini‑ORM en Java¶
Guía de laboratorio orientada a arquitectura¶
Objetivo: Construir un mini‑ORM en Java siguiendo una arquitectura en capas que favorezca orden, testabilidad y evite código espagueti. El proyecto usa Maven, Lombok y reflección para mapear entidades con anotaciones.
Duración sugerida: 4–6 horas (puede dividirse en 2 sesiones)
Revisemos los conceptos clave¶
- ORM (Object-Relational Mapping): Técnica para mapear objetos en código a tablas en bases de datos relacionales, son usados para abstraer la interacción con una base de datos.
- Arquitectura en capas: Esta arquitetura separa responsabilidades en capas distintas (por ejemplo, presentación, negocio, datos) para mejorar la organización y mantenibilidad del código.
- Maven: Herramienta de gestión y construcción de proyectos Java que maneja dependencias y ciclos de vida del proyecto.
- Lombok: Biblioteca que reduce el código boilerplate (constructores, getters/setters) mediante anotaciones.
- Reflección: Capacidad de un programa para inspeccionar y modificar su propia estructura en tiempo de ejecución.
Resumen del enfoque arquitectónico¶
Usaremos una arquitectura en capas (similar a la típica en backend):
model(entidades anotadas) — responsabilidad: representar datos.repository(repositorios genéricos) — responsabilidad: abstracción de persistencia (In‑Memory en este laboratorio).service(lógica de negocio) — responsabilidad: orquestar operaciones y reglas.core/orm(EntityManager / mapeador) — responsabilidad: coordinar mapeo entre objetos y repositorios.app(interfaz CLI / demostración) — responsabilidad: punto de entrada, interacción con el usuario.
Principios aplicados: Separación de responsabilidades (SRP), Inversión de dependencias (usar interfaces), pequeñas clases/métodos, evitar estado global mutable.
Definamos algunnos conceptos clave, se menciono de los principios aplicados, estos principios son parte de aquellos principios SOLID, los cuales son:
- S de Principio de responsabilidad única (Single Responsibility Principle): una clase debe tener una, y solo una, razón para cambiar esto es separación de responsabilidades (SRP).
- O de Principio de abierto/cerrado (Open/Closed Principle): las entidades deben estar abiertas para la extensión, pero cerradas para la modificación esto es Inversión de dependencias (usar interfaces).
- L de Principio de sustitución de Liskov (Liskov Substitution Principle): los objetos de una clase derivada deben poder reemplazar a los objetos de la clase base sin alterar el comportamiento del programa esto es pequeñas clases/métodos.
- I de Principio de segregación de interfaces (Interface Segregation Principle): es mejor tener muchas interfaces específicas en lugar de una interfaz única y general esto es evitar estado global mutable.
- D de Principio de inversión de dependencias (Dependency Inversion Principle): las dependencias deben ser abstraídas, y no depender de implementaciones concretas.
Paso 0: Organización de archivos / proyecto (sistema de archivos)¶
Crearemos la siguiente estructura de carpetas y archivos:
mini-orm/
├─ pom.xml
├─ README.md
└─ src/
└─ main/
└─ java/
└─ com/miniorm/
├─ annotations/ # @Entity, @Column, @Id
├─ exceptions/ # Excepciones custom
├─ models/ # Clases de dominio (User, Product...) + Lombok
├─ repository/ # Interfaces y repositorios InMemory
├─ core/ # EntityManager, util reflection
├─ service/ # Servicios que usan repositorios
└─ app/ # Main / CLI demo
└─ test/
└─ java/
└─ com/miniorm/
└─ ... (tests unitarios)
Nota: usar paquete raíz
com.miniormcomo convención del laboratorio.
Implementación paso a paso¶
Paso 1: Crear proyecto Maven¶
- Inicie IntelliJ IDEA (o su IDE favorito).
- Cree un nuevo proyecto seleccionando Maven.
- Configure el
GroupIdcomocom.miniormy elArtifactIdcomomini-orm. - Haga clic en Finish para crear el proyecto.
- Abra el archivo
pom.xmly agregue las siguientes dependencias:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
</dependencies>
- Debe crear la estructura de carpetas mencionada en el paso 0 dentro de
src/main/java/com/miniorm/.
Paso 2: Definir anotaciones personalizadas¶
Que son las anotaciones? Es una forma de agregar metadatos a nuestro código, que luego pueden ser procesados en tiempo de compilación o en tiempo de ejecución. En este caso, usaremos anotaciones para marcar nuestras clases y campos con información relevante para el mapeo ORM.
En esta seccion estaremos creando las anotaciones personalizadas que usaremos para mapear nuestras entidades.
- Cree la carpeta
annotationsdentro decom.miniormsi no existe. -
Dentro de
annotations, cree los siguientes archivos: -
Entity.java
package com.miniorm.annotations;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {
String tableName();
}
Donde:
- `@Retention(RetentionPolicy.RUNTIME)`: Indica que la anotación estará disponible en tiempo de ejecución mediante reflexión.
- `@Target(ElementType.TYPE)`: Especifica que esta anotación se puede aplicar a clases, interfaces o enumeraciones.
- `public @interface Entity`: Define una nueva anotación llamada `Entity`.
- `String tableName()`: Declara un elemento obligatorio `tableName` que debe proporcionarse al usar la anotación.
Column.java
package com.miniorm.annotations;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) // Indica que esta anotación se aplica a campos (atributos)
public @interface Column {
String name();
}
Id.java
Ahora es tu turno, crea el archivo Id.java dentro de la carpeta annotations siguiendo el mismo formato que las anteriores. Recuerda que esta anotación se aplicará a campos (atributos) y no a clases.
GeneratedValue.java
package com.miniorm.annotations;
import com.miniorm.enums.GenerationType;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface GeneratedValue {
GenerationType strategy() default GenerationType.AUTO_INCREMENT;
}
Crea la carpeta `enums` dentro de `com.miniorm` si no existe.
Dentro de `enums`, cree el siguiente archivo:
GenerationType.java
package com.miniorm.enums;
public enum GenerationType {
TABLE,
SEQUENCE,
IDENTITY,
UUID,
AUTO_INCREMENT
}
Paso 3: Definamos las entidades del dominio (modelos)¶
Que son las entidades del dominio (modelos)? Son las clases que representan los datos que manejaremos en nuestra aplicación. Cada entidad corresponde a una tabla en la base de datos y sus atributos corresponden a las columnas de esa tabla.
En esta seccion estaremos creando las entidades del dominio que usaremos en nuestra aplicacion.
- Cree la carpeta
modelsdentro decom.miniormsi no existe. -
Dentro de
models, cree los siguientes archivos: -
User.java
package com.miniorm.models;
import com.miniorm.annotations.*;
import com.miniorm.enums.GenerationType;
import lombok.*;
import java.time.LocalDateTime;
import java.util.UUID;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity(tableName = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id")
public UUID id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "password")
private String password;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Override
public String toString() {
return
"""
User {
id='%s',
name='%s',
email='%s',
password='%s',
createdAt='%s',
updatedAt='%s'
}""".formatted(id, name, email, password, createdAt, updatedAt);
}
}
!!! warning "⚠️ Investigue como funcionan las relaciones entre entidades en JPA y como se implementan en Spring Data JPA" - Trata de implementar una relación entre dos entidades, por ejemplo, un Estudiante puede estar inscrito en muchos Course y un Course puede tener muchos Estudiante (relación muchos a muchos). - Investiga las anotaciones @OneToMany, @ManyToOne, @ManyToMany y @OneToOne y trata de replicarlas e implementarlas en el proyecto.
Me gustaria que investiguee acerca de como funciona esta clase, para ello le dejo las siguientes preguntas que le ayudaran a entender el codigo:
-
¿Qué hace la anotación
@Entityy qué significa el atributotableName? -
La anotación
@Entityindica que la clase es una entidad JPA y se mapeará a una tabla en la base de datos. El atributotableNameespecifica el nombre de la tabla en la base de datos a la que se mapeará esta entidad, en este caso, la tabla se llamará "users". -
¿Cuál es el propósito de la anotación
@Idy cómo se usa en esta clase? -
La anotación
@Idse utiliza para marcar el campo que actuará como la clave primaria de la entidad. En esta clase, el campoidestá anotado con@Id, lo que significa que será el identificador único para cada instancia de la entidadUseren la base de datos. -
¿Qué hace la anotación
@GeneratedValuey qué significa el atributostrategy? -
La anotación
@GeneratedValueindica que el valor del campo anotado será generado automáticamente por el sistema. El atributostrategyespecifica la estrategia de generación del valor. En este caso, se utilizaGenerationType.UUID, lo que significa que el valor del campoidserá generado como un UUID (Identificador Universalmente Único) automáticamente cuando se cree una nueva instancia deUser. -
¿Cómo se usan las anotaciones
@Columny qué información proporcionan? -
La anotación
@Columnse utiliza para mapear un campo de la clase a una columna específica en la tabla de la base de datos. Proporciona información sobre el nombre de la columna en la base de datos mediante el atributoname. Por ejemplo, el camponameestá mapeado a la columna "name" en la tabla "users". -
¿Qué papel juegan las anotaciones de Lombok (
@NoArgsConstructor,@AllArgsConstructor,@Getter,@Setter) en esta clase? -
Las anotaciones de Lombok simplifican la generación de código repetitivo (boilerplate) en la clase: -
@NoArgsConstructorgenera un constructor sin argumentos. -@AllArgsConstructorgenera un constructor con todos los argumentos. -@Gettergenera métodos getter para todos los campos. -@Settergenera métodos setter para todos los campos. Esto reduce la cantidad de código boilerplate que el desarrollador tiene que escribir manualmente. -
¿Qué tipo de datos se utilizan para los atributos
id,createdAtyupdatedAt, y por qué son apropiados para esos campos? -
El atributo
idutiliza el tipo de datosUUID, que es apropiado para claves primarias porque proporciona un identificador único y difícil de predecir, lo que es útil en sistemas distribuidos. Los atributoscreatedAtyupdatedAtutilizan el tipo de datosLocalDateTime, que es adecuado para almacenar marcas de tiempo sin zona horaria, permitiendo registrar cuándo se creó y actualizó la entidad respectivamente. -
¿Cómo funciona el método
toStringy qué información devuelve sobre la instancia deUser, y qué ventajas tiene usarString.formaten este contexto? -
El método
toStringdevuelve una representación en forma de cadena de la instancia deUser, mostrando los valores de sus atributos en un formato legible. Utiliza un bloque de texto multilínea (text block) para estructurar la salida de manera clara. La ventaja de usarString.format(o en este caso, el métodoformattedde Java 15+) es que permite insertar los valores de los atributos directamente en la cadena utilizando marcadores de posición (%s), lo que mejora la legibilidad y facilita el mantenimiento del código al separar la estructura del texto de los datos dinámicos.
Paso 4: Creamos el repositorio base genérico¶
Que es un repositorio?
Es una capa de abstracción que maneja la persistencia de datos. Proporciona métodos para realizar operaciones CRUD (Crear, Leer, Actualizar, Eliminar) sobre las entidades sin exponer los detalles de cómo se almacenan los datos.
Que es un generico (generic)?
Es una característica de Java que permite definir clases, interfaces y métodos con tipos de datos parametrizados. Esto permite crear código más flexible y reutilizable, ya que se puede trabajar con diferentes tipos de datos sin necesidad de duplicar código.
En esta seccion estaremos creando el repositorio base generico que usaremos en nuestra aplicacion.
- Cree la carpeta `repository` dentro de `com.miniorm` si no existe.
- Dentro de `repository`, cree los siguientes archivos:
GenericRepository.java
package com.miniorm.core;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface GenericRepository<ID, T> {
T save(ID id, T entity);
Optional<T> findById(ID id, Object o);
T update(ID id, T entity);
Boolean delete(ID id, T entity);
List<Map<ID, T>> findAll();
}
Que esta haciendo este codigo?
public interface GenericRepository<T, ID>: Define una interfaz genérica llamadaGenericRepositorycon dos parámetros de tipo:T(tipo de entidad) eID(tipo de identificador).T save(T entity): Declara un método para guardar una entidad del tipoT.Optional<T> findById(ID id): Declara un método para buscar una entidad por su identificador del tipoID, devolviendo unOptionalque puede contener la entidad o estar vacío si no se encuentra.T update(T entity): Declara un método para actualizar una entidad del tipoT.Boolean delete(T entity): Declara un método para eliminar una entidad del tipoT, devolviendo un booleano que indica si la eliminación fue exitosa.List<T> findAll(): Declara un método para obtener una lista de todas las entidades del tipoT.
Este repositorio es una interfaz genérica que define operaciones CRUD básicas para cualquier tipo de entidad y su identificador, permitiendo la reutilización del código para diferentes entidades en la aplicación.
5: Implementamos el repositorio In-Memory¶
Que es un repositorio In-Memory? Es una implementación de un repositorio que almacena los datos en la memoria del programa en lugar de una base de datos persistente. Esto es útil para pruebas, desarrollo rápido o aplicaciones simples donde no se requiere almacenamiento a largo plazo. Es decir, esta clase servira como una base de datos en memoria para almacenar nuestras entidades.
En esta seccion estaremos creando el repositorio In-Memory que usaremos en nuestra aplicacion.
-
Dentro de la carpeta
core, cree el siguiente archivo: -
InMemoryRepository.javay copie el siguiente codigo:
package com.miniorm.core;
import com.miniorm.annotations.Id;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class InMemoryRepository <ID, T> implements GenericRepository<ID, T> {
private final Class<T> entityClass;
private final Map<Object, List<Map<ID, T>>> storage = new ConcurrentHashMap<>(); // Thread-safe map for in-memory storage
private final AtomicInteger sequence = new AtomicInteger(1); // For generating unique IDs if needed
private Field idField;
public InMemoryRepository(Class<T> entityClass) {
this.entityClass = entityClass;
for (Field field : this.entityClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Id.class)) {
field.setAccessible(true);
this.idField = field;
break;
}
}
if (this.idField == null) {
throw new IllegalArgumentException("No @Id field found in class " + entityClass.getName());
}
}
private Object getId(T entity) {
try {
return idField.get(entity);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to access ID field", e);
}
}
private void setIdValue(T entity, Object id) {
try {
idField.set(entity, id);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to set ID field", e);
}
}
@Override
public T save(ID id,T entity) {
Object entityClassId = getId(entity);
if (id == null || (id instanceof Integer && ((Integer) id) == 0)) {
setIdValue(entity, sequence.getAndIncrement());
}
if (!storage.containsKey(entityClassId)) {
storage.put(entityClassId, new ArrayList<>());
}
assert id != null;
storage.get(entityClassId).add(Map.of(id, entity));
return entity;
}
@Override
public Optional<T> findById(ID id, Object o) {
return storage.get(o).stream()
.map(m -> m.get(id))
.filter(Objects::nonNull)
.findFirst();
}
@Override
public List<Map<ID, T>> findAll() {
List<Map<ID, T>> all = new ArrayList<>();
for (List<Map<ID, T>> maps : storage.values()) {
all.addAll(maps);
}
return all;
}
@Override
public Boolean delete(ID id, T entity) {
return storage.remove(entity) != null;
}
@Override
public T update(ID id, T entity) {
return storage.put(getId(entity), List.of(Map.of(id, entity))) != null ? entity : null;
}
}
Notas importantes:
- Aquí usamos reflexión para encontrar el campo anotado con
@Idy asignar ids automáticos (secuencia simple). ConcurrentHashMapofrece seguridad básica en concurrencia.
Preguntas para entender el código:
Ahora tu tarea es entender que es lo que hace este codigo, para ello te dejo una una serie de preguntas que te ayudaran a entender el codigo:
-
¿Qué hace la clase
InMemoryRepositoryy qué interfaces implementa? -
La clase
InMemoryRepositoryes una implementación genérica de un repositorio que almacena entidades en memoria utilizando un mapa concurrente (ConcurrentHashMap). Implementa la interfazGenericRepository, lo que significa que proporciona métodos para realizar operaciones CRUD (Crear, Leer, Actualizar, Eliminar) sobre las entidades. -
¿Cuál es el propósito del mapa
storagey cómo se utiliza? -
El mapa
storagees una estructura de datos que almacena las entidades en memoria. La clave del mapa es el identificador de la entidad, y el valor es una lista de mapas que contienen pares de identificador y entidad. Este mapa se utiliza para guardar, buscar, actualizar y eliminar entidades en memoria. -
¿Cómo se genera un ID único para las entidades si no se proporciona uno?
-
Si no se proporciona un ID al guardar una entidad (es decir, si el ID es nulo o cero), se genera un ID único utilizando un
AtomicIntegerllamadosequence. Este entero se incrementa automáticamente cada vez que se guarda una nueva entidad sin ID, asegurando que cada entidad tenga un identificador único. -
¿Qué hace el método
getIdy cómo accede al campo ID de una entidad? -
El método
getIdutiliza reflexión para acceder al campo anotado con@Iden la entidad. Intenta obtener el valor del campo ID de la entidad pasada como argumento y lo devuelve. Si no puede acceder al campo debido a restricciones de acceso, lanza una excepción. -
¿Cómo funciona el método
savey qué hace si el ID es nulo o cero? -
El método
saveguarda una entidad en el repositorio. Si el ID proporcionado es nulo o cero, genera un ID único utilizando la secuencia. Luego, verifica si ya existe una entrada en el mapastoragepara el ID de la entidad; si no existe, crea una nueva lista para ese ID. Finalmente, agrega la entidad al mapa bajo su ID correspondiente y devuelve la entidad guardada. -
¿Qué hace el método
findByIdy cómo busca una entidad por su ID? -
El método
findByIdbusca una entidad en el repositorio utilizando su ID. Toma el ID y un objeto como parámetros. Busca en el mapastoragela lista de mapas asociada al ID proporcionado, luego itera sobre esa lista para encontrar y devolver la entidad correspondiente al ID. -
¿Cómo funciona el método
findAlly qué devuelve? -
El método
findAllrecopila todas las entidades almacenadas en el repositorio y las devuelve como una lista de mapas. Itera sobre todas las listas de mapas en el mapastorage, agregando cada mapa a una lista final que se devuelve al final del método. -
¿Qué hace el método
deletey cómo elimina una entidad del almacenamiento? -
El método
deleteelimina una entidad del repositorio utilizando su ID y la entidad misma. Intenta eliminar la entrada correspondiente en el mapastoragey devuelve un booleano que indica si la eliminación fue exitosa (es decir, si la entidad existía y fue eliminada). -
¿Cómo funciona el método
updatey qué hace si la entidad no existe en el almacenamiento? -
El método
updateactualiza una entidad existente en el repositorio utilizando su ID. Intenta reemplazar la lista de mapas asociada al ID de la entidad con una nueva lista que contiene la entidad actualizada. Si la entidad existía y fue actualizada, devuelve la entidad; de lo contrario, devuelve nulo.
6: Implementamos el EntityManager (capa de orquestación)¶
Dentro del paquete com.miniorm.core es decir dentro de la carpeta core, cree el siguiente archivo:
EntityManager.javay copie el siguiente codigo:
package com.miniorm.core;
public class EntityManager {
public <ID, T> GenericRepository<ID, T> getRepository(Class<T> clazz) {
return new InMemoryRepository<>(clazz);
}
}
Responsabilidad: ofrecer repositorios a la capa de servicio; punto único para cambiar la estrategia de persistencia.
Preguntas para entender el código:
-
¿Qué hace el método
getRepositoryy qué parámetros recibe? -
El método
getRepositoryes un método genérico que recibe como parámetro una clase (Class<T> clazz) y devuelve una instancia deGenericRepository<ID, T>. Este método crea y devuelve un nuevo repositorio en memoria (InMemoryRepository) para la clase proporcionada. -
¿Qué tipo de repositorio devuelve y cómo se instancia?
-
Devuelve un repositorio genérico (
GenericRepository<ID, T>) que es una instancia deInMemoryRepository<ID, T>. Se instancia pasando la clase proporcionada (clazz) al constructor deInMemoryRepository. -
¿Cómo se utiliza el
EntityManageren la arquitectura general del proyecto? -
El
EntityManageractúa como una capa de orquestación que proporciona repositorios a la capa de servicio. Permite a los servicios obtener repositorios específicos para las entidades sin preocuparse por los detalles de implementación del repositorio. Esto facilita la gestión de la persistencia y permite cambiar la estrategia de almacenamiento (por ejemplo, cambiar de un repositorio en memoria a uno basado en una base de datos) sin afectar la lógica de negocio. -
¿Qué ventajas ofrece tener un
EntityManageren lugar de instanciar repositorios directamente en los servicios? -
Tener un
EntityManagerofrece varias ventajas: - Abstracción: Los servicios no necesitan conocer los detalles de implementación de los repositorios, lo que reduce el acoplamiento entre capas.
- Flexibilidad: Permite cambiar la estrategia de persistencia (por ejemplo, cambiar a una base de datos real) sin modificar la lógica de negocio en los servicios.
- Centralización: Proporciona un punto único para gestionar la creación y configuración de repositorios, facilitando el mantenimiento y la evolución del código.
- Reutilización: Facilita la reutilización del código al permitir que múltiples servicios compartan la misma lógica para obtener repositorios.
7: Implementamos la capa de servicio (lógica de negocio)¶
Dentro de la carpeta service, cree el siguiente archivo:
UserService.javay copie el siguiente codigo:
package com.miniorm.service;
import com.miniorm.core.GenericRepository;
import com.miniorm.dto.RegisterUserDto;
import com.miniorm.models.User;
import java.sql.Timestamp;
import java.util.UUID;
public class UserService {
private final GenericRepository<UUID, User> userRepository;
public UserService(GenericRepository<UUID, User> userRepository) {
this.userRepository = userRepository;
}
public User createUser(RegisterUserDto userDto) {
User user = new User();
user.setName(userDto.name());
user.setEmail(userDto.email());
user.setPassword(userDto.password());
UUID uuid = UUID.randomUUID();
user.setId(uuid);
user.setCreatedAt(new Timestamp(System.currentTimeMillis()).toLocalDateTime());
user.setUpdatedAt(new Timestamp(System.currentTimeMillis()).toLocalDateTime());
return userRepository.save(uuid, user);
}
public User getUserById(UUID id) {
return userRepository.findById(id, null).orElse(null);
}
public User updateUser(UUID id, User user) {
return userRepository.update(id, user);
}
public Boolean deleteUser(UUID id, User user) {
return userRepository.delete(id, user);
}
public void listAllUsers() {
userRepository.findAll().forEach(System.out::println);
}
}
Por qué esta separación: la service no conoce la implementación concreta del repositorio — se inyecta mediante el constructor (Dependency Injection manual).
Preguntas para entender el código:
-
¿Cuál es la responsabilidad principal de la clase
UserService? -
La responsabilidad principal de la clase
UserServicees manejar la lógica de negocio relacionada con la entidadUser. Proporciona métodos para crear, obtener, actualizar, eliminar y listar usuarios, utilizando un repositorio genérico para interactuar con la capa de persistencia. -
¿Cómo se inyecta el repositorio en el servicio y por qué es importante?
-
El repositorio se inyecta en el servicio a través del constructor de la clase
UserService. Esto es importante porque permite la inversión de dependencias, lo que significa que el servicio no depende de una implementación concreta del repositorio. En su lugar, puede trabajar con cualquier implementación que cumpla con la interfazGenericRepository, lo que mejora la flexibilidad y facilita las pruebas unitarias. -
¿Qué hace el método
createUsery cómo utiliza el DTORegisterUserDto? -
El método
createUsercrea una nueva instancia deUserutilizando los datos proporcionados en el DTORegisterUserDto. Asigna los valores del DTO a los atributos correspondientes del usuario, genera un UUID único para el ID del usuario, establece las marcas de tiempo de creación y actualización, y luego guarda el usuario en el repositorio utilizando el métodosave. Finalmente, devuelve la instancia del usuario creado. -
¿Cómo funcionan los métodos
getUserById,updateUser,deleteUserylistAllUsers? -
getUserById: Busca un usuario en el repositorio por su ID utilizando el métodofindByIddel repositorio. Si el usuario no se encuentra, devuelvenull. updateUser: Actualiza un usuario existente en el repositorio utilizando el métodoupdatedel repositorio y devuelve la instancia actualizada del usuario.deleteUser: Elimina un usuario del repositorio utilizando el métododeletedel repositorio y devuelve un booleano que indica si la eliminación fue exitosa.-
listAllUsers: Recupera todos los usuarios del repositorio utilizando el métodofindAlly los imprime en la consola. -
¿Qué ventajas ofrece tener una capa de servicio separada de la capa de repositorio?
-
Tener una capa de servicio separada ofrece varias ventajas:
- Separación de responsabilidades: La capa de servicio se encarga de la lógica de negocio, mientras que la capa de repositorio se encarga de la persistencia de datos. Esto facilita el mantenimiento y la evolución del código.
- Reutilización: La lógica de negocio puede ser reutilizada en diferentes contextos sin depender de la implementación específica del repositorio.
- Facilidad para pruebas unitarias: La capa de servicio puede ser probada de manera aislada utilizando mocks o stubs para el repositorio, lo que facilita la detección y corrección de errores.
-
Flexibilidad: Permite cambiar la implementación del repositorio (por ejemplo, cambiar a una base de datos real) sin afectar la lógica de negocio en la capa de servicio.
-
¿Cómo maneja el servicio la creación de IDs y las marcas de tiempo para los usuarios?
-
El servicio genera un ID único para cada usuario utilizando
UUID.randomUUID()cuando se crea un nuevo usuario en el métodocreateUser. Además, establece las marcas de tiempo de creación y actualización utilizando la claseTimestamppara obtener la hora actual y convertirla aLocalDateTime. Estas marcas de tiempo se asignan a los atributoscreatedAtyupdatedAtdel usuario antes de guardarlo en el repositorio. -
¿Qué tipo de objeto devuelve el método
getUserByIdsi no encuentra un usuario con el ID proporcionado? -
Si el método
getUserByIdno encuentra un usuario con el ID proporcionado, devuelvenull. Esto se debe a que utiliza el métodoorElse(null)en el resultado del repositorio, que devuelvenullsi elOptionalestá vacío (es decir, si no se encontró ningún usuario con ese ID). -
¿Cómo se asegura el servicio de que los datos del usuario estén completos antes de guardarlos en el repositorio?
-
El servicio no realiza validaciones explícitas para asegurarse de que los datos del usuario estén completos antes de guardarlos en el repositorio. Sin embargo, se espera que el DTO
RegisterUserDtoproporcione todos los datos necesarios (nombre, correo electrónico y contraseña) al crear un nuevo usuario. Si se requiere una validación más estricta, se podrían agregar verificaciones adicionales en el métodocreateUserpara asegurarse de que los campos no estén vacíos o nulos antes de proceder con la creación del usuario. -
¿Qué patrón de diseño se está utilizando al inyectar el repositorio en el servicio a través del constructor?
-
El patrón de diseño que se está utilizando al inyectar el repositorio en el servicio a través del constructor es el Patrón de Inversión de Dependencias (Dependency Injection). Este patrón permite que las dependencias (en este caso, el repositorio) sean proporcionadas al objeto (el servicio) desde el exterior, en lugar de que el objeto cree o gestione sus propias dependencias. Esto mejora la modularidad, facilita las pruebas unitarias y permite una mayor flexibilidad en la elección de las implementaciones de las dependencias.
Bien hasta este punto de seguro tendra algunos errores que, en especidifco en la funcion createUser, esto se debe a que estamos usando un DTO que aun no hemos creado, para ello cree la carpeta dto dentro de com.miniorm si no existe.
Pero ahora que demonios, que es un DTO?
DTO significa "Data Transfer Object" (Objeto de Transferencia de Datos). Es un patrón de diseño utilizado para transferir datos entre diferentes capas o componentes de una aplicación, especialmente en aplicaciones distribuidas o basadas en servicios.
Entonces cree el siguiente archivo dentro de la carpeta dto:
RegisterUserDto.javay copie el siguiente codigo:
package com.miniorm.dto;
public record RegisterUserDto(String name, String email, String password) {}
- Aquí usamos un
recordde Java (disponible desde Java 14) para definir un DTO inmutable y conciso. - Un
recordes una clase especial en Java que está diseñada para ser una simple portadora de datos. Proporciona automáticamente implementaciones para métodos comunes comoequals(),hashCode(), ytoString(), así como constructores y getters para sus componentes. public record RegisterUserDto(String name, String email, String password) {}: Define unrecordllamadoRegisterUserDtocon tres componentes:name,emailypassword. Estos componentes son inmutables y se inicializan a través del constructor generado automáticamente.
8: Implementamos la capa de presentación (CLI demo)¶
Dentro de la carpeta app, cree el siguiente archivo:
Main.javay copie el siguiente codigo:
package com.miniorm.app;
import com.miniorm.core.EntityManager;
import com.miniorm.dto.RegisterUserDto;
import com.miniorm.models.Product;
import com.miniorm.models.User;
import com.miniorm.service.ProductService;
import com.miniorm.service.UserService;
public class Main {
public static void main(String[] args) {
EntityManager entityManager = new EntityManager();
UserService userService = new UserService(entityManager.getRepository(User.class));
userService.createUser(new RegisterUserDto("John Doe", "john.doe@example.com", "password123"));
userService.createUser(new RegisterUserDto("Jane Smith", "jane.smith@gmail.com", "securepass"));
userService.listAllUsers();
System.out.println("--- Products ---");
}
}
Al ejecutar este código, debería ver en la consola la lista de usuarios creados, similar a la siguiente salida:
ID: c40b46d2-3436-407b-baa7-c0b25d61e3fb,
User: User {
id='c40b46d2-3436-407b-baa7-c0b25d61e3fb',
name='Jane Smith',
email='jane.smith@gmail.com',
password='securepass',
createdAt='2025-10-06T23:52:40.112',
updatedAt='2025-10-06T23:52:40.112'
}
ID: d94a73fb-1048-4270-a31c-ee2b19e72ec7,
User: User {
id='d94a73fb-1048-4270-a31c-ee2b19e72ec7',
name='John Doe',
email='john.doe@example.com',
password='password123',
createdAt='2025-10-06T23:52:40.100',
updatedAt='2025-10-06T23:52:40.109'
}
--- Products ---
- Aquí usamos el
EntityManagerpara obtener un repositorio deUsery lo inyectamos en elUserService. - Luego, creamos algunos usuarios y listamos todos los usuarios almacenados.
- Puede expandir esta demo agregando más funcionalidades, como actualizar o eliminar usuarios, o creando y gestionando productos usando un
ProductServicesimilar.
10: Evaluación final¶
En esta sección usted deberá hacer lo siguiente:
- Extender el proyecto para soportar otra entidad, por ejemplo,
Productcon atributos comoid,name,price,createdAt,updatedAt. - Implementar un
ProductServicesimilar aUserServicepara manejar operaciones CRUD sobre productos. - Crear un DTO para registrar productos, similar a
RegisterUserDto. - Modificar la clase
Mainpara demostrar la creación, actualización, eliminación y listado de productos además de usuarios, se recomienda que lo hagas debajo de la líneaSystem.out.println("--- Products ---");. - Documentar el proyecto en el archivo
README.md, explicando la arquitectura, cómo ejecutar la aplicación y cualquier otro detalle relevante.
Ejercicio 2: 🧭 Mini‑ORM con Spring Boot¶
En este ejercicio usted debera replicar el ejercicio anterior pero usando Spring Boot, para ello le dejo los siguientes pasos a seguir:
-
Cree un nuevo proyecto Spring Boot usando Spring Initializr (https://start.spring.io/) con las siguientes dependencias:
-
Spring Web
- Spring Data JPA
- Simule una base de datos en memoria usando (Map, List, etc.) segun su criterio
-
Lombok
-
Replicar la estructura de carpetas y archivos del ejercicio anterior.
- Implementar las mismas funcionalidades (entidades, repositorios, servicios, controladores)
- Probar la aplicación usando Postman o cualquier otra herramienta para hacer peticiones HTTP.
- Documentar el proyecto en el archivo
README.md, explicando la arquitectura, cómo ejecutar la aplicación y cualquier otro detalle relevante. - Subir el proyecto a un repositorio de GitHub y compartir el enlace en classroom.
Conclusión¶
En esta práctica, hemos explorado los fundamentos de Spring Boot y cómo construir una aplicación sencilla utilizando sus características principales. Hemos aprendido a configurar un entorno de desarrollo, crear un proyecto Spring Boot, y desarrollar una aplicación que incluye controladores, servicios y repositorios. Además, hemos visto cómo utilizar herramientas modernas como Lombok y Maven para mejorar nuestra productividad y gestionar dependencias de manera eficiente.