CodeGym /Cursos /Spring Data JPA /Semántica de save y métodos por id

Semántica de save y métodos por id

Spring Data JPA
Nivel 7, Lección 2
Disponible

1. Semántica de los métodos del repositorio

Sinceramente, los nombres de los métodos de Spring Data se parecen a palabras inglesas que «ya se entienden»: save — guardar, find — buscar, exists — existe. El problema es que en la capa de datos «entendible» no es lo mismo que «predecible». Un método puede ir a la base de datos, otro no, un tercero ir más tarde, cuando ya no lo esperabas para nada.

Tu tarea como desarrollador backend no es simplemente memorizar un conjunto de métodos, sino aprender a hacerte una pregunta corta: ¿qué quiero saber o hacer exactamente con el almacenamiento ahora mismo? Si aprendes a escuchar el significado (semántica), dejarás de escribir código que hace dos consultas SQL en lugar de una, o que de repente falla «donde no se llamó».

Para fijar esto, pensemos así: cada método del repositorio es una pregunta formalizada a la base de datos. Y si confundes la pregunta, obtienes una consulta extra, una complejidad innecesaria o un error inesperado.

2. save: «grabar» significa insertar o actualizar

Cuando ves por primera vez save, la mano se estira para usarlo como un martillo universal. En el mundo del desarrollador principiante todo parece simple: «he cambiado algo — entonces hay que llamar a save». En general, para tu nivel actual es incluso un buen hábito, porque hace explícita la escritura en la BD. Pero es importante entender que save es una operación de escritura y puede significar dos cosas diferentes.

A nivel muy práctico, save responde a la pregunta: «haz que este estado de la entidad esté en la base de datos». Para una entidad nueva se convierte en un INSERT, y para una existente en una actualización (UPDATE), pero el contrato externo es el mismo: llamas a save y Spring Data JPA se encarga de hablar con JPA/Hibernate.

save para una entidad nueva: obtener cómodamente el id

El escenario «primero» más frecuente es la creación de una categoría o un producto. La entidad es nueva, aún no tiene id, y después de guardarlo aparecerá.

import com.example.shopdatajpa.catalog.entity.Category;

// Creamos una nueva entidad: aún no está vinculada a un registro en la BD
Category category = new Category();
category.setCode("electronics");
category.setName("Electrónica");

// En un servicio real aquí estaría categoryRepository.save(category)
// Antes de guardar, el id normalmente no está presente (aún no generado por la BD)
System.out.println(category.getId()); // null (antes de guardar)

En el código real guardamos a través del repositorio y luego podemos usar el id que se asignará en la BD. Si guardas y luego quieres trabajar exactamente con el objeto guardado, conviene asignar el resultado de save a una variable.

import com.example.shopdatajpa.catalog.entity.Category;

// Llenamos la entidad como un objeto normal en memoria
Category category = new Category();
category.setCode("books");
category.setName("Libros");

// La llamada a save inicia la escritura en la BD (INSERT para una entidad nueva)
Category saved = categoryRepository.save(category);

// La entidad guardada ya tiene un id asignado por la BD/ORM
System.out.println(saved.getId()); // por ejemplo: 1

Aquí es importante no convertirlo en una religión «guarda siempre en una variable», pero recuerda: save devuelve el resultado de la escritura, y eso suele ser útil.

save para actualización: normalmente se actualiza «después de la lectura»

En un proyecto de aprendizaje, el patrón de actualización más claro es: primero leemos la entidad, cambiamos los campos, luego guardamos.

import com.example.shopdatajpa.catalog.entity.Category;

// 1) Primero leemos la entidad existente de la BD
Category category = categoryRepository.findById(1L).orElseThrow();

// 2) Cambiamos los campos necesarios en el objeto
category.setName("Libros y cómics");

// 3) Fijamos explícitamente los cambios con una escritura (normalmente UPDATE para un registro existente)
categoryRepository.save(category);

Sí, aquí ya usamos findById y Optional; pasaremos a ellos ahora. Por ahora fijemos la idea principal: save es para escribir, y si necesitas «solo leer», save no es una forma de lectura (por más que quieras).

3. findById y Optional: lectura honesta

