Spring Data
Spring Data JPA abstrai o acesso a banco de dados, eliminando a maior parte do código SQL manual. Com ele, você define entidades e repositórios, e o framework cuida da persistência.
Dependências
Seção intitulada “Dependências”No pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<!-- Para usar PostgreSQL --><dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope></dependency>
<!-- Para usar H2 (banco em memória para desenvolvimento) --><dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope></dependency>JPA e Hibernate
Seção intitulada “JPA e Hibernate”JPA (Jakarta Persistence API) é a especificação Java para mapeamento objeto-relacional. Hibernate é a implementação mais popular dessa especificação - é o que Spring Data usa por baixo.
O objetivo é mapear classes Java para tabelas de banco de dados e gerenciar as operações automaticamente.
Entidades
Seção intitulada “Entidades”Uma entidade é uma classe Java mapeada para uma tabela do banco:
import jakarta.persistence.*;
@Entity@Table(name = "usuarios")public class Usuario {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false, length = 100) private String nome;
@Column(nullable = false, unique = true, length = 150) private String email;
@Column(name = "data_nascimento") private LocalDate dataNascimento;
@Column(nullable = false) private boolean ativo = true;
@CreationTimestamp @Column(name = "criado_em", updatable = false) private LocalDateTime criadoEm;
// construtores, getters, setters...}Anotações principais
Seção intitulada “Anotações principais”| Anotação | Função |
|---|---|
@Entity | Marca a classe como entidade JPA |
@Table(name = "...") | Define o nome da tabela (padrão: nome da classe) |
@Id | Chave primária |
@GeneratedValue | Estratégia de geração do ID |
@Column | Personaliza a coluna (nome, tamanho, nullable) |
@Transient | Campo ignorado pelo JPA (não persiste) |
@CreationTimestamp | Preenchido automaticamente no INSERT |
@UpdateTimestamp | Preenchido automaticamente no UPDATE |
Uma entidade precisa ser uma classe mutável de verdade, com construtor sem argumentos: um record (visto em Java Moderno) não funciona como entidade JPA, porque a especificação exige exatamente o que um record proíbe por definição (campos não-final, classe não-final, construtor vazio). Records continuam sendo a escolha certa para DTO e projeção de leitura desse mesmo dado, só não para a entidade gerenciada pelo Hibernate.
@Enumerated e a armadilha do EnumType.ORDINAL
Seção intitulada “@Enumerated e a armadilha do EnumType.ORDINAL”No código acima o campo status é um enum (StatusPedido), mas a coluna no banco é um número ou um texto. Quem decide qual dos dois é a anotação @Enumerated, e o default dela é justamente a opção que mais dá dor de cabeça.
Sem argumento nenhum, @Enumerated equivale a @Enumerated(EnumType.ORDINAL): o Hibernate grava a posição da constante no enum, o valor de ordinal().
public enum StatusPedido { NOVO, // 0 PAGO, // 1 ENVIADO, // 2 ENTREGUE // 3}
@Enumerated // ORDINAL implícitoprivate StatusPedido status;Um pedido PAGO vira o número 1 na coluna. Funciona, ocupa pouco espaço, e é uma bomba-relógio. No dia em que alguém precisar de um status AGUARDANDO_PAGAMENTO entre NOVO e PAGO:
public enum StatusPedido { NOVO, // 0 AGUARDANDO_PAGAMENTO, // 1 <- novo PAGO, // 2 (era 1) ENVIADO, // 3 (era 2) ENTREGUE // 4 (era 3)}Todas as linhas que tinham 1 continuam com 1, só que agora 1 significa AGUARDANDO_PAGAMENTO. Cada pedido pago virou “aguardando pagamento” de uma vez, sem erro e sem log. O banco não faz ideia de que o significado dos números mudou.
@Enumerated(EnumType.STRING) resolve isso gravando o nome da constante:
@Enumerated(EnumType.STRING)@Column(length = 20)private StatusPedido status;Agora a coluna guarda o texto PAGO. Você pode reordenar o enum, inserir constantes no meio ou remover as que não usa mais, e as linhas antigas continuam apontando para a constante certa. O preço é modesto: a coluna vira um varchar em vez de um smallint, e renomear uma constante (PAGO para PAGAMENTO_CONFIRMADO) passa a exigir um UPDATE para acertar os dados que já estão gravados.
Quando você quer um código curto e estável na coluna, desacoplado do nome da constante Java, dá para assumir o controle total com um AttributeConverter:
@Converter(autoApply = true)public class StatusPedidoConverter implements AttributeConverter<StatusPedido, String> {
@Override public String convertToDatabaseColumn(StatusPedido status) { return status == null ? null : status.getCodigo(); // "N", "P", "E"... }
@Override public StatusPedido convertToEntityAttribute(String codigo) { return StatusPedido.peloCodigo(codigo); }}Assim o nome da constante e o valor no banco evoluem separados: renomear o enum não toca no banco, e mudar o código gravado não toca no enum.
Regra prática: em toda entidade nova, use EnumType.STRING. Nunca deixe o @Enumerated no default. Se precisar economizar espaço ou já herdou uma coluna com códigos, parta para o AttributeConverter em vez de voltar para ORDINAL.
Ciclo de vida de uma entidade
Seção intitulada “Ciclo de vida de uma entidade”A maior parte da confusão com JPA some quando você entende que uma entidade não está sempre “conectada” ao banco. Ela passa por estados, e o comportamento do Hibernate muda em cada um.
O centro de tudo é o contexto de persistência (persistence context), também chamado de cache de primeiro nível. Durante uma transação, o Hibernate mantém ali dentro uma cópia de cada entidade que ele está gerenciando, com a garantia de que existe uma única instância por identidade: se você buscar o Usuario de id 7 duas vezes na mesma transação, recebe o mesmo objeto Java, não duas cópias.
Os quatro estados
Seção intitulada “Os quatro estados”stateDiagram-v2
[*] --> Transient: new
Transient --> Managed: persist()
Managed --> Detached: fim da transação / detach() / clear()
Detached --> Managed: merge()
Managed --> Removed: remove()
Removed --> [*]: DELETE no flush
Transient (a especificação JPA chama de new): um objeto que você criou com new e mais nada. Não tem linha correspondente no banco, não está no contexto de persistência, e o Hibernate não sabe que ele existe. Mudar os campos dele não gera SQL nenhum.
Usuario u = new Usuario("Ana", "ana@exemplo.com"); // transientManaged (ou persistent): a entidade está no contexto de persistência e é um espelho de uma linha da tabela. Todo campo que você alterar é detectado e sincronizado com o banco automaticamente. Uma entidade fica managed depois de um persist(), ou quando você a carrega com findById, uma query, etc.
Usuario u = repository.findById(7L).orElseThrow(); // managedu.setNome("Ana Paula"); // sem chamar save, o UPDATE vai sair no fim da transaçãoDetached: a entidade já teve (ou tem) uma linha no banco, mas não está mais sendo acompanhada por nenhum contexto de persistência, porque a transação terminou, ou você chamou detach()/clear(). O objeto continua na memória com os dados que tinha, mas alterá-lo não afeta o banco.
Removed: a entidade foi marcada para exclusão com remove() (ou o delete do repositório). Ela ainda está no contexto, mas o Hibernate já agendou um DELETE para o próximo flush.
As transições
Seção intitulada “As transições”| De | Para | Como |
|---|---|---|
| Transient | Managed | entityManager.persist(e) (ou repository.save(e) num objeto novo) |
| Managed | Detached | fim da transação, detach(e), clear(), ou close() do EntityManager |
| Detached | Managed | entityManager.merge(e) |
| Managed | Removed | entityManager.remove(e) (ou repository.delete(e)) |
Um detalhe que pega muita gente: merge() não transforma o objeto que você passou em managed. Ele copia os dados desse objeto para uma instância managed (buscando no banco se preciso) e devolve essa outra instância. Depois de Usuario gerenciado = em.merge(destacado), quem está managed é gerenciado, não destacado. Continuar mexendo em destacado não faz nada.
Dirty checking
Seção intitulada “Dirty checking”O motivo de uma entidade managed não precisar de save() para persistir mudanças é o dirty checking. Quando a entidade entra no contexto, o Hibernate guarda um retrato (snapshot) do estado dela. No flush (quando ele envia o SQL pendente ao banco), ele compara o estado atual campo a campo com esse snapshot e, para cada entidade com pelo menos um campo diferente, gera um UPDATE.
Isso tem um custo: em toda entidade carregada, o Hibernate carrega o dobro de dados na memória (o objeto e o snapshot) e faz a comparação no flush. É por isso que uma consulta só de leitura se beneficia de projeções (o resultado de uma projeção não entra no contexto, então não tem snapshot nem dirty checking) e de @Transactional(readOnly = true). Esse assunto está na seção Projeções mais abaixo.
Flush x commit
Seção intitulada “Flush x commit”Não são a mesma coisa. O flush é o momento em que o Hibernate traduz as mudanças pendentes em SQL e manda para o banco. O commit é o momento em que a transação é confirmada e essas mudanças ficam definitivas.
O Hibernate faz flush automaticamente antes do commit, e às vezes antes de uma query (para o resultado refletir o que você já alterou). Então o SQL pode “sair” antes do fim do método @Transactional, mas ainda dá para reverter tudo com um rollback até o commit acontecer. O @Transactional que fecha esse ciclo está na seção do fim da nota.
Relacionamentos
Seção intitulada “Relacionamentos”@OneToOne
Seção intitulada “@OneToOne”Relacionamento 1:1: cada Usuario tem no máximo um Perfil, e vice-versa. A forma mais simples é unidirecional, com a chave estrangeira do lado que faz mais sentido “possuir” a referência:
@Entitypublic class Perfil { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String bio; private String avatarUrl;
@OneToOne @JoinColumn(name = "usuario_id", unique = true) private Usuario usuario;}Para navegar dos dois lados (usuario.getPerfil() e perfil.getUsuario()), o relacionamento vira bidirecional. Só um dos lados pode ter a coluna de chave estrangeira (o lado dono, marcado com @JoinColumn); o outro lado só declara mappedBy, apontando o nome do campo dono:
@Entitypublic class Usuario { // ...
@OneToOne(mappedBy = "usuario", cascade = CascadeType.ALL) private Perfil perfil;}Uma FK separada com unique = true funciona, mas duplica a garantia de unicidade que já existe na chave primária da outra tabela. Quando as duas entidades sempre nascem e morrem juntas (não existe Perfil sem Usuario), o @MapsId é a opção mais enxuta: a tabela perfil usa o mesmo valor de usuario_id como sua própria chave primária, em vez de ter um id autoincrementado e uma coluna de FK à parte.
@Entitypublic class Perfil { @Id private Long id; // sem @GeneratedValue: o valor vem do usuário associado
private String bio;
@OneToOne @MapsId @JoinColumn(name = "usuario_id") private Usuario usuario;}Antes de modelar como @OneToOne, vale perguntar se as duas classes realmente precisam ser entidades separadas. Se Perfil não tem ciclo de vida próprio nem é consultado sozinho, colocar bio e avatarUrl como colunas direto na entidade Usuario é mais simples e evita um JOIN a mais em toda consulta.
@ManyToOne e @OneToMany
Seção intitulada “@ManyToOne e @OneToMany”@Entitypublic class Pedido { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
// Muitos pedidos para um usuário @ManyToOne(optional = false) @JoinColumn(name = "usuario_id") private Usuario usuario;
@Column(nullable = false) private BigDecimal valor;
@Enumerated(EnumType.STRING) private StatusPedido status;}
// No Usuario (opcional - mapeamento bidirecional)@Entitypublic class Usuario { // ...
@OneToMany(mappedBy = "usuario", cascade = CascadeType.ALL) private List<Pedido> pedidos = new ArrayList<>();}@ManyToMany
Seção intitulada “@ManyToMany”@Entitypublic class Produto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToMany @JoinTable( name = "produto_categoria", joinColumns = @JoinColumn(name = "produto_id"), inverseJoinColumns = @JoinColumn(name = "categoria_id") ) private List<Categoria> categorias = new ArrayList<>();}Escolhendo a estratégia de chave primária
Seção intitulada “Escolhendo a estratégia de chave primária”@GeneratedValue(strategy = GenerationType.IDENTITY) (delegando para um AUTO_INCREMENT/SERIAL do banco) é a opção mais comum para começar, mas em sistemas distribuídos ou de alto volume, a escolha do tipo de ID afeta performance de um jeito que só aparece depois que a tabela já cresceu.
UUID.randomUUID() (a versão 4 do UUID) é totalmente aleatório, e isso é justamente o problema para uma chave primária indexada. Bancos relacionais organizam índices numa estrutura de árvore (B-tree), pensada para inserções que chegam em sequência crescente. Um UUID aleatório cai num ponto imprevisível da árvore a cada inserção, o que aumenta a fragmentação do índice e o custo de I/O conforme a tabela cresce.
@Idprivate UUID id = UUID.randomUUID(); // funciona, mas fragmenta o índice ao longo do tempoIsso não significa que a alternativa seja voltar para um ID sequencial simples. Sequências puras trazem problemas próprios: expõem informação do sistema (criar um registro no primeiro e no último dia do mês permite inferir quantos registros existem no período), e em arquitetura distribuída dependem de um contador centralizado, o que cria contenção e dificulta escalar horizontalmente sem coordenação entre instâncias.
O meio-termo mais usado hoje é UUIDv7 ou ULID, que incorporam um componente de tempo nos bits mais significativos do identificador, o que os torna aproximadamente ordenados por ordem de criação, e por isso muito mais amigáveis para índice B-tree e para particionamento por faixa (sharding) do que o UUIDv4 puro. UUIDv7 tem a vantagem de preservar o formato padrão de UUID, funcionando como substituto direto de UUID.randomUUID() sem mudar o tipo da coluna nem quebrar nenhum contrato existente.
Regra prática: se a tabela é pequena ou local a um único banco, IDENTITY continua sendo a opção mais simples. Se o sistema é distribuído ou de alto volume e você precisa gerar o ID antes de persistir (fora do banco), prefira UUIDv7 a UUIDv4 aleatório.
Repositórios
Seção intitulada “Repositórios”Spring Data gera a implementação automaticamente. Você só define a interface:
CrudRepository
Seção intitulada “CrudRepository”Operações básicas de CRUD:
import org.springframework.data.repository.CrudRepository;
public interface UsuarioRepository extends CrudRepository<Usuario, Long> { // findAll, findById, save, deleteById - já herdados}JpaRepository
Seção intitulada “JpaRepository”Estende CrudRepository com funcionalidades extras (paginação, ordenação):
import org.springframework.data.jpa.repository.JpaRepository;
public interface UsuarioRepository extends JpaRepository<Usuario, Long> { // Todos os métodos do CrudRepository + findAll(Pageable) etc.}JpaRepository<Entidade, TipoDoId> - use este na maioria dos casos.
Query methods
Seção intitulada “Query methods”Spring Data deriva queries automaticamente a partir do nome do método:
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
// SELECT * FROM usuarios WHERE email = ? Optional<Usuario> findByEmail(String email);
// SELECT * FROM usuarios WHERE nome LIKE '%?%' List<Usuario> findByNomeContaining(String nome);
// SELECT * FROM usuarios WHERE ativo = true ORDER BY nome List<Usuario> findByAtivoTrueOrderByNome();
// SELECT * FROM usuarios WHERE ativo = ? AND email LIKE ? List<Usuario> findByAtivoAndEmailContaining(boolean ativo, String email);
// Existência boolean existsByEmail(String email);
// Contagem long countByAtivo(boolean ativo);
// Deletar por critério void deleteByAtivoFalse();}@Query para queries customizadas
Seção intitulada “@Query para queries customizadas”// JPQL - usa nomes de classes e atributos Java, não SQL@Query("SELECT u FROM Usuario u WHERE u.email = :email AND u.ativo = true")Optional<Usuario> buscarAtivoPorEmail(@Param("email") String email);
// SQL nativo@Query(value = "SELECT * FROM usuarios WHERE YEAR(data_nascimento) = :ano", nativeQuery = true)List<Usuario> buscarPorAnoNascimento(@Param("ano") int ano);
// Update/Delete via @Modifying@Modifying@Query("UPDATE Usuario u SET u.ativo = false WHERE u.id = :id")void desativar(@Param("id") Long id);Paginação e ordenação
Seção intitulada “Paginação e ordenação”import org.springframework.data.domain.*;
// No repositórioPage<Usuario> findByAtivo(boolean ativo, Pageable pageable);
// No service/controllerPageable pageable = PageRequest.of(0, 20, Sort.by("nome").ascending());Page<Usuario> pagina = repository.findByAtivo(true, pageable);
System.out.println(pagina.getContent()); // lista da páginaSystem.out.println(pagina.getTotalElements()); // total de registrosSystem.out.println(pagina.getTotalPages()); // total de páginasProjeções: trazer só os campos que a tela usa
Seção intitulada “Projeções: trazer só os campos que a tela usa”Imagine uma tela que lista usuários e mostra só nome e email. O jeito óbvio é findByAtivoTrue(), que devolve List<Usuario>. Só que a entidade Usuario tem uns 15 campos, talvez um relacionamento com Endereco, outro com Pedido. O banco carrega tudo isso, o Hibernate monta os objetos, e você usa dois campos.
Numa lista de 50 linhas isso passa despercebido. Numa de milhares, ou com relacionamentos que puxam mais tabelas junto, a diferença aparece: consultas que levam segundos para montar dados que ninguém vai olhar.
O custo extra de carregar a entidade completa numa leitura tem três partes:
- Dirty checking: toda entidade carregada entra no contexto de persistência, e o Hibernate guarda um snapshot dela para, no fim da transação, comparar campo a campo e ver o que mudou. Numa consulta só de leitura, esse trabalho é jogado fora.
- Memória: o objeto mais o snapshot, multiplicados pela quantidade de linhas.
- SELECT gordo: todas as colunas da tabela, mais os joins dos relacionamentos que forem
EAGER.
Projeção é pedir ao Spring Data para trazer só um subconjunto de campos, num objeto que não é a entidade. Existem três formatos.
Projeção com record (a mais direta)
Seção intitulada “Projeção com record (a mais direta)”Um record com os campos que você quer, e um método no repositório que retorna esse tipo:
public record ResumoUsuario(String nome, String email) {}public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
List<ResumoUsuario> findByAtivoTrue();}O Spring Data olha o record, vê que os nomes dos componentes (nome, email) batem com atributos da entidade, e gera um SELECT u.nome, u.email FROM Usuario u WHERE u.ativo = true. O resultado é uma lista de ResumoUsuario, e nenhum desses objetos entra no contexto de persistência: sem snapshot, sem dirty checking, sem flush comparando estado.
Para juntar dados de mais de uma tabela, use @Query com uma expressão de construtor JPQL (começa com new e o nome completo da classe):
@Query(""" SELECT new com.exemplo.dto.ResumoPedido(p.numero, u.nome, p.valorTotal, p.criadoEm) FROM Pedido p JOIN p.usuario u WHERE p.status = :status """)List<ResumoPedido> resumoPorStatus(@Param("status") StatusPedido status);Isso também funciona em query nativa, com nativeQuery = true, desde que os nomes das colunas retornadas batam com o construtor.
Projeção por interface
Seção intitulada “Projeção por interface”Em vez de um record, uma interface só com os getters:
public interface ResumoUsuario { String getNome(); String getEmail();}
List<ResumoUsuario> findByAtivoTrue();O Spring Data cria um proxy em tempo de execução que implementa a interface. É a opção mais enxuta quando você não precisa de lógica nenhuma no objeto de saída, só ler os campos.
Projeção dinâmica
Seção intitulada “Projeção dinâmica”Quando o mesmo método precisa às vezes devolver a entidade e às vezes um resumo, dá para deixar o tipo aberto:
<T> List<T> findByAtivoTrue(Class<T> tipo);repository.findByAtivoTrue(Usuario.class); // entidade completarepository.findByAtivoTrue(ResumoUsuario.class); // só o resumoQuando ainda usar a entidade
Seção intitulada “Quando ainda usar a entidade”Projeção é para leitura. Se o fluxo vai alterar e salvar, você precisa da entidade gerenciada, porque é o dirty checking (aquele mesmo que era desperdício na leitura) que detecta a mudança e gera o UPDATE. A regra prática: consulta que só exibe dados pede projeção; consulta que carrega algo para modificar pede a entidade.
Vale notar a simetria com o DTO de entrada visto em Validação, DTO e Logging: lá, um objeto separado protege a entidade dos dados que chegam na requisição; aqui, um objeto separado evita expor e carregar a entidade inteira na resposta. Mesma ideia, pontas opostas do fluxo.
H2 Database
Seção intitulada “H2 Database”H2 é um banco relacional em memória, ideal para desenvolvimento e testes. Não precisa de instalação.
spring.datasource.url=jdbc:h2:mem:devdbspring.datasource.driver-class-name=org.h2.Driverspring.datasource.username=saspring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# Console web do H2 (acessível em /h2-console)spring.h2.console.enabled=truespring.h2.console.path=/h2-consoleAcesse http://localhost:8080/h2-console para inspecionar o banco durante o desenvolvimento.
Configuração com PostgreSQL
Seção intitulada “Configuração com PostgreSQL”spring.datasource.url=jdbc:postgresql://localhost:5432/meudbspring.datasource.username=postgresspring.datasource.password=senha
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
# Mostrar SQL gerado no log (útil em dev)spring.jpa.show-sql=truespring.jpa.properties.hibernate.format_sql=true
# DDL: create, create-drop, update, validate, nonespring.jpa.hibernate.ddl-auto=validateEm produção, use validate ou none e gerencie o schema com Flyway ou Liquibase.
Exemplo completo - CRUD de Produto
Seção intitulada “Exemplo completo - CRUD de Produto”Entidade
Seção intitulada “Entidade”@Entity@Table(name = "produtos")public class Produto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false, length = 200) private String nome;
@Column(nullable = false, precision = 10, scale = 2) private BigDecimal preco;
@Column(nullable = false) private int estoque;
// getters e setters}Repositório
Seção intitulada “Repositório”@Repositorypublic interface ProdutoRepository extends JpaRepository<Produto, Long> { List<Produto> findByNomeContainingIgnoreCase(String nome); List<Produto> findByEstoqueGreaterThan(int quantidade);}Service
Seção intitulada “Service”@Service@Transactionalpublic class ProdutoService { private final ProdutoRepository repository;
public ProdutoService(ProdutoRepository repository) { this.repository = repository; }
@Transactional(readOnly = true) public List<Produto> listar() { return repository.findAll(); }
@Transactional(readOnly = true) public Produto buscar(Long id) { return repository.findById(id) .orElseThrow(() -> new RecursoNaoEncontradoException("Produto não encontrado")); }
public Produto criar(Produto produto) { return repository.save(produto); }
public Produto atualizar(Long id, Produto dados) { Produto produto = buscar(id); produto.setNome(dados.getNome()); produto.setPreco(dados.getPreco()); produto.setEstoque(dados.getEstoque()); return repository.save(produto); }
public void deletar(Long id) { buscar(id); // verifica se existe repository.deleteById(id); }}Controller
Seção intitulada “Controller”@RestController@RequestMapping("/produtos")public class ProdutoController { private final ProdutoService service;
public ProdutoController(ProdutoService service) { this.service = service; }
@GetMapping public List<Produto> listar() { return service.listar(); }
@GetMapping("/{id}") public Produto buscar(@PathVariable Long id) { return service.buscar(id); }
@PostMapping public ResponseEntity<Produto> criar(@RequestBody Produto produto) { Produto salvo = service.criar(produto); return ResponseEntity.status(201).body(salvo); }
@PutMapping("/{id}") public Produto atualizar(@PathVariable Long id, @RequestBody Produto produto) { return service.atualizar(id, produto); }
@DeleteMapping("/{id}") public ResponseEntity<Void> deletar(@PathVariable Long id) { service.deletar(id); return ResponseEntity.noContent().build(); }}@Transactional
Seção intitulada “@Transactional”Garante que operações de banco aconteçam dentro de uma transação:
@Transactionalpublic void transferir(Long origemId, Long destinoId, BigDecimal valor) { Conta origem = buscar(origemId); Conta destino = buscar(destinoId);
origem.debitar(valor); destino.creditar(valor);
repository.save(origem); repository.save(destino); // Se qualquer linha acima lançar exceção, tudo é revertido (rollback)}Use @Transactional(readOnly = true) em métodos de apenas leitura - é uma dica de otimização para o banco.
Referências
Seção intitulada “Referências”- Entidades Managed, Transient e Detached no Hibernate e JPA - Alura, pt-BR
- Hibernate Entity Lifecycle - Baeldung, inglês
- Entity Lifecycle Model in JPA & Hibernate - Thorben Janssen, inglês
- The best way to map a @OneToOne relationship with JPA and Hibernate - Vlad Mihalcea, inglês
- Accessing Data with JPA - guia oficial do Spring, inglês