انتقل إلى المحتوى الرئيسي

الفصل 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 (واجهة علامة)
الواجهةالطرق المضافة
CrudRepositorysave(), findById(), findAll(), deleteById(), count()
PagingAndSortingRepositoryfindAll(Pageable), findAll(Sort)
JpaRepositoryflush(), 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 بإنشاء استعلامات غير فعالة (يحذف كل شيء ثم يعيد الإدراج).


⬅️ العودة إلى الوحدة 18 | الفصل 2: تمارين إعادة الهيكلة →