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
- Definições de Entidade JPA: Anotações
@Entity/@Id/@GeneratedValue/@Column - Interface Repository:
CrudRepository/JpaRepository/ Métodos de Consulta Personalizados - Mapeamento de Relacionamento de Entidade
@OneToMany/@ManyToOne(Order ↔ OrderItem) - Consultas
@QueryJPQL e SQL Nativo - Inicialização do Banco de Dados:
schema.sql/data.sqle Estratégias Hibernateddl-auto
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":
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
@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:
// 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) |
validate ou none; usar update pode resultar em perda de dados.
4. Interface Repository
(1) Hierarquia de Herança do Repository
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
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:
// 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
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
@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:
// 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
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:
// 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
-- 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:
CREATE TABLE
-- 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
// 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
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.@Transactional Manter a sessão aberta; 2) JOIN FETCH Carregar tudo de uma vez; 3) @EntityGraph Definir um plano de carregamento.ddl-auto=update é seguro?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.@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.ddl-auto=none.📖 Resumo
- Entidades JPA usam anotações para mapear objetos para tabelas relacionais;
@Id+@GeneratedValuegerenciam a chave primária JpaRepositoryFornece funcionalidade CRUD completa + paginação + ordenação; gera consultas automaticamente com base em convenções de nomenclatura de métodos@OneToMany/@ManyToOneMapeiam relacionamentos de entidade; observe as estratégias de cascata e fetch@QueryJPQL é adequado para consultas de entidade, enquanto SQL nativo é adequado para estatísticas complexas- Use
validate+ ferramentas de migração (Flyway/Liquibase) no ambiente de produção; não useddl-auto=update
📝 Exercícios
-
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.
-
Problema Avançado (Dificuldade ⭐⭐): Implemente o relacionamento um-para-muitos entre
OrdereOrderItem, useJOIN FETCHpara resolver o problema N+1, e escreva um método Repository para consultar pedidos por intervalo de datas. -
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.