En programación hay dos tipos de «sin valor». El primero es cuando es un error («algo se rompió»). El segundo es cuando es una situación normal («simplemente no hay valor»). En la capa de datos, la ausencia de un registro por id normalmente pertenece al segundo tipo: el usuario pudo pasar un id inexistente, el registro pudo ser eliminado, podrías estar mirando una base vacía en un entorno de desarrollo. Por eso findById devuelve no un Product, sino un Optional<Product>.

findById responde a una pregunta directa y honesta: «¿existe una entidad con ese id, y si es así, dámela?». Si la entidad no existe, el método no está obligado a «fallar», porque es un escenario normal. En su lugar, devuelve un Optional vacío.

La opción más simple: devolver Optional más arriba

A veces al servicio le resulta conveniente devolver un Optional al código llamante – es honesto y quita parte de las decisiones de la capa actual.

import com.example.shopdatajpa.catalog.entity.Product;
import java.util.Optional;

public Optional<Product> findProduct(Long id) {
    // Devolvemos Optional como parte del contrato: el producto puede no existir
    return productRepository.findById(id);
}

Esta variante es buena porque el método dice inmediatamente: «el producto puede no existir». Es mala solo en un caso: si tu siguiente código igualmente quiere considerar la ausencia como un error, simplemente estás delegando la decisión a otra persona (o a ti mismo dentro de cinco minutos).

Variante común en el servicio: «no hay entidad — entonces es un error de escenario»

Si para un caso de uso concreto la ausencia de un registro significa que el escenario no puede continuar, conviertes el Optional en una excepción. Aquí es importante no exagerar con la filosofía: no estamos discutiendo el diseño de errores para REST, solo mostramos la reacción básica del servicio.

import com.example.shopdatajpa.catalog.entity.Product;

public Product getProductOrThrow(Long id) {
    // Buscamos el producto por id; si no existe, lo convertimos explícitamente en un error de escenario
    return productRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Product not found: " + id));
}

Este código se lee muy sencillo: «intenta encontrar, si no lo encuentras — este no es el estado con el que queremos vivir».

Optional.get() — es como caminar por el apartamento de noche con los ojos cerrados

Técnicamente, Optional tiene el método get. Y técnicamente a veces funciona. Pero si el valor está ausente, obtendrás una NoSuchElementException, y será un error totalmente inútil porque tú mismo elegiste «caer sin explicación».

import com.example.shopdatajpa.catalog.entity.Product;
import java.util.Optional;

Optional<Product> productOpt = productRepository.findById(10L);

// Opción peligrosa: si Optional está vacío, caeremos con NoSuchElementException sin contexto comprensible
Product product = productOpt.get(); // puede lanzar NoSuchElementException

Las opciones normales «seguras» son orElseThrow(...), orElse(...) (si tienes un valor por defecto razonable) o la comprobación mediante isPresent() / isEmpty().

4. existsById: pregunta «¿existe el registro?»

Con existsById a menudo ocurre una tragedia doméstica: se usa «por si acaso». La lógica del novato es comprensible e incluso humana: «primero compruebo que el registro existe, luego lo leo». Al final, el código hace dos consultas en lugar de una, y el desarrollador dice orgulloso: «pero es seguro». Lástima, la base de datos llora en silencio en ese momento.

existsById responde a otra pregunta: «¿existe un registro con ese id, sí o no?». No promete una entidad, no trae datos, solo devuelve un boolean. Es útil cuando realmente solo necesitas una respuesta lógica.

Ejemplo apropiado: «muestra que la categoría existe»

En el servicio puede haber un método que se necesita para una decisión simple: por ejemplo, queremos prohibir una acción si la categoría no existe, pero no necesitamos leer la categoría en sí.

public boolean categoryExists(Long categoryId) {
    // Aquí solo necesitamos el hecho de la existencia, sin cargar la entidad completa
    return categoryRepository.existsById(categoryId);
}

Si devuelves un boolean, el escenario suele sonar como «permitido/prohibido», «existe/no existe», «se puede/no se puede». Y existsById encaja perfectamente.

Patrón desafortunado: existsById + findById «por seguridad»

Suave y sin listas: es una mala idea en el caso de que de todas formas quieras la entidad. Porque primero haces la pregunta «¿existe?» y luego «dame la entidad». La base de datos no se vuelve más feliz. Simplemente ejecuta dos consultas.

import com.example.shopdatajpa.catalog.entity.Product;

public Product loadProductBad(Long id) {
    // Primera consulta: comprobamos existencia (boolean)
    if (!productRepository.existsById(id)) {
        throw new IllegalArgumentException("Product not found: " + id);
    }

    // Segunda consulta: igualmente cargamos la entidad — total dos accesos a la BD en lugar de uno
    return productRepository.findById(id).orElseThrow(); // segunda consulta
}

Si necesitas la entidad, en la mayoría de los casos haces directamente findById y manejas el Optional.

5. getReferenceById: entidad que puede «disparar» más tarde

El método getReferenceById suena amigable, pero se comporta astutamente. Es como una persona que dice: «Sí, sí, estoy al tanto», y luego resulta que está al tanto en algún momento después. Este método no responde a la pregunta «dame la entidad», sino a: «dame un objeto-referencia a la entidad con ese id».

Prácticamente es útil en situaciones donde necesitas exactamente una referencia a una entidad existente por un id conocido, no una instantánea completa de sus campos ahora mismo. Una parte del modelo a veces se conforma con el mero hecho de «aquí hay una referencia a la entidad nº10». Para la lectura normal por id, el default operativo sigue siendo findById: getReferenceById vale como referencia, no como «find rápido».

La principal característica es que esta referencia no está obligada a ir inmediatamente a la base de datos para cargar todos los campos. Esencialmente obtienes un objeto que representa la entidad, y la lectura de los datos reales puede ocurrir más tarde, cuando empieces a usar los campos. De ahí la principal trampa: si el registro no existe en la BD, el error puede manifestarse no en el momento de llamar a getReferenceById, sino después.

Observación más simple: «recibí la referencia, pero aún no he visto los datos»

Mostremos esto con la máxima precisión, sin excursión a los internos. Tienes un id y obtienes una referencia:

import com.example.shopdatajpa.catalog.entity.Product;

// Obtenemos una "referencia" a la entidad: Hibernate puede no hacer un SELECT ahora mismo
Product ref = productRepository.getReferenceById(10L);

// El id normalmente se conoce de inmediato, por lo que se puede leer sin acceder a la BD
System.out.println(ref.getId()); // 10 (id conocido inmediatamente)

El sentido del ejemplo no es que System.out.println sea un «logger de producción correcto» (no), sino que el id de la referencia suele estar disponible de inmediato. En cambio, campos como name ya pueden requerir acceso a la BD, porque hay que obtenerlos de algún sitio.

getReferenceById no puede considerarse un reemplazo de findById

Si el id puede no ser válido, findById te da un «contrato honesto»: o la entidad existe o no. Con getReferenceById es como si cogieras un papel con un número y dijeras: «tráiganme el pedido nº10», pero sin comprobar si existe en el sistema. A veces está bien si estás seguro de que el pedido existe. A veces es horrible si no estás seguro.

Para resaltar la trampa, mostremos un ejemplo de acceso a un campo que puede terminar en una excepción si la entidad no existe en la BD:

import com.example.shopdatajpa.catalog.entity.Product;

// La referencia se creará incluso para un id inexistente
Product ref = productRepository.getReferenceById(999_999L);

// En el primer acceso a los datos (no al id) el ORM puede intentar cargar la entidad y "disparar" un error
// ref.getName(); // al acceder a los datos puede ocurrir un error si el registro no existe

Hemos dejado la línea comentada deliberadamente, porque ahora nos importa más entender el contrato que montar un espectáculo de excepciones.

6. Chuleta rápida: qué método plantea qué pregunta

Cuando escribes un método de servicio, conviene detenerse un segundo y «traducir» el código al lenguaje humano. La tabla siguiente no es para memorizarla, sino para una autocomprobación rápida: ¿realmente le estás haciendo a la base de datos la pregunta que quieres hacer?

Método del repositorio Qué pregunta plantea Qué devuelve Si no hay registro Uso típico
save(entity) «Guarda este estado en la BD» entidad guardada no es sobre ausencia, es escritura crear/actualizar entidad
findById(id) «Busca la entidad por id» Optional<T> Optional.empty() leer entidad o saber que no existe
existsById(id) «¿Existe un registro con ese id?» boolean false solo se necesita el hecho de existencia
getReferenceById(id) «Dame una referencia a la entidad con ese id» T (referencia/proxy) el error puede manifestarse después cuando se necesita exactamente una referencia y se está seguro de la corrección del id

Si te metes esta tabla en la cabeza, dejarás de cometer automáticamente muchos errores comunes. Y si además empiezas a decirte a ti mismo la «pregunta» antes de escribir código, de repente sentirás que la capa de datos se ha vuelto un lugar más tranquilo para vivir.

7. Cómo estos métodos se combinan en una API de servicio

Cuando el servicio empieza a hablar el lenguaje del catálogo, no inventa nuevas operaciones de acceso a datos. Simplemente toma las preguntas ya entendidas al almacenamiento y les da nombres de dominio. Por eso, antes de ensamblar el CatalogService, es útil ver esta correspondencia completa una vez.

Qué quiere expresar el servicio Qué pregunta hace al repositorio Qué es importante no confundir
createCategory(...) /
createProduct(...)
save(...) es un punto explícito de escritura, no de lectura
findProduct(...) findById(...) la ausencia de registro es un escenario normal, por eso llega Optional
getProductOrThrow(...) findById(...).orElseThrow(...) si el producto es obligatorio, hacemos el error explícito en el servicio
productExists(...) existsById(...) aquí solo se necesita un boolean, sin cargar la entidad innecesariamente
getProductReference(...) getReferenceById(...) es una referencia a la entidad, no un reemplazo de la lectura normal

De tales combinaciones se ensambla una API de servicio normal. Primero elegimos una pregunta honesta al almacenamiento, y luego llamamos al método del servicio según el significado del caso de uso. Así el servicio sigue siendo comprensible y el repositorio no se convierte en una bolsa de llamadas aleatorias.

8. Errores típicos al trabajar con métodos por id

Los errores en la API del repositorio no suelen verse como una «luz roja» de inmediato. A menudo se manifiestan como una consulta extra, un punto de fallo extraño, o un código que no se puede leer sin adivinar qué quiso decir el autor. Por eso es útil conocer de antemano los rastrillos más populares y pasar de largo sin pisarlos a toda velocidad.

Error n.º 1: findById(...).get() sin comprobación.
Es el clásico del desarrollador Java principiante: «bueno, yo sé que el registro existe». A veces realmente lo sabes. Pero el código no se vuelve más fiable por eso — se vuelve frágil. Si el registro desaparece o el id es incorrecto, obtendrás una excepción sin sentido ni contexto. Es mucho más honesto devolver Optional hacia arriba o usar orElseThrow(...) con un mensaje adecuado.

Error n.º 2: hacer existsById y luego igualmente findById.
Ese código parece «cuidadoso», pero en realidad hace dos preguntas a la base: «¿existe?» y «dame». Si necesitas la entidad, usa directamente findById y maneja el Optional. Deja existsById para escenarios donde realmente solo se necesita un boolean.

Error n.º 3: percibir save como un botón universal “hazlo bien”.
save es escritura. Si intentas «leer a través de save» (por ejemplo, esperando que save de alguna manera verifique la existencia y te devuelva la entidad), simplemente estás confundiendo las responsabilidades de los métodos. En el mejor de los casos obtendrás una escritura extra; en el peor, una situación difícil de depurar donde los datos «por alguna razón cambiaron».

Error n.º 4: pensar que getReferenceById es simplemente “un findById más rápido”.
getReferenceById devuelve una referencia, y es otro contrato. Si el id no existe, el error puede aparecer después — en un lugar que no asocias con la lectura de la base. Si no estás seguro de la corrección del id, necesitas findById y Optional, no una «referencia a la suerte».

Error n.º 5: ignorar el valor de retorno de save cuando luego se necesita el resultado de la escritura.
A veces el desarrollador hace repository.save(entity); y sigue usando la referencia antigua al objeto, esperando que todo se haya «actualizado». En la mayoría de los casos simples funcionará, pero el hábito sigue siendo peligroso: pierdes claridad. Si te importa el id o el trabajo posterior con el objeto guardado, es mejor asignar el resultado de save a una variable y usarlo, para que el código sea transparente.

1
Tarea
Spring Data JPA, nivel 7, lección 2
Bloqueada
Guardado y lectura de categoría por `id`
Guardado y lectura de categoría por `id`
1
Tarea
Spring Data JPA, nivel 7, lección 2
Bloqueada
`existsById` y `getReferenceById` para `Product`
`existsById` y `getReferenceById` para `Product`
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION