استمرارية البيانات باستخدام Spring Data JPA
JPA يجعل التعيين بين الكائنات وقواعد البيانات العلائقية إعلانياً—تكتب كيانات، وتُعرّف مستودعات، ويتم توليد SQL تلقائياً.
1. ما ستتعلمه
- تعريفات كيانات JPA: التعليقات
@Entity/@Id/@GeneratedValue/@Column - واجهة Repository:
CrudRepository/JpaRepository/ طرق الاستعلام المخصصة - تعيين علاقات الكيانات
@OneToMany/@ManyToOne(Order ↔ OrderItem) @Queryاستعلامات JPQL وSQL الأصلية- تهيئة قاعدة البيانات:
schema.sql/data.sqlواستراتيجيات Hibernateddl-auto
2. قصة حقيقية عن استمرارية البيانات
(1) نقطة الألم: البيانات في الذاكرة تُفقد عند إعادة التشغيل
كانت Alice تستخدم خريطة في الذاكرة لتخزين بيانات المنتجات والطلبات لـ OrderFlow. في كل مرة يُعاد تشغيل التطبيق، تُفقد جميع البيانات، مما يجبرها على إعادة إنشاء المنتجات يدوياً أثناء الاختبار. والأكثر إزعاجاً أن الخريطة لا تستطيع دعم الاستعلامات المعقدة (مثل البحث عن الطلبات ضمن نطاق تاريخ)، لذا كان لا بد من استخدام قاعدة بيانات حقيقية في بيئة الإنتاج.
(2) الحل باستخدام Spring Data JPA
Spring Data JPA يجعل الاستمرارية مسألة "تعريف واجهات":
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) ▶ مثال: كيان المنتج
@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 للاختصار
}
الناتج:
// التنفيذ ناجح
قيمة ddl-auto |
السلوك | البيئة المناسبة |
|---|---|---|
create-drop |
إنشاء جدول عند التشغيل، حذفه عند الإيقاف | اختبار |
create |
إعادة بناء الجدول في كل تشغيل | تطوير |
update |
إضافة فقط، بدون حذف؛ تعديل أعمدة | تطوير |
validate |
التحقق فقط، بدون تعديل | إنتاج |
none |
عدم اتخاذ أي إجراء | إنتاج (إدارة يدوية) |
validate أو none؛ استخدام update قد يؤدي إلى فقدان البيانات.
4. واجهة Repository
(1) التسلسل الهرمي لوراثة Repository
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
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);
}
الناتج:
// التنفيذ ناجح
(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: علاقة واحد إلى متعدد
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
@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() {}
}
الناتج:
// التنفيذ ناجح
| نوع Cascade | المعنى | الاستخدام الموصى به |
|---|---|---|
ALL |
جميع العمليات متسلسلة | يُستخدم فقط للتبعيات القوية |
PERSIST |
استمرار متسلسل | شائع |
MERGE |
دمج متسلسل | شائع |
REMOVE |
حذف متسلسل | استخدم بحذر |
| نوع الجلب | السلوك | حالات الاستخدام |
|---|---|---|
LAZY |
استعلام فقط عند الوصول | التوصية الافتراضية لتجنب N+1 |
EAGER |
تحميل فوري | القيمة الافتراضية لـ @ManyToOne |
6. @Query استعلامات JPQL مقابل SQL الأصلية
(1) ▶ مثال: استعلامات JPQL وSQL الأصلية
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();
}
الناتج:
// التنفيذ ناجح
| البُعد | JPQL | SQL الأصلي |
|---|---|---|
| الصياغة | مبنية على الكيانات والحقول | مبنية على الجداول والأعمدة |
| نقل قاعدة البيانات | قابل للنقل | غير قابل للنقل |
| الميزات | محدودة (لا تدعم UNION، إلخ) | كاملة |
| نوع الإرجاع | كيان / DTO | Object[] / DTO |
7. تهيئة قاعدة البيانات
(1) ▶ مثال: schema.sql وdata.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
);
الناتج:
CREATE TABLE
-- 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
// 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);
}
❓ أسئلة شائعة
protected لمنع الاستخدام العرضي من الخارج. بما أن records لا تملك مُنشئاً بدون معاملات، فلا يمكن استخدامها مباشرة ككيانات JPA.@Transactional لإبقاء الجلسة مفتوحة؛ 2) JOIN FETCH لتحميل كل شيء دفعة واحدة؛ 3) @EntityGraph لتعريف خطة تحميل.ddl-auto=update آمن؟update يُدرج فقط ولا يحذف، مما قد يترك أعمدة مهملة. بالإضافة إلى ذلك، قد يؤدي تغيير أنواع الأعمدة إلى فقدان البيانات. في بيئة الإنتاج، يجب استخدام validate مع Flyway أو Liquibase لإدارة ترحيلات قاعدة البيانات.@Query("JOIN FETCH") تحميل البيانات ذات الصلة دفعة واحدة؛ 2) @EntityGraph خطة تحميل إعلانية؛ 3) @BatchSize تحميل دفعي. هذا هو الجانب الأكثر أهمية في تحسين أداء JPA.ddl-auto=none.📖 ملخص
- كيانات JPA تستخدم التعليقات لتعيين الكائنات إلى الجداول العلائقية؛
@Id+@GeneratedValueيديران المفتاح الأساسي JpaRepositoryيوفر وظائف CRUD كاملة + ترقيم الصفحات + فرز؛ يُولّد استعلامات تلقائياً بناءً على اصطلاحات تسمية الطرق@OneToMany/@ManyToOneيعينان علاقات الكيانات؛ انتبه لاستراتيجيات Cascade والجلب@QueryJPQL مناسب لاستعلامات الكيانات، بينما SQL الأصلي مناسب للإحصائيات المعقدة- استخدم
validate+ أدوات الترحيل (Flyway/Liquibase) في بيئة الإنتاج؛ لا تستخدمddl-auto=update
📝 تمارين
-
تمرين أساسي (الصعوبة: ⭐): قم بتهيئة مصدر بيانات H2 لـ OrderFlow، أنشئ كيان Product وProductRepository، نفذ واجهة REST API من نوع CRUD للمنتجات، واستمر البيانات في H2.
-
مسألة متقدمة (الصعوبة ⭐⭐): نفّذ علاقة واحد إلى متعدد بين
OrderوOrderItem، استخدمJOIN FETCHلحل مشكلة N+1، واكتب طريقة Repository للاستعلام عن الطلبات حسب نطاق التاريخ. -
تحدٍ (الصعوبة: ⭐⭐⭐): انتقل إلى مصدر بيانات MySQL، استخدم Flyway لإدارة نصوص ترحيل قاعدة البيانات، وأنشئ ملفات الترحيل V1__init_schema.sql وV2__add_order_status_index.sql.



