اختبار: Spring Data JPA المتقدم
اختبر معرفتك بـ Spring Data JPA، العلاقات والاستعلامات.
30 سؤالًا - انقر على كل سؤال لكشف الإجابة.
الأسئلة 1-10: الاتفاقيات والاستعلامات
السؤال 1: ما هي طرق الاستعلام في Spring Data JPA؟
الإجابة: طرق يتم فيها تحديد الاستعلام تلقائيًا من خلال الاسم:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByEmail(String email);
List<User> findByLastNameAndFirstName(String ln, String fn);
List<User> findByAgeGreaterThan(int age);
}
يقوم Spring بإنشاء تنفيذ SQL تلقائيًا.
السؤال 2: ما هي الكلمات المفتاحية الشائعة للاستعلام؟
الإجابة:
| الكلمة المفتاحية | المثال |
|---|---|
| And | findByNameAndAge |
| Or | findByNameOrEmail |
| Between | findByAgeBetween |
| LessThan | findByAgeLessThan |
| GreaterThan | findByAgeGreaterThan |
| Like | findByNameLike |
| Containing | findByNameContaining |
| OrderBy | findByAgeOrderByNameAsc |
| Not | findByNameNot |
| In | findByAgeIn(Collection) |
| True/False | findByActiveTrue |
السؤال 3: كيف تحد من عدد النتائج؟
الإجابة:
// Top/First
User findFirstByOrderByCreatedAtDesc();
List<User> findTop10ByOrderByScoreDesc();
// مع Pageable
Page<User> findAll(Pageable pageable);
السؤال 4: كيف تكتب استعلام JPQL مخصص؟
الإجابة: باستخدام @Query:
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
@Query("SELECT u FROM User u WHERE u.age > :age ORDER BY u.name")
List<User> findOlderThan(@Param("age") int age);
السؤال 5: كيف تكتب استعلام SQL أصلي؟
الإجابة:
@Query(value = "SELECT * FROM users WHERE email LIKE %?1", nativeQuery = true)
List<User> findByEmailNative(String domain);
@Query(value = "SELECT COUNT(*) FROM users", nativeQuery = true)
long countAllNative();
السؤال 6: كيف تقوم بعمل استعلام تحديث؟
الإجابة:
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
int deactivateInactiveUsers(@Param("date") LocalDate date);
@Modifying
@Query("DELETE FROM User u WHERE u.active = false")
int deleteInactiveUsers();
@Modifying إلزامي لـ UPDATE/DELETE.
السؤال 7: كيف تسقط فقط بعض الحقول؟
الإجابة: باستخدام الإسقاطات:
// إسقاط الواجهة
public interface UserSummary {
String getName();
String getEmail();
}
List<UserSummary> findByActiveTrue();
// إسقاط DTO
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u")
List<UserDto> findAllAsDto();
السؤال 8: كيف تتحقق من وجود كيان؟
الإجابة:
boolean existsByEmail(String email);
boolean existsByIdAndActiveTrue(Long id);
// تجنب تحميل الكيان بالكامل
السؤال 9: كيف تعد الكيانات؟
الإجابة:
long countByActiveTrue();
long countByDepartment(String department);
@Query("SELECT COUNT(u) FROM User u WHERE u.age > :age")
long countOlderThan(@Param("age") int age);
السؤال 10: كيف تحذف حسب المعايير؟
الإجابة:
void deleteByEmail(String email);
long deleteByActivefalse(); // يعيد العدد المحذوف
@Modifying
@Query("DELETE FROM User u WHERE u.createdAt < :date")
int deleteOldUsers(@Param("date") LocalDate date);
الأسئلة 11-20: العلاقات
السؤال 11: كيف تحدد علاقة OneToMany؟
الإجابة:
@Entity
public class Department {
@Id
private Long id;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
}
@Entity
public class Employee {
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
}
السؤال 12: ما هو mappedBy؟
الإجابة: mappedBy يشير إلى الجانب غير المالك للعلاقة. يشير إلى الحقل في الكيان الآخر الذي يملك المفتاح الأجنبي.
@OneToMany(mappedBy = "department") // "department" هو الحقل في Employee
private List<Employee> employees;
السؤال 13: كيف تحدد علاقة 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<>();
}
@Entity
public class Course {
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
السؤال 14: ما الفرق بين FetchType.LAZY و EAGER؟
الإجابة:
- LAZY: يتم التحميل فقط عند الوصول (موصى به)
- EAGER: يتم التحميل فورًا مع الكيان الأب
@OneToMany(fetch = FetchType.LAZY) // الافتراضي للمجموعات
private List<Order> orders;
@ManyToOne(fetch = FetchType.EAGER) // الافتراضي للفردي
private User user;
السؤال 15: ما هي مشكلة N+1؟
الإجابة: عندما يتم تحميل N كيانات ثم يتم تنفيذ N استعلامات إضافية للعلاقات:
// 1 استعلام للأقسام
List<Department> deps = repo.findAll();
// N استعلامات للموظفين (1 لكل قسم)
deps.forEach(d -> d.getEmployees().size());
الحل: JOIN FETCH أو @EntityGraph.
السؤال 16: كيف تحل مشكلة N+1 باستخدام JOIN FETCH؟
الإجابة:
@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
استعلام واحد فقط يحمل كل شيء.
السؤال 17: ما هو @EntityGraph؟
الإجابة: يحدد العلاقات التي يجب تحميلها:
@EntityGraph(attributePaths = {"employees", "manager"})
List<Department> findAll();
// أو محدد على الكيان
@NamedEntityGraph(
name = "Department.full",
attributeNodes = @NamedAttributeNode("employees")
)
@Entity
public class Department { }
@EntityGraph("Department.full")
List<Department> findByName(String name);
السؤال 18: كيف تدير الكاسكيدات؟
الإجابة:
@OneToMany(cascade = CascadeType.ALL) // جميع العمليات
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
// الأنواع:
// PERSIST - حفظ
// MERGE - تحديث
// REMOVE - حذف
// REFRESH - تحديث
// DETACH - فصل
// ALL - الكل
السؤال 19: ما هو orphanRemoval؟
الإجابة: يحذف تلقائيًا الأطفال الأيتام:
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children;
// إذا أزلنا طفلًا من القائمة، يتم حذفه من قاعدة البيانات
parent.getChildren().remove(child); // DELETE في قاعدة البيانات
السؤال 20: كيف تتجنب المراجع الدائرية في JSON؟
الإجابة:
// الخيار 1: @JsonIgnore
@JsonIgnore
@ManyToOne
private Department department;
// الخيار 2: @JsonManagedReference / @JsonBackReference
@JsonManagedReference
@OneToMany(mappedBy = "department")
private List<Employee> employees;
@JsonBackReference
@ManyToOne
private Department department;
// الخيار 3: استخدام DTOs (موصى به)
الأسئلة 21-30: الترقيم والأداء
السؤال 21: كيف تنفذ الترقيم؟
الإجابة:
// المستودع
Page<User> findAll(Pageable pageable);
Page<User> findByActiveTrue(Pageable pageable);
// الخدمة
Pageable pageable = PageRequest.of(0, 20, Sort.by("name"));
Page<User> page = userRepository.findAll(pageable);
// النتيجة
page.getContent(); // قائمة
page.getTotalElements(); // الإجمالي
page.getTotalPages(); // عدد الصفحات
page.getNumber(); // الصفحة الحالية
السؤال 22: كيف ترتب النتائج؟
الإجابة:
// بواسطة الطريقة
List<User> findByActiveOrderByNameAsc(boolean active);
// بواسطة المعامل Sort
List<User> findByActive(boolean active, Sort sort);
userRepository.findByActive(true, Sort.by("name").descending());
// معايير متعددة
Sort sort = Sort.by("lastName").and(Sort.by("firstName"));
السؤال 23: كيف تستخدم Slice بدلاً من Page؟
الإجابة: Slice لا يحسب الإجمالي (أكثر كفاءة):
Slice<User> findByActiveTrue(Pageable pageable);
// النتيجة
slice.getContent();
slice.hasNext(); // هل هناك صفحة تالية؟
slice.hasPrevious();
// لا يوجد getTotalElements() ولا getTotalPages()
السؤال 24: كيف تدقق الكيانات (createdAt، updatedAt)؟
الإجابة:
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class Auditable {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
}
// التفعيل
@EnableJpaAuditing
@Configuration
public class JpaConfig { }
السؤال 25: كيف تنفذ الحذف الناعم؟
الإجابة:
@Entity
@Where(clause = "deleted = false") // Hibernate
public class User {
private boolean deleted = false;
}
// أو مع @SQLDelete
@SQLDelete(sql = "UPDATE users SET deleted = true WHERE id = ?")
السؤال 26: كيف تحسن الاستعلامات باستخدام الفهارس؟
الإجابة:
@Entity
@Table(indexes = {
@Index(name = "idx_email", columnList = "email"),
@Index(name = "idx_name_dept", columnList = "lastName, department")
})
public class User { }
السؤال 27: ما هو الكاش من المستوى الثاني؟
الإجابة: كاش مشترك بين جلسات Hibernate:
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product { }
// التكوين
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=...
السؤال 28: كيف تستخدم المواصفات للاستعلامات الديناميكية؟
الإجابة:
public interface UserRepository extends JpaSpecificationExecutor<User> { }
// المواصفات
Specification<User> hasName(String name) {
return (root, query, cb) -> cb.equal(root.get("name"), name);
}
Specification<User> isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
// الجمع
userRepository.findAll(hasName("John").and(isActive()));
السؤال 29: كيف تنفذ الاستعلامات في دفعات؟
الإجابة:
// التكوين
spring.jpa.properties.hibernate.jdbc.batch_size=50
// الكود
@Transactional
public void saveAll(List<User> users) {
for (int i = 0; i < users.size(); i++) {
entityManager.persist(users.get(i));
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}
السؤال 30: كيف تدير الإصدارات للقفل المتفائل؟
الإجابة:
@Entity
public class Product {
@Version
private Long version;
}
// إذا قامت معاملتان بتعديل في نفس الوقت،
// واحدة تثير OptimisticLockException
النتيجة
| النتيجة | المستوى |
|---|---|
| 27-30 | ممتاز - إتقان JPA |
| 21-26 | جيد - بعض المراجعات |
| 15-20 | متوسط - اقرأ الفصول مرة أخرى |
| أقل من 15 | بحاجة إلى مراجعة |