ALGO25 / BLOG

Spring Boot: JPA, repository și tranzacții

Adaugă Spring Data JPA proiectului Java: entități, repository, DTO-uri și granițe tranzacționale. Înțelege H2 în memorie și limitele exemplului local.

Până acum serviciul păstra sarcinile într-un Map. Îl înlocuim cu o implementare care folosește Spring Data JPA, păstrând controllerul și DTO-urile. Obiectivul este să înțelegi separarea dintre contractul HTTP, modelul de persistență și tranzacția de business.

Punctul de plecare

Continuă proiectul după validare și erori. Folosim Java 21, Spring Boot 4.1.1, Spring Data JPA și H2. Dependențele sunt deja în POM. Baza rămâne în memorie și schema folosește create-drop: exersăm persistența prin JPA, dar nu promitem păstrarea datelor după restart.

Pentru o bază reală, ar trebui să alegi explicit motorul, migrarea schemei, credențialele și testele pe acel motor. H2 nu demonstrează automat compatibilitatea cu PostgreSQL sau cu altă bază. În acest articol izolăm mecanismul de bază înainte să adăugăm acele decizii.

Entitatea descrie ce stocăm

Creează TaskEntity.java în pachetul ro.algo25.tasks:

package ro.algo25.tasks;

import jakarta.persistence.*;

@Entity
@Table(name = "learning_tasks")
public class TaskEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 80)
    private String title;

    private boolean completed;

    protected TaskEntity() {}

    public TaskEntity(String title) {
        this.title = title;
    }

    public TaskView view() {
        return new TaskView(id, title, completed);
    }
}

ID-ul este generat de baza de date. Constructorul fără argumente este disponibil pentru mecanismul JPA, iar constructorul public exprimă crearea unei sarcini. @Column descrie constrângeri pentru schema exemplului, dar nu înlocuiește toate validările de intrare.

Nu returnăm direct entitatea din controller. Metoda view() construiește DTO-ul public, fără a cupla clientul la adnotări JPA sau la eventuale relații adăugate ulterior. La convertire presupunem o entitate care a primit deja ID; nu folosim metoda pe o entitate nouă, nesalvată.

Repository și implementarea serviciului

Creează TaskRepository.java:

package ro.algo25.tasks;

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

public interface TaskRepository extends JpaRepository<TaskEntity, Long> {}

Spring Data furnizează implementarea interfeței pentru operațiile moștenite. Înlocuiește complet vechiul TaskService.java, nu păstra două clase cu același nume:

package ro.algo25.tasks;

import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class TaskService {
    private final TaskRepository repository;

    public TaskService(TaskRepository repository) {
        this.repository = repository;
    }

    public List<TaskView> list() {
        return repository.findAll(Sort.by("id")).stream().map(TaskEntity::view).toList();
    }

    public TaskView get(long id) {
        return repository.findById(id).orElseThrow(() -> new TaskMissing(id)).view();
    }

    @Transactional
    public TaskView create(String title) {
        return repository.save(new TaskEntity(title.strip())).view();
    }
}

Controllerul rămâne cel din articolul de validare. list() ordonează explicit după ID; nu presupunem că baza întoarce implicit înregistrările într-o ordine stabilă. get() transformă absența în excepția de domeniu deja mapată la 404. create() elimină spațiile de la capete și salvează entitatea, apoi construiește răspunsul.

Lista completă este potrivită numai pentru acest set mic de date. Când volumul crește, adaugă paginare și o ordine deterministă; nu încărca necondiționat toată tabela într-un endpoint de producție.

Granița tranzacției

Clasa declară tranzacții de citire, iar metoda create() suprascrie configurația pentru scriere. readOnly comunică intenția și poate permite optimizări; nu trebuie tratat ca un mecanism de autorizare sau ca o garanție universală că nicio scriere nu poate avea loc.

În configurația obișnuită bazată pe proxy, apelul trebuie să treacă prin obiectul gestionat de Spring pentru ca interceptorul tranzacțional să se aplice. Un apel dintr-o metodă a aceluiași obiect către alta, adnotată, nu creează automat o nouă graniță tranzacțională. Această limită contează când refactorizezi metode sau încerci să testezi tranzacția.

Implicit, excepțiile unchecked determină rollback; regulile pot fi configurate, inclusiv pentru excepții checked. Nu înghiți o eroare în interiorul metodei și apoi presupune că tranzacția știe automat că doreai anularea. Specifică rezultatul operației și testează starea datelor după eșec.

Exersează

  1. Creează titlul Invata JPA . Răspunsul trebuie să conțină Invata JPA, iar GET pe Location trebuie să întoarcă aceeași valoare.
  2. Repornește aplicația. Lista este goală deoarece H2 este în memorie, chiar dacă ai folosit un repository și tranzacții.
  3. Desenează două scrieri care trebuie să reușească împreună. Plasează granița tranzacției la operația de business, apoi descrie un test cu eșec între cele două scrieri.

Verifică înainte să continui

Poți continua dacă distingi DTO-ul de entitate, repository-ul de serviciu și tranzacția de o simplă adnotare decorativă. Urmează testele automate ale API-ului, unde verificăm atât răspunsul, cât și efectul asupra datelor.

Întrebări frecvente

JPA este baza de date?

Nu. Este un model/API de persistență. În proiect avem o implementare JPA și motorul H2, care au roluri diferite.

Datele persistă după restart în acest exemplu?

Nu. URL-ul este pentru H2 în memorie, iar configurația este didactică. Nu confunda utilizarea JPA cu durabilitatea configurației alese.

Pot returna entitatea direct ca JSON?

Este posibil în unele aplicații, dar aici păstrăm un DTO separat pentru controlul contractului public și al câmpurilor expuse.

Transactional funcționează la orice apel de metodă?

Nu în modelul implicit cu proxy. Apelurile interne pe aceeași instanță nu sunt interceptate în același mod ca apelurile externe prin bean.

create-drop este recomandat pentru producție?

Nu. Poate recrea și elimina schema. În această serie îl folosim doar pentru datele locale temporare de exercițiu.

Surse

Spring Data JPA: persistența entităților, Transactional și reguli de rollback, consultate la 13 octombrie 2026.