تطوير الويب

JPA وHibernate المتقدم في Spring Boot: أنماط الإنتاج 2026

6 min de lecture

دروس السلسلة: Virtual threads Java 25 · Spring Boot 4 + GraalVM Native · Spring Cloud Gateway 4.3 · Resilience4j 3

JPA وHibernate في قلب كل تطبيق Java مؤسسي. الإتقان الأساسي (entities، repositories Spring Data، CRUD) يُغطّي الأشهر الأولى؛ ثم يأتي مجموع الأنماط المتقدمة التي تُميّز خدمة تتوسّع عن خدمة تتعطّل في الإنتاج: N+1 query، fetch joins، projections، استعلامات native، multi-tenant، soft delete، audit تلقائي، locks pessimistic/optimistic، batch inserts، hibernate-types لـ JSON/Array، cache مستوى ثانٍ. مع Spring Boot 4.0.6 (الذي يحوي Hibernate 7.2.4.Final) وJava 25 LTS، هذه الأنماط أصبحت أكثر تعبيرًا وأداءً.

المتطلبات

  • Java 25 LTS وSpring Boot 4.0+
  • قاعدة PostgreSQL 16+ أو MySQL 8.4+
  • أساسيات JPA (entities، repositories)
  • الوقت المُقدَّر: 90 دقيقة

الخطوة 1 — اكتشاف وحلّ مشكلة N+1

مشكلة N+1 هي bug الأداء الأكثر شيوعًا مع JPA: نُحمّل 100 مستخدم في استعلام واحد، ثم Hibernate يُطلق 100 استعلام إضافي لتحميل commandes المرتبطة (lazy loading). التشخيص بـ spring.jpa.show-sql=true وlogging.level.org.hibernate.SQL=DEBUG. الحل: fetch joins أو entity graphs.

@Entity
public class Utilisateur {
    @Id Long id;
    String nom;

    @OneToMany(mappedBy = "utilisateur", fetch = FetchType.LAZY)
    List<Commande> commandes;
}

// MAUVAIS : déclenche N+1
List<Utilisateur> users = userRepo.findAll();
for (var u : users) {
    System.out.println(u.getCommandes().size()); // 1 query par user !
}

// BON : fetch join via JPQL
@Query("SELECT u FROM Utilisateur u LEFT JOIN FETCH u.commandes WHERE u.actif = true")
List<Utilisateur> findAvecCommandes();

// BON aussi : entity graph
@EntityGraph(attributePaths = {"commandes", "commandes.lignes"})
List<Utilisateur> findAllByActifTrue();

الـ entity graph أكثر تعريفية من fetch join JPQL: نُعلن فقط associations للتحميل، وHibernate يُولّد SQL الأمثل. لـ associations متداخلة، entity graph يبقى قابلًا للقراءة. حد: fetch join متعدد على collections مميزة يُسبّب منتجًا ديكارتيًا — استخدم DISTINCT.

الخطوة 2 — Projections DTO لتجنّب تحميل entities كاملة

عندما نحتاج فقط بعض الحقول (قائمة مُرَقَّمة، dropdown، تصدير CSV)، تحميل entity كاملة يُضيع ذاكرة وعرض نطاق. Projections JPA تُحمّل DTO مباشرة دون hydratation entity — مكسب ذاكرة 5-10×.

// Projection via interface (Spring Data)
public interface UtilisateurResume {
    Long getId();
    String getNom();
    String getEmail();
}

public interface UtilisateurRepository extends JpaRepository<Utilisateur, Long> {
    @Query("SELECT u.id AS id, u.nom AS nom, u.email AS email FROM Utilisateur u WHERE u.actif = true")
    Page<UtilisateurResume> trouverResumes(Pageable page);
}

// Projection via record (Java 21+)
public record UtilisateurDto(Long id, String nom, String email) {}

@Query("""
    SELECT new com.app.dto.UtilisateurDto(u.id, u.nom, u.email)
    FROM Utilisateur u WHERE u.actif = true
    """)
List<UtilisateurDto> chercherActifs();

// Projection dynamique (générique)
<T> List<T> findByActif(boolean actif, Class<T> type);

Projection عبر record هي الخيار الأحدث (Java 21+): ثابتة، مُنمَّطة، قابلة للتسلسل Jackson بلا config. Projection ديناميكية عامة تُتيح لنفس repository كشف عدة projections حسب سياق الاستدعاء.

الخطوة 3 — JPA Specifications لاستعلامات ديناميكية

public class UtilisateurSpecs {
    public static Specification<Utilisateur> actif() {
        return (r, q, cb) -> cb.isTrue(r.get("actif"));
    }

    public static Specification<Utilisateur> nomContient(String nom) {
        return (r, q, cb) -> cb.like(cb.lower(r.get("nom")), "%" + nom.toLowerCase() + "%");
    }

    public static Specification<Utilisateur> creeApres(LocalDate date) {
        return (r, q, cb) -> cb.greaterThan(r.get("creeLe"), date.atStartOfDay());
    }
}

// Repository
public interface UserRepository extends JpaRepository<Utilisateur, Long>,
                                        JpaSpecificationExecutor<Utilisateur> {}

// Usage : combine dynamiquement
Specification<Utilisateur> spec = where(actif());
if (filtre.getNom() != null) spec = spec.and(nomContient(filtre.getNom()));
if (filtre.getDepuis() != null) spec = spec.and(creeApres(filtre.getDepuis()));

Page<Utilisateur> resultats = repo.findAll(spec, pageable);

Specifications + JpaSpecificationExecutor = بحث مرن مع pagination وتركيبات AND/OR/NOT آمنة الأنواع. لحالات أعقد (joins شرطية، sub-queries)، انتقل إلى QueryDSL أو jOOQ.

الخطوة 4 — Locking optimistic vs pessimistic

عندما تُعدّل معاملتان نفس الصف، Hibernate يُقدّم استراتيجيتين. Optimistic lock: versioning عمود، فشل عند commit إذا تغيّر الإصدار. Pessimistic lock: قفل قاعدة عند SELECT، المعاملات الأخرى تنتظر.

@Entity
public class Compte {
    @Id Long id;
    BigDecimal solde;

    @Version // versioning automatique pour optimistic lock
    int version;
}

@Service
public class CompteService {
    @Transactional
    public void crediter(Long compteId, BigDecimal montant) {
        var c = compteRepo.findById(compteId).orElseThrow();
        c.setSolde(c.getSolde().add(montant));
        // commit échoue avec OptimisticLockException si modifié entre temps
    }
}

// Pessimistic lock pour cas critiques (réservation de stock)
@Repository
public interface ProduitRepository extends JpaRepository<Produit, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Produit p WHERE p.id = :id")
    Optional<Produit> findByIdForUpdate(@Param("id") Long id);
}

@Transactional
public void reserverStock(Long produitId, int quantite) {
    var p = produitRepo.findByIdForUpdate(produitId).orElseThrow();
    if (p.getStock() < quantite) throw new StockInsuffisant();
    p.setStock(p.getStock() - quantite);
}

الاختيار يعتمد على معدّل التصادم وكلفة فشل تطبيقي. على خدمة دفع بـ 1000 TPS على نفس الحساب، pessimistic يصبح حاجزًا.

الخطوة 5 — Audit تلقائي مع @EnableJpaAuditing

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .map(Authentication::getName);
    }
}

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Article {
    @Id @GeneratedValue Long id;

    @CreatedDate Instant createdAt;
    @LastModifiedDate Instant modifiedAt;
    @CreatedBy String createdBy;
    @LastModifiedBy String modifiedBy;
}

لتتبّع كامل (من عدّل ماذا بتفصيل)، Hibernate Envers يُقدّم audit تاريخي: كل تعديل entity يُؤرشف في جداول _aud. مفروض بـ GDPR أو SOC 2 على البيانات الحساسة.

الخطوة 6 — JSON وArray PostgreSQL عبر hibernate-types

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-72</artifactId>
    <version>3.15.2</version>
</dependency>

@Entity
public class Article {
    @Id Long id;
    String titre;

