Let’s go through this step by step so it’s 100% clear 👇
🧩 1. What the Repository Layer Actually Does
In Spring Boot (with Spring Data JPA), the repository layer is responsible for interacting with the database — performing CRUD (Create, Read, Update, Delete) operations on entities.
Spring gives you the base repository interfaces such as:
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID> { }
When you extend this interface (e.g., JpaRepository<Product, String>), you get many built-in methods automatically, like:
-
save() -
findById() -
findAll() -
deleteById() -
count(), etc.
⚙️ 2. Custom Method Naming — the "Magic" of Derived Query Methods
You can define your own methods — but you must follow a naming convention if you want Spring Data JPA to automatically generate the SQL/JPQL for you.
For example 👇
public interface ProductRepository extends JpaRepository<Product, String> {
List<Product> findByCategory(String category);
Product findByName(String name);
List<Product> findByPriceGreaterThan(double price);
List<Product> findByStockLessThanEqual(int stock);
}
Here’s what happens:
-
findByCategory(String category)→SELECT * FROM product WHERE category = ? -
findByPriceGreaterThan(double price)→SELECT * FROM product WHERE price > ? -
findByStockLessThanEqual(int stock)→SELECT * FROM product WHERE stock <= ?
✅ Spring parses your method name and builds the query automatically.
That’s why the method name must follow specific patterns, like:
-
findBy -
countBy -
existsBy -
deleteBy -
readBy -
queryBy
You can also combine multiple conditions:
List<Product> findByCategoryAndPriceLessThan(String category, double price);
🧠3. If You Don’t Want to Follow the Naming Convention
You can still define your own custom methods with any name you like, but then you must tell Spring explicitly what query to run — using the @Query annotation.
Example:
public interface ProductRepository extends JpaRepository<Product, String> {
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword% OR p.category LIKE %:keyword%")
List<Product> searchProducts(@Param("keyword") String keyword);
}
✅ Here, searchProducts() is your own name — it doesn’t follow the findBy... pattern, but it’s perfectly fine because you’ve provided the query manually.
🧩 4. If You Need Complex or Native SQL Queries
You can use nativeQuery = true to write SQL instead of JPQL:
@Query(value = "SELECT * FROM product WHERE price BETWEEN :min AND :max", nativeQuery = true)
List<Product> findProductsInPriceRange(@Param("min") double min, @Param("max") double max);
⚖️ 5. Summary — When You Can and Can’t Use Your Own Names
| Case | Can You Use Your Own Method Name? | Requirement |
|---|---|---|
✅ Derived query (like findByName) | No — must follow naming pattern | Spring auto-generates query |
✅ Annotated query (@Query) | Yes | You write your own JPQL/SQL |
| ✅ Custom implementation (in a custom repository class) | Yes | You write the logic manually in an implementation class |
❌ Arbitrary name without @Query or implementation | No | Spring won’t know what to do |
🧠6. Example — Custom Repository with Both Styles
@Repository
public interface ProductRepository extends JpaRepository<Product, String> {
// ✅ Auto-generated query by naming convention
List<Product> findByCategory(String category);
// ✅ Custom name with manual query
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchProducts(@Param("keyword") String keyword);
}
An example of how custom repository methods (with manual implementation class) work — for example, when you want a more complex query than @Query allows?
let’s go step by step through how to create custom repository methods with manual implementation, especially when your logic is too complex for @Query or you need custom database operations (like dynamic filtering or joins).
🧩 Step 1: The Usual Repository Interface
Let’s say you already have a basic repository for your entity:
package com.bej.products.repository;
import com.bej.products.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, String>, CustomProductRepository {
// You can still keep your normal JPA methods here.
List<Product> findByCategory(String category);
}
👇 Notice:
We’ve added CustomProductRepository to this interface:
..., CustomProductRepository
This means ProductRepository now also includes custom methods that you’ll define manually in a separate interface and class.
🧩 Step 2: Create the Custom Repository Interface
Define a new interface where you declare your custom method signatures.
package com.bej.products.repository;
import com.bej.products.model.Product;
import java.util.List;
public interface CustomProductRepository {
List<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice);
}
✅ Here, we’re saying:
“I want to create a method that finds products based on optional filters — category, min price, and max price.”
But we’re not telling Spring how to do it yet.
We’ll implement it manually next.
🧩 Step 3: Create the Custom Repository Implementation Class
Now, we’ll implement that interface with the actual logic.
👉 Naming rule:
The class must follow this pattern:
<MainRepositoryName>Impl
So here it should be:
ProductRepositoryImpl
Let’s write it:
package com.bej.products.repository;
import com.bej.products.model.Product;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class ProductRepositoryImpl implements CustomProductRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice) {
// Build base JPQL query
StringBuilder jpql = new StringBuilder("SELECT p FROM Product p WHERE 1=1");
// Add conditions dynamically
if (category != null && !category.isEmpty()) {
jpql.append(" AND p.category = :category");
}
if (minPrice != null) {
jpql.append(" AND p.price >= :minPrice");
}
if (maxPrice != null) {
jpql.append(" AND p.price <= :maxPrice");
}
// Create query
TypedQuery<Product> query = entityManager.createQuery(jpql.toString(), Product.class);
// Set parameters only if present
if (category != null && !category.isEmpty()) {
query.setParameter("category", category);
}
if (minPrice != null) {
query.setParameter("minPrice", minPrice);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
// Execute and return results
return query.getResultList();
}
}
🧠Step 4: What’s Happening Here?
-
We used EntityManager (the low-level JPA interface) to build a dynamic query.
-
The method allows optional parameters — meaning the user can search by:
-
only category,
-
or only price range,
-
or all of them together.
-
Spring will automatically wire this implementation to your main ProductRepository.
You don’t need to manually connect them — as long as:
✅ Your custom class name matches the pattern
✅ It’s in the same package (or subpackage) as the main repository
🧩 Step 5: Use It in Your Service Layer
Example:
package com.bej.products.service;
import com.bej.products.model.Product;
import com.bej.products.repository.ProductRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> searchProducts(String category, Double minPrice, Double maxPrice) {
return productRepository.findProductsWithDynamicFilter(category, minPrice, maxPrice);
}
}
🧩 Step 6: Example Usage (Controller Layer)
@GetMapping("/products/search")
public ResponseEntity<List<Product>> searchProducts(
@RequestParam(required = false) String category,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice) {
List<Product> results = productService.searchProducts(category, minPrice, maxPrice);
return ResponseEntity.ok(results);
}
✅ Final Summary
Layer File Purpose ProductRepositoryExtends JpaRepository + CustomProductRepository Declares custom + normal methods CustomProductRepositoryInterface Declares signatures for custom methods ProductRepositoryImplImplementation class Implements those custom methods manually ServiceUses the repository Invokes the custom method ControllerExposes endpoint Lets user call the search feature
Would you like me to extend this example to show pagination or sorting with the same dynamic query (so that large datasets are handled efficiently)?
now you’re diving into real-world repository design.
Let’s extend the same custom repository example to include pagination and sorting, step by step.
This is a very common scenario when building production-grade APIs — for example, searching products in an e-commerce app.
🧩 Step 1: Why Pagination and Sorting Matter
When your database table has hundreds or thousands of records, returning all results at once is:
-
❌ slow,
-
❌ memory-heavy,
-
❌ and bad for performance.
So we use pagination (to fetch limited results per page) and sorting (to order them by a field like price or name).
Spring Data JPA provides this via:
-
Pageable (for pagination + sorting)
-
Page<T> (for the returned results with metadata)
🧩 Step 2: Update the Custom Repository Interface
We now want our method to support Pageable and return a Page<Product>.
package com.bej.products.repository;
import com.bej.products.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface CustomProductRepository {
Page<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice, Pageable pageable);
}
🧩 Step 3: Update the Implementation Class
Now, modify ProductRepositoryImpl to handle pagination and sorting.
package com.bej.products.repository;
import com.bej.products.model.Product;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class ProductRepositoryImpl implements CustomProductRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Page<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice, Pageable pageable) {
StringBuilder jpql = new StringBuilder("SELECT p FROM Product p WHERE 1=1");
if (category != null && !category.isEmpty()) {
jpql.append(" AND p.category = :category");
}
if (minPrice != null) {
jpql.append(" AND p.price >= :minPrice");
}
if (maxPrice != null) {
jpql.append(" AND p.price <= :maxPrice");
}
// 🔹 Apply sorting
if (pageable.getSort().isSorted()) {
jpql.append(" ORDER BY ");
jpql.append(pageable.getSort().toString().replace(":", ""));
// Converts something like "price: ASC" → "price ASC"
}
TypedQuery<Product> query = entityManager.createQuery(jpql.toString(), Product.class);
if (category != null && !category.isEmpty()) {
query.setParameter("category", category);
}
if (minPrice != null) {
query.setParameter("minPrice", minPrice);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
// 🔹 Pagination logic: set offset and limit
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
// 🔹 Execute query
List<Product> results = query.getResultList();
// 🔹 Count total records (for Page metadata)
String countQueryStr = jpql.toString().replaceFirst("SELECT p FROM", "SELECT COUNT(p) FROM");
TypedQuery<Long> countQuery = entityManager.createQuery(countQueryStr, Long.class);
if (category != null && !category.isEmpty()) {
countQuery.setParameter("category", category);
}
if (minPrice != null) {
countQuery.setParameter("minPrice", minPrice);
}
if (maxPrice != null) {
countQuery.setParameter("maxPrice", maxPrice);
}
Long total = countQuery.getSingleResult();
// 🔹 Return as Page object
return new PageImpl<>(results, pageable, total);
}
}
🧠Step 4: How This Works Internally
Step Operation 1️⃣ Dynamically builds JPQL query with filters 2️⃣ Adds ORDER BY if sorting is provided 3️⃣ Applies pagination via setFirstResult() and setMaxResults() 4️⃣ Executes main query to fetch data 5️⃣ Executes count query to get total records 6️⃣ Wraps everything inside a PageImpl<Product> object
So you get a proper Spring Page<Product> with:
-
current page content,
-
total number of items,
-
total pages,
-
and more.
🧩 Step 5: Modify the Service Layer
Now your service can directly use pagination and sorting parameters.
package com.bej.products.service;
import com.bej.products.model.Product;
import com.bej.products.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Page<Product> searchProducts(String category, Double minPrice, Double maxPrice, Pageable pageable) {
return productRepository.findProductsWithDynamicFilter(category, minPrice, maxPrice, pageable);
}
}
🧩 Step 6: Update the Controller to Accept Pagination and Sorting
Spring makes it easy to handle pagination and sorting through query parameters like page, size, sort.
package com.bej.products.controller;
import com.bej.products.model.Product;
import com.bej.products.service.ProductService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/search")
public ResponseEntity<Page<Product>> searchProducts(
@RequestParam(required = false) String category,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice,
@PageableDefault(page = 0, size = 5, sort = "price") Pageable pageable
) {
Page<Product> results = productService.searchProducts(category, minPrice, maxPrice, pageable);
return ResponseEntity.ok(results);
}
}
🧩 Step 7: Example API Calls
Example Request Description /products/search?page=0&size=5Get first 5 products /products/search?category=Electronics&page=1&size=1010 products from Electronics, page 2 /products/search?minPrice=500&maxPrice=2000&sort=price,descProducts between ₹500–₹2000 sorted by descending price
🧾 Step 8: Example JSON Response
{
"content": [
{
"id": "P102",
"name": "Smartphone",
"price": 899.0,
"category": "Electronics"
},
{
"id": "P103",
"name": "Bluetooth Speaker",
"price": 750.0,
"category": "Electronics"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"offset": 0
},
"totalElements": 12,
"totalPages": 6,
"last": false
}
✅ Summary — Custom Repository with Pagination + Sorting
Feature Technique Used Dynamic filters Custom JPQL query using StringBuilder Pagination query.setFirstResult() + setMaxResults()Sorting ORDER BY from pageable.getSort()Page metadata PageImpl<>(results, pageable, total)Integration Works seamlessly with Spring Data’s Pageable
you’re now moving into advanced repository customization, where we’ll add multi-field keyword search (like “search by name, category, or description”) — just like real-world e-commerce or article search features.
Let’s go step-by-step again so it’s crystal clear 👇
🧩 Step 1: What We’re Building
We want a method that allows flexible keyword-based searching:
👉 Example searches:
-
"phone" → matches products with "phone" in name or description.
-
"electronics" → matches category "Electronics".
-
"speaker" → matches "Bluetooth Speaker" even if only in part.
We’ll combine this with our dynamic filters, pagination, and sorting — all in one powerful query.
🧩 Step 2: Update the Custom Repository Interface
Add a new method for keyword-based search:
package com.bej.products.repository;
import com.bej.products.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface CustomProductRepository {
Page<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice, Pageable pageable);
// 🔹 New keyword-based search
Page<Product> searchProductsByKeyword(String keyword, Pageable pageable);
}
🧩 Step 3: Implement the Keyword Search in the Impl Class
Now, implement it inside your ProductRepositoryImpl class.
package com.bej.products.repository;
import com.bej.products.model.Product;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class ProductRepositoryImpl implements CustomProductRepository {
@PersistenceContext
private EntityManager entityManager;
// Existing method
@Override
public Page<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice, Pageable pageable) {
StringBuilder jpql = new StringBuilder("SELECT p FROM Product p WHERE 1=1");
if (category != null && !category.isEmpty()) {
jpql.append(" AND p.category = :category");
}
if (minPrice != null) {
jpql.append(" AND p.price >= :minPrice");
}
if (maxPrice != null) {
jpql.append(" AND p.price <= :maxPrice");
}
if (pageable.getSort().isSorted()) {
jpql.append(" ORDER BY ").append(pageable.getSort().toString().replace(":", ""));
}
TypedQuery<Product> query = entityManager.createQuery(jpql.toString(), Product.class);
if (category != null && !category.isEmpty()) query.setParameter("category", category);
if (minPrice != null) query.setParameter("minPrice", minPrice);
if (maxPrice != null) query.setParameter("maxPrice", maxPrice);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Product> results = query.getResultList();
String countQueryStr = jpql.toString().replaceFirst("SELECT p FROM", "SELECT COUNT(p) FROM");
TypedQuery<Long> countQuery = entityManager.createQuery(countQueryStr, Long.class);
if (category != null && !category.isEmpty()) countQuery.setParameter("category", category);
if (minPrice != null) countQuery.setParameter("minPrice", minPrice);
if (maxPrice != null) countQuery.setParameter("maxPrice", maxPrice);
Long total = countQuery.getSingleResult();
return new PageImpl<>(results, pageable, total);
}
// 🔹 New: Multi-field keyword search
@Override
public Page<Product> searchProductsByKeyword(String keyword, Pageable pageable) {
String jpql = """
SELECT p FROM Product p
WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))
OR LOWER(p.category) LIKE LOWER(CONCAT('%', :keyword, '%'))
OR LOWER(p.description) LIKE LOWER(CONCAT('%', :keyword, '%'))
""";
TypedQuery<Product> query = entityManager.createQuery(jpql, Product.class);
query.setParameter("keyword", keyword);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Product> results = query.getResultList();
// 🔹 Count query for pagination
String countJpql = """
SELECT COUNT(p) FROM Product p
WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))
OR LOWER(p.category) LIKE LOWER(CONCAT('%', :keyword, '%'))
OR LOWER(p.description) LIKE LOWER(CONCAT('%', :keyword, '%'))
""";
TypedQuery<Long> countQuery = entityManager.createQuery(countJpql, Long.class);
countQuery.setParameter("keyword", keyword);
Long total = countQuery.getSingleResult();
return new PageImpl<>(results, pageable, total);
}
}
🧠Step 4: How the Keyword Search Works
Feature Explanation LOWER()Makes the search case-insensitive LIKE CONCAT('%', :keyword, '%')Matches anywhere inside the text Multi-field Searches across name, category, and description Pagination Uses offset and limit via Pageable Count query Ensures total count for proper Page object
So, if you search "phone", it matches:
-
"Smartphone" (name)
-
"Mobile Phones" (category)
-
"This phone has a 64MP camera" (description)
🧩 Step 5: Service Layer
Add a method for keyword search.
package com.bej.products.service;
import com.bej.products.model.Product;
import com.bej.products.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Page<Product> searchProductsByKeyword(String keyword, Pageable pageable) {
return productRepository.searchProductsByKeyword(keyword, pageable);
}
}
🧩 Step 6: Controller Layer
Expose a new endpoint for keyword search:
package com.bej.products.controller;
import com.bej.products.model.Product;
import com.bej.products.service.ProductService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/keyword-search")
public ResponseEntity<Page<Product>> searchByKeyword(
@RequestParam String keyword,
@PageableDefault(page = 0, size = 5, sort = "name") Pageable pageable
) {
Page<Product> results = productService.searchProductsByKeyword(keyword, pageable);
return ResponseEntity.ok(results);
}
}
🧩 Step 7: Example API Calls
Example Description /products/keyword-search?keyword=phoneSearch across name/category/description /products/keyword-search?keyword=mobile&page=1&size=5Paginated search /products/keyword-search?keyword=speaker&sort=price,descSearch + sort by descending price
🧾 Example JSON Output
{
"content": [
{
"id": "P105",
"name": "Bluetooth Speaker",
"category": "Electronics",
"price": 750.0,
"description": "Portable speaker with deep bass and clear sound"
},
{
"id": "P102",
"name": "Smartphone",
"category": "Electronics",
"price": 999.0,
"description": "High-performance smartphone with 64MP camera"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"offset": 0
},
"totalElements": 8,
"totalPages": 4
}
🧠Step 8: Summary
Feature Description Keyword Search Works across multiple fields (name, category, description) Case-Insensitive Uses LOWER() Pagination Uses Pageable Sorting Can sort by any field (e.g. sort=price,desc) Efficient Executes count query for total results Flexible Can combine with filters in future if needed
Would you like me to show you how to combine keyword search + filters (category, minPrice, maxPrice) into one single flexible search method (so the API supports both keyword and filters in one call)?
That’s the ultimate version — like Amazon-style search filtering.
Would you like me to show you how to combine keyword search + filters (category, minPrice, maxPrice) into one single flexible search method (so the API supports both keyword and filters in one call)?
That’s the ultimate version — like Amazon-style search filtering.
now you’re about to learn the ultimate version of a custom repository search system — the Amazon-style dynamic search, where you can combine:
-
✅ keyword search (multi-field)
-
✅ category filter
-
✅ price range filter
-
✅ pagination
-
✅ sorting
All in one flexible query, so users can search anything, filter it, and get fast paginated results.
Let’s build it step by step 👇
🧩 Step 1: Final Goal — Example Use Cases
Your final search API will handle all these cases in a single endpoint:
Example Request What It Does /products/advanced-search?keyword=phoneKeyword search /products/advanced-search?category=Electronics&minPrice=500&maxPrice=2000Filtered search /products/advanced-search?keyword=bluetooth&page=1&size=5&sort=price,descKeyword + Pagination + Sorting /products/advanced-search?keyword=smart&category=MobilesKeyword + Category
🧩 Step 2: Update the Custom Repository Interface
Add a new advanced search method:
package com.bej.products.repository;
import com.bej.products.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface CustomProductRepository {
Page<Product> findProductsWithDynamicFilter(String category, Double minPrice, Double maxPrice, Pageable pageable);
Page<Product> searchProductsByKeyword(String keyword, Pageable pageable);
// 🔹 New unified search method
Page<Product> advancedSearch(String keyword, String category, Double minPrice, Double maxPrice, Pageable pageable);
}
🧩 Step 3: Implement the advancedSearch() Method
Now, inside your ProductRepositoryImpl class, we’ll create a dynamic JPQL builder that combines keyword and filter conditions intelligently.
package com.bej.products.repository;
import com.bej.products.model.Product;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class ProductRepositoryImpl implements CustomProductRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Page<Product> advancedSearch(String keyword, String category, Double minPrice, Double maxPrice, Pageable pageable) {
StringBuilder jpql = new StringBuilder("SELECT p FROM Product p WHERE 1=1");
// 🔹 Keyword-based search across multiple fields
if (keyword != null && !keyword.isEmpty()) {
jpql.append(" AND (LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))");
jpql.append(" OR LOWER(p.category) LIKE LOWER(CONCAT('%', :keyword, '%'))");
jpql.append(" OR LOWER(p.description) LIKE LOWER(CONCAT('%', :keyword, '%')))");
}
// 🔹 Category filter
if (category != null && !category.isEmpty()) {
jpql.append(" AND p.category = :category");
}
// 🔹 Price filters
if (minPrice != null) {
jpql.append(" AND p.price >= :minPrice");
}
if (maxPrice != null) {
jpql.append(" AND p.price <= :maxPrice");
}
// 🔹 Sorting
if (pageable.getSort().isSorted()) {
jpql.append(" ORDER BY ");
jpql.append(pageable.getSort().toString().replace(":", ""));
}
// ✅ Create query
TypedQuery<Product> query = entityManager.createQuery(jpql.toString(), Product.class);
// 🔹 Set parameters dynamically
if (keyword != null && !keyword.isEmpty()) query.setParameter("keyword", keyword);
if (category != null && !category.isEmpty()) query.setParameter("category", category);
if (minPrice != null) query.setParameter("minPrice", minPrice);
if (maxPrice != null) query.setParameter("maxPrice", maxPrice);
// 🔹 Pagination
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
// 🔹 Execute query
List<Product> results = query.getResultList();
// 🔹 Count query for total records
String countJpql = jpql.toString().replaceFirst("SELECT p FROM", "SELECT COUNT(p) FROM");
TypedQuery<Long> countQuery = entityManager.createQuery(countJpql, Long.class);
if (keyword != null && !keyword.isEmpty()) countQuery.setParameter("keyword", keyword);
if (category != null && !category.isEmpty()) countQuery.setParameter("category", category);
if (minPrice != null) countQuery.setParameter("minPrice", minPrice);
if (maxPrice != null) countQuery.setParameter("maxPrice", maxPrice);
Long total = countQuery.getSingleResult();
// ✅ Return paginated result
return new PageImpl<>(results, pageable, total);
}
// (keep your previous methods too if you want)
}
🧠Step 4: How This Works Internally
Feature Explanation 1=1Makes adding conditions easier dynamically LOWER()Makes keyword search case-insensitive CONCAT('%', :keyword, '%')Allows partial matching Dynamic parameters Only set values if they are non-null Sorting Automatically handled by pageable.getSort() Pagination Uses pageable.getOffset() and setMaxResults() Counting Builds a separate count query for total results
🧩 Step 5: Service Layer
Call this advancedSearch() from your ProductService.
package com.bej.products.service;
import com.bej.products.model.Product;
import com.bej.products.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Page<Product> advancedSearch(String keyword, String category, Double minPrice, Double maxPrice, Pageable pageable) {
return productRepository.advancedSearch(keyword, category, minPrice, maxPrice, pageable);
}
}
🧩 Step 6: Controller Layer
Expose a single powerful endpoint for advanced search:
package com.bej.products.controller;
import com.bej.products.model.Product;
import com.bej.products.service.ProductService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/advanced-search")
public ResponseEntity<Page<Product>> advancedSearch(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice,
@PageableDefault(page = 0, size = 5, sort = "name") Pageable pageable
) {
Page<Product> results = productService.advancedSearch(keyword, category, minPrice, maxPrice, pageable);
return ResponseEntity.ok(results);
}
}
🧩 Step 7: Example API Calls and Behavior
Request Description /products/advanced-search?keyword=laptopSearch all products related to “laptop” /products/advanced-search?category=Electronics&minPrice=5000&maxPrice=50000Filter by category + price range /products/advanced-search?keyword=phone&category=Mobiles&sort=price,descKeyword + Category + Sorting /products/advanced-search?keyword=wireless&page=1&size=3Paginated search
🧾 Example JSON Response
{
"content": [
{
"id": "P107",
"name": "Wireless Headphones",
"category": "Electronics",
"price": 1500.0,
"description": "High-quality wireless headphones with noise cancellation"
},
{
"id": "P110",
"name": "Bluetooth Speaker",
"category": "Electronics",
"price": 1200.0,
"description": "Portable speaker with deep bass and clear sound"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"sort": [
{
"property": "price",
"direction": "DESC"
}
]
},
"totalElements": 12,
"totalPages": 6
}
🧠Step 8: Summary — Full Amazon-style Search Feature
Feature Supported Explanation ✅ Keyword search ✔ Matches name, category, description ✅ Category filter ✔ Exact category match ✅ Price range ✔ >= min and <= max✅ Case-insensitive ✔ Uses LOWER() ✅ Pagination ✔ Controlled via page and size ✅ Sorting ✔ Controlled via sort parameter ✅ Efficient ✔ Uses PageImpl + count query
🧠Tip:
If your data grows large, you can optimize further by:
-
Using Spring Data Specifications (Criteria API) for dynamic queries.
-
Adding database indexes on columns like name, category, price.
-
Using Elasticsearch for full-text search if scaling is needed.
https://chatgpt.com/share/6911d84b-87b4-8011-a001-9cd1139ab648
0 Comments