Spring Data JPA

التدقيق التلقائي مع @CreatedDate

18 دقيقة الدرس 8 من 13

التدقيق التلقائي مع @CreatedDate

تستفيد كل جدول قاعدة بيانات في الإنتاج من معرفة متى أُنشئ الصف ومتى جرى تعديله آخر مرة، وكثيرًا ما تحتاج أيضًا إلى معرفة من أجرى تلك التعديلات. ضبط تلك الأعمدة يدويًا في كل دالة خدمة أمر ممل ومعرَّض للأخطاء. تُزيح ميزة التدقيق (Auditing) في Spring Data JPA هذه المسؤولية عن كود التطبيق كليًا وتنقلها إلى البنية التحتية للإطار — فتُضبط الطوابع الزمنية بشكل متسق وتلقائي دون تلويث منطق الأعمال.

كيف يعمل التدقيق في Spring Data

يُبنى التدقيق في Spring Data فوق دورة حياة JPA (@PrePersist، @PreUpdate). عند تفعيل التدقيق، يسجّل Spring مُستمعًا باسم AuditingEntityListener يعترض هذه الاستدعاءات ويملأ الحقول التي وسمتها بـ @CreatedDate أو @LastModifiedDate أو @CreatedBy أو @LastModifiedBy. النتيجة النهائية أن تلك الحقول الأربعة تُملأ تلقائيًا قبل كل عملية INSERT أو UPDATE دون سطر واحد في طبقة الخدمة.

الخطوة الأولى: تفعيل التدقيق في JPA

أضف @EnableJpaAuditing إلى فئة التطبيق الرئيسية أو إلى أي فئة @Configuration:

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing public class StoreApplication { public static void main(String[] args) { SpringApplication.run(StoreApplication.class, args); } }

يُخبر هذا التوصيف الواحد Spring بتنشيط AuditingEntityListener بشكل عام. بدونه لا يكون لأي توصيف تدقيق على الكيانات أي أثر.

الخطوة الثانية: توصيف حقول الكيان

طبّق توصيفات التدقيق على الحقول التي تريد أن تُملأ تلقائيًا. يجب أيضًا تسجيل AuditingEntityListener على الكيان، إما مباشرةً عبر @EntityListeners، أو — الأكثر ملاءمةً — على فئة أساسية مشتركة من نوع @MappedSuperclass:

import jakarta.persistence.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.Instant; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class Auditable { @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; @LastModifiedDate @Column(name = "updated_at", nullable = false) private Instant updatedAt; // الـ getters محذوفة للإيجاز }

تمتدّ الكيانات الفعلية من هذه الفئة الأساسية:

import jakarta.persistence.*; @Entity @Table(name = "orders") public class Order extends Auditable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String status; // المنشئات والـ getters والـ setters }
لماذا Instant بدلًا من LocalDateTime؟ Instant نقطة على محور الزمن بتوقيت UTC دون أي غموض في المنطقة الزمنية. إذا عمل تطبيقك في مناطق زمنية متعددة أو عبر مناطق سحابية مختلفة، فإن Instant يضمن أن الطوابع الزمنية قابلة للمقارنة دائمًا. استخدم LocalDateTime فقط إذا أردت عن قصد تخزين التوقيت المحلي للساعة في المنطقة الزمنية الحالية لـ JVM.

القيد updatable = false

السمة updatable = false على createdAt بالغة الأهمية. تُخبر Hibernate بتضمين هذا العمود في جملة INSERT لكن استبعاده من كل جملة UPDATE لاحقة. بدونها، قد تُعيد استدعاء خاطئ لـ save() على كيان موجود كتابة طابع وقت الإنشاء الأصلي — خطأ سلامة بيانات يصعب اكتشافه في الاختبارات.

الخطوة الثالثة: تدقيق المؤلف — @CreatedBy و@LastModifiedBy

لالتقاط من نفّذ الإجراء، نفّذ واجهة AuditorAware واعرضها كـ bean. يستدعي Spring Data الدالة getCurrentAuditor() قبيل كل persist أو update:

import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.Optional; @Component("auditorProvider") public class SpringSecurityAuditorAware implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { return Optional.of("system"); } return Optional.of(auth.getName()); } }

أخبر Spring Data بأي bean يستخدم عبر تمرير اسمه إلى @EnableJpaAuditing:

@EnableJpaAuditing(auditorAwareRef = "auditorProvider")

ثم أضف حقول المؤلف إلى Auditable:

@CreatedBy @Column(name = "created_by", updatable = false) private String createdBy; @LastModifiedBy @Column(name = "updated_by") private String updatedBy;

اختبار سلوك التدقيق

مشكلة شائعة: لا تُحمّل شريحة @DataJpaTest الفئة @SpringBootApplication، لذا يكون @EnableJpaAuditing غائبًا ويعمل التدقيق بصمت. الحل هو تضمين فئة إعداد صغيرة في الشريحة الاختبارية:

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.context.annotation.Configuration; @DataJpaTest @Import(OrderRepositoryTest.TestConfig.class) class OrderRepositoryTest { @Configuration @EnableJpaAuditing static class TestConfig {} @Autowired private OrderRepository orderRepository; @Test void createdAt_isPopulatedOnSave() { Order order = new Order(); order.setStatus("PENDING"); Order saved = orderRepository.save(order); assertNotNull(saved.getCreatedAt()); assertNotNull(saved.getUpdatedAt()); } }
التحكم في الساعة في الاختبارات: إذا أردت التحقق من أن createdAt تساوي قيمة بعينها تمامًا، أدخل bean من نوع DateTimeProvider يُعيد Instant ثابتة ومرّره إلى @EnableJpaAuditing(dateTimeProviderRef = "fixedClock"). هذا يجعل الاختبارات الحساسة للوقت محددة النتائج.

اعتبارات الأداء

يعتمد التدقيق في Spring Data على دورة حياة JPA، مما يعني أن Hibernate يجب أن يحمّل الكيان في ذاكرة التخزين المؤقت للمستوى الأول قبل أن يتمكن من استدعاء @PreUpdate. بالنسبة للتحديثات الجماعية التي تُنفَّذ عبر JPQL (باستخدام @Modifying @Queryلا تُستدعى استدعاءات دورة الحياة، لذا لن يُحدَّث updatedAt تلقائيًا. إذا كانت دقة التدقيق مطلوبة في مسارات الجملة الجماعية، فإما أن تحدّث الطابع الزمني يدويًا في جملة JPQL أو تتجنب التحديثات الجماعية على الكيانات المدققة.

التحديثات الجماعية تتجاوز التدقيق. جملة من قبيل UPDATE Order o SET o.status = 'ARCHIVED' WHERE o.createdAt < :cutoff ستتجاوز AuditingEntityListener بصمت. إما أدرج SET o.updatedAt = :now في الاستعلام، أو انتقل إلى نهج الحفظ الدُفعي إذا كانت سجلات التدقيق إلزامية.

الخلاصة

يُزيل التدقيق في Spring Data JPA إدارة الطوابع الزمنية الاعتيادية من طبقة الخدمة. فعّله بـ @EnableJpaAuditing، وسجّل AuditingEntityListener على @MappedSuperclass، ووسّم الحقول بـ @CreatedDate و@LastModifiedDate. استخدم Instant للسلامة من فوارق التوقيت وupdatable = false لحماية طوابع الإنشاء. لتتبّع المؤلف، نفّذ AuditorAware. تذكّر أن التحديثات الجماعية عبر JPQL تتجاوز المُستمع وتحتاج معالجة updatedAt بشكل صريح.