    @Type(JsonType.class)
    @Column(columnDefinition = "jsonb")
    Map<String, Object> metadata;

    @Type(ListArrayType.class)
    @Column(columnDefinition = "text[]")
    List<String> tags;
}

@Query(value = """
    SELECT * FROM article
    WHERE metadata->>'category' = :cat
    """, nativeQuery = true)
List<Article> findByCategoryInJson(@Param("cat") String category);

الميزة: لا حاجة لجداول منفصلة للسمات شبه المُهيكَلة. PostgreSQL يُفهرس مسارات JSONB عبر GIN، فيجعل البحث فعّالًا حتى على ملايين الصفوف.

الخطوة 7 — Batch inserts لمعالجة الكتلية

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true
        batch_versioned_data: true

@Service
@Transactional
public class ImportService {
    public void importer(List<LigneCsv> lignes) {
        for (int i = 0; i < lignes.size(); i++) {
            var article = lignes.get(i).versEntite();
            entityManager.persist(article);

            if (i % 50 == 0 && i > 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
    }
}

على PostgreSQL + JDBC batch + rewriteBatchedStatements، ننتقل من ~50 inserts/ثانية إلى ~2000/ثانية. للاستيرادات الضخمة، COPY SQL أصلي يبقى الطريق المثالي.

الخطوة 8 — Cache مستوى ثانٍ مع Caffeine

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>jcache</artifactId>
    <version>3.2.0</version>
</dependency>

# Activer le L2
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
spring.jpa.properties.hibernate.javax.cache.provider=com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "pays")
public class Pays {
    @Id String code;
    String nom;
}

للخدمات متعددة instances (cluster)، Caffeine محلي لا يكفي — Redis أو Hazelcast لـ L2 موزّع. لا تُخزّن إلا entities مقروءة كثيرًا ونادرًا ما تتعدّل. قِس بـ Actuator /actuator/metrics/hibernate.cache.hit.

أخطاء شائعة

العَرَض السبب الحل
LazyInitializationException خارج معاملة وصول لـ association lazy بعد commit حمّل عبر fetch join أو entity graph داخل المعاملة
OptimisticLockException متقطعة تزامن متوقع لكن غير مُدار retry بـ backoff تصاعدي، أو انتقل لـ pessimistic
StackOverflowError على Jackson دورة ثنائية الاتجاه @JsonManagedReference/@JsonBackReference أو DTO projection
SQL inserts فردية رغم batch_size @GeneratedValue(strategy = IDENTITY) استخدم SEQUENCE (متوافق batch)
QuerySQL معقد بطيء EXPLAIN ANALYZE غير مُراجَع افحص الخطة، أضف index؛ لـ OLAP، view مُجَسَّدة
Cache L2 لا hit أبدًا eviction عدوانية أو لا @Cacheable قس hit ratio، أعدّ max size

الأسئلة الشائعة

JPA أم jOOQ؟
JPA للـ CRUD النموذجي بتغيير schema متكرر. jOOQ لاستعلامات SQL معقدة (CTE، window functions). الاثنان يتعايشان غالبًا: JPA لـ 90%، jOOQ للـ reporting.

Hibernate vs EclipseLink؟
Hibernate يُهيمن (90%+). EclipseLink يبقى صالحًا لكن المنظومة (extensions، tooling، توثيق) أفقر بكثير.

Spring Data JPA أم Spring Data JDBC؟
JPA لغنى الوظائف (cache، lazy، audit، JSON). JDBC للبساطة وقراءة SQL المُولَّد، مناسب جدًا في DDD مع aggregates ثابتة.

كم entities/tables أقصى؟
Hibernate يُدير بلا مشكلة 1000+ entities. التعقيد ينتقل إلى المطوّر والبدء (~50ms لكل 100 entities).

ترحيل من ORM legacy (iBatis، Hibernate 3)؟
إعادة هيكلة تدريجية: إضافة @Entity تدريجيًا، تبديل الـ repos واحدًا واحدًا، اختبار متوازٍ. احسب 1-2 sprints لكل 10 entities معقدة.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité