Spring Boot এবং JPA/Hibernate ব্যবহার করার সময় আমরা সাধারণত JPQL বা Native SQL দিয়ে query চালাই। তবে অনেক সময় দরকার হয় ডাইনামিক query তৈরি করা — যেখানে filter গুলো runtime এ নির্ধারিত হয়। যেমন, "name দেওয়া থাকলে name দিয়ে filter করো, salary দেওয়া থাকলে salary দিয়ে করো, না থাকলে সব আনো।" এই জায়গাতেই আসে Criteria API — JPA-র type-safe এবং programmatic query-building system।


Criteria API কী?

Criteria API হলো JPA specification এর একটি অংশ, যা দিয়ে আমরা Java object দিয়ে query তৈরি করতে পারি — কোনো string লেখার দরকার নেই। এতে compile-time type checking থাকে, ফলে field নাম ভুল লিখলে IDE বা compiler ধরে ফেলে।


কেন Criteria API ব্যবহার করা হয়?

সুবিধা ব্যাখ্যা
Dynamic Query Runtime এ শর্ত যোগ/বিয়োগ করা যায়
Type-safe Entity property গুলো IDE তে auto-complete হয়
Refactoring friendly Field rename করলে কোড ভাঙে না (Metamodel ব্যবহারে)
Framework-independent যেকোনো JPA provider এর সাথে কাজ করে
Composable Predicate গুলো আলাদা করে তৈরি করে combine করা যায়

মূল উপাদানসমূহ

Criteria API বোঝার আগে কয়েকটি মূল class জানা দরকার:

Class / Interface ভূমিকা
CriteriaBuilder Query তৈরির factory, entityManager.getCriteriaBuilder() দিয়ে পাওয়া যায়
CriteriaQuery Query এর blueprint, return type নির্ধারণ করে
Root Query এর মূল entity (FROM clause এর মতো)
Predicate একটি শর্ত (WHERE clause এর অংশ)
Join<X, Y> দুটি entity এর মধ্যে join
TypedQuery Executable query যা type-safe result দেয়

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;
    private String status; // ACTIVE, INACTIVE

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

    @OneToMany(mappedBy = "employee")
    private List<Project> projects;

    // getters and setters
}

Step-by-Step Basic Implementation

Step 1: Custom Repository তৈরি

@Repository
public class EmployeeCriteriaRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Employee> findEmployees(String department, Double minSalary) {

        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
        Root<Employee> root = cq.from(Employee.class);

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

        if (department != null && !department.isBlank()) {
            predicates.add(cb.equal(root.get("department"), department));
        }

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

        cq.where(predicates.toArray(new Predicate[0]));

        return entityManager.createQuery(cq).getResultList();
    }
}

Step 2: Service Layer

@Service
public class EmployeeService {

    @Autowired
    private EmployeeCriteriaRepository criteriaRepo;

    public List<Employee> search(String department, Double minSalary) {
        return criteriaRepo.findEmployees(department, minSalary);
    }
}

Step 3: Controller Layer

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @GetMapping
    public List<Employee> filterEmployees(
            @RequestParam(required = false) String department,
            @RequestParam(required = false) Double minSalary) {
        return employeeService.search(department, minSalary);
    }
}

Example Request

GET /api/employees?department=IT&minSalary=50000

Generated SQL:

SELECT * FROM employees WHERE department = 'IT' AND salary >= 50000;

Predicate গুলো আলাদা Method এ ভাগ করা (Clean Code)

বড় query তে সব predicates এক জায়গায় রাখলে code messy হয়ে যায়। ভালো practice হলো প্রতিটা শর্তকে আলাদা private method এ রাখা:

@Repository
public class EmployeeCriteriaRepository {

    @PersistenceContext
    private EntityManager em;

    public List<Employee> search(EmployeeSearchRequest req) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
        Root<Employee> root = cq.from(Employee.class);

        List<Predicate> predicates = buildPredicates(cb, root, req);
        cq.where(predicates.toArray(new Predicate[0]));

        return em.createQuery(cq).getResultList();
    }

    private List<Predicate> buildPredicates(CriteriaBuilder cb,
                                             Root<Employee> root,
                                             EmployeeSearchRequest req) {
        List<Predicate> predicates = new ArrayList<>();

        if (req.getDepartment() != null) {
            predicates.add(byDepartment(cb, root, req.getDepartment()));
        }
        if (req.getMinSalary() != null) {
            predicates.add(byMinSalary(cb, root, req.getMinSalary()));
        }
        if (req.getNameKeyword() != null) {
            predicates.add(byNameLike(cb, root, req.getNameKeyword()));
        }
        if (req.getStatus() != null) {
            predicates.add(byStatus(cb, root, req.getStatus()));
        }

        return predicates;
    }

    private Predicate byDepartment(CriteriaBuilder cb, Root<Employee> root, String dept) {
        return cb.equal(root.get("department"), dept);
    }

    private Predicate byMinSalary(CriteriaBuilder cb, Root<Employee> root, Double min) {
        return cb.greaterThanOrEqualTo(root.get("salary"), min);
    }

    private Predicate byNameLike(CriteriaBuilder cb, Root<Employee> root, String keyword) {
        return cb.like(cb.lower(root.get("name")), "%" + keyword.toLowerCase() + "%");
    }

    private Predicate byStatus(CriteriaBuilder cb, Root<Employee> root, String status) {
        return cb.equal(root.get("status"), status);
    }
}

এই pattern টি Single Responsibility Principle মেনে চলে এবং unit test করাও সহজ হয়।


OR Condition (cb.or)

Default এ একাধিক predicate AND হিসেবে join হয়। OR condition এর জন্য:

// salary > 80000 OR department = 'HR'
Predicate highSalary = cb.gt(root.get("salary"), 80000);
Predicate hrDept = cb.equal(root.get("department"), "HR");

cq.where(cb.or(highSalary, hrDept));

AND এবং OR একসাথে:

// (department = 'IT' OR department = 'Finance') AND salary > 50000
Predicate itOrFinance = cb.or(
    cb.equal(root.get("department"), "IT"),
    cb.equal(root.get("department"), "Finance")
);
Predicate minSalary = cb.gt(root.get("salary"), 50000);

cq.where(cb.and(itOrFinance, minSalary));

Sorting যোগ করা

// একটি column দিয়ে sort
cq.orderBy(cb.desc(root.get("salary")));

// একাধিক column দিয়ে sort
cq.orderBy(
    cb.asc(root.get("department")),
    cb.desc(root.get("salary"))
);

Runtime এ sort direction নির্ধারণ:

public List<Employee> findSorted(String sortField, boolean ascending) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
    Root<Employee> root = cq.from(Employee.class);

    Order order = ascending
        ? cb.asc(root.get(sortField))
        : cb.desc(root.get(sortField));

    cq.orderBy(order);
    return em.createQuery(cq).getResultList();
}

JOIN Query

// INNER JOIN
Join<Employee, Department> deptJoin = root.join("dept", JoinType.INNER);
predicates.add(cb.equal(deptJoin.get("name"), "Engineering"));

// LEFT JOIN
Join<Employee, Department> leftJoin = root.join("dept", JoinType.LEFT);
predicates.add(cb.or(
    cb.equal(leftJoin.get("name"), "IT"),
    cb.isNull(leftJoin)
));

Fetch Join (N+1 সমস্যা সমাধান)

// Lazy association একসাথে load করতে FETCH JOIN
root.fetch("dept", JoinType.LEFT);

// অথবা projects ও একসাথে
root.fetch("dept", JoinType.LEFT);
root.fetch("projects", JoinType.LEFT);

সতর্কতা: একাধিক collection fetch করলে duplicate result আসতে পারে। এক্ষেত্রে cq.distinct(true) ব্যবহার করো।

cq.distinct(true);
root.fetch("projects", JoinType.LEFT);

Aggregate Functions এবং GROUP BY

public List<Object[]> getDepartmentStats() {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
    Root<Employee> root = cq.from(Employee.class);

    cq.multiselect(
        root.get("department"),
        cb.count(root),
        cb.avg(root.get("salary")),
        cb.max(root.get("salary"))
    );

    cq.groupBy(root.get("department"));

    // HAVING: শুধু ৫ এর বেশি employee আছে এমন department
    cq.having(cb.gt(cb.count(root), 5L));

    return em.createQuery(cq).getResultList();
}

Subquery

public List<Employee> findAboveAverageSalary() {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
    Root<Employee> root = cq.from(Employee.class);

    // Subquery: average salary বের করা
    Subquery<Double> avgSubquery = cq.subquery(Double.class);
    Root<Employee> subRoot = avgSubquery.from(Employee.class);
    avgSubquery.select(cb.avg(subRoot.get("salary")));

    // Main query: salary > average salary
    cq.where(cb.greaterThan(root.get("salary"), avgSubquery));

    return em.createQuery(cq).getResultList();
}

EXISTS subquery:

// যেসব department এ কোনো ACTIVE employee নেই
Subquery<Long> existsQuery = cq.subquery(Long.class);
Root<Employee> sub = existsQuery.from(Employee.class);
existsQuery.select(cb.literal(1L));
existsQuery.where(
    cb.equal(sub.get("department"), root.get("department")),
    cb.equal(sub.get("status"), "ACTIVE")
);

predicates.add(cb.not(cb.exists(existsQuery)));

DTO Projection (Tuple Query)

পুরো entity না এনে নির্দিষ্ট field আনতে Tuple ব্যবহার করা যায়:

public List<EmployeeDTO> findEmployeeDTOs(String dept) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Tuple> cq = cb.createTupleQuery();
    Root<Employee> root = cq.from(Employee.class);

    cq.multiselect(
        root.get("name").alias("name"),
        root.get("salary").alias("salary"),
        root.get("department").alias("department")
    );

    cq.where(cb.equal(root.get("department"), dept));

    List<Tuple> tuples = em.createQuery(cq).getResultList();

    return tuples.stream()
        .map(t -> new EmployeeDTO(
            t.get("name", String.class),
            t.get("salary", Double.class),
            t.get("department", String.class)
        ))
        .collect(Collectors.toList());
}

Pagination সহ Query (Total Count + Data)

Production এ pagination এর জন্য দুটো query লাগে — একটি data এর জন্য, আরেকটি total count এর জন্য:

public Page<Employee> findPaged(EmployeeSearchRequest req, int page, int size) {
    CriteriaBuilder cb = em.getCriteriaBuilder();

    // ── Data Query ──
    CriteriaQuery<Employee> dataQuery = cb.createQuery(Employee.class);
    Root<Employee> dataRoot = dataQuery.from(Employee.class);
    List<Predicate> predicates = buildPredicates(cb, dataRoot, req);
    dataQuery.where(predicates.toArray(new Predicate[0]));
    dataQuery.orderBy(cb.desc(dataRoot.get("salary")));

    List<Employee> data = em.createQuery(dataQuery)
        .setFirstResult(page * size)
        .setMaxResults(size)
        .getResultList();

    // ── Count Query ──
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<Employee> countRoot = countQuery.from(Employee.class);
    List<Predicate> countPredicates = buildPredicates(cb, countRoot, req);
    countQuery.select(cb.count(countRoot));
    countQuery.where(countPredicates.toArray(new Predicate[0]));

    Long total = em.createQuery(countQuery).getSingleResult();

    return new PageImpl<>(data, PageRequest.of(page, size), total);
}

JPA Metamodel — সত্যিকারের Type Safety

Criteria API তে এখন পর্যন্ত root.get("salary") এর মতো string ব্যবহার করেছি। এটি type-safe নয় — field নাম ভুল লিখলে runtime এ error হবে।

JPA Static Metamodel ব্যবহার করলে compile-time safety পাওয়া যায়। hibernate-jpamodelgen dependency দিলে Employee_ class auto-generate হয়:

<!-- pom.xml -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <scope>provided</scope>
</dependency>

Auto-generated Metamodel class:

// Target folder এ generate হয়
@StaticMetamodel(Employee.class)
public class Employee_ {
    public static volatile SingularAttribute<Employee, Long> id;
    public static volatile SingularAttribute<Employee, String> name;
    public static volatile SingularAttribute<Employee, String> department;
    public static volatile SingularAttribute<Employee, Double> salary;
}

এখন string এর বদলে Metamodel ব্যবহার করা যাবে:

// String-based — runtime এ error হতে পারে
root.get("salary")

// Metamodel-based — compile-time safe
root.get(Employee_.salary)
root.get(Employee_.department)

Spring Data JPA Specification — Criteria API এর সহজ রূপ

Spring Data JPA Specification interface দিয়ে Criteria API কে আরও সহজ করা যায়। প্রতিটি শর্ত একটি আলাদা Specification হয় এবং সেগুলো and(), or() দিয়ে compose করা যায়।

Setup

public interface EmployeeRepository
    extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
}

Specification তৈরি

public class EmployeeSpecifications {

    public static Specification<Employee> hasDepartment(String dept) {
        return (root, query, cb) ->
            dept == null ? null : cb.equal(root.get("department"), dept);
    }

    public static Specification<Employee> hasMinSalary(Double min) {
        return (root, query, cb) ->
            min == null ? null : cb.greaterThanOrEqualTo(root.get("salary"), min);
    }

    public static Specification<Employee> nameLike(String keyword) {
        return (root, query, cb) ->
            keyword == null ? null :
            cb.like(cb.lower(root.get("name")), "%" + keyword.toLowerCase() + "%");
    }

    public static Specification<Employee> isActive() {
        return (root, query, cb) -> cb.equal(root.get("status"), "ACTIVE");
    }
}

Compose করে ব্যবহার

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository repo;

    public Page<Employee> search(String dept, Double minSalary, String keyword, Pageable pageable) {
        Specification<Employee> spec = Specification
            .where(hasDepartment(dept))
            .and(hasMinSalary(minSalary))
            .and(nameLike(keyword))
            .and(isActive());

        return repo.findAll(spec, pageable);
    }
}

এটি Criteria API এর চেয়ে অনেক বেশি readable এবং reusable। প্রতিটি Specification আলাদাভাবে unit test করাও সহজ।


Criteria API vs JPQL vs Specification

বিষয় JPQL Criteria API Specification
Type-safety আংশিক সম্পূর্ণ (Metamodel এ) সম্পূর্ণ
Dynamic Query কঠিন সহজ সবচেয়ে সহজ
Readability সহজ জটিল মাঝারি
Reusability কম মাঝারি সর্বোচ্চ
Complex Join সহজ মাঝারি জটিল
শেখার curve কম বেশি মাঝারি
কখন ব্যবহার Static query Complex dynamic query Simple filter query

Common Pitfalls এবং সমাধান

১. Null Predicate এড়ানো

// null predicate থাকলে exception হতে পারে
predicates.add(null); // বিপজ্জনক

// null check করো
if (value != null) {
    predicates.add(cb.equal(root.get("field"), value));
}

// অথবা Specification এ null return করো — Spring Data তা ignore করে
return (root, query, cb) -> value == null ? null : cb.equal(root.get("field"), value);

২. Fetch এর সাথে Count Query এ সমস্যা

// Fetch join করলে count query তে issue হতে পারে
// Specification এ এভাবে handle করো:
public static Specification<Employee> withDept() {
    return (root, query, cb) -> {
        // Count query তে fetch করা যাবে না
        if (query.getResultType() != Long.class && query.getResultType() != long.class) {
            root.fetch("dept", JoinType.LEFT);
        }
        return cb.conjunction(); // always true predicate
    };
}

৩. Cartesian Product (Duplicate Results)

// একাধিক collection fetch করলে duplicate আসে
root.fetch("projects", JoinType.LEFT);
root.fetch("skills", JoinType.LEFT); // ❌ duplicate results!

// distinct ব্যবহার করো
query.distinct(true);

Best Practices

  • Static query এর জন্য JPQL ব্যবহার করো — সহজ এবং readable।
  • Dynamic filter এর জন্য Specification ব্যবহার করো — Criteria API এর চেয়ে সহজ।
  • Complex join বা subquery এর জন্য Criteria API ব্যবহার করো।
  • Predicate গুলো আলাদা private method এ রাখো — Single Responsibility নিশ্চিত হয়।
  • JPA Metamodel generate করো — string-based field access এড়াও।
  • Pagination এ data query এবং count query আলাদা রাখো।
  • Fetch join এর পরে cq.distinct(true) দাও।
  • @Transactional(readOnly = true) ব্যবহার করো read-only query তে — performance ভালো হয়।

সারসংক্ষেপ

Criteria API হলো JPA-র সবচেয়ে flexible query-building tool। কিন্তু এটি verbose, তাই সরাসরি ব্যবহারের চেয়ে Spring Data JPA Specification pattern এ মোড়া অবস্থায় ব্যবহার করা বেশি pragmatic। Complex reporting query বা multi-level join এর ক্ষেত্রে Criteria API সরাসরি ব্যবহার করো, আর সাধারণ dynamic filter এর জন্য Specification যথেষ্ট। Metamodel generate করলে type safety নিশ্চিত হয় এবং refactoring অনেক নিরাপদ হয়।

Share