الفصل 1: أسئلة المقابلة — JPA وقاعدة البيانات
يغطي هذا الفصل الأسئلة الأكثر شيوعًا حول JPA وHibernate والعلاقات بين الكيانات ومشاكل الأداء في قاعدة البيانات. تتضمن كل سؤال إجابة مبسطة وجملة رئيسية لقولها في المقابلة.
Q1. ما هي مشكلة N+1 في JPA؟
تشبيه المطعم
تخيل أنك تطلب من النادل في المطعم:
"أحضر لي 10 طلبات"
ثم، لكل طلب تعيد السؤال:
"ومن هو العميل لهذا الطلب؟"
أنت لا تقوم بعمل طلب كبير واحد. أنت تقوم بـ:
- طلب واحد للطلبات
- ثم 10 طلبات صغيرة للعملاء
هذا يؤدي إلى الكثير من الذهاب والإياب غير الضروري.
ما هو N+1 بشكل ملموس؟
1 استعلام للقائمة الرئيسية
+ N استعلامات للبيانات المرتبطة
────────────────────────────────
= N + 1 استعلامات إجمالاً
إذا كان N = 10 طلبات:
SELECT * FROM orders; -- 1 استعلام
SELECT * FROM customers WHERE id = 1; -- استعلام 2
SELECT * FROM customers WHERE id = 2; -- استعلام 3
SELECT * FROM customers WHERE id = 3; -- استعلام 4
...
SELECT * FROM customers WHERE id = 10; -- استعلام 11
الإجمالي = 11 استعلامات، بينما كان يمكن أن يكفي استعلام واحد فقط.
لماذا يحدث ذلك؟
لأن العلاقة order.customer غالبًا ما تكون محملة بطريقة lazy loading. JPA لا يقوم بتحميل العميل في البداية — ينتظر حتى تقوم باستدعاء order.getCustomer().
الكود الذي يسبب N+1
List<Order> orders = orderRepository.findAll();
for (Order o : orders) {
System.out.println(o.getCustomer().getName());
// ↑ كل استدعاء يطلق استعلام SQL!
}
هنا:
findAll()يقوم بتحميل الطلبات → 1 استعلامgetCustomer()يطلق 1 استعلام لكل طلب → N استعلامات
الحل: JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomers();
JPA يقوم بعمل استعلام واحد يجلب الطلبات وعملائهم في نفس الوقت.
البديل: @EntityGraph
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();
البديل: @BatchSize
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 50)
private Customer customer;
}
بدلاً من عمل N استعلامات، يقوم JPA بعمل N/50 (يحمّل العملاء في دفعات من 50).
صورة ذهنية بسيطة
| النهج | الرحلات |
|---|---|
| بدون تصحيح (N+1) | 11 رحلة لـ 10 طلبات |
| مع JOIN FETCH | رحلة واحدة للجميع |
EN : "The N+1 problem happens when JPA fires one query per related entity. I fix it with JOIN FETCH or @EntityGraph to load everything in a single query."
AR : "تحدث مشكلة N+1 عندما يقوم JPA بتنفيذ استعلام واحد لكل كيان مرتبط. أقوم بحلها باستخدام JOIN FETCH أو @EntityGraph لتحميل كل شيء في استعلام واحد."
Q2. ما الفرق بين Lazy Loading وEager Loading؟
| الاستراتيجية | السلوك | الافتراضي لـ |
|---|---|---|
| LAZY | تحميل البيانات المرتبطة فقط عند الوصول إليها | @OneToMany, @ManyToMany |
| EAGER | تحميل البيانات المرتبطة فورًا مع الكيان | @ManyToOne, @OneToOne |
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
EAGER في كل مكان = كارثة أداء. إذا قام كل كيان بتحميل 5 علاقات في EAGER، يمكن أن يؤدي استعلام بسيط إلى إطلاق مئات الاستعلامات المتتالية.
أفضل ممارسة: كل شيء في LAZY + JOIN FETCH عند الضرورة.
"I default everything to LAZY loading and use JOIN FETCH or EntityGraph when I need to eagerly load specific associations for a use case."
Q3. ما الفرق بين JpaRepository, CrudRepository وPagingAndSortingRepository؟
JpaRepository (الأكثر شمولاً)
↑ يرث من
PagingAndSortingRepository (التقسيم + الفرز)
↑ يرث من
CrudRepository (CRUD الأساسي)
↑ يرث من
Repository (واجهة علامة)
| الواجهة | الطرق المضافة |
|---|---|
| CrudRepository | save(), findById(), findAll(), deleteById(), count() |
| PagingAndSortingRepository | findAll(Pageable), findAll(Sort) |
| JpaRepository | flush(), saveAndFlush(), deleteInBatch(), يعيد List<T> بدلاً من Iterable<T> |
في الممارسة العملية، نستخدم تقريبًا دائمًا JpaRepository.
Q4. اشرح استراتيجيات الانتشار لـ @Transactional
| الانتشار | السلوك |
|---|---|
REQUIRED (الافتراضي) | ينضم إلى المعاملة الحالية أو ينشئ واحدة جديدة |
REQUIRES_NEW | ينشئ دائمًا معاملة جديدة (يعلق الحالية) |
SUPPORTS | ينضم إذا كانت موجودة، وإلا ينفذ بدون معاملة |
NOT_SUPPORTED | ينفذ دائمًا بدون معاملة |
MANDATORY | معاملة موجودة مطلوبة، وإلا استثناء |
NEVER | لا معاملة، وإلا استثناء |
NESTED | معاملة متداخلة (نقطة حفظ) |
حالة استخدام ملموسة لـ REQUIRES_NEW
@Transactional
public void processPayment(Order order) {
paymentGateway.charge(order); // المعاملة الرئيسية
auditService.logPayment(order); // إذا فشل هذا، يجب أن يستمر الدفع
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPayment(Order order) {
// معاملة جديدة مستقلة
// حتى إذا فشلت هذه المعاملة، يتم حفظ الدفع
}
}
"I use REQUIRED by default. I switch to REQUIRES_NEW when I need an independent transaction, like audit logging that must persist regardless of the main transaction's outcome."
Q5. الفرق بين @Transactional(readOnly = true) و@Transactional
@Transactional(readOnly = true) // قراءة: Hibernate يعطل التحقق من التغييرات
public List<User> findAll() { ... }
@Transactional // كتابة: commit إذا نجح، rollback إذا حدث استثناء
public User create(UserDto dto) { ... }
أفضل ممارسة: وضع @Transactional(readOnly = true) على مستوى الفئة، و@Transactional على طرق الكتابة:
@Service
@Transactional(readOnly = true)
public class UserService {
public List<User> findAll() { ... } // يرث readOnly = true
public User findById(Long id) { ... } // يرث readOnly = true
@Transactional // تجاوز: كتابة
public User create(UserDto dto) { ... }
@Transactional // تجاوز: كتابة
public void delete(Long id) { ... }
}
Q6. كيف تنفذ التقسيم في Spring Data JPA؟
// Repository — لا شيء لتفعله، JpaRepository يدعم Pageable بشكل طبيعي
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByRole(Role role, Pageable pageable);
}
// Controller
@GetMapping("/users")
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return userRepository.findAll(pageable);
}
يحتوي Page<T> المعاد على: المحتوى، رقم الصفحة، الحجم، إجمالي العناصر، عدد الصفحات.
Q7. ما هي Query Methods في Spring Data JPA؟
يقوم Spring Data تلقائيًا بإنشاء استعلامات SQL من اسم الطريقة:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
// → SELECT * FROM users WHERE name = ?
List<User> findByNameAndEmail(String name, String email);
// → SELECT * FROM users WHERE name = ? AND email = ?
List<User> findByAgeBetween(int min, int max);
// → SELECT * FROM users WHERE age BETWEEN ? AND ?
List<User> findByNameContainingIgnoreCase(String keyword);
// → SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%keyword%')
Optional<User> findByEmail(String email);
// → SELECT * FROM users WHERE email = ? LIMIT 1
long countByRole(Role role);
// → SELECT COUNT(*) FROM users WHERE role = ?
boolean existsByEmail(String email);
// → SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM users WHERE email = ?
}
"Spring Data JPA derives queries from method names. For complex queries, I use @Query with JPQL or native SQL."
Q8. متى تستخدم @Query مقابل Query Methods؟
| النهج | متى تستخدمه |
|---|---|
| Query Methods | استعلامات بسيطة (1-2 شروط) |
| @Query (JPQL) | استعلامات معقدة، JOIN، تجميعات |
| @Query (native) | استعلامات SQL خاصة بـ SGBD، أداء حرج |
// JPQL
@Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true")
List<User> findActiveByDepartment(@Param("dept") String dept);
// Native SQL
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<User> findRecentUsers();
Q9. كيف تدير العلاقات بين الكيانات؟
@ManyToOne / @OneToMany (الأكثر شيوعًا)
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
@Entity
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
}
@ManyToMany
@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
استخدم دائمًا Set لـ @ManyToMany، لا تستخدم أبدًا List — وإلا قد يقوم Hibernate بإنشاء استعلامات غير فعالة (يحذف كل شيء ثم يعيد الإدراج).