Məzmuna keçLogo

Command Palette

Search for a command to run...

JPA — Spesifikasiyadan İmplementasiyaya

Dərc olundu
14 apr 2025
JPA — Spesifikasiyadan İmplementasiyaya

✨ Spesifikasiyadan İmplementasiyaya

Keçən dəfəki yazımızda ORM (Object-Relational Mapping) adlı geniş kəşfiyyat aparmışdıq.

Obyektlərlə relyasiyalı verilənlər bazaları arasındakı o məşhur "impedans uyğunsuzluğu" körpüsünü necə keçəcəyimizi müzakirə etmişdik.

İndi isə o körpünün Java dünyasındakı ən möhkəm və standartlaşdırılmış konstruksiyasına, yəni JPA (Java Persistence API)-a fokuslanacağıq.


⚠️ Xəbərdarlıq edim:

Bu yazı qısa olmayacaq.

Çünki JPA sadəcə bir neçə annotasiyadan ibarət deyil.

O, öz arxitekturası, həyat dövrü (lifecycle) qaydaları, mürəkkəb əlaqə (relationship) idarəetmə mexanizmləriperformansla bağlı nəzərə alınmalı bir çox nüansı olan dərin bir mövzudur.

Məqsədimiz səthi biliklərdən kənara çıxıb, JPA-nın ruhunu anlamaq, onun gücündən tam istifadə etmək və potensial tələlərindən yayınmaqdır.

Bu yazı, təcrübəsini dərinləşdirmək istəyən hər bir ciddi Java / Backend developer üçün bir növ cəbbəxana rolunu oynayacaq.


📌 Spesifikasiya Nədir?

Kiçik (Amma Vacib) Xatırlatma

Əvvəlki yazımızda toxunduğumuz kimi, gəlin "spesifikasiya" anlayışını bir daha xatırlayaq, çünki JPA məhz budur.

Spesifikasiya (Specification):

Texniki mənada, bir texnologiyanın və ya komponentin nə etməli olduğunu, hansı interfeysləri təqdim etməli, hansı funksionallığı dəstəkləməli olduğunu təsvir edən rəsmi sənəd, qaydalar toplusu və ya müqavilədir.

O, *"necə"*ni deyil, *"nə"*yi diktə edir.


🏠 Evin Tikintisi Analogiyası

Bir ev tikintisi analogiyası düşünün:

  • 🗂️ Spesifikasiya (Plan / Çertyoj):

    Evin neçə otaqlı olacağını, qapıların, pəncərələrin yerini, elektrik və su xətlərinin necə çəkiləcəyini göstərən detallı memarlıq planı (blueprint).

    Bu plan evin necə görünməli və funksiya göstərməli olduğunu təyin edir.

  • 🧱 Implementasiya (Tikinti Şirkəti):

    Fərqli tikinti şirkətləri eyni plana (spesifikasiyaya) baxaraq evi tikə bilərlər.

    Hər bir şirkət öz metodlarından, materiallarından, alətlərindən istifadə edərək

    (fərqli *"necə"*lər) planın tələblərinə uyğun (eyni "nə") bir ev inşa edir.


☕ Java Dünyasında Məsələlər

Java dünyasında:

  • Servlet API – veb sorğularını necə idarə etməli
  • JDBC API – verilənlər bazasına necə qoşulmalı və sorğu göndərməli
  • və bizim bugünkü qəhrəmanımız JPA – Java tətbiqlərində relyasiyalı datanın idarə olunması üçün hazırlanmış persistensiya spesifikasiyasıdır

JPA: Java-nın Rəsmi Persistensiya Orkestri

Java Persistence API (JPA):

Java platforması üçün obyekt-relyasiyalı mapləmə (ORM) və persistensiyanı (dataların saxlanması və əldə edilməsi) standartlaşdıran bir spesifikasiyadır.

O, birbaşa ORM framework-ü deyil. Əksinə, Hibernate, EclipseLink kimi ORM framework-lərinin implementasiya etməli olduğu interfeysləri və annotasiyaları təyin edir.


Əsas Məqsədi

  • Standardlaşdırma:

    Java developerlərinə fərqli ORM provider-ları arasında keçid edə bilmələri üçün vahid bir API təqdim etmək.

  • Portativlik:

    JPA istifadə edərək yazılmış persistensiya kodunun minimum dəyişikliklə fərqli JPA implementasiyaları üzərində işləməsini təmin etmək (nəzəri olaraq).

  • Developer Məhsuldarlığı:

    ORM konseptlərini (mapping, lifecycle, query) standart yollarla təmin edərək boilerplate kodu azaltmaq.


JPA Arxitekturası: Pərdə Arxasına Baxış

JPA-nın necə işlədiyini anlamaq üçün onun əsas komponentlərini bilmək vacibdir:

architecture.mmd
graph LR
    A[Application Code] --> B[EntityManager]
    B --> C{Persistence Context - First Level Cache}
    B --> D[EntityManagerFactory]
    D --> E[Persistence Unit - persistence.xml or Config]
    E --> F((Database))
    B --> G[JPA Provider - Hibernate / EclipseLink]
    G --> F
    C -- Manages --> H[Entities]
    B -- Uses --> I[JPQL / Criteria API / Native SQL]
    I --> G
  • Persistence Unit: Bir və ya daha çox entity class-ını, verilənlər bazası bağlantı məlumatlarını (datasource), istifadə olunacaq JPA provider-ını (implementasiyanı) və digər konfiqurasiyaları (məsələn, cədvəl yaratma strategiyası) təyin edən konfiqurasiya vahididir. Ənənəvi olaraq META-INF/persistence.xml faylında təyin olunur, lakin Spring Boot kimi framework-lərdə application.properties və ya application.yml vasitəsilə də konfiqurasiya edilə bilər.
  • EntityManagerFactory: Persistence Unit konfiqurasiyasına əsasən EntityManager instansiyaları yaradan fabrikdir. Yaradılması bahalı bir əməliyyatdır (çünki bütün mapləmə məlumatları, keş konfiqurasiyaları və s. bu mərhələdə yüklənir). Buna görə də adətən tətbiq həyat dövrü boyunca hər persistensiya vahidi üçün yalnız bir EntityManagerFactory instansiyası yaradılır və saxlanılır. Thread-safedir.
  • EntityManager: JPA ilə əsas qarşılıqlı əlaqə interfeysidir. Entity-ləri idarə etmək (yaratmaq, oxumaq, yeniləmək, silmək), sorğular icra etmək və tranzaksiyaları idarə etmək üçün metodlar təqdim edir. Hər bir EntityManager öz **Persistence Context**inə malikdir. Yaradılması nisbətən ucuz bir əməliyyatdır. Thread-safe deyil! Hər bir thread (və ya hər bir tranzaksiya) adətən öz EntityManager instansiyasını istifadə etməlidir. Bir Unit of Work (İş Vahidi) konsepsiyasını təmsil edir.
  • Persistence Context: EntityManagerin idarə etdiyi aktiv entity instansiyalarının saxlandığı yerdir. Bu, effektiv şəkildə First-Level Cache (Birinci Səviyyə Keş) rolunu oynayır. EntityManager bir entity-ni databazadan oxuduqda və ya yeni bir entity persist edildikdə, həmin entity bu kontekstə (və Identity Map-ə) əlavə olunur və "managed" vəziyyətinə keçir. Eyni EntityManager daxilində eyni ID ilə təkrar find edilən entity-lər databazaya getmədən birbaşa bu kontekstdən qaytarılır.
  • Entity: Verilənlər bazasındakı bir cədvəli təmsil edən sadə Java Class-ıdır (POJO - Plain Old Java Object). @Entity annotasiyası ilə işarələnir və adətən digər JPA annotasiyaları ilə (məsələn, @Id, @Column, @Table, əlaqə annotasiyaları) detallandırılır.
  • Sorgu Mexanizmləri:
    • JPQL (Java Persistence Query Language): SQL-ə bənzər, lakin cədvəl və sütunlar əvəzinə entity-lər və onların atributları üzərində işləyən obyekt-yönümlü sorğu dilidir. Databaza müstəqilliyini təmin etməyə kömək edir.
    • Criteria API: JPQL string-ləri əvəzinə Java kodu ilə proqramatik və tip-təhlükəsiz (type-safe) şəkildə sorğular yaratmaq üçün bir API-dir. Compile-time yoxlaması təmin edir, lakin sadə sorğular üçün belə daha çox kod tələb edə bilər.
    • Native SQL: Lazım olduqda, databazanın spesifik xüsusiyyətlərindən istifadə etmək və ya mürəkkəb, optimallaşdırılmış SQL yazmaq üçün birbaşa SQL sorğularını icra etmək imkanı.

JPA Əsas Konseptləri: Dərin Təhlil

İndi isə JPA-nın əsas dirəklərini daha yaxından incələyək.

1. Entity Lifecycle

Bir entity instansiyası EntityManager ilə qarşılıqlı əlaqədə olduğu müddətdə fərqli vəziyyətlərdən keçir. Bu vəziyyətləri və keçidləri anlamaq, JPA-nın davranışını (xüsusilə avtomatik dəyişikliklərin izlənməsi - dirty checking) dərk etmək üçün kritikdir.

entity-lifecycle.mmd
stateDiagram
    [*] --> Transient : New Entity()
    Transient --> Managed : persist() / merge()
    Managed --> Removed : remove()
    Managed --> Detached : detach() / clear() / close() / Outside Tx
    Managed --> Managed : find() / getReference() / refresh()
    Managed --> [*] : Commit (Save to DB)
    Removed --> [*] : Commit (DELETE)
    Detached --> Managed : merge()
    Removed --> Managed : persist() after remove()
    Detached --> [*] : Garbage Collected
    [*] --> Managed : find() / getReference() / JPQL
  • Transient (Keçici) / New: Yeni yaradılmış (new Product()) və hələ heç bir Persistence Contextə bağlanmamış entity instansiyası. Verilənlər bazasında bir qarşılığı yoxdur və JPA tərəfindən idarə olunmur.
  • Managed: Persistence Contextə bağlı olan və JPA tərəfindən idarə olunan entity instansiyası. Bu vəziyyətdəki obyektlərdə edilən dəyişikliklər EntityManager tərəfindən avtomatik olaraq izlənilir (Dirty Checking) və tranzaksiya commit olunduqda (və ya flush() çağırıldıqda) avtomatik olaraq verilənlər bazasına yazılır (UPDATE sorğusu generasiya olunur). find(), getReference(), persist()merge() (əgər obyekt əvvəllər persist olunubsa) metodları obyekti bu vəziyyətə gətirir.
  • Detached (Ayrılmış): Əvvəllər Managed vəziyyətində olmuş, lakin indi aktiv Persistence Contextdən ayrılmış entity instansiyası. Bu, adətən EntityManager bağlandıqda, detach() metodu çağırıldıqda, clear() metodu ilə kontekst təmizləndikdə və ya entity tranzaksiya xaricində istifadə edildikdə baş verir. Verilənlər bazasında qarşılığı var, lakin JPA artıq onun dəyişikliklərini izləmir. Detached obyekti yenidən Managed vəziyyətinə qaytarmaq üçün merge() metodu istifadə olunur.
  • Removed : remove() metodu ilə silinmək üçün işarələnmiş Managed entity instansiyası. Hələ Persistence Contextdə mövcuddur, lakin tranzaksiya commit olunduqda verilənlər bazasından silinəcək (DELETE sorğusu generasiya olunur).

