CodeGym /Cursos /Módulo 5. Spring /Práctica: Implementación de transacciones en una aplicaci...

Práctica: Implementación de transacciones en una aplicación Spring

Módulo 5. Spring
Nivel 6 , Lección 5
Disponible

Las transacciones son el mecanismo clave para trabajar de forma segura con bases de datos. Vamos a ver, con el ejemplo de una tienda online, por qué son tan importantes.

Imagina: un cliente hace un pedido. Parece una operación sencilla, pero en realidad tenemos que:

  1. Descontar el dinero de la cuenta del comprador
  2. Reservar el producto en el almacén
  3. Crear el registro del pedido en la base de datos

¿Qué puede salir mal? ¡Casi de todo! Por ejemplo, se descuentan los fondos y el servidor "se cae" antes de crear el pedido. Sin transacciones, esto se convierte en una pesadilla para soporte. Por suerte, las transacciones solucionan esto: o todas las operaciones se completan correctamente, o el sistema revierte todo automáticamente.


Configuración del TransactionManager

Antes de escribir código, asegurémonos de que nuestra aplicación está lista para manejar transacciones. TransactionManager es el corazón del mecanismo transaccional en Spring. Coordina la ejecución de las operaciones.

Si usas JPA/Hibernate (como haremos nosotros), la configuración del TransactionManager es sencilla.

Añadimos las dependencias

Lo primero que haremos es incluir las dependencias para trabajar con JPA y la base de datos (por ejemplo, H2 Database para pruebas). Asegúrate de que en pom.xml aparece el siguiente código:


<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Configuración de la base de datos

En el archivo application.properties pondremos los ajustes para conectar con H2:


spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Configuración del TransactionManager

Spring Boot configurará el TransactionManager automáticamente si usas spring-boot-starter-data-jpa. Eso significa que ya estamos listos para trabajar con transacciones. ¡Yay!


Implementación de la transacción: creando la aplicación ejemplo

Ahora vamos a crear una pequeña aplicación que modele el registro de un pedido en una tienda online. Nuestras tareas:

  1. Guardar la información del pedido.
  2. Actualizar la cantidad disponible del producto.
  3. Si algo falla, revertir todos los cambios.

Creamos las entidades

Empezamos por la base de datos y las entidades. Tendremos dos tablas: Order y Product.

Entidad Product


import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Product {

    @Id
    private Long id;
    private String name;
    private int stock; // Cantidad en stock

    // Getters y setters (puedes usar Lombok)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }
}

Entidad Order


import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long productId;
    private int quantity;

    // Getters y setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

Creamos los repositories

Ahora creamos los repositories para trabajar con nuestras entidades. Usaremos JpaRepository.

ProductRepository


import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
    // No se necesitan métodos adicionales por ahora
}

OrderRepository


import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
    // No se necesitan métodos adicionales por ahora
}

Gestión de la transacción con @Transactional

Ahora viene lo más interesante. Crearemos un servicio que:

  1. Registre el pedido.
  2. Revierta los cambios si no hay suficiente stock.

OrderService


import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final ProductRepository productRepository;
    private final OrderRepository orderRepository;

    public OrderService(ProductRepository productRepository, OrderRepository orderRepository) {
        this.productRepository = productRepository;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void placeOrder(Long productId, int quantity) {
        // 1. Obtenemos el producto
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found!"));

        // 2. Comprobamos si hay suficiente stock
        if (product.getStock() < quantity) {
            throw new RuntimeException("Not enough stock for product!");
        }

        // 3. Reducimos la cantidad en stock
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);

        // 4. Creamos y guardamos la orden
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        // Guardamos los cambios solo si todo ha ido bien
        System.out.println("Order placed successfully!");
    }
}

¿Qué hace aquí la anotación @Transactional? Garantiza que todos los cambios o bien se confirman, o se revierten si ocurre un error. Por ejemplo, si no hay suficiente stock, el método lanza una excepción y los cambios se revierten.


Testing de las transacciones

Vamos a probar cómo funciona esto. Escribamos una clase de test sencilla.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testPlaceOrder_Success() {
        // Creamos un producto de prueba
        Product product = new Product();
        product.setId(1L);
        product.setName("Test Product");
        product.setStock(10);
        productRepository.save(product);

        // Creamos el pedido
        orderService.placeOrder(1L, 2);

        // Comprobamos la cantidad en stock
        Product updatedProduct = productRepository.findById(1L).get();
        assert updatedProduct.getStock() == 8;
    }

    @Test
    public void testPlaceOrder_NotEnoughStock() {
        // Creamos un producto de prueba
        Product product = new Product();
        product.setId(2L);
        product.setName("Another Product");
        product.setStock(5);
        productRepository.save(product);

        try {
            // Intentamos pedir más de lo que hay en stock
            orderService.placeOrder(2L, 10);
        } catch (Exception e) {
            System.out.println(e.getMessage()); // Expect "Not enough stock for product!"
        }

        // Comprobamos que el producto no ha cambiado
        Product updatedProduct = productRepository.findById(2L).get();
        assert updatedProduct.getStock() == 5;
    }
}

Resumen

Ahora tenemos un servicio transaccional que:

  • Garantiza la integridad de los datos.
  • Revierte automáticamente los cambios en caso de error.
  • Es fácil de testear.

En la práctica, estos enfoques hacen que las aplicaciones sean fiables y predecibles. Y sí, ¡no te olvides de darte una palmadita por el progreso de hoy!

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION