Spring Data
repository pattern, CrudRepository, JpaRepository, query derivation, custom queries
Spring Data
A Spring Data a repository mintát emeli keretrendszer szintre: interfész deklarációból automatikus implementációt generál, query derivation-nel pedig metódusnévből SQL-t.
1. Definíció
A Spring Data egy ernyőprojekt, amely egységes, repository-alapú programozási modellt ad különböző adattárolókhoz (JPA, MongoDB, Redis, Elasticsearch, JDBC, R2DBC). A fejlesztő interfészt deklarál, a Spring Data futásidőben proxy-n keresztül implementálja a CRUD műveleteket és a lekérdezéseket.
A központi ötlet: a perzisztencia réteg boilerplate kódjának eliminálása úgy, hogy a keretrendszer a metódusnévből, annotációból vagy Specification-ből generálja a query-t.
Interface declaration → Spring Data proxy → JPA/JDBC/Mongo query
UserRepository SimpleJpaRepository SELECT ...
A Spring Boot a spring-boot-starter-data-jpa starter-rel automatikusan konfigurálja a DataSource-ot, az EntityManagerFactory-t és a tranzakciókezelőt.
2. Alapfogalmak
Repository hierarchia
| Interfész | Szerep |
|---|---|
| Repository<T,ID> | Marker interfész, nincs metódusa |
| CrudRepository<T,ID> | save, findById, findAll, deleteById, count |
| ListCrudRepository<T,ID> | Mint CrudRepository, de List<T> return type |
| PagingAndSortingRepository<T,ID> | findAll(Pageable), findAll(Sort) |
| JpaRepository<T,ID> | Flush, batch delete, getReferenceById, JPA-specifikus |
A JpaRepository a ListCrudRepository + ListPagingAndSortingRepository + JPA-specifikus metódusok összessége. A legtöbb projekt ebből származtat.
Query derivation — metódusnévből SQL
A Spring Data a metódusnév kulcsszavaiból automatikusan generál JPQL-t:
public interface UserRepository extends JpaRepository<User, Long> {
// SELECT u FROM User u WHERE u.email = ?1
Optional<User> findByEmail(String email);
// SELECT u FROM User u WHERE u.lastName = ?1 AND u.active = ?2
List<User> findByLastNameAndActive(String lastName, boolean active);
// SELECT u FROM User u WHERE u.age > ?1 ORDER BY u.lastName ASC
List<User> findByAgeGreaterThanOrderByLastNameAsc(int age);
// SELECT COUNT(u) FROM User u WHERE u.active = ?1
long countByActive(boolean active);
// DELETE FROM User u WHERE u.active = false
void deleteByActiveFalse();
}
Kulcsszavak: And, Or, Between, LessThan, GreaterThan, Like, In, IsNull, IsNotNull, OrderBy, Not, True, False, Top, First, Distinct.
@Query — manuális JPQL/SQL
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
@Query(value = "SELECT * FROM users WHERE status = :status", nativeQuery = true)
List<User> findByStatusNative(@Param("status") String status);
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
int deactivateInactiveUsers(@Param("date") LocalDate date);
Projection — szelektív mezőlekérés
// Interface-based (closed) projection
public interface UserSummary {
String getName();
String getEmail();
}
List<UserSummary> findByActive(boolean active);
// DTO-based (class) projection
@Query("SELECT new com.example.dto.UserDto(u.name, u.email) FROM User u")
List<UserDto> findAllAsDto();
3. Gyakorlati használat
Repository definiálás
@Repository
public interface OrderRepository extends JpaRepository<Order, UUID> {
List<Order> findByCustomerIdAndStatus(Long customerId, OrderStatus status);
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") UUID id);
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
}
A @Repository opcionális JpaRepository esetén (Spring Boot automatikusan regisztrálja), de explicit jelölés a szándékot mutatja és exception translation-t aktivál.
Pageable és Sort
@GetMapping("/orders")
public Page<OrderDto> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).descending());
return orderRepository.findByStatus(OrderStatus.ACTIVE, pageable)
.map(OrderDto::from);
}
A Page<T> tartalmazza az adatokat, az összesíthető elemszámot és a lapozási metaadatokat.
Custom repository implementáció
public interface OrderRepositoryCustom {
List<Order> findByComplexCriteria(OrderSearchCriteria criteria);
}
@Repository
public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {
private final EntityManager em;
public OrderRepositoryCustomImpl(EntityManager em) {
this.em = em;
}
@Override
public List<Order> findByComplexCriteria(OrderSearchCriteria criteria) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> root = cq.from(Order.class);
List<Predicate> predicates = new ArrayList<>();
if (criteria.getStatus() != null) {
predicates.add(cb.equal(root.get("status"), criteria.getStatus()));
}
if (criteria.getMinAmount() != null) {
predicates.add(cb.ge(root.get("totalAmount"), criteria.getMinAmount()));
}
cq.where(predicates.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
}
}
// Az OrderRepository kiterjeszti mindkettőt:
public interface OrderRepository
extends JpaRepository<Order, UUID>, OrderRepositoryCustom {}
Specification — dinamikus query
public class OrderSpecs {
public static Specification<Order> hasStatus(OrderStatus status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
public static Specification<Order> createdAfter(LocalDate date) {
return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), date);
}
}
// Használat:
public interface OrderRepository
extends JpaRepository<Order, UUID>, JpaSpecificationExecutor<Order> {}
List<Order> orders = orderRepository.findAll(
OrderSpecs.hasStatus(ACTIVE).and(OrderSpecs.createdAfter(cutoff))
);
4. Kód példák
Auditing — automatikus created/modified
@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 updatedBy;
}
@Configuration
@EnableJpaAuditing
public class AuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getName);
}
}
QueryByExample — egyszerű dinamikus keresés
User probe = new User();
probe.setActive(true);
probe.setRole("ADMIN");
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withStringMatcher(StringMatcher.CONTAINING);
List<User> admins = userRepository.findAll(Example.of(probe, matcher));
Stream nagy adathalmaz feldolgozáshoz
@Transactional(readOnly = true)
public void exportAllUsers(Writer writer) {
try (Stream<User> stream = userRepository.streamAllBy()) {
stream.map(UserDto::from)
.forEach(dto -> writeCsv(writer, dto));
}
}
⚠️ A Stream<T> használatánál nyitott tranzakció és cursor szükséges. Mindig try-with-resources-szel zárd le.
Soft delete Specification-nel
public class SoftDeleteSpec {
public static <T> Specification<T> notDeleted() {
return (root, query, cb) -> cb.isFalse(root.get("deleted"));
}
}
// Minden query automatikusan szűr:
List<Order> activeOrders = orderRepository.findAll(
OrderSpecs.hasStatus(ACTIVE).and(SoftDeleteSpec.notDeleted())
);
5. Trade-offok
| Előny | Hátrány |
|---|---|
| Nincs boilerplate CRUD kód | A generált query nem mindig optimális |
| Query derivation gyors prototyping | Túl hosszú metódusnév olvashatatlan |
| Egységes API (JPA, Mongo, Redis) | Adattár-specifikus optimalizáció elvész |
| Pageable/Sort beépített | COUNT query drága nagy táblákon |
| Audit, Specification, Projection | Tanulási görbe a haladó feature-öknél |
Mikor Spring Data
- Standard CRUD + lekérdezések relációs adatbázison
- Gyors prototípus egyszerű domain modellel
- Több adattár egységes kezelése
Mikor NEM Spring Data (repository abstrakció)
- Komplex analitikai query-k (jobb a JOOQ vagy natív SQL)
- Extrém teljesítményigény (JDBC template, raw SQL)
- Nem relációs modell, ahol a repository minta nem illeszkedik
6. Gyakori hibák
❌ Túl hosszú query derivation név
// ROSSZ: olvashatatlan
List<User> findByActiveAndRoleAndCreatedAtAfterAndEmailContaining(
boolean active, String role, LocalDate date, String email);
// JÓ: @Query-vel vagy Specification-nel
@Query("SELECT u FROM User u WHERE u.active = :active AND u.role = :role " +
"AND u.createdAt > :date AND u.email LIKE %:email%")
List<User> searchUsers(@Param("active") boolean active,
@Param("role") String role,
@Param("date") LocalDate date,
@Param("email") String email);
❌ Page használata COUNT nélkül
// ROSSZ: Page<T> mindig lefuttat egy COUNT query-t is
Page<Order> page = orderRepository.findAll(pageable);
// JÓ: ha nem kell totalCount, használj Slice-ot
Slice<Order> slice = orderRepository.findByStatus(status, pageable);
❌ @Modifying nélküli UPDATE/DELETE @Query
// ROSSZ: exception
@Query("DELETE FROM User u WHERE u.active = false")
void deleteInactive();
// JÓ: @Modifying jelzi, hogy írási művelet
@Modifying
@Query("DELETE FROM User u WHERE u.active = false")
void deleteInactive();
❌ N+1 query a findAll-ban
// ROSSZ: ha az Order-nek van LAZY items kollekcója
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size()); // N extra query!
// JÓ: JOIN FETCH egyedi query-vel
@Query("SELECT o FROM Order o JOIN FETCH o.items")
List<Order> findAllWithItems();
❌ flush() és saveAndFlush() fölösleges használata
A tranzakció végén a JPA automatikusan flush-öl. Explicit flush() csak akkor kell, ha a generált ID-t vagy a DB constraint ellenőrzést azonnal látni akarod.
7. Mélyebb összefüggések
A proxy mechanizmus belülről
- A Spring Boot classpath scanning során megtalálja a
JpaRepositoryleszármazottakat JpaRepositoryFactoryBeanlétrehoz egySimpleJpaRepositoryproxy-t- A query derivation a metódusnevet tokenekre bontja és
CriteriaQuery-vé alakítja - A
@QueryannotációtNamedQueryvagyNativeQuery-ként regisztrálja - A proxy minden hívást a
SharedEntityManagerCreatorsession-jén keresztül futtat
Specification vs QueryDSL vs @Query
| Megoldás | Típusbiztonság | Dinamikus | Komplexitás |
|---|---|---|---|
| Query derivation | Fordítási idejű | Nem | Alacsony |
| @Query JPQL | Nincs (string) | Nem | Közepes |
| Specification | Predikátum-szintű | Igen | Közepes |
| QueryDSL | Q-class szintű | Igen | Közepes |
| Criteria API | Metamodel szintű | Igen | Magas |
Spring Data JDBC vs JPA
| Szempont | Spring Data JPA | Spring Data JDBC |
|---|---|---|
| ORM | Hibernate (full ORM) | Nincs ORM, aggregate root |
| Lazy loading | Van | Nincs |
| Cache | 1st + 2nd level | Nincs |
| Dirty checking | Automatikus | Nincs |
| Komplexitás | Magas | Alacsony |
A Spring Data JDBC jó választás, ha nem kell a Hibernate komplexitás, de a repository minta hasznos.
Derived delete gotcha
A deleteBy... metódusok betöltik az entity-ket, majd egyenként törlik őket (cascade, lifecycle hook). Ha sok sor van, @Modifying @Query hatékonyabb, mert egyetlen SQL DELETE-et futtat.
8. Interjúkérdések
Mi a különbség a CrudRepository és a JpaRepository között? CrudRepository: alap CRUD (save, findById, delete, count). JpaRepository: CrudRepository + flush, batch delete, getReferenceById, Pageable/Sort. A legtöbb projekt JpaRepository-t használ.
Hogyan működik a query derivation? A Spring Data a metódusnevet kulcsszavakra bontja (findBy, And, OrderBy, stb.), és ezekből JPQL/Criteria query-t generál. Fordítási időben validálja az entity property-ket.
Mikor használsz @Query-t a derivation helyett? Ha a metódusnév túl hosszú (3+ feltétel), ha JOIN FETCH kell, ha natív SQL kell, vagy ha JPQL aggregáció kell (SUM, AVG).
Mi a Specification és mikor használod? Típusbiztos, újrafelhasználható predikátum-építő dinamikus kereséshez. Akkor jó, ha a feltételek runtime-ban kombinálódnak (pl. keresési form).
Mi a különbség a Page és a Slice között? Page: tartalmazza a totalCount-ot (extra COUNT query). Slice: csak azt jelzi, van-e következő oldal. Slice gyorsabb nagy táblákon.
Hogyan oldod meg az N+1 problémát Spring Data-val? JOIN FETCH egyedi @Query-vel,
@EntityGraph, vagy@BatchSizea kollekcióra.Mikor jobb a Spring Data JDBC a JPA-nál? Ha nem kell lazy loading, dirty checking, L2 cache, és egyszerűbb aggregate root modellt akarsz.
9. Szószedet
| Fogalom | Jelentés |
|---|---|
| Repository | Data access interfész, amelyet a Spring Data proxy-val implementál |
| Query derivation | Metódusnévből automatikus query generálás |
| @Query | Manuális JPQL vagy natív SQL megadása |
| Projection | Szelektív mezőlekérés interfész vagy DTO formában |
| Specification | Típusbiztos, kombinálható predikátum-építő |
| Pageable | Lapozási kérés (page, size, sort) |
| Page<T> | Lapozási válasz totalCount-tal |
| Slice<T> | Lapozási válasz totalCount nélkül |
| @Modifying | Jelzi, hogy a @Query írási művelet (UPDATE/DELETE) |
| Auditing | @CreatedDate, @LastModifiedDate automatikus timestamp |
| SimpleJpaRepository | A JpaRepository alapértelmezett proxy implementációja |
| EntityGraph | Fetch stratégia deklaratív megadása |
10. Gyorsreferencia
REPOSITORY HIERARCHIA:
Repository Marker interfész
CrudRepository save, findById, findAll, deleteById, count
JpaRepository + flush, batch, getReferenceById, Pageable
QUERY MÓDSZEREK:
findByX() Query derivation (metódusnévből)
@Query("JPQL") Manuális JPQL
@Query(nativeQuery) Natív SQL
Specification Dinamikus, kombinálható predikátumok
QueryByExample Probe objektumból keresés
LAPOZÁS:
Pageable PageRequest.of(page, size, Sort)
Page<T> Adat + totalCount (COUNT query)
Slice<T> Adat + hasNext (nincs COUNT)
WRITE QUERY-K:
@Modifying UPDATE/DELETE @Query jelölés
@Modifying(clear) flushAutomatically, clearAutomatically
AUDITING:
@EnableJpaAuditing Konfiguráció aktiválás
@CreatedDate Automatikus létrehozás timestamp
@LastModifiedDate Automatikus módosítás timestamp
AuditorAware<T> Felhasználó neve audit-hoz
CUSTOM REPOSITORY:
XxxRepositoryCustom Interfész
XxxRepositoryCustomImpl Implementáció (EntityManager)
JpaSpecificationExecutor Specification support
🎮 Játékok
10 kérdés