Lifecycle Metodları :

examples/lifecycle-demo.java
// Tutaq ki, EntityManager 'em' mövcuddur və aktiv bir tranzaksiya var
// Transaction tx = em.getTransaction(); tx.begin();

// 1. Transient -> Managed
Product product = new Product(); // Transient
product.setName("New Gadget");
em.persist(product); // product indi Managed vəziyyətdədir.
                     // INSERT sorğusu tranzaksiya commit olunduqda göndəriləcək.

// 2. DB -> Managed
Product foundProduct = em.find(Product.class, 1L); // DB-dən oxuyur, indi Managed.

// 3. Managed state dəyişikliklərinin izlənməsi (Dirty Checking)
foundProduct.setPrice(199.99); // Managed obyektin dəyəri dəyişdirilir.
                                // Başqa heç nə etməyə ehtiyac yoxdur!
                                // UPDATE sorğusu tranzaksiya commit olunduqda avtomatik göndəriləcək.

// 4. Managed -> Removed
em.remove(foundProduct); // foundProduct indi Removed vəziyyətdədir.
                         // DELETE sorğusu tranzaksiya commit olunduqda göndəriləcək.

// 5. Managed -> Detached (Explicitly)
em.detach(product); // 'product' indi Detached. Dəyişikliklər izlənilməyəcək.

// 6. Detached -> Managed
Product detachedProduct = new Product(); // Tutaq ki, bu obyekt əvvəllər DB-də olub və indi detached
detachedProduct.setId(5L);
detachedProduct.setName("Updated Name Outside Context");
// Merge detachedProduct-in state-ni persistence context-dəki managed instance-a kopyalayır
// və ya DB-dən yükləyib kopyalayır, sonra managed instansı qaytarır.
Product managedProduct = em.merge(detachedProduct); // 'managedProduct' indi Managed.
                                                   // UPDATE sorğusu commit-də göndəriləcək.

// tx.commit();

2. Primary Keys & Generation Strategies

Hər bir Entity-nin unikal bir identifikatoru olmalıdır (@Id). JPA bu ID-lərin avtomatik generasiyası üçün müxtəlif strategiyalar təqdim edir:

  • @GeneratedValue(strategy = GenerationType.IDENTITY): Verilənlər bazasının auto-increment sütununa güvənir (məsələn, MySQL AUTO_INCREMENT, PostgreSQL SERIAL). persist() çağırılan kimi dərhal INSERT edilir və ID alınır. Sadədir, amma batch insert-lərdə effektiv olmaya bilər.
  • @GeneratedValue(strategy = GenerationType.SEQUENCE): Verilənlər bazasındakı sequence obyektini istifadə edir (məsələn, Oracle, PostgreSQL). @SequenceGenerator ilə sequence adı və digər parametrlər təyin oluna bilər. Batch insert üçün daha optimallaşdırıla bilər, çünki ID-ləri bazaya getmədən öncədən almaq mümkündür.
  • @GeneratedValue(strategy = GenerationType.TABLE): Ayrı bir cədvəli ID generasiyası üçün istifadə edir. Performans baxımından adətən ən zəifidir, portativlik üçün nəzərdə tutulsa da, çox məsləhət görülmür.
  • @GeneratedValue(strategy = GenerationType.AUTO) (Default): JPA provider-ına (Hibernate, EclipseLink) ən uyğun strategiyanı seçmək imkanı verir (adətən databazaya görə IDENTITY və ya SEQUENCE).
entities/Order.java
@Entity
public class Order {
    @Id
    // PostgreSQL və Oracle üçün SEQUENCE daha çox tövsiyə olunur
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
    @SequenceGenerator(name = "order_seq", sequenceName = "order_id_sequence", allocationSize = 1)
    private Long id;

    // MySQL/MariaDB üçün IDENTITY daha çox istifadə olunur
    // @Id
    // @GeneratedValue(strategy = GenerationType.IDENTITY)
    // private Long id;

    // ... other fields
}

3. Mapping Annotasiyaları (Ətraflı)

  • @Entity: Class-ı JPA entity-si kimi işarələyir.
  • @Table(name = "user_accounts", schema = "auth"): Entity-nin map olunacağı cədvəlin adını (defolt class adı), schema-sını və digər xüsusiyyətlərini təyin edir.
  • @Column(name = "email_address", nullable = false, unique = true, length = 100): Field-in map olunacağı sütunun adını (defolt field adı), boş olub-ola bilməyəcəyini, unikal olub-olmadığını, uzunluğunu və s. təyin edir.
  • @Basic(fetch = FetchType.LAZY): Əsas tiplər (String, primitive, wrapper, Date, etc.) üçün mapləməni detallandırır. Workspace atributu ilə böyük datalar üçün (məsələn, byte[]) lazy loading aktivləşdirilə bilər (amma çox istifadə olunmur).
  • @Transient: Bu field-in persistensiya prosesində nəzərə alınmamasını, yəni databazada sütun qarşılığının olmamasını bildirir.
  • @Temporal(TemporalType.TIMESTAMP): java.util.Datejava.util.Calendar tiplərinin databazadakı hansı tipə (DATE, TIME, TIMESTAMP) map olunacağını göstərir. (Java 8 java.time API-si ilə adətən buna ehtiyac qalmır, provider-lar avtomatik düzgün mapləmə edir).
  • @Enumerated(EnumType.STRING): Enum tiplərinin necə saxlanılacağını təyin edir.
    • EnumType.ORDINAL (Default): Enum sabitinin sıfırdan başlayan sırasını (integer) saxlayır. TƏHLÜKƏLİ! Enum-a yeni sabit əlavə etsəniz və ya sırasını dəyişsəniz, databazadakı mövcud dəyərlər mənasını itirə bilər. Qaçının!
    • EnumType.STRING: Enum sabitinin adını (String) saxlayır. Daha etibarlıdır, oxunaqlıdır, amma bir qədər daha çox yer tutur. Məsləhət görülən budur.
  • @Lob: Böyük obyektlər (Large Objects) üçün istifadə olunur (məsələn, String üçün CLOB, byte[] üçün BLOB).

