Spring Boot এর সাথে কাজ করার সময় আমরা প্রায়ই ডাটাবেজে query চালাতে চাই। তবে সাধারণ SQL এর পরিবর্তে JPA framework একটি বিশেষ query language দেয় — যেটির নাম JPQL (Java Persistence Query Language)। এটি Hibernate বা JPA provider কে বলে দেয় entity object এর উপর ভিত্তি করে ডেটা query করতে। অর্থাৎ এটি database table নয়, বরং Java entity class নিয়ে কাজ করে — যা object-oriented paradigm এর সাথে পুরোপুরি মিলে যায়।


JPQL কী?

JPQL হলো SQL-এর মতো একটি query language, কিন্তু এটি table নাম বা column নামের পরিবর্তে Entity class এবং property name ব্যবহার করে।

উদাহরণস্বরূপ, তুমি যদি Employee নামে একটি entity ব্যবহার করো, তাহলে:

SELECT e FROM Employee e

এর মানে হবে: Employee entity থেকে সব রেকর্ড আনো।

এটি SQL থেকে আলাদা কারণ এখানে employee টেবিল নয়, বরং Employee Java class কে refer করা হচ্ছে।


কেন JPQL ব্যবহার করবো?

সুবিধা ব্যাখ্যা
Object-oriented Entity এবং property নাম ব্যবহার করে
Database independent SQL dialect আলাদা হলেও query একই থাকে
Type-safe Refactoring এ কম error হয়
ORM integrated Hibernate এর entity lifecycle এর সাথে কাজ করে
Lazy/Eager loading Association fetch strategy নিয়ন্ত্রণ করা যায়

Entity উদাহরণ

@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String department;
    private double salary;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department dept;

    // Getter & Setter
}

Simple Select Query

@Query("SELECT e FROM Employee e")
List<Employee> findAllEmployees();

SQL সমতুল্য:

SELECT * FROM employees;

Conditional Query (WHERE clause)

@Query("SELECT e FROM Employee e WHERE e.department = :dept")
List<Employee> findByDepartment(@Param("dept") String department);

Sorting & Ordering

@Query("SELECT e FROM Employee e ORDER BY e.salary DESC")
List<Employee> findAllOrderBySalary();

Named Parameters ব্যবহার

Named parameter ব্যবহার করা যায় :parameterName দিয়ে:

@Query("SELECT e FROM Employee e WHERE e.name = :name AND e.salary >= :salary")
List<Employee> searchByNameAndSalary(@Param("name") String name,
                                     @Param("salary") double salary);

LIKE অপারেটর (Pattern Search)

@Query("SELECT e FROM Employee e WHERE e.name LIKE %:keyword%")
List<Employee> findByNameContains(@Param("keyword") String keyword);

Aggregate Functions (COUNT, SUM, AVG, MAX, MIN)

@Query("SELECT COUNT(e) FROM Employee e WHERE e.department = :dept")
long countByDepartment(@Param("dept") String department);

@Query("SELECT AVG(e.salary) FROM Employee e")
Double findAverageSalary();

@Query("SELECT MAX(e.salary) FROM Employee e WHERE e.department = :dept")
Double findMaxSalaryByDepartment(@Param("dept") String dept);

JOIN Query

ধরা যাক একটি Department entity আছে:

@Entity
public class Department {
    @Id
    private Long id;
    private String name;

    @OneToMany(mappedBy = "dept")
    private List<Employee> employees;
}

এখন JOIN query:

@Query("SELECT e FROM Employee e JOIN e.dept d WHERE d.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);

LEFT JOIN এবং FETCH JOIN

LEFT JOIN

@Query("SELECT e FROM Employee e LEFT JOIN e.dept d WHERE d.name = :deptName OR d IS NULL")
List<Employee> findWithOrWithoutDepartment(@Param("deptName") String deptName);

FETCH JOIN — N+1 সমস্যার সমাধান

JPQL এর সবচেয়ে গুরুত্বপূর্ণ advanced feature হলো JOIN FETCH। সাধারণত Lazy-loaded association গুলো আলাদাভাবে query করে (N+1 problem)। JOIN FETCH দিয়ে একটাই query তে সব data আনা যায়:

// N+1 problem হতে পারে
@Query("SELECT e FROM Employee e")
List<Employee> findAll();

// FETCH JOIN দিয়ে N+1 সমাধান
@Query("SELECT e FROM Employee e JOIN FETCH e.dept")
List<Employee> findAllWithDepartment();

এটি SQL এ একটিমাত্র JOIN query চালায়, ফলে performance অনেক ভালো হয়।

সতর্কতা: JOIN FETCH এর সাথে Pageable ব্যবহার করলে Hibernate warning দেয় এবং সব data memory তে load করে। এক্ষেত্রে @EntityGraph বা batch size ব্যবহার করা উচিত।


Projection (Specific Fields আনা)

Object Array Projection

@Query("SELECT e.name, e.salary FROM Employee e WHERE e.salary > :minSalary")
List<Object[]> findNameAndSalary(@Param("minSalary") double minSalary);

DTO Projection (Best Practice)

@Query("SELECT new com.example.dto.EmployeeDTO(e.name, e.salary) FROM Employee e WHERE e.salary > :minSalary")
List<EmployeeDTO> findEmployeeDTOs(@Param("minSalary") double minSalary);
public class EmployeeDTO {
    private String name;
    private double salary;

    public EmployeeDTO(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

Interface-based Projection (Spring Data)

আরও elegant উপায় হলো interface projection:

public interface EmployeeSummary {
    String getName();
    Double getSalary();
}

@Query("SELECT e.name AS name, e.salary AS salary FROM Employee e")
List<EmployeeSummary> findEmployeeSummaries();

Spring Data JPA স্বয়ংক্রিয়ভাবে proxy তৈরি করে interface implement করে।


GROUP BY এবং HAVING

SQL এর মতো JPQL এ GROUP BY এবং HAVING ব্যবহার করা যায়:

@Query("SELECT e.department, COUNT(e), AVG(e.salary) " +
       "FROM Employee e " +
       "GROUP BY e.department " +
       "HAVING COUNT(e) > :minCount")
List<Object[]> findDepartmentStats(@Param("minCount") long minCount);

এটি প্রতিটি department এর employee count এবং average salary বের করে, এবং শুধুমাত্র সেই department গুলো দেখায় যেগুলোতে নির্দিষ্ট সংখ্যার বেশি employee আছে।


Subquery

JPQL এ subquery ব্যবহার করা যায় WHERE এবং HAVING clause এ:

// যাদের salary গড় salary এর চেয়ে বেশি
@Query("SELECT e FROM Employee e WHERE e.salary > " +
       "(SELECT AVG(e2.salary) FROM Employee e2)")
List<Employee> findAboveAverageSalary();

// যেসব department এ কোনো manager নেই
@Query("SELECT e FROM Employee e WHERE NOT EXISTS " +
       "(SELECT m FROM Employee m WHERE m.department = e.department AND m.role = 'MANAGER')")
List<Employee> findEmployeesWithNoManager();

Update Query

@Modifying
@Transactional
@Query("UPDATE Employee e SET e.salary = e.salary + :bonus WHERE e.department = :dept")
int updateSalary(@Param("bonus") double bonus, @Param("dept") String dept);

@Modifying এবং @Transactional লাগবে কারণ এটি data পরিবর্তন করে।

টিপস: @Modifying(clearAutomatically = true) দিলে query execute হওয়ার পর persistence context clear হয়, যা stale data সমস্যা এড়ায়।

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Transactional
@Query("UPDATE Employee e SET e.salary = :newSalary WHERE e.id = :id")
int updateSalaryById(@Param("id") Long id, @Param("newSalary") double newSalary);

Delete Query

@Modifying
@Transactional
@Query("DELETE FROM Employee e WHERE e.department = :dept")
int deleteByDepartment(@Param("dept") String department);

Named Query (Reusable Query)

Entity এর উপরে Named Query define করা যায়:

@Entity
@NamedQueries({
    @NamedQuery(name = "Employee.findByDept",
        query = "SELECT e FROM Employee e WHERE e.department = :dept"),
    @NamedQuery(name = "Employee.findHighEarners",
        query = "SELECT e FROM Employee e WHERE e.salary > :minSalary ORDER BY e.salary DESC")
})
public class Employee { ... }

Repository থেকে ব্যবহার:

@Query(name = "Employee.findByDept")
List<Employee> getByDepartment(@Param("dept") String dept);

Pagination সহ JPQL

Spring Data Pageable এর সাথে JPQL সহজেই কাজ করে:

@Query("SELECT e FROM Employee e WHERE e.salary > :minSalary ORDER BY e.name ASC")
Page<Employee> findHighEarners(@Param("minSalary") double minSalary, Pageable pageable);

ব্যবহার:

Pageable pageable = PageRequest.of(0, 10, Sort.by("salary").descending());
Page<Employee> page = employeeRepository.findHighEarners(50000, pageable);

List<Employee> employees = page.getContent();
long totalElements = page.getTotalElements();
int totalPages = page.getTotalPages();

সতর্কতা: JOIN FETCH এর সাথে pagination ব্যবহারে countQuery আলাদাভাবে দিতে হয়:

@Query(value = "SELECT e FROM Employee e JOIN FETCH e.dept WHERE e.salary > :min",
       countQuery = "SELECT COUNT(e) FROM Employee e WHERE e.salary > :min")
Page<Employee> findWithDept(@Param("min") double min, Pageable pageable);

@EntityGraph — Lazy Loading এর বিকল্প

JOIN FETCH এর চেয়ে cleaner alternative হলো @EntityGraph:

@EntityGraph(attributePaths = {"dept", "projects"})
@Query("SELECT e FROM Employee e WHERE e.salary > :min")
List<Employee> findWithRelations(@Param("min") double min);

অথবা entity level এ define করে:

@Entity
@NamedEntityGraph(name = "Employee.withDept",
    attributeNodes = @NamedAttributeNode("dept"))
public class Employee { ... }
@EntityGraph("Employee.withDept")
List<Employee> findBySalaryGreaterThan(double salary);

Criteria API বনাম JPQL

কখনো কখনো dynamic query দরকার হয় যেখানে filter গুলো runtime এ নির্ধারিত হয়। সেক্ষেত্রে JPQL এর সীমাবদ্ধতা দেখা যায় — কারণ JPQL string-based। এক্ষেত্রে Criteria API বা QueryDSL ব্যবহার করা উচিত।

// Criteria API দিয়ে dynamic query
public List<Employee> findByCriteria(String dept, Double minSalary) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
    Root<Employee> root = query.from(Employee.class);

    List<Predicate> predicates = new ArrayList<>();

    if (dept != null) {
        predicates.add(cb.equal(root.get("department"), dept));
    }
    if (minSalary != null) {
        predicates.add(cb.greaterThanOrEqualTo(root.get("salary"), minSalary));
    }

    query.where(predicates.toArray(new Predicate[0]));
    return entityManager.createQuery(query).getResultList();
}
বিষয় JPQL Criteria API
Type-safe আংশিক সম্পূর্ণ
Dynamic query সীমিত সম্পূর্ণ সমর্থন
পঠনযোগ্যতা সহজ জটিল
Refactoring safety কম বেশি
ব্যবহার সাধারণ static query Dynamic filter query

JPQL বনাম Native SQL

বিষয় JPQL Native SQL
Object-oriented হ্যাঁ না
Database-independent হ্যাঁ না
Performance মাঝারি দ্রুত (specific DB tuning এ)
Syntax Entity-based Table-based
Use case সাধারণ query, reusable Complex DB-specific query
Window functions সমর্থন করে না করে
Full-text search সমর্থন করে না করে

Native SQL ব্যবহার করো যখন:

  • Database-specific features দরকার (e.g., PostgreSQL jsonb, MySQL FULLTEXT)
  • Window functions দরকার
  • Complex reporting query যা JPQL এ সম্ভব নয়
@Query(value = "SELECT * FROM employees WHERE MATCH(name) AGAINST(:keyword IN BOOLEAN MODE)",
       nativeQuery = true)
List<Employee> fullTextSearch(@Param("keyword") String keyword);

Common Pitfalls এবং সমাধান

১. N+1 Query Problem

// প্রতিটি employee এর জন্য আলাদা query হবে department load করতে
List<Employee> employees = repo.findAll();
employees.forEach(e -> System.out.println(e.getDept().getName())); // N+1!

// সমাধান: JOIN FETCH বা @EntityGraph
@Query("SELECT e FROM Employee e JOIN FETCH e.dept")
List<Employee> findAllWithDept();

২. MultipleBagFetchException

একাধিক collection একসাথে FETCH করলে Hibernate exception দেয়:

// এটি MultipleBagFetchException দেবে
@Query("SELECT e FROM Employee e JOIN FETCH e.dept JOIN FETCH e.projects")
List<Employee> findAll();

// সমাধান: একটি FETCH করো, বাকিটার জন্য @BatchSize ব্যবহার করো
@Entity
public class Employee {
    @OneToMany
    @BatchSize(size = 20)
    private List<Project> projects;
}

৩. LazyInitializationException

Transaction এর বাইরে lazy property access করলে exception হয়:

// Transaction শেষ হওয়ার পরে lazy field access
Employee emp = repo.findById(1L).get(); // Transaction এখানে শেষ
emp.getDept().getName(); // LazyInitializationException!

// সমাধান: @Transactional রাখো service layer এ
@Transactional
public String getDepartmentName(Long empId) {
    Employee emp = repo.findById(empId).get();
    return emp.getDept().getName(); // Transaction এখনো active
}

Best Practices

  • Entity নাম ও property নাম ব্যবহার করো, table নাম নয়
  • DTO Projection ব্যবহার করো performance এর জন্য, পুরো entity না এনে শুধু দরকারি field আনো
  • JOIN FETCH বা @EntityGraph দিয়ে N+1 problem সমাধান করো
  • @Modifying(clearAutomatically = true) ব্যবহার করো bulk update/delete এ
  • Pagination এ countQuery আলাদা দাও যদি JOIN FETCH থাকে
  • Dynamic filter query এর জন্য Criteria API বা QueryDSL ব্যবহার করো
  • Complex DB-specific query এর জন্য Native SQL এ fall back করো

Full Repository Example

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // Basic filter
    @Query("SELECT e FROM Employee e WHERE e.salary > :minSalary")
    List<Employee> findRichEmployees(@Param("minSalary") double minSalary);

    // FETCH JOIN to avoid N+1
    @Query("SELECT e FROM Employee e JOIN FETCH e.dept WHERE e.salary > :min")
    List<Employee> findWithDepartment(@Param("min") double min);

    // DTO Projection
    @Query("SELECT new com.example.dto.EmployeeDTO(e.name, e.salary) FROM Employee e WHERE e.department = :dept")
    List<EmployeeDTO> findDTOByDepartment(@Param("dept") String dept);

    // Pagination with countQuery
    @Query(value = "SELECT e FROM Employee e JOIN FETCH e.dept WHERE e.salary > :min",
           countQuery = "SELECT COUNT(e) FROM Employee e WHERE e.salary > :min")
    Page<Employee> findPagedWithDept(@Param("min") double min, Pageable pageable);

    // GROUP BY stats
    @Query("SELECT e.department, COUNT(e), AVG(e.salary) FROM Employee e GROUP BY e.department")
    List<Object[]> getDepartmentStats();

    // Bulk update
    @Modifying(clearAutomatically = true)
    @Transactional
    @Query("UPDATE Employee e SET e.department = :dept WHERE e.id = :id")
    void updateDepartment(@Param("id") Long id, @Param("dept") String dept);
}

সারসংক্ষেপ

JPQL হলো JPA/Hibernate এর সাথে কাজ করার সবচেয়ে idiomatic উপায়। এটি object-oriented এবং database-independent, কিন্তু advanced ব্যবহারে কিছু বিষয় মাথায় রাখতে হয়: N+1 সমস্যা, Lazy loading, Pagination এর সাথে FETCH JOIN এর সীমাবদ্ধতা, এবং কখন Native SQL বা Criteria API এ switch করতে হবে। এই বিষয়গুলো ভালোভাবে জানলে Spring Boot application এর database layer অনেক বেশি efficient এবং maintainable হয়।

Share