404 Not Found

404 Not Found


nginx

استمرارية البيانات باستخدام Spring Data JPA

JPA يجعل التعيين بين الكائنات وقواعد البيانات العلائقية إعلانياً—تكتب كيانات، وتُعرّف مستودعات، ويتم توليد SQL تلقائياً.

1. ما ستتعلمه


2. قصة حقيقية عن استمرارية البيانات

(1) نقطة الألم: البيانات في الذاكرة تُفقد عند إعادة التشغيل

كانت Alice تستخدم خريطة في الذاكرة لتخزين بيانات المنتجات والطلبات لـ OrderFlow. في كل مرة يُعاد تشغيل التطبيق، تُفقد جميع البيانات، مما يجبرها على إعادة إنشاء المنتجات يدوياً أثناء الاختبار. والأكثر إزعاجاً أن الخريطة لا تستطيع دعم الاستعلامات المعقدة (مثل البحث عن الطلبات ضمن نطاق تاريخ)، لذا كان لا بد من استخدام قاعدة بيانات حقيقية في بيئة الإنتاج.

(2) الحل باستخدام Spring Data JPA

Spring Data JPA يجعل الاستمرارية مسألة "تعريف واجهات":

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
}

بدون صنف تنفيذ، Spring Data يُولّد SQL تلقائياً.

(3) العوائد

بعد أن أدخلت Alice JPA، أصبحت بيانات OrderFlow مستمرة في MySQL وتبقى بعد إعادة التشغيل. الاستعلامات المعقدة تُنفذ باستخدام اصطلاحات تسمية الطرق أو @Query، ولا تحتوي واجهة Repository على أي كود تنفيذ.


3. تعريف كيانات JPA

(1) تعليقات الكيان الأساسية

التعليق الغرض مثال
@Entity تعليم ككيان JPA @Entity public class Product
@Table تحديد اسم الجدول @Table(name = "products")
@Id تعليم المفتاح الأساسي @Id private Long id;
@GeneratedValue استراتيجية توليد المفتاح الأساسي @GeneratedValue(strategy = IDENTITY)
@Column تعيين العمود @Column(nullable = false, length = 200)
@CreationTimestamp ملء تلقائي لوقت الإنشاء @CreationTimestamp private Instant createdAt;

(1) ▶ مثال: كيان المنتج

JAVA
@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    // مُنشئ افتراضي مطلوب من JPA
    protected Product() {}

    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    // تم حذف Getters وsetters للاختصار
}

الناتج:

TEXT
// التنفيذ ناجح
قيمة ddl-auto السلوك البيئة المناسبة
create-drop إنشاء جدول عند التشغيل، حذفه عند الإيقاف اختبار
create إعادة بناء الجدول في كل تشغيل تطوير
update إضافة فقط، بدون حذف؛ تعديل أعمدة تطوير
validate التحقق فقط، بدون تعديل إنتاج
none عدم اتخاذ أي إجراء إنتاج (إدارة يدوية)
⚠️ ملاحظة: في بيئة الإنتاج، يجب استخدام validate أو none؛ استخدام update قد يؤدي إلى فقدان البيانات.


4. واجهة Repository

(1) التسلسل الهرمي لوراثة Repository

100%
graph LR
    A[Repository] --> B[CrudRepository]
    B --> C[ListCrudRepository]
    B --> D[PagingAndSortingRepository]
    D --> E[JpaRepository]
    C --> E
الواجهة الطرق الأساسية حالات الاستخدام
CrudRepository save / findById / findAll / delete CRUD أساسي
ListCrudRepository findAll تُرجع List تجنب تحويل Iterable
PagingAndSortingRepository findAll(Pageable) ترقيم الصفحات والفرز
JpaRepository كل ما سبق + flush / saveAndFlush الأكثر استخداماً، موصى به

