Ce este Spring Data JPA?
Spring Data JPA este un modul din ecosistemul Spring care simplifică dramatic lucrul cu baze de date relaționale. Elimină codul boilerplate și oferă o abstracție puternică peste JPA (Java Persistence API).
Avantaje principale
- Eliminarea codului repetitiv — nu mai scrii DAO-uri manual
- Query derivate din numele metodei — Spring generează SQL automat
- Suport pentru paginare și sortare — built-in
- Auditing automat — createdAt, updatedBy etc.
- Integrare nativă cu Spring Boot — auto-configurare
Configurare
Dependențe Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
application.properties
# Conexiune bază de date
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
# Hibernate DDL (create, update, validate, none)
spring.jpa.hibernate.ddl-auto=update
# Afișare SQL în consolă (pentru debug)
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Dialect specific bazei de date
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
ddl-auto=validate sau none. Migrările de schemă ar trebui gestionate cu Flyway sau Liquibase.
Entități JPA
O entitate reprezintă un tabel din baza de date. Fiecare instanță a clasei corespunde unui rând din tabel.
Entitate de bază
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true)
private String email;
@Enumerated(EnumType.STRING)
private Status status;
// getters, setters, constructors
}
Adnotări esențiale
| Adnotare | Descriere |
|---|---|
@Entity |
Marchează clasa ca entitate JPA |
@Table |
Specifică numele tabelului (opțional) |
@Id |
Definește cheia primară |
@GeneratedValue |
Strategie de generare ID (IDENTITY, SEQUENCE, AUTO, UUID) |
@Column |
Configurare coloană: name, nullable, unique, length |
@Transient |
Câmpul nu va fi persistat în DB |
@Lob |
Large Object (BLOB/CLOB) |
@Temporal |
Pentru Date/Calendar (DATE, TIME, TIMESTAMP) |
Strategii de generare ID
// Auto-increment (MySQL, PostgreSQL serial)
@GeneratedValue(strategy = GenerationType.IDENTITY)
// Sequence (PostgreSQL, Oracle)
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_sequence")
// UUID (pentru ID-uri distribuite)
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
Embedded Objects
@Embeddable
public class Address {
private String street;
private String city;
private String zipCode;
}
@Entity
public class User {
@Embedded
private Address address;
// Override column names if needed
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street",
column = @Column(name = "work_street"))
})
private Address workAddress;
}
Repository Pattern
Repository-ul este interfața principală pentru operații CRUD. Spring Data JPA generează implementarea automat la runtime.
Ierarhia Repository
Definire Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Metodele CRUD sunt moștenite automat
}
Metode moștenite din JpaRepository
// Save (insert sau update)
User saved = repository.save(user);
List<User> savedAll = repository.saveAll(users);
// Find
Optional<User> user = repository.findById(1L);
List<User> all = repository.findAll();
List<User> byIds = repository.findAllById(List.of(1L, 2L));
// Exists / Count
boolean exists = repository.existsById(1L);
long count = repository.count();
// Delete
repository.deleteById(1L);
repository.delete(user);
repository.deleteAll();
repository.deleteAllById(List.of(1L, 2L));
// JPA specific
repository.flush();
repository.saveAndFlush(user);
User ref = repository.getReferenceById(1L); // lazy proxy
getReferenceById returnează un proxy lazy (nu execută query). Util când ai nevoie doar de referință pentru relații. findById face SELECT imediat.
Query Methods (Derived Queries)
Spring Data JPA generează query-uri automat pe baza numelui metodei. Aceasta este una dintre cele mai puternice caracteristici.
Sintaxă
public interface UserRepository extends JpaRepository<User, Long> {
// findBy + PropertyName
Optional<User> findByEmail(String email);
List<User> findByStatus(Status status);
// Multiple conditions (And, Or)
List<User> findByNameAndStatus(String name, Status status);
List<User> findByStatusOrRole(Status status, String role);
// Comparisons
List<User> findByAgeGreaterThan(int age);
List<User> findByAgeBetween(int min, int max);
List<User> findByCreatedAtAfter(LocalDateTime date);
// String matching
List<User> findByNameContaining(String part);
List<User> findByNameStartingWith(String prefix);
List<User> findByEmailEndingWith(String domain);
List<User> findByNameIgnoreCase(String name);
// Null checks
List<User> findByDeletedAtIsNull();
List<User> findByAvatarIsNotNull();
// Collections
List<User> findByStatusIn(Collection<Status> statuses);
List<User> findByRoleNotIn(List<String> roles);
// Ordering
List<User> findByStatusOrderByCreatedAtDesc(Status status);
// Limiting results
Optional<User> findFirstByOrderByCreatedAtDesc();
List<User> findTop10ByStatus(Status status);
// Count / Exists / Delete
long countByStatus(Status status);
boolean existsByEmail(String email);
void deleteByStatus(Status status);
// Nested properties (navigare prin relații)
List<User> findByAddressCity(String city);
List<User> findByDepartmentCompanyName(String companyName);
}
Cuvinte cheie disponibile
| Keyword | SQL echivalent |
|---|---|
And | AND |
Or | OR |
Is, Equals | = |
Between | BETWEEN x AND y |
LessThan | < |
LessThanEqual | <= |
GreaterThan | > |
GreaterThanEqual | >= |
IsNull, Null | IS NULL |
IsNotNull, NotNull | IS NOT NULL |
Like | LIKE |
NotLike | NOT LIKE |
StartingWith | LIKE 'x%' |
EndingWith | LIKE '%x' |
Containing | LIKE '%x%' |
OrderBy | ORDER BY |
Not | <> |
In | IN (...) |
NotIn | NOT IN (...) |
True | = TRUE |
False | = FALSE |
IgnoreCase | UPPER(x) = UPPER(?) |
JPQL și Native Queries
Pentru query-uri complexe ce nu pot fi exprimate prin numele metodei, folosești @Query.
JPQL (Java Persistence Query Language)
public interface UserRepository extends JpaRepository<User, Long> {
// Parametri poziționali (?1, ?2)
@Query("SELECT u FROM User u WHERE u.status = ?1")
List<User> findByStatusJpql(Status status);
// Parametri numiți (:name)
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
Optional<User> findByEmailAndStatus(
@Param("email") String email,
@Param("status") Status status
);
// JOIN și condiții complexe
@Query("SELECT u FROM User u JOIN u.orders o WHERE o.total > :minTotal")
List<User> findUsersWithOrdersAbove(@Param("minTotal") BigDecimal minTotal);
// Agregări
@Query("SELECT COUNT(u) FROM User u WHERE u.status = :status")
long countByStatusJpql(@Param("status") Status status);
// Update/Delete cu @Modifying
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.lastLogin < :date")
int deactivateInactiveUsers(
@Param("status") Status status,
@Param("date") LocalDateTime date
);
@Modifying
@Query("DELETE FROM User u WHERE u.status = :status")
void deleteByStatusJpql(@Param("status") Status status);
}
@Modifying și trebuie executate într-o tranzacție. Adaugă @Transactional la service sau la metodă.
Native SQL Queries
// Query SQL nativ
@Query(
value = "SELECT * FROM users WHERE status = :status LIMIT :limit",
nativeQuery = true
)
List<User> findTopNativeByStatus(
@Param("status") String status,
@Param("limit") int limit
);
// Native cu paginare
@Query(
value = "SELECT * FROM users WHERE status = ?1",
countQuery = "SELECT COUNT(*) FROM users WHERE status = ?1",
nativeQuery = true
)
Page<User> findByStatusNative(String status, Pageable pageable);
SpEL în Queries
// #{#entityName} se înlocuiește cu numele entității
@Query("SELECT e FROM #{#entityName} e WHERE e.status = ?1")
List<User> findByStatus(Status status);
// Util pentru repository-uri generice/moștenite
Relații între Entități
@OneToMany / @ManyToOne
@Entity
public class Department {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
}
@Entity
public class Employee {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
}
@OneToOne
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id")
private UserProfile profile;
}
@Entity
public class UserProfile {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
@OneToOne(mappedBy = "profile")
private User user;
}
@ManyToMany
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
Fetch Types și N+1 Problem
LAZY (default pentru colecții) — încarcă doar când accesezi
EAGER — încarcă imediat cu entitatea părinte
// Evitarea N+1 cu JOIN FETCH
@Query("SELECT d FROM Department d JOIN FETCH d.employees WHERE d.id = :id")
Optional<Department> findByIdWithEmployees(@Param("id") Long id);
// Sau cu @EntityGraph
@EntityGraph(attributePaths = {"employees"})
Optional<Department> findById(Long id);
Cascade Types
| Tip | Descriere |
|---|---|
PERSIST | save() se propagă la entitățile asociate |
MERGE | update se propagă |
REMOVE | delete se propagă |
REFRESH | refresh se propagă |
DETACH | detach se propagă |
ALL | toate operațiile |
orphanRemoval
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children;
// Când scoți un child din listă, va fi șters din DB
parent.getChildren().remove(child); // → DELETE
Paginare și Sortare
Pageable
// În Repository
Page<User> findByStatus(Status status, Pageable pageable);
Slice<User> findSliceByStatus(Status status, Pageable pageable);
List<User> findAllBy(Pageable pageable);
// În Service/Controller
Pageable pageable = PageRequest.of(0, 10); // pagina 0, 10 elemente
Page<User> page = userRepository.findByStatus(Status.ACTIVE, pageable);
// Accesare rezultate
List<User> users = page.getContent();
long total = page.getTotalElements();
int totalPages = page.getTotalPages();
boolean hasNext = page.hasNext();
boolean isFirst = page.isFirst();
Sort
// Sortare simplă
Sort sort = Sort.by("name");
Sort sortDesc = Sort.by(Sort.Direction.DESC, "createdAt");
// Sortare multiplă
Sort sort = Sort.by("status").and(Sort.by("name").descending());
// Cu PageRequest
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
// În metoda repository
List<User> findByStatus(Status status, Sort sort);
Page vs Slice
| Page | Slice |
|---|---|
| Execută COUNT query suplimentar | Nu face COUNT |
| Știi totalul de elemente/pagini | Știi doar dacă există next |
| Mai lent pentru tabele mari | Mai rapid, ideal pentru infinite scroll |
Tranzacții
@Transactional
@Service
public class UserService {
// Tranzacție read-write (default)
@Transactional
public void transferCredits(Long fromId, Long toId, int amount) {
User from = userRepository.findById(fromId).orElseThrow();
User to = userRepository.findById(toId).orElseThrow();
from.setCredits(from.getCredits() - amount);
to.setCredits(to.getCredits() + amount);
// Dacă apare excepție, totul se face rollback
}
// Read-only (optimizat, fără dirty checking)
@Transactional(readOnly = true)
public List<User> getAllActive() {
return userRepository.findByStatus(Status.ACTIVE);
}
// Cu timeout și rollback rules
@Transactional(
timeout = 30,
rollbackFor = BusinessException.class,
noRollbackFor = EmailException.class
)
public void complexOperation() { /* ... */ }
}
Propagation
| Tip | Comportament |
|---|---|
REQUIRED | Folosește tranzacția existentă sau creează una nouă (default) |
REQUIRES_NEW | Întotdeauna creează tranzacție nouă |
NESTED | Savepoint în tranzacția curentă |
SUPPORTS | Participă dacă există, altfel non-transactional |
NOT_SUPPORTED | Suspendă tranzacția existentă |
MANDATORY | Trebuie să existe tranzacție, altfel excepție |
NEVER | Nu trebuie să existe tranzacție |
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action) {
// Se salvează chiar dacă tranzacția părinte face rollback
auditRepository.save(new AuditLog(action));
}
@Transactional funcționează doar când metoda e apelată din exterior (prin proxy). Apeluri interne din aceeași clasă nu trec prin proxy!
Auditing
Spring Data JPA poate popula automat câmpuri precum createdAt, updatedBy.
Configurare
// Activare auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
// Optional: pentru createdBy/modifiedBy
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(
SecurityContextHolder.getContext()
.getAuthentication()
.getName()
);
}
}
Entitate cu Auditing
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String modifiedBy;
// getters
}
@Entity
public class User extends BaseEntity {
// câmpurile de audit sunt moștenite
}
Callbacks JPA
@Entity
public class User {
@PrePersist
protected void onCreate() {
this.slug = generateSlug(this.name);
}
@PreUpdate
protected void onUpdate() {
this.version++;
}
@PostLoad
protected void onLoad() {
// după încărcare din DB
}
}
Projections
Projections permit selectarea doar a câmpurilor necesare, evitând încărcarea entității complete.
Interface Projection (Closed)
// Definire projection
public interface UserSummary {
Long getId();
String getName();
String getEmail();
}
// În repository
List<UserSummary> findByStatus(Status status);
// Spring generează: SELECT id, name, email FROM users WHERE status = ?
Open Projection (cu SpEL)
public interface UserView {
String getName();
@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();
@Value("#{target.email.split('@')[1]}")
String getEmailDomain();
}
Class-based Projection (DTO)
// DTO class
public record UserDto(Long id, String name, String email) {}
// În repository - cu JPQL
@Query("SELECT new com.example.dto.UserDto(u.id, u.name, u.email) FROM User u")
List<UserDto> findAllAsDto();
// Sau automat (Spring Data)
List<UserDto> findByStatus(Status status);
Dynamic Projections
// Generic method
<T> List<T> findByStatus(Status status, Class<T> type);
// Usage
List<UserSummary> summaries = repo.findByStatus(Status.ACTIVE, UserSummary.class);
List<UserDto> dtos = repo.findByStatus(Status.ACTIVE, UserDto.class);
List<User> entities = repo.findByStatus(Status.ACTIVE, User.class);
Specifications (Criteria API)
Specifications permit construirea dinamică de query-uri complexe. Ideale pentru filtre de căutare.
Setup
// Repository extinde JpaSpecificationExecutor
public interface UserRepository extends
JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
Definire Specifications
public class UserSpecifications {
public static Specification<User> hasStatus(Status status) {
return (root, query, cb) ->
cb.equal(root.get("status"), status);
}
public static Specification<User> nameContains(String name) {
return (root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
cb.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> inDepartment(Long deptId) {
return (root, query, cb) ->
cb.equal(root.get("department").get("id"), deptId);
}
}
Combinare Specifications
// În service
public List<User> search(UserSearchCriteria criteria) {
Specification<User> spec = Specification.where(null);
if (criteria.getStatus() != null) {
spec = spec.and(UserSpecifications.hasStatus(criteria.getStatus()));
}
if (criteria.getName() != null) {
spec = spec.and(UserSpecifications.nameContains(criteria.getName()));
}
if (criteria.getFromDate() != null) {
spec = spec.and(UserSpecifications.createdAfter(criteria.getFromDate()));
}
return userRepository.findAll(spec, PageRequest.of(0, 20));
}
// Sau fluent
List<User> users = userRepository.findAll(
hasStatus(Status.ACTIVE)
.and(nameContains("john"))
.or(createdAfter(LocalDateTime.now().minusDays(7)))
);
Query by Example (QBE)
Metodă simplă de query bazată pe o entitate "probe".
// Creează probe (exemplu)
User probe = new User();
probe.setStatus(Status.ACTIVE);
probe.setRole("ADMIN");
// ExampleMatcher pentru customizare
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
.withIgnorePaths("id", "createdAt");
Example<User> example = Example.of(probe, matcher);
// Query
List<User> users = userRepository.findAll(example);
long count = userRepository.count(example);
boolean exists = userRepository.exists(example);