Embeddable Obyektlər (@Embeddable, @Embedded)

Bəzən bir neçə field məntiqi olaraq bir qrup təşkil edir (məsələn, Ünvan: küçə, şəhər, poçt kodu). Bu qrupu ayrı bir @Embeddable class-da təyin edib, əsas entity-də @Embedded ilə istifadə edə bilərsiniz. Bu, kodu daha modulyar və oxunaqlı edir. Embeddable obyektin field-ləri əsas entity-nin cədvəlindəki sütunlara map olunur.

entities/EmbeddableAddress.java
@Embeddable
public class Address {
    private String street;
    private String city;
    private String zipCode;
    // Getters, Setters, Constructors...
}

@Entity
public class User {
    @Id
    private Long id;
    private String name;

    @Embedded
    private Address homeAddress;

    // Fərqli sütun adları lazım olarsa:
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name="street", column=@Column(name="work_street")),
        @AttributeOverride(name="city", column=@Column(name="work_city")),
        @AttributeOverride(name="zipCode", column=@Column(name="work_zip_code"))
    })
    private Address workAddress;

    // ...
}

4. Relationships: ORM-in Ürəyi

Ən mürəkkəb, amma həm də ən güclü hissə budur.

  • Kardinallıq (Cardinality): @OneToOne, @OneToMany, @ManyToOne, @ManyToMany.
  • İstiqamət (Directionality):
    • Unidirectional: Əlaqə yalnız bir tərəfdən təyin olunur.
    • Bidirectional: Əlaqə hər iki tərəfdən təyin olunur. Bir tərəf "owning side" (əlaqəni idarə edən, adətən foreign key sütununu saxlayan tərəf), digər tərəf isə "inverse side" (mappedBy atributu ilə işarələnən tərəf) olur.
  • Əsas Attributlar:
    • targetEntity: Əlaqənin digər tərəfindəki entity class-ı (adətən lazım olmur, generiklərdən tapılır).

    • cascade = {CascadeType...}: Bir entity üzərində aparılan əməliyyatın (persist, merge, remove, refresh, detach) əlaqəli entity-lərə də təsir edib-etməyəcəyini təyin edir. Çox diqqətlə istifadə olunmalıdır! Məsələn, CascadeType.ALL asan görünsə də, lazımsız silinmələrə səbəb ola bilər.

    • Workspace = FetchType...: Əlaqəli entity-lərin nə zaman yüklənəcəyini təyin edir.

      • WorkspaceType.EAGER (Təcili): Əsas entity yüklənən kimi əlaqəli entity-lər də yüklənir. @ManyToOne@OneToOne üçün defoltdur. N+1 probleminə səbəb ola bilər.
      • WorkspaceType.LAZY (Tənbəl): Əlaqəli entity-lərə ilk dəfə müraciət edilənə qədər yüklənmir (proxy obyekt qaytarılır). @OneToMany@ManyToMany üçün defoltdur. Performans üçün adətən daha yaxşıdır, amma "LazyInitializationException" tələsinə düşməmək üçün diqqətli olmaq lazımdır (əgər session/context bağlıdırsa və lazy yükləməyə cəhd edirsinizsə).
    • optional = false/true: @ManyToOne@OneToOneda əlaqənin məcburi olub-olmadığını bildirir (database constraint-ə təsir edə bilər).

    • mappedBy = "propertyName": Bidirectional əlaqələrdə "inverse" (sahib olmayan) tərəfdə istifadə olunur və əlaqənin "owning" (sahib) tərəfdəki hansı field tərəfindən idarə olunduğunu göstərir.

    • orphanRemoval = true: @OneToMany@OneToOne (bidirectional) əlaqələrdə istifadə olunur. Əgər "parent" entity-nin collection-ından bir "child" entity çıxarılarsa (məsələn, parent.getChildren().remove(child)), orphanRemoval=true olduqda həmin "child" entity avtomatik olaraq databazadan da silinir (REMOVE əməliyyatı). CascadeType.REMOVEdan fərqlidir (o, parent silindikdə child-ları silir).

      Nümunələr:

      examples/relationships.java
      // ----- ManyToOne (Unidirectional: Order -> Customer) -----
      @Entity
      public class Customer {
          @Id @GeneratedValue private Long id;
          private String name;
          // Customer tərəfində Order-lərə birbaşa referans yoxdur (Unidirectional)
      }
      
      @Entity
      @Table(name="orders")
      public class Order {
          @Id @GeneratedValue private Long id;
          private LocalDateTime orderDate;
      
          @ManyToOne(fetch = FetchType.LAZY) // LAZY adətən daha yaxşıdır
          @JoinColumn(name = "customer_id", nullable = false) // Foreign key sütununun adı
          private Customer customer;
          // ...
      }
      
      // ----- OneToMany / ManyToOne (Bidirectional: Department <-> Employee) -----
      @Entity
      public class Department {
          @Id @GeneratedValue private Long id;
          private String name;
      
          // Inverse side (sahib deyil)
          @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
          private List<Employee> employees = new ArrayList<>();
      
          // Utility methods for bidirectional consistency
          public void addEmployee(Employee employee) {
              employees.add(employee);
              employee.setDepartment(this);
          }
          public void removeEmployee(Employee employee) {
              employees.remove(employee);
              employee.setDepartment(null);
          }
          // ...
      }
      
      @Entity
      public class Employee {
          @Id @GeneratedValue private Long id;
          private String firstName;
      
          // Owning side (sahibdir, foreign key buradadır)
          @ManyToOne(fetch = FetchType.LAZY)
          @JoinColumn(name = "dept_id") // Foreign key sütunu
          private Department department;
          // ...
      }
      
      // ----- ManyToMany (Bidirectional: Post <-> Tag) -----
      @Entity
      public class Post {
          @Id @GeneratedValue private Long id;
          private String title;
      
          @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) // Remove adətən cascade edilmir
          @JoinTable(name = "post_tag", // Join cədvəlinin adı
                     joinColumns = @JoinColumn(name = "post_id"), // Bu tərəfin FK adı
                     inverseJoinColumns = @JoinColumn(name = "tag_id")) // Qarşı tərəfin FK adı
          private Set<Tag> tags = new HashSet<>();
           // Utility methods for adding/removing tags...
          // ...
      }
      
      @Entity
      public class Tag {
          @Id @GeneratedValue private Long id;
          private String name;
      
          @ManyToMany(mappedBy = "tags") // Inverse side
          private Set<Post> posts = new HashSet<>();
          // ...
      }

5. İrsiyyət Mapləməsi (Inheritance Mapping)

OOP-nin güclü tərəflərindən olan irsiyyəti (inheritance) databazada təmsil etmək üçün JPA 3 əsas strategiya təqdim edir:

StrategiyaAnnotasiyaAçıqlamaÜstünlüklərÇatışmazlıqlar
Single Table@Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn @DiscriminatorValueBütün iyerarxiyadakı class-lar üçün bir cədvəl istifadə olunur. Hansı subclass-a aid olduğunu bilmək üçün diskriminator sütunu (DTYPE) əlavə olunur.Sadədir. Polimorfik sorğular (bütün tipləri birgə sorğulamaq) çox sürətlidir. Join tələb etmir.Cədvəl çox geniş ola bilər. Subclass-lara aid sütunlar nullable olmalıdır (boş qalacaq). Not-null constraint tətbiq etmək çətindir.
Joined Table@Inheritance(strategy = InheritanceType.JOINED)Hər class (base və subclass) üçün ayrı cədvəl yaradılır. Subclass cədvəlləri yalnız özünə aid sütunları və base class-a foreign key saxlayır.Data normalizasiyası yaxşıdır. Hər cədvəl yalnız öz sütunlarını saxlayır. Not-null constraint-lər asanlıqla tətbiq edilir.Polimorfik sorğular və subclass-ların məlumatını əldə etmək üçün JOIN-lar tələb olunur, bu da performansı azalda bilər.
Table Per Class@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)Hər bir konkret (abstract olmayan) class üçün tam bir cədvəl yaradılır (base class-ın sütunları da daxil olmaqla). Abstract base class üçün cədvəl olmur.Subclass sorğuları sadədir (join yoxdur).Polimorfik sorğular çox çətin və qeyri-effektivdir (UNION-lar tələb edir). Base class-a foreign key ilə əlaqə qurmaq mümkün deyil. Adətən məsləhət görülmür.
examples/inheritance.java
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // Və ya SINGLE_TABLE
// @DiscriminatorColumn(name="VEHICLE_TYPE") // SINGLE_TABLE üçün lazımdır
public abstract class Vehicle {
    @Id @GeneratedValue private Long id;
    private String manufacturer;
}

@Entity
// @DiscriminatorValue("CAR") // SINGLE_TABLE üçün lazımdır
public class Car extends Vehicle {
    private int numberOfDoors;
}

@Entity
// @DiscriminatorValue("TRUCK") // SINGLE_TABLE üçün lazımdır
public class Truck extends Vehicle {
    private double payloadCapacity;
}

6. JPQL (Java Persistence Query Language) Dərinlikləri

JPQL, SQL-ə çox bənzəsə də, cədvəllər deyil, Entity-lər və onların atributları (field/property adları) üzərində işləyir. Case-sensitive-dir (Entity və atribut adlarında).

examples/jpql-examples.java
// Bütün aktiv müştəriləri tapmaq (Named Query)
@Entity
@NamedQuery(name = "Customer.findAllActive",
            query = "SELECT c FROM Customer c WHERE c.isActive = true ORDER BY c.registrationDate DESC")
public class Customer { ... }

// Named Query istifadəsi
List<Customer> activeCustomers = em.createNamedQuery("Customer.findAllActive", Customer.class)
                                     .getResultList();

// Dinamik JPQL sorğusu
String customerNameParam = "Acme Corp";
TypedQuery<Customer> query = em.createQuery(
    "SELECT c FROM Customer c WHERE c.name = :custName AND c.type = :custType", Customer.class);
query.setParameter("custName", customerNameParam);
query.setParameter("custType", CustomerType.CORPORATE);
Customer acme = query.getSingleResult(); // Və ya getResultList()

// JOIN FETCH ilə N+1 probleminin həlli
// Order-ləri yükləyərkən əlaqəli Customer-ları da EAGER kimi yüklə (hətta LAZY olsa belə)
TypedQuery<Order> orderQuery = em.createQuery(
    "SELECT DISTINCT o FROM Order o LEFT JOIN FETCH o.customer c WHERE c.city = :city", Order.class);
orderQuery.setParameter("city", "Baku");
List<Order> bakuOrders = orderQuery.getResultList();
// İndi bakuOrders listindəki hər bir Order üçün order.getCustomer() əlavə sorğu göndərməyəcək.

// Projections (DTO ilə): Yalnız lazımi datanı çəkmək
String jpql = "SELECT new com.example.dto.CustomerSummaryDTO(c.id, c.name, COUNT(o)) " +
              "FROM Customer c LEFT JOIN c.orders o " +
              "WHERE c.isActive = true " +
              "GROUP BY c.id, c.name " +
              "ORDER BY COUNT(o) DESC";
TypedQuery<CustomerSummaryDTO> dtoQuery = em.createQuery(jpql, CustomerSummaryDTO.class);
List<CustomerSummaryDTO> summaries = dtoQuery.getResultList();

// UPDATE və DELETE sorğuları (Bulk operations)
// DİQQƏT: Bu əməliyyatlar Persistence Context-i bypass edir!
// Yəni context-dəki managed obyektlər bu dəyişikliklərdən xəbərdar olmur.
// Adətən bulk update/delete-dən sonra context-i təmizləmək (em.clear()) lazımdır.
Query bulkUpdate = em.createQuery(
    "UPDATE Product p SET p.price = p.price * 1.1 WHERE p.category = :category");
bulkUpdate.setParameter("category", "Electronics");
int updatedCount = bulkUpdate.executeUpdate(); // Tranzaksiya daxilində olmalıdır

7. Criteria API: Tip Təhlükəsiz Sorğular

String əsaslı JPQL-də compile-time yoxlaması yoxdur. Criteria API bu problemi həll edir.

examples/criteria-api.java
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> product = cq.from(Product.class); // FROM Product product

Predicate pricePredicate = cb.greaterThan(product.get("price"), 100.0); // WHERE price > 100.0
Predicate categoryPredicate = cb.equal(product.get("category"), "Electronics"); // AND category = 'Electronics'
cq.where(cb.and(pricePredicate, categoryPredicate));

cq.orderBy(cb.desc(product.get("name"))); // ORDER BY name DESC

TypedQuery<Product> query = em.createQuery(cq);
List<Product> results = query.getResultList();

Criteria API daha təhlükəsiz olsa da, gördüyünüz kimi, daha çox kod tələb edir və mürəkkəb sorğularda oxunması çətinləşə bilər.

8. Kilidləmə (Locking)

Eyni anda birdən çox tranzaksiyanın eyni datanı dəyişməyə çalışdığı hallarda data bütövlüyünü qorumaq üçün kilidləmə mexanizmləri var:

  • Optimistic Locking (Nikbin Kilidləmə): Ən çox istifadə olunan yanaşmadır. Eyni datanı eyni anda dəyişmə ehtimalının az olduğu fərziyyəsinə əsaslanır. Entity-yə @Version annotasiyalı bir sütun (adətən long və ya int tipli) əlavə edilir. Hər UPDATE əməliyyatında JPA bu versiya nömərəsini yoxlayır və artırır. Əgər UPDATE ediləcək sətirin versiya nömrəsi EntityManagerin bildiyi versiya ilə eyni deyilsə (yəni başqa bir tranzaksiya arada onu dəyişibsə), OptimisticLockException atılır.

    examples/optimistic-locking.java
    @Entity
    public class Inventory {
        @Id private Long id;
        private String itemCode;
        private int quantity;
    
        @Version // Optimistic lock sütunu
        private long version;
        // ...
    }
  • Pessimistic Locking (Bədbin Kilidləmə): Konflikt ehtimalının yüksək olduğu hallarda istifadə olunur. Bir tranzaksiya datanı oxuyarkən onu digər tranzaksiyaların dəyişməməsi (və ya hətta oxumaması) üçün databaza səviyyəsində kilidləyir. EntityManager.find() və ya EntityManager.lock() metodlarında LockModeType (məsələn, PESSIMISTIC_READ, PESSIMISTIC_WRITE) göstərilir. Bu, performansa ciddi təsir edə və deadlock riskini artıra bilər, ehtiyatla istifadə olunmalıdır.

9. Callbacks & Listeners

Entity-nin həyat dövrünün müəyyən mərhələlərində (məsələn, persist etmədən əvvəl, update etdikdən sonra) avtomatik çağırılan metodlar təyin etmək mümkündür.

  • Callback Metodları (Entity içində): @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove, @PostLoad.

    examples/callbacks.java
    @Entity
    public class AuditLog {
        // ...
        private LocalDateTime createdAt;
        private LocalDateTime updatedAt;
    
        @PrePersist
        public void setCreationTimestamp() {
            createdAt = LocalDateTime.now();
        }
    
        @PreUpdate
        public void setUpdateTimestamp() {
            updatedAt = LocalDateTime.now();
        }
    }
  • Entity Listeners (Ayrı class-da): Eyni məntiqi bir neçə entity üçün tətbiq etmək lazım olduqda istifadə olunur. @EntityListeners annotasiyası ilə entity-yə bağlanır.

    examples/entity-listener.java
    public class AuditListener {
        @PrePersist
        public void beforePersist(Object entity) {
            if (entity instanceof Auditable) {
                ((Auditable) entity).setCreatedAt(LocalDateTime.now());
            }
        }
         @PreUpdate
        public void beforeUpdate(Object entity) {
             if (entity instanceof Auditable) {
                ((Auditable) entity).setUpdatedAt(LocalDateTime.now());
            }
        }
    }
    
    public interface Auditable {
        void setCreatedAt(LocalDateTime time);
        void setUpdatedAt(LocalDateTime time);
    }
    
    @Entity
    @EntityListeners(AuditListener.class)
    public class User implements Auditable {
        // ...
        private LocalDateTime createdAt;
        private LocalDateTime updatedAt;
        // Implement Auditable methods
    }

