404 Not Found

404 Not Found


nginx

Persistência de Dados com Spring Data JPA

O JPA torna o mapeamento entre objetos e bancos de dados relacionais declarativo—você escreve Entidades, define Repositories, e SQL é gerado automaticamente.

1. O Que Você Vai Aprender


2. Uma História Real Sobre Persistência de Dados

(1) Ponto de Dor: Dados Em Memória São Perdidos Ao Reiniciar

Alice anteriormente usava um mapa em memória para armazenar dados de produtos e pedidos do OrderFlow. Toda vez que a aplicação era reiniciada, todos os dados eram perdidos, forçando-a a recriar manualmente os produtos durante os testes. Ainda mais problemático era que o mapa não suportava consultas complexas (como buscar pedidos dentro de um intervalo de datas), então um banco de dados real tinha que ser usado no ambiente de produção.

(2) A Solução Usando Spring Data JPA

O Spring Data JPA torna a persistência uma questão de "definir interfaces":

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
}

Sem uma classe de implementação, o Spring Data gera SQL automaticamente.

(3) Resultado

Depois que Alice introduziu o JPA, os dados do OrderFlow são persistidos no MySQL e retidos após uma reinicialização. Consultas complexas são implementadas usando convenções de nomenclatura de métodos ou @Query, e a interface Repository não contém código de implementação.


3. Definindo Entidades JPA

(1) Anotações Principais de Entidade

Anotação Finalidade Exemplo
@Entity Marcada como entidade JPA @Entity public class Product
@Table Especificar nome da tabela @Table(name = "products")
@Id Marcar Chave Primária @Id private Long id;
@GeneratedValue Estratégia de Geração de Chave Primária @GeneratedValue(strategy = IDENTITY)
@Column Mapeamento de Coluna @Column(nullable = false, length = 200)
@CreationTimestamp Preencher automaticamente data de criação @CreationTimestamp private Instant createdAt;

(1) ▶ Exemplo: Entidade Product

JAVA
@Entity
@Table(name = "products")
public class Product {

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

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    // Construtor padrão exigido pelo JPA
    protected Product() {}

    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    // Getters e setters omitidos por brevidade
}

Saída:

TEXT
// Execução bem-sucedida
Valor ddl-auto Comportamento Ambiente Aplicável
create-drop Habilita criação de tabela, desabilita exclusão de tabela Teste
create Reconstruir tabela a cada inicialização Desenvolvimento
update Apenas adicionar, não excluir; modificar colunas Desenvolvimento
validate Apenas verificar, não modificar Produção
none Não tomar nenhuma ação Produção (Gerenciamento Manual)
⚠️ Nota: Em ambiente de produção, você deve usar validate ou none; usar update pode resultar em perda de dados.


4. Interface Repository

(1) Hierarquia de Herança do Repository

100%
graph LR
    A[Repository] --> B[CrudRepository]
    B --> C[ListCrudRepository]
    B --> D[PagingAndSortingRepository]
    D --> E[JpaRepository]
    C --> E
Interface Métodos Principais Casos de Uso
CrudRepository save / findById / findAll / delete CRUD Básico
ListCrudRepository findAll retorna uma List Evitar conversão de Iterable
PagingAndSortingRepository findAll(Pageable) Paginação e Ordenação
JpaRepository Todos os acima + flush / saveAndFlush Mais comumente usado, recomendado

(1) ▶ Exemplo: ProductRepository

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {

    // Consulta derivada pelo nome do método
    List<Product> findByNameContaining(String keyword);

    List<Product> findByStockLessThan(Integer threshold);

    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);

    // JPQL personalizado
    @Query("SELECT p FROM Product p WHERE p.stock = 0")
    List<Product> findOutOfStockProducts();

    // SQL Nativo
    @Query(value = "SELECT * FROM products WHERE price > :minPrice ORDER BY price DESC",
           nativeQuery = true)
    List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}

Saída:

TEXT
// Execução bem-sucedida

(2) Convenções de Nomes de Métodos

Palavra-chave Exemplo SQL Gerado
findBy findByName WHERE name = ?
Containing findByNameContaining WHERE name LIKE %?%
Between findByPriceBetween WHERE price BETWEEN ? AND ?
LessThan findByStockLessThan WHERE stock < ?
OrderBy findByPriceOrderByStockDesc ORDER BY stock DESC
And / Or findByNameAndStock WHERE name = ? AND stock = ?

5. Mapeamento de Relacionamento de Entidade

(1) Order ↔ OrderItem: Relacionamento Um-para-Muitos