(1) ▶ مثال: ProductRepository

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {

    // استعلام مشتق من اسم الطريقة
    List<Product> findByNameContaining(String keyword);

    List<Product> findByStockLessThan(Integer threshold);

    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);

    // JPQL مخصص
    @Query("SELECT p FROM Product p WHERE p.stock = 0")
    List<Product> findOutOfStockProducts();

    // SQL أصلي
    @Query(value = "SELECT * FROM products WHERE price > :minPrice ORDER BY price DESC",
           nativeQuery = true)
    List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}

الناتج:

TEXT
// التنفيذ ناجح

(2) اصطلاحات تسمية الطرق

الكلمة المفتاحية مثال SQL المُولّد
findBy findByName WHERE name = ?
Containing findByNameContaining WHERE name LIKE %?%
Between findByPriceBetween WHERE price BETWEEN ? AND ?
LessThan findByStockLessThan WHERE stock < ?
OrderBy findByPriceOrderByStockDesc ORDER BY stock DESC
And / Or findByNameAndStock WHERE name = ? AND stock = ?

5. تعيين علاقات الكيانات

(1) Order ↔ OrderItem: علاقة واحد إلى متعدد

100%
erDiagram
    ORDER ||--o{ ORDER_ITEM : contains
    PRODUCT ||--o{ ORDER_ITEM : included_in
    ORDER {
        bigint id PK
        varchar status
        decimal total_amount
        timestamp created_at
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        decimal unit_price
    }
    PRODUCT {
        bigint id PK
        varchar name
        decimal price
        int stock
    }

(1) ▶ مثال: كيانات Order وOrderItem

JAVA
@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
}

@Entity
@Table(name = "order_items")
public class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
}

الناتج:

TEXT
// التنفيذ ناجح
نوع Cascade المعنى الاستخدام الموصى به
ALL جميع العمليات متسلسلة يُستخدم فقط للتبعيات القوية
PERSIST استمرار متسلسل شائع
MERGE دمج متسلسل شائع
REMOVE حذف متسلسل استخدم بحذر
نوع الجلب السلوك حالات الاستخدام
LAZY استعلام فقط عند الوصول التوصية الافتراضية لتجنب N+1
EAGER تحميل فوري القيمة الافتراضية لـ @ManyToOne

6. @Query استعلامات JPQL مقابل SQL الأصلية

(1) ▶ مثال: استعلامات JPQL وSQL الأصلية

JAVA
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL: البحث عن الطلبات حسب الحالة مع عدد العناصر
    @Query("SELECT o FROM Order o WHERE o.status = :status")
    List<Order> findByStatus(@Param("status") String status);

    // JPQL: JOIN FETCH لتجنب N+1
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    // SQL أصلي: إحصائيات الطلبات
    @Query(value = """
        SELECT o.status, COUNT(*) as cnt, SUM(o.total_amount) as total
        FROM orders o
        GROUP BY o.status
        """, nativeQuery = true)
    List<Object[]> getOrderStatistics();
}

الناتج:

TEXT
// التنفيذ ناجح
البُعد JPQL SQL الأصلي
الصياغة مبنية على الكيانات والحقول مبنية على الجداول والأعمدة
نقل قاعدة البيانات قابل للنقل غير قابل للنقل
الميزات محدودة (لا تدعم UNION، إلخ) كاملة
نوع الإرجاع كيان / DTO Object[] / DTO

7. تهيئة قاعدة البيانات

(1) ▶ مثال: schema.sql وdata.sql

SQL
-- src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

الناتج:

TEXT
CREATE TABLE
SQL
-- src/main/resources/data.sql
INSERT INTO products (name, price, stock) VALUES
    ('Laptop Pro', 1299.99, 50),
    ('Wireless Mouse', 29.99, 200),
    ('USB-C Hub', 49.99, 100);
طريقة التهيئة التهيئة حالات الاستخدام
Hibernate ddl-auto spring.jpa.hibernate.ddl-auto=update تطوير
schema.sql + data.sql spring.sql.init.mode=always التحكم الدقيق في DDL
Flyway spring.flyway.enabled=true ترحيل الإنتاج
Liquibase spring.liquibase.enabled=true ترحيل الإنتاج