Implementasiya: Spesifikasiyadan Reallığa

Unutmayın, JPA sadəcə bir spesifikasiyadır, bir interfeyslər və annotasiyalar toplusudur. Kodunuzun işləməsi üçün bu spesifikasiyanı həyata keçirən konkret bir ORM framework-ünə (JPA Provider) ehtiyacınız var.

Ən Populyar JPA Provider-ları:

  1. Hibernate:
    • Tarixçə: Java üçün ilk və ən populyar ORM framework-lərindən biridir. JPA standartının formalaşmasında böyük rolu olub. Uzun müddət de-fakto standart kimi qəbul edilib.
    • Xüsusiyyətlər: Son dərəcə yetkin, zəngin funksionallıqlı (JPA standartından kənar əlavə xüsusiyyətlər də təqdim edir, məsələn, Envers ilə data auditinqi, təkmil keşləmə strategiyaları, HQL), geniş sənədləşmə və nəhəng bir icmaya malikdir.
    • Üstünlüklər: Stabillik, funksiya zənginliyi, güclü icma dəstəyi, Spring Boot ilə mükəmməl inteqrasiya (defolt provider).
    • Çatışmazlıqlar: Bəzən konfiqurasiyası və daxili işləmə mexanizmi mürəkkəb görünə bilər, bəzi hallarda "ağır" (heavyweight) olduğu düşünülür.
  2. EclipseLink:
    • Tarixçə: Oracle-ın TopLink məhsulundan törəmişdir və JPA spesifikasiyasının rəsmi referans implementasiyasıdır (RI).
    • Xüsusiyyətlər: Tam JPA uyğunluğu, yüksək performans, çevik konfiqurasiya imkanları. O da JPA standartından kənar əlavə funksionallıqlar təqdim edir.
    • Üstünlüklər: Referans implementasiya olması (standarta ən yaxın), yaxşı performans göstəriciləri.
    • Çatışmazlıqlar: İcması və online resursları Hibernate qədər geniş olmaya bilər.
  3. OpenJPA:
    • Tarixçə: Apache Software Foundation layihəsidir.
    • Xüsusiyyətlər: Tam JPA uyğunluğu təmin edən başqa bir alternativdir.
    • Üstünlüklər: Liberal Apache lisenziyası.
    • Çatışmazlıqlar: Yeni layihələrdə Hibernate və EclipseLink qədər yayğın deyil.

Provider Seçimi: Əksər hallarda, xüsusilə Spring Boot istifadə edirsinizsə, Hibernate defolt və etibarlı seçimdir. Əgər xüsusi bir tələbiniz yoxdursa və ya komandanız başqa bir provider ilə daha təcrübəlidirsə, digərləri də nəzərdən keçirilə bilər. Əsas məsələ JPA standartına uyğun kod yazmaqdır ki, nəzəri olaraq provider dəyişmək mümkün olsun.

Spring Boot ilə Konfiqurasiya (Nümunə - application.properties):

application.properties
# Verilənlər bazası bağlantısı
spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA Konfiqurasiyası (Hibernate defolt provider)
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect # Hansı DB dialektini istifadə etməli
spring.jpa.hibernate.ddl-auto=update # və ya validate, none (production üçün!), create-drop (test üçün)
spring.jpa.show-sql=true # Generasiya olunan SQL-ləri loglarda göstər
spring.jpa.properties.hibernate.format_sql=true # SQL-i oxunaqlı formatda göstər
# spring.jpa.properties.hibernate.default_schema=myschema # Defolt schema təyini

# İkinci Səviyyə Keş (Əgər lazımdırsa)
# spring.jpa.properties.hibernate.cache.use_second_level_cache=true
# spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory # Və ya Ehcache vs.
# spring.jpa.properties.hibernate.cache.use_query_cache=true # Sorğu nəticələrini də keşləmək üçün

Real Case Ssenarilər və Best Practice-lər

JPA-nın gücündən düzgün istifadə etmək üçün bəzi vacib məqamlar var:

  1. N+1 Problemi ilə Mübarizə: Bu, ən çox rast gəlinən performans problemidir. Bir siyahıdakı (N sayda) entity-nin hər biri üçün əlaqəli başqa bir entity-ni (və ya collection-u) lazy yükləmək üçün əlavə bir (+1) sorğu göndərilməsi.

    • Həll Yolları:
      • JOIN FETCH (JPQL/Criteria API): Əsas sorğuda əlaqəli datanı da EAGER kimi çəkmək. Ən effektiv üsullardan biridir.
      • Entity Graphs (@NamedEntityGraph, javax.persistence.fetchgraph/loadgraph): Hansı əlaqələrin EAGER kimi yüklənəcəyini daha detallı və dinamik şəkildə təyin etmək üçün bir mexanizmdir.
      • Batch Fetching (@BatchSize annotasiyası - Hibernate-ə məxsus): Lazy yükləmə lazım olduqda, bir əvəzinə bir neçə (məsələn, 10) entity-nin əlaqəli datasını bir sorğu ilə çəkmək.
  2. Tranzaksiya İdarəetməsi (@Transactional): JPA əməliyyatlarının (xüsusilə dəyişikliklərin - persist, merge, remove) demək olar ki, həmişə aktiv bir tranzaksiya daxilində aparılması lazımdır. Spring dünyasında @Transactional annotasiyası (adətən service qatında) bu işi çox asanlaşdırır. Tranzaksiya sərhədlərini düzgün təyin etmək, Persistence Contextin həyat dövrünü və LazyInitializationExceptiondan yayınmağı təmin edir.

  3. DTO (Data Transfer Object) Pattern: Heç vaxt (və ya çox nadir hallarda) JPA Entity-lərini birbaşa API cavabı (response) kimi qaytarmayın!Java

    • Səbəblər:
      • Lazy əlaqələr səbəbilə LazyInitializationException baş verə bilər (əgər tranzaksiya artıq bitibsə).
      • Entity-nin bütün field-lərini (hətta lazım olmayanları və ya həssas olanları) ifşa etmiş olursunuz.
      • API kontraktınız daxili data modelinizə (Entity) bağlı olur, bu da gələcəkdə refaktorinqi çətinləşdirir.
    • Həll: Service qatında Entity-ləri alıb, onları yalnız lazımi məlumatları saxlayan sadə DTO obyektlərinə mapləyin və API controller-dən bu DTO-ları qaytarın. Mapləmə üçün MapStruct, ModelMapper kimi kitabxanalardan və ya sadəcə manual koddan istifadə edə bilərsiniz.
    examples/dto-mapping.java
    // DTO Class
    public class UserDTO {
        private Long id;
        private String username;
        private String email;
        // NO password, NO internal flags etc.
        // Getters/Setters...
    }
    
    // Service Layer
    @Transactional(readOnly = true) // Oxuma əməliyyatları üçün
    public UserDTO getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
        // Manual Mapping (və ya MapStruct istifadə edin)
        UserDTO dto = new UserDTO();
        dto.setId(user.getId());
        dto.setUsername(user.getUsername());
        dto.setEmail(user.getEmail());
        return dto;
    }
  4. Testing: JPA repository-lərini və persistensiya məntiqini test etmək vacibdir.

    • In-Memory Database (H2, HSQLDB): Sürətli testlər üçün istifadə edilə bilər, amma real databaza ilə (məsələn, PostgreSQL) 100% uyğun olmaya bilər.
    • Testcontainers: Testlərinizi real databaza (Docker konteynerində işləyən PostgreSQL, MySQL və s.) üzərində aparmağa imkan verir. Daha etibarlıdır, amma bir qədər yavaşdır.
  5. Performans Optimizasiyası:

    • İkinci Səviyyə Keş (Second-Level Cache): Çox oxunan, az dəyişən datalar (məsələn, referans məlumatlar, konfiqurasiyalar) üçün aktivləşdirilə bilər. Düzgün konfiqurasiya tələb edir.
    • Sorğu Keşi (Query Cache): Eyni parametrlərlə tez-tez icra olunan sorğuların nəticələrini keşləmək üçün istifadə olunur (İkinci Səviyyə Keş ilə birlikdə).
    • Projections: Bütün entity obyektini çəkmək əvəzinə, yalnız lazım olan sütunları çəkmək (JPQL-də SELECT c.id, c.name... və ya DTO constructorları ilə).
    • İndeksləmə: Verilənlər bazası səviyyəsində düzgün indekslərin yaradılması kritikdir. JPA bunu birbaşa idarə etmir, amma @Table(indexes = ...) ilə Hibernate-ə indeks yaratmaq üçün ipucu verə bilərsiniz (və ya Flyway/Liquibase kimi DB migration alətləri ilə idarə edin).


🎯 JPA – Mürəkkəb Amma Güclü Dost

Bu uzun səyahətin sonuna gəldik. Gördüyünüz kimi, JPA sadə bir API-dən çox daha artığıdır. O, özündə:

  • 🔧 Dərin bir arxitektura
  • ⚙️ Detallı həyat dövrü qaydaları
  • 🧩 Çevik mapləmə imkanları
  • 🔍 Güclü sorğu mexanizmləri

birləşdirən hərtərəfli bir persistensiya həllidir.


JPA-nın mürəkkəbliyi gözünüzü qorxutmasın.

Bəli, onu dərindən öyrənmək zaman və səbr tələb edir. Annotasiyaların, konseptlərin və potensial tələlərin (xüsusilə N+1 problemLazyInitializationException) fərqində olmaq şərtdir. Amma bu biliyə yiyələndikdən sonra:

  • 🏆 JPA, Java tətbiqlərinizdə verilənlər bazası ilə işləmək üçün inanılmaz dərəcədə güclüməhsuldar bir alətə çevrilir.
  • 🚀 Sizi aşağı səviyyəli JDBC kodundan, manual mapləmədən xilas edir.
  • 👨‍💻 Diqqətinizi biznes məntiqinə yönəltməyə imkan yaradır.

JPA-nı mənimsəmək, müasir Java ekosistemində effektiv, standartlara uyğunmaintainable (saxlanıla bilən) persistensiya qatı qurmaq üçün əvəzsiz bir bacarıqdır. 🚀


İndi isə, bu dərin biliklərlə silahlanaraq kodunuzda JPA-nın gücünü sərbəst buraxmaq zamanıdır!

Thanks for reading.