大家晚上好
我现在正在学习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>