Középhaladó Olvasási idő: ~9 perc

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

  1. A Spring Boot classpath scanning során megtalálja a JpaRepository leszármazottakat
  2. JpaRepositoryFactoryBean létrehoz egy SimpleJpaRepository proxy-t
  3. A query derivation a metódusnevet tokenekre bontja és CriteriaQuery-vé alakítja
  4. A @Query annotációt NamedQuery vagy NativeQuery-ként regisztrálja
  5. A proxy minden hívást a SharedEntityManagerCreator session-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

  1. 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.

  2. 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.

  3. 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).

  4. 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).

  5. 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.

  6. Hogyan oldod meg az N+1 problémát Spring Data-val? JOIN FETCH egyedi @Query-vel, @EntityGraph, vagy @BatchSize a kollekcióra.

  7. 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