100%
erDiagram
    ORDER ||--o{ ORDER_ITEM : contains
    PRODUCT ||--o{ ORDER_ITEM : included_in
    ORDER {
        bigint id PK
        varchar status
        decimal total_amount
        timestamp created_at
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        decimal unit_price
    }
    PRODUCT {
        bigint id PK
        varchar name
        decimal price
        int stock
    }

(1) ▶ Exemplo: Entidades Order e OrderItem

JAVA
@Entity
@Table(name = "orders")
public class Order {

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

    @Column(nullable = false, length = 20)
    private String status;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
}

@Entity
@Table(name = "order_items")
public class OrderItem {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
}

Saída:

TEXT
// Execução bem-sucedida
Tipo de Cascata Significado Uso Recomendado
ALL Todas as operações são cascateadas Usado apenas para dependências fortes
PERSIST cascata persist Comum
MERGE cascata merge Comum
REMOVE cascata delete Usar com cautela
Tipo de Fetch Comportamento Casos de Uso
LAZY Consultar apenas no acesso Recomendação padrão para evitar N+1
EAGER Carregar Imediatamente Valor Padrão @ManyToOne

6. @Query JPQL vs. SQL Nativo

(1) ▶ Exemplo: Consultas JPQL e SQL Nativo

JAVA
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL: buscar pedidos por status com contagem de itens
    @Query("SELECT o FROM Order o WHERE o.status = :status")
    List<Order> findByStatus(@Param("status") String status);

    // JPQL: join fetch para evitar N+1
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    // SQL Nativo: estatísticas de pedidos
    @Query(value = """
        SELECT o.status, COUNT(*) as cnt, SUM(o.total_amount) as total
        FROM orders o
        GROUP BY o.status
        """, nativeQuery = true)
    List<Object[]> getOrderStatistics();
}

Saída:

TEXT
// Execução bem-sucedida
Dimensão JPQL SQL Nativo
Sintaxe Baseada em Entidades e Campos Baseada em Tabelas e Colunas
Migração de Banco de Dados Portátil Não Portátil
Recursos Limitado (não suporta UNION, etc.) Completo
Tipo de Retorno Entidade / DTO Object[] / DTO

7. Inicialização do Banco de Dados

(1) ▶ Exemplo: schema.sql e data.sql

SQL
-- src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Saída:

TEXT
CREATE TABLE
SQL
-- src/main/resources/data.sql
INSERT INTO products (name, price, stock) VALUES
    ('Laptop Pro', 1299.99, 50),
    ('Wireless Mouse', 29.99, 200),
    ('USB-C Hub', 49.99, 100);
Método de Inicialização Configuração Casos de Uso
Hibernate ddl-auto spring.jpa.hibernate.ddl-auto=update Desenvolvimento
schema.sql + data.sql spring.sql.init.mode=always Requer controle preciso de DDL
Flyway spring.flyway.enabled=true Migração de Produção
Liquibase spring.liquibase.enabled=true Migração de Produção

8. Exemplo Abrangente: Implementação Completa da Persistência JPA do OrderFlow

JAVA
// Product.java
@Entity
@Table(name = "products")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    protected Product() {}
    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name; this.price = price; this.stock = stock;
    }
    // getters/setters
}

// Order.java
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status = "PENDING";

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(this, product, quantity, product.getPrice());
        items.add(item);
        totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
    }
    // getters/setters
}

// OrderItem.java
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
    public OrderItem(Order order, Product product, Integer quantity, BigDecimal unitPrice) {
        this.order = order; this.product = product;
        this.quantity = quantity; this.unitPrice = unitPrice;
    }
    // getters/setters
}

// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
    List<Product> findByStockLessThan(Integer threshold);
}

❓ Perguntas Frequentes

P Por que uma entidade JPA deve ter um construtor sem argumentos?
R O JPA usa reflexão para criar instâncias de entidade, o que requer um construtor sem argumentos. Você pode anotá-lo com protected para evitar uso acidental de fora. Como records não têm construtor sem argumentos, eles não podem ser usados diretamente como entidades JPA.
P Como resolver uma LazyInitializationException?
R A causa é acessar uma propriedade carregada preguiçosamente após a sessão ter sido fechada. Soluções: 1) @Transactional Manter a sessão aberta; 2) JOIN FETCH Carregar tudo de uma vez; 3) @EntityGraph Definir um plano de carregamento.
P Qual devo escolher, CrudRepository ou JpaRepository?
R Recomendamos JpaRepository. Ele herda de todas as subinterfaces e fornece o conjunto mais abrangente de métodos (incluindo paginação, ordenação e operações em lote), tornando-se a escolha padrão para desenvolvimento real.
P ddl-auto=update é seguro?
R Não, não é seguro. A opção update apenas insere dados e não os exclui, o que pode deixar colunas obsoletas para trás. Além disso, alterar tipos de colunas pode resultar em perda de dados. Em ambiente de produção, você deve usar validate junto com Flyway ou Liquibase para gerenciar migrações de banco de dados.
P Como tratar o problema de consulta N+1?
R 1) @Query("JOIN FETCH") Carregar dados relacionados todos de uma vez; 2) @EntityGraph Plano de carregamento declarativo; 3) @BatchSize Carregamento em lote. Este é o aspecto mais crítico da otimização de desempenho do JPA.
P schema.sql e ddl-auto do Hibernate podem ser usados juntos?
R Não recomendamos usá-los juntos, pois podem conflitar. Se você usar Hibernate para gerenciar DDL, não precisa de schema.sql; se usar schema.sql, defina ddl-auto=none.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Configure uma fonte de dados H2 para o OrderFlow, crie a Entidade Product e o ProductRepository, implemente uma API REST CRUD para produtos e persista dados no H2.

  2. Problema Avançado (Dificuldade ⭐⭐): Implemente o relacionamento um-para-muitos entre Order e OrderItem, use JOIN FETCH para resolver o problema N+1, e escreva um método Repository para consultar pedidos por intervalo de datas.

  3. Desafio (Dificuldade: ⭐⭐⭐): Alterne para a fonte de dados MySQL, use Flyway para gerenciar scripts de migração de banco de dados, e crie os arquivos de migração V1__init_schema.sql e V2__add_order_status_index.sql.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%