8. مثال شامل: التنفيذ الكامل لاستمرارية JPA في OrderFlow

JAVA
// Product.java
@Entity
@Table(name = "products")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    protected Product() {}
    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name; this.price = price; this.stock = stock;
    }
    // getters/setters
}

// Order.java
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status = "PENDING";

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(this, product, quantity, product.getPrice());
        items.add(item);
        totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
    }
    // getters/setters
}

// OrderItem.java
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
    public OrderItem(Order order, Product product, Integer quantity, BigDecimal unitPrice) {
        this.order = order; this.product = product;
        this.quantity = quantity; this.unitPrice = unitPrice;
    }
    // getters/setters
}

// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
    List<Product> findByStockLessThan(Integer threshold);
}

❓ أسئلة شائعة

س لماذا يجب أن يكون لكيان JPA مُنشئ بدون معاملات؟
ج JPA يستخدم الانعكاس لإنشاء مثيلات الكيان، مما يتطلب مُنشئاً بدون معاملات. يمكنك التعليق عليه بـ protected لمنع الاستخدام العرضي من الخارج. بما أن records لا تملك مُنشئاً بدون معاملات، فلا يمكن استخدامها مباشرة ككيانات JPA.
س كيف أُحل LazyInitializationException؟
ج السبب هو الوصول إلى خاصية محملة بتكاسل بعد إغلاق الجلسة. الحلول: 1) @Transactional لإبقاء الجلسة مفتوحة؛ 2) JOIN FETCH لتحميل كل شيء دفعة واحدة؛ 3) @EntityGraph لتعريف خطة تحميل.
س أيهما أختار، CrudRepository أم JpaRepository؟
ج نوصي بـ JpaRepository. يرث من جميع الواجهات الفرعية ويوفر أكثر مجموعة شاملة من الطرق (بما في ذلك ترقيم الصفحات والفرز والعمليات المجمعة)، مما يجعله الاختيار المعياري في التطوير الفعلي.
س هل ddl-auto=update آمن؟
ج لا، ليس آمناً. خيار update يُدرج فقط ولا يحذف، مما قد يترك أعمدة مهملة. بالإضافة إلى ذلك، قد يؤدي تغيير أنواع الأعمدة إلى فقدان البيانات. في بيئة الإنتاج، يجب استخدام validate مع Flyway أو Liquibase لإدارة ترحيلات قاعدة البيانات.
س كيف تُعالج مشكلة استعلام N+1؟
ج 1) @Query("JOIN FETCH") تحميل البيانات ذات الصلة دفعة واحدة؛ 2) @EntityGraph خطة تحميل إعلانية؛ 3) @BatchSize تحميل دفعي. هذا هو الجانب الأكثر أهمية في تحسين أداء JPA.
س هل يمكن استخدام schema.sql وddl-auto الخاص بـ Hibernate معاً؟
ج لا نوصي باستخدامهما معاً، فقد يتعارضان. إذا استخدمت Hibernate لإدارة DDL، فلا تحتاج إلى schema.sql؛ وإذا استخدمت schema.sql، فاضبط ddl-auto=none.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة: ⭐): قم بتهيئة مصدر بيانات H2 لـ OrderFlow، أنشئ كيان Product وProductRepository، نفذ واجهة REST API من نوع CRUD للمنتجات، واستمر البيانات في H2.

  2. مسألة متقدمة (الصعوبة ⭐⭐): نفّذ علاقة واحد إلى متعدد بين Order وOrderItem، استخدم JOIN FETCH لحل مشكلة N+1، واكتب طريقة Repository للاستعلام عن الطلبات حسب نطاق التاريخ.

  3. تحدٍ (الصعوبة: ⭐⭐⭐): انتقل إلى مصدر بيانات MySQL، استخدم Flyway لإدارة نصوص ترحيل قاعدة البيانات، وأنشئ ملفات الترحيل V1__init_schema.sql وV2__add_order_status_index.sql.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%