spring-data-jpathymeleafspecificationsdynamicquerymetamodel

Dynamic queries and JpaSpecificationExecutor in Spring


I am trying to create a simple Spring project where restaurants can add menu items to shared database and users can use a html form to search the dishes based on a range of criteria- especially dietary requirements

Form example:

Restaurant Name: Chez Hans
Gluten Free: (X)
Egg Free: (X)
Vegan: ()

Example SQL command

Select all FROM "dishes" Dish WHERE restaurant_name = "Chez Hans" AND egg_free = TRUE AND 
gluten_Free = TRUE;

A list of dishes that fit their criteria would then be returned to the user.

Any field in the form can be left blank, and not checking a box for example, "vegan" does not mean that criteria should be set as 'false', but rather not included within the query. Because of this it seemed the best way to handle the issue was using JpaSpecificationExecutor to create dynamic SQL queries (similar to the implementation in the answer to the problem below)

Filtering database rows with spring-data-jpa and spring-mvc

I have created a solution based on my research and prior knowledge. However, when I implement my solution, no dishes are returned- even though there are dishes in the database that fit the search criteria. No errors are being produced, but simply a blank table, so I am not sure where I am going wrong.

I have researched countless articles/videos regarding JpaSpecificationExecutor/dynamic queries but there are parts of the that theory I am still unsure about. This is what I gather about JpaSpecificationExecutor/dynamic queries (but I may be wrong)

  1. The meta model is not need to create dynamic queries but to verify the correctness of database query statements

  2. To create queries using meta-model classes a wrapper class is required (In my example- DishSearch)

  3. The following lines are to cast metamodel SingularAttribute class back to the original class value.

    Path dname = root.get(Dish_.dname); Path vegan= root.get(Dish_.vegan);

I am quite new to Spring and still finding it pretty difficult. Any help or advice would be very much appreciated!

Please see below my DishSpecification class:

package com.bron.demoJPA.specification;
    
public class DishSpecification implements Specification<Dish>  {
    
    private final DishSearch criteria;
    
    public DishSpecification(DishSearch ds) {
        criteria =ds;
    }

    @Override
    public Predicate toPredicate(Root<Dish> root, CriteriaQuery<?> query,
            CriteriaBuilder cb) {
  
        Path<String> dname = root.get(Dish_.dname);
        Path<Boolean> vegan= root.get(Dish_.vegan);
        Path<Boolean> eggFree= root.get(Dish_.eggFree);
        Path<Boolean> glutenFree= root.get(Dish_.glutenFree);
   
        final List<Predicate> predicates = new ArrayList<Predicate>();
        
        if(criteria.getDname()!=null) {
            predicates.add(cb.equal(dname, criteria.getDname()));
        }
        
        if(criteria.isVegan()!=false) {
            predicates.add(cb.equal(vegan, criteria.isVegan()));
        }
        
        if(criteria.isEggFree()!=false) {
            predicates.add(cb.equal(eggFree, criteria.isEggFree()));
        }
        
        if(criteria.isGlutenFree()!=false) {
            predicates.add(cb.equal(glutenFree, criteria.isGlutenFree()));
        }
    
        return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
}

Please see my DishSearch Class:

package com.bron.demoJPA.specification;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class DishSearch {

    private Long dishId;
    private String dname;
    private String description;
    private double price;
    private boolean vegan;
    private boolean glutenFree;
    private boolean eggFree;
    private AppUser app;

}

Please see my SearchController Class:

@Controller
public class SearchController {
    
    @Autowired
    DishRepository drep;
        
    @GetMapping("/showSearchForm")
    public String showNewDishForm(Model model) {
        // Create model attribute to bind form data
        DishSearch dishSearch = new DishSearch();
        model.addAttribute("dishSearch", dishSearch);
        return "search_Dish";
    }

    @PostMapping("/showDishList")
    public String saveUser(@ModelAttribute("dishSearch")DishSearch dishSearch) {
          Specification<Dish> spec = new DishSpecification(dishSearch);
            drep.findAll(spec); 
            return "show_dish_List";
            }
}

Please see my DishRepository Class:

@Repository
public interface DishRepository extends JpaRepository<Dish, Long>, JpaSpecificationExecutor<Dish>{
      
    @Transactional
    @Modifying
    List<Dish> findAll(Specification<Dish> spec);

  
}

Please see my search_Dish.html Thymeleaf Template:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="ISO-8859-1">
<title>Dish Management System</title>

<link rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

    <meta name="viewport" content="width=device-width, initial-scale=1">
    
</head>
<body>

<br>

<div class="col-sm-10 offset-sm-1 text-center">
    <div class="container"> 

        <h2> Manage Dishes </h2>
        <hr>
        </div>
    
        <form action="#" th:action="@{/showDishList}" th:object="${dishSearch}" method="POST">
        <div class="col-sm-10 offset-sm-1 text-center">
              <input type="text" th:field="*{dname}"
                placeholder="Dish Name" class="form-control mb-4 col-10">

        
                </div>
                
                
                

    <div class="form-check form-check-inline">
  
   <label class=" form-check-label" for="inlineCheckbox1 ">Vegan?</label>
   <input type="checkbox" th:field="*{vegan}" />
  
   <label class="form-check-label" for="inlineCheckbox1">Gluten Free?</label>
   <input type="checkbox" th:field="*{glutenFree}" />
   
   <label class="form-check-label" for="inlineCheckbox1">Egg Free?</label>
   <input type="checkbox" th:field="*{EggFree}" />
   </div>
   <br>
   <br>
   
   
                <br>
                <br>
                <button type="submit" class="btn btn-info col-4"> Search Database</button>
                
        </form>
        </div>
        <hr>
    

</body>
</html>

Please see my show_dish_List.html Thymeleaf Template:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>

<title>Search Results</title>
<link rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

</head>
<body>
    
<br>
<div class="col-sm-10 offset-sm-1 text-center">
<h1>Dish List</h1>
</div>

<table border="1" class="table table-striped table-responsive-md">
    <thead>
        <tr>
            <th>Dish Name</th>
            <th>Dish description</th>
            <th>Dish Price</th>
            <th>Restaurant</th>
        </tr>


    </thead>
    <tbody>
        <tr th:each="dishSearch : ${listDishSearch}">
            <td th:text="${dishSearch.dname}"></td>
            <td th:text="${dishSearch.description}"></td>
            <td th:text="${dishSearch.price}"></td>
        </tr>
                    
    </tbody>
    
</table>
<div class="col-sm-10 offset-sm-1 text-center">
 <a th:href="@{/showNewDishForm}"
    class="btn btn-primary btn-sm mb-3"> Search Again</a>
    </div>

----------------------------Update------------------------------

In addition to the answer provided below, in the Dish Specification Class I changed

if(criteria.getDname()!=null) {
            predicates.add(cb.equal(dname, criteria.getDname()));
        }

to

  if(criteria.getDname()!="") {
            predicates.add(cb.equal(dname, criteria.getDname()));
        }
    

and the search is working fine now!


Solution

  • I believe the issue is that you are not adding the result in the Model which is being used to render the page show_dish_List.html, therefore nothing is being populated in the UI. Your UI is expecting the data to be in listDishSearch and there is nothing in that variable.

    Update your code to:

    @PostMapping("/showDishList")
    public String saveUser(@ModelAttribute("dishSearch") DishSearch dishSearch, Model model) {
        Specification<Dish> spec = new DishSpecification(dishSearch);
    
        model.addAttribute("listDishSearch", drep.findAll(spec));
    
        return "show_dish_List";
    }
    

    and everything should be working fine.

    Remove the method findAll from your DishRepository repository. It is already being provided by the interface JpaSpecificationExecutor.