The key concept in understanding how the Repository layer (especially in Spring Data JPA) works

 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

CaseCan You Use Your Own Method Name?Requirement
✅ Derived query (like findByName)No — must follow naming patternSpring auto-generates query
✅ Annotated query (@Query)YesYou write your own JPQL/SQL
✅ Custom implementation (in a custom repository class)YesYou write the logic manually in an implementation class
❌ Arbitrary name without @Query or implementationNoSpring 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

LayerFilePurpose
ProductRepositoryExtends JpaRepository + CustomProductRepositoryDeclares custom + normal methods
CustomProductRepositoryInterfaceDeclares signatures for custom methods
ProductRepositoryImplImplementation classImplements those custom methods manually
ServiceUses the repositoryInvokes the custom method
ControllerExposes endpointLets 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

StepOperation
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 RequestDescription
/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

FeatureTechnique Used
Dynamic filtersCustom JPQL query using StringBuilder
Paginationquery.setFirstResult() + setMaxResults()
SortingORDER BY from pageable.getSort()
Page metadataPageImpl<>(results, pageable, total)
IntegrationWorks 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

FeatureExplanation
LOWER()Makes the search case-insensitive
LIKE CONCAT('%', :keyword, '%')Matches anywhere inside the text
Multi-fieldSearches across name, category, and description
PaginationUses offset and limit via Pageable
Count queryEnsures 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

ExampleDescription
/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

FeatureDescription
Keyword SearchWorks across multiple fields (name, category, description)
Case-InsensitiveUses LOWER()
PaginationUses Pageable
SortingCan sort by any field (e.g. sort=price,desc)
EfficientExecutes count query for total results
FlexibleCan 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 RequestWhat 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

FeatureExplanation
1=1Makes adding conditions easier dynamically
LOWER()Makes keyword search case-insensitive
CONCAT('%', :keyword, '%')Allows partial matching
Dynamic parametersOnly set values if they are non-null
SortingAutomatically handled by pageable.getSort()
PaginationUses pageable.getOffset() and setMaxResults()
CountingBuilds 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

RequestDescription
/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

FeatureSupportedExplanation
✅ Keyword searchMatches name, category, description
✅ Category filterExact category match
✅ Price range>= min and <= max
✅ Case-insensitiveUses LOWER()
✅ PaginationControlled via page and size
✅ SortingControlled via sort parameter
✅ EfficientUses 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

Post a Comment

0 Comments