Spring Boot + thyymleaf:对列表进行过滤和分页



大家晚上好

我现在正在学习Thymeleaf和一般的web应用程序,对于初学者来说,我正在尝试实现一个web服务,其中您可以查看所有注册用户并对其进行过滤。

因为我想要一些分页,我在这个页面上有两个表单:

  • 一组链接到第一页、上一页、下一页和最后一页的按钮
  • 一个带有各种过滤选项的表单,例如"username contains"或"min/max age">

我的控制器是这样的:

    @RequestMapping("/users/all")
    String showSearchPage(@RequestParam(value="page", required=false, defaultValue = "0") int page,
                          @CurrentSecurityContext(expression="authentication?.name") String username,
                          Model model) {
        Page<User> userPage = userService.filterUsers(username, "", 0, 100, PageRequest.of(page, 10));
        model.addAttribute("userPage", userPage);
        model.addAttribute("pageNr", page);
        return "users.html";
    }

如您所见,我只实现了按钮,并且总是过滤一些默认值。(username参数确保当前登录的用户不会在列表中找到自己。)我的按钮表单是这样的:

<form class="button" th:action="@{/users/all}" method="POST">
    <button th:disabled="${pageNr == 0}" type="submit"
            class="btn btn-primary"
            name="page" th:value="0"><<</button>
    <button th:disabled="${pageNr == 0}" type="submit"
            class="btn btn-primary"
            name="page" th:value="${pageNr - 1}"><</button>
    <button th:disabled="${pageNr == userPage.getTotalPages - 1}" type="submit"
            class="btn btn-primary"
            name="page" th:value="${pageNr + 1}">></button>
    <button th:disabled="${pageNr == userPage.getTotalPages - 1}" type="submit"
            class="btn btn-primary"
            name="page" th:value="${userPage.getTotalPages - 1}">>></button>
</form>

所以我使用请求参数page只显示所请求的页面。

现在我要实现过滤,我的第一种方法是将表单添加到HTML中,并向控制器添加一些@ModelAttribute FilterForm filterForm,以便能够获得提交的过滤器值并使用它们来检索过滤的用户列表。但是,在思考的时候,我发现了两个表单都只提交自己的内容的问题,控制器只会得到两者中的一个。因此,在过滤用户之后,在更改页面时,我可能会无意中恢复到完整的用户列表。

确保过滤和分页这两个功能一起正常工作的最佳方法是什么?提前感谢!

我会使用HTTP GET方法而不是POST。为什么如此?读取用户是一个幂等且安全的操作。在这种情况下,应用的过滤器和页码可以很容易地添加书签。对于过滤器,你可以添加更多的参数。这没什么不好的。让它更宁静一点。"/用户/all"是不必要的。"/users"应该够了。

@GetMapping("/users")
String showSearchPage(@RequestParam(value="page", required=false, defaultValue = "0") int page, 
                      @RequestParam("minAge") Optional<Integer> minAge, 
                      @RequestParam("maxAge") Optional<Integer> maxAge,
                      @CurrentSecurityContext(expression="authentication?.name") String username,
                      Model model) {
    // Apply filters too...
    Page<User> userPage = userService.filterUsers(username, "", 0, 100, PageRequest.of(page, 10));
    model.addAttribute("userPage", userPage);
    model.addAttribute("pageNr", page);
    model.addAttribute("nextPage", getPageWithFilterUrl(page + 1, minAge, maxAge));
    model.addAttribute("previousPage", getPageWithFilterUrl(page - 1, minAge, maxAge));
    return "users.html";
}

来回移动时保留过滤器:

private String getPageWithFilterUrl(int page, Optional<Integer> minAge, Optional<Integer> maxAge) {
    String defaultNextPageUrl = "/users?page=" + page;
    String withMinAge = minAge.map(ma -> defaultNextPageUrl + "&minAge=" + ma).orElse(defaultNextPageUrl);
    String withMaxAge = maxAge.map(ma -> withMinAge + "&maxAge=" + ma).orElse(withMinAge);
    return withMaxAge;
}

我想这里还没有给出答案。我现在也有同样的问题。我正在尝试在我的项目与redirectAttributes.addFlashAttribute("searchProductItemDTO", searchProductItemDTO);但是当点击Next/Previous按钮时,问题就出现了——点击它们不会考虑到搜索条件。

Here is my total solution:
1) Pay attention here to the JpaSpecificationExecutor<ItemEntity>
@Repository
public interface AllItemsRepository   extends 
PagingAndSortingRepository<ItemEntity, Long>, JpaSpecificationExecutor<ItemEntity>{ 
}
2) Pay attention here to the CriteriaBuilder
public class ProductItemSpecification implements Specification<ItemEntity> {
    private final SearchProductItemDTO searchProductItemDTO;
    private final String type;
    public ProductItemSpecification(SearchProductItemDTO searchProductItemDTO, String type) {
        this.searchProductItemDTO = searchProductItemDTO;
        this.type = type;
    }
    @Override
    public Predicate toPredicate(Root<ItemEntity> root,
                                 CriteriaQuery<?> query,
                                 CriteriaBuilder cb) {
        Predicate predicate = cb.conjunction();
        predicate.getExpressions().add(cb.equal(root.get("type"), type));
        if (searchProductItemDTO.getModel() != null && !searchProductItemDTO.getModel().isBlank()) {
            Path<Object> model = root.get("model");
            predicate.getExpressions().add(
                    //!!!!! when we have two relationally connected tables
//                    cb.and(cb.equal(root.join("model").get("name"), searchProductItemDTO.getModel()));
                    //when all fields are from the same table ItemEntity:::: the like works case insensitive
   cb.and(cb.like(root.get("model").as(String.class), "%" + searchProductItemDTO.getModel() + "%"))
            );
        }
        if (searchProductItemDTO.getMinPrice() != null) {
            predicate.getExpressions().add(
                    cb.and(cb.greaterThanOrEqualTo(root.get("sellingPrice"),
                            searchProductItemDTO.getMinPrice()))
            );
        }
        if (searchProductItemDTO.getMaxPrice() != null) {
            predicate.getExpressions().add(
                    cb.and(cb.lessThanOrEqualTo(root.get("sellingPrice"),
                            searchProductItemDTO.getMaxPrice()))
            );
        }
        return predicate;
    }
}

3) Pay attention here to the this.allItemsRepository.findAll default usage
//Complicated use
public Page<ComputerViewGeneralModel> getAllComputersPageableAndSearched(
        Pageable pageable, SearchProductItemDTO searchProductItemDTO, String type) {
    Page<ComputerViewGeneralModel> allComputers = this.allItemsRepository
            .findAll(new ProductItemSpecification(searchProductItemDTO, type), pageable)
            .map(comp -> this.structMapper
                    .computerEntityToComputerSalesViewGeneralModel((ComputerEntity) comp));
    return allComputers;
}

4) Pay attention here to the redirectAttributes.addFlashAttribute
@Controller
@RequestMapping("/items/all")
public class ViewItemsController {
    private final ComputerService computerService;
@GetMapping("/computer")
public String viewAllComputers(Model model,
                               @Valid SearchProductItemDTO searchProductItemDTO,
                               @PageableDefault(page = 0,
                                       size = 3,
                                       sort = "sellingPrice",
                                       direction = Sort.Direction.ASC) Pageable pageable,
                               RedirectAttributes redirectAttributes) {
    if (!model.containsAttribute("searchProductItemDTO")) {
        model.addAttribute("searchProductItemDTO", searchProductItemDTO);
    }
    Page<ComputerViewGeneralModel> computers = this.computerService
            .getAllComputersPageableAndSearched(pageable, searchProductItemDTO, "computer");
    model.addAttribute("computers", computers);
    redirectAttributes.addFlashAttribute("searchProductItemDTO", searchProductItemDTO);
    return "/viewItems/all-computers";
}
5) Pay attention here to all the search params that we add in the html file, in the 4 sections where pagination navigation
<main>
    <div class="container-fluid">
        <div class="container">
            <h2 class="text-center text-white">Search for offers</h2>
            <form
                    th:method="GET"
                    th:action="@{/items/all/computer}"
                    th:object="${searchProductItemDTO}"
                    class="form-inline"
                    style="justify-content: center; margin-top: 50px;"
            >
                <div style="position: relative">
                    <input
                            th:field="*{model}"
                            th:errorclass="is-invalid"
                            class="form-control mr-sm-2"
                            style="width: 280px;"
                            type="search"
                            placeholder="Model name case Insensitive..."
                            aria-label="Search"
                            id="model"
                    />
                    <input
                            th:field="*{minPrice}"
                            th:errorclass="is-invalid"
                            class="form-control mr-sm-2"
                            style="width: 280px;"
                            type="search"
                            placeholder="Min price..."
                            aria-label="Search"
                            id="minPrice"
                    />
                    <input
                            th:field="*{maxPrice}"
                            th:errorclass="is-invalid"
                            class="form-control mr-sm-2"
                            style="width: 280px;"
                            type="search"
                            placeholder="Max price..."
                            aria-label="Search"
                            id="maxPrice"
                    />
                </div>
                <button class="btn btn-outline-info my-2 my-sm-0" type="submit">Search</button>
            </form>
        </div>
        <h2 class="text-center text-white mt-5 greybg" th:text="#{view_all_computers}">.........All
            Computers.......</h2>
        <div class="offers row mx-auto d-flex flex-row justify-content-center .row-cols-auto">
            <div
                    th:each="c : ${computers}" th:object="${c}"
                    class="offer card col-sm-2 col-md-3  col-lg-3 m-2 p-0">
                <div class="card-img-top-wrapper" style="height: 20rem">
                    <img
                            class="card-img-top"
                            alt="Computer image"
                            th:src="*{photoUrl}">
                </div>
                <div class="card-body pb-1">
                    <h5 class="card-title"
                        th:text="' Model: ' + *{model}">
                        Model name</h5>
                </div>
                <ul class="offer-details list-group list-group-flush">
                    <li class="list-group-item">
                        <div class="card-text"><span th:text="'* ' + *{processor}">Processor</span></div>
                        <div class="card-text"><span th:text="'* ' + *{videoCard}">Video card</span></div>
                        <div class="card-text"><span th:text="'* ' + *{ram}">Ram</span></div>
                        <div class="card-text"><span th:text="'* ' + *{disk}">Disk</span></div>
                        <div th:if="*{!ssd.isBlank()}" class="card-text"><span th:text="'* ' + *{ssd}">SSD</span></div>
                        <div th:if="*{!moreInfo.isBlank()}" class="card-text"><span th:text="'* ' + *{moreInfo}">More info</span>
                        </div>
                        <div class="card-text"><span th:text="'We sell at: ' + *{sellingPrice} + ' лв'"
                                                     style="font-weight: bold">Selling price</span></div>
                    </li>
                </ul>
                <div class="card-body">
                    <div class="row">
                        <a class="btn btn-link"
                           th:href="@{/items/all/computer/details/{id} (id=*{itemId})}">Details</a>
                        <th:block sec:authorize="hasRole('ADMIN') || hasRole('EMPLOYEE_PURCHASES')">
                            <a class="btn btn-link alert-danger"
                               th:href="@{/pages/purchases/computers/{id}/edit (id=*{itemId})}">Update</a>
                            <form th:action="@{/pages/purchases/computers/delete/{id} (id=*{itemId})}"
                                  th:method="delete">
                                <input type="submit" class="btn btn-link alert-danger" value="Delete"></input>
                            </form>
                        </th:block>
                    </div>
                </div>
            </div>
        </div>
        <div class="container-fluid row justify-content-center">
            <nav>
                <ul class="pagination">
                    <li class="page-item" th:classappend="${computers.isFirst()} ? 'disabled' : ''">
                        <a th:unless="${computers.isFirst()}"
                           class="page-link"
                           th:href="@{/items/all/computer(size=${computers.getSize()},page=0,model=${searchProductItemDTO.getModel()}, minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">First</a>
                    </li>
                </ul>
            </nav>
            <nav>
                <ul class="pagination">
                    <li class="page-item" th:classappend="${computers.hasPrevious() ? '' : 'disabled'}">
                        <a th:if="${computers.hasPrevious()}"
                           class="page-link"
                           th:href="@{/items/all/computer(size=${computers.getSize()},page=${computers.getNumber() - 1},model=${searchProductItemDTO.getModel()}, minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">Previous</a>
                    </li>
                </ul>
            </nav>
            <nav>
                <ul class="pagination">
                    <li class="page-item" th:classappend="${computers.hasNext() ? '' : 'disabled'}">
                        <a th:if="${computers.hasNext()}"
                           class="page-link"
                           th:href="@{/items/all/computer(size=${computers.getSize()},page=${computers.getNumber() + 1},model=${searchProductItemDTO.getModel()}, minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">Next</a>
                    </li>
                </ul>
            </nav>
            <nav>
                <ul class="pagination">
                    <li class="page-item" th:classappend="${computers.isLast()} ? 'disabled' : ''">
                        <a th:unless="${computers.isLast()}"
                           class="page-link"
                           th:href="@{/items/all/computer(size=${computers.getSize()},page=${computers.getTotalPages()-1},model=${searchProductItemDTO.getModel()},minPrice=${searchProductItemDTO.getMinPrice()},maxPrice=${searchProductItemDTO.getMaxPrice()})}">Last</a>
                    </li>
                </ul>
            </nav>`enter code here`
        </div>
    </div>
</main>

最新更新