Springboot 系列 (5) - 在 Spring Boot 项目里使用 JPA 和 Thymeleaf 实现分页显示
JPA 是Java Persistence API 的简称,它是 Sun 公司在充分吸收现有ORM框架(Hibernate)的基础上,开发而来的一个Java EE 5.0 平台标准的开源的对象关系映射(ORM)规范。
Hibernate 与 JPA 的关系:
Hibernate 是一个开放源代码的对象关系映射(ORM)框架,它对 JDBC 进行了非常轻量级的对象封装,将 POJO 与 数据库表建立映射关系,是一个全自动的ORM框架,Hibernate 可以自动生成 SQL 语句,自动执行,使 Java 程序员可以方便地使用面向对象思维来操作数据库。
而 JPA 是 Sun 官方提出的 Java 持久化规范,而 JPA 是在充分吸收 Hibernate、TopLink 等 ORM 框架的基础上发展而来的。
JPA 是持久化的关系映射规范、接口 API,而 Hibernate 是其实现。
1. 开发环境
Windows版本:Windows 10 Home (20H2)
IntelliJ IDEA (https://www.jetbrains.com/idea/download/):Community Edition for Windows 2020.1.4
Apache Maven (https://maven.apache.org/):3.8.1
注:Spring 开发环境的搭建,可以参考 “ Spring基础知识(1)- Spring简介、Spring体系结构和开发环境配置 ”。
2. 创建 Spring Boot 基础项目
项目实例名称:SpringbootExample05
Spring Boot 版本:2.6.6
创建步骤:
(1) 创建 Maven 项目实例 SpringbootExample05;
(2) Spring Boot Web 配置;
(3) 导入 Thymeleaf 依赖包;
(4) 配置静态资源(jQuery、Bootstrap、Images);
具体操作请参考 “Springboot 系列 (2) - 在 Spring Boot 项目里使用 Thymeleaf、JQuery+Bootstrap 和国际化” 里的项目实例 SpringbootExample02,文末包含如何使用 spring-boot-maven-plugin 插件运行打包的内容。
SpringbootExample05 和 SpringbootExample02 相比,SpringbootExample05 不包含 Thymeleaf 模版文件(templates/*.htm l)和国际化。
3. 配置数据库(MariaDB)
1) XAMPP for Windows
https://www.apachefriends.org/download.html
本文安装版本 7.4.25,用到 XAMPP 的如下功能:
+ Apache 2.4.51
+ MariaDB 10.4.21 (MySQL的分支)
+ PHP 7.4.25 (VC15 X86 64bit thread safe) + PEAR
+ phpMyAdmin 5.1.1
2) 创建数据库 springboot_example 和 product 表,SQL 脚本如下
1 CREATE TABLE `product` ( 2 `id` int(11) NOT NULL, 3 `name` varchar(50) NOT NULL, 4 `description` varchar(100) DEFAULT NULL, 5 `price` double DEFAULT NULL, 6 `createtime` timestamp NULL DEFAULT NULL 7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 8 9 ALTER TABLE `product` ADD PRIMARY KEY (`id`); 10 11 ALTER TABLE `product` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 12 13 INSERT INTO `product` (`id`, `name`, `description`, `price`, `createtime`) VALUES 14 (1, 'apple', 'Red apple', 7.84, '2020-01-01 01:01:01'), 15 (2, 'orange', 'Yellow orange', 11.6, '2020-02-02 02:02:02'), 16 (3, 'phone', 'Android phone', 105.30, '2020-03-03 03:03:03'), 17 (4, 'tea', 'Green tea', 6.3, '2020-04-04 04:04:04'), 18 (5, 'paper', 'White paper A4', 3.5, '2020-05-05 05:05:05'), 19 (6, 'water', 'Pure water', 1.2, '2020-06-06 06:06:06'), 20 (7, 'test1', 'Test description 1', 1.0, '2020-07-07 07:07:07'), 21 (8, 'test2', 'Test description 2', 2.0, '2020-08-08 08:08:08'), 22 (9, 'test3', 'Test description 3', 3.0, '2020-09-09 09:09:09'), 23 (10, 'test4', 'Test description 4', 4.0, '2020-10-10 10:10:10'), 24 (11, 'test5', 'Test description 5', 5.0, '2020-11-11 11:11:11'), 25 (12, 'test6', 'Test description 6', 6.0, '2020-12-12 12:12:12'), 26 (13, 'test7', 'Test description 7', 7.0, '2020-12-13 13:13:13'), 27 (14, 'test8', 'Test description 8', 8.0, '2020-12-14 14:14:14'), 28 (15, 'test9', 'Test description 9', 9.0, '2020-12-15 15:15:15'), 29 (16, 'test10', 'Test description 10', 10.0, '2020-12-16 16:16:16'), 30 (17, 'test11', 'Test description 11', 11.0, '2020-12-17 17:17:17');
注:定义表名时,不要和 SQL 的关键字同名
3)创建 src/main/java/com/example/entity/Product.java 文件
1 package com.example.entity; 2 3 import java.util.Date; 4 import javax.persistence.Column; 5 import javax.persistence.Entity; 6 import javax.persistence.Id; 7 import javax.persistence.Table; 8 import javax.persistence.GenerationType; 9 import javax.persistence.GeneratedValue; 10 11 @Entity 12 @Table(name = "product") 13 public class Product { 14 @Id 15 @GeneratedValue(strategy=GenerationType.AUTO) 16 private Integer id; 17 @Column(name = "name", length = 50) 18 private String name; 19 @Column(name = "description", length = 100) 20 private String description; 21 @Column(name = "price") 22 private Double price; 23 @Column(name = "createtime") 24 private Date createtime; 25 26 public Product() { 27 28 } 29 30 public Integer getId() { 31 return id; 32 } 33 public void setId(Integer id) { 34 this.id = id; 35 } 36 37 public String getName() { 38 return name; 39 } 40 public void setName(String name) { 41 this.name = name; 42 } 43 44 public String getDescription() { 45 return description; 46 } 47 public void setDescription(String description) { 48 this.description = description; 49 } 50 51 public Double getPrice() { 52 return price; 53 } 54 public void setPrice(Double price) { 55 this.price = price; 56 } 57 58 public Date getCreatetime() { 59 return createtime; 60 } 61 public void setCreatetime(Date createtime) { 62 this.createtime = createtime; 63 } 64 65 @Override 66 public String toString() { 67 return "Product {" + 68 "name = " + name + 69 ", description = " + description + 70 ", price = " + price + 71 ", createtime = " + createtime + 72 '}'; 73 } 74 }
4. 配置 JPA
1) 修改 pom.xml,导入 JPA 依赖包
1 <project ... > 2 ... 3 <dependencies> 4 ... 5 6 <!-- MariaDB --> 7 <dependency> 8 <groupId>org.mariadb.jdbc</groupId> 9 <artifactId>mariadb-java-client</artifactId> 10 </dependency> 11 <!-- JDBC --> 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-data-jdbc</artifactId> 15 </dependency> 16 <!-- JPA --> 17 <dependency> 18 <groupId>org.springframework.boot</groupId> 19 <artifactId>spring-boot-starter-data-jpa</artifactId> 20 </dependency> 21 22 ... 23 </dependencies> 24 25 ... 26 </project>
在IDE中项目列表 -> SpringbootExample05 -> 点击鼠标右键 -> Maven -> Reload Project
2) 修改 src/main/resources/application.properties 文件
1 spring.main.banner-mode=off 2 3 # Web server 4 server.display-name=SpringbootExample05-Test 5 server.address=localhost 6 server.port=9090 7 8 # 数据源连接信息 9 spring.datasource.driver-class-name=org.mariadb.jdbc.Driver 10 spring.datasource.url=jdbc:mysql://localhost:3306/springboot_example 11 spring.datasource.username=root 12 spring.datasource.password=123456 13 14 # JPA 相关配置 15 #spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect 16 spring.jpa.show-sql=true 17 #spring.jpa.properties.hibernate.format_sql=true 18 #spring.jpa.hibernate.ddl-auto=update
3) 创建 src/main/java/com/example/dao/ProductJpaRepository.java 文件
1 package com.example.dao; 2 3 import org.springframework.data.jpa.repository.JpaRepository; 4 5 import com.example.entity.Product; 6 7 public interface ProductJpaRepository extends JpaRepository<Product, Integer> { 8 9 // 10 }
注:在 ProductJpaRepository 里无需重写和重载方法。
5. 视图和控制器
1) 创建 src/main/resources/templates/common.html 文件
1 <div th:fragment="header(var)"> 2 <meta charset="UTF-8"> 3 <title th:text="${var}">Title</title> 4 <link rel="stylesheet" th:href="@{/lib/bootstrap-4.2.1-dist/css/bootstrap.min.css}" href="/lib/bootstrap-4.2.1-dist/css/bootstrap.min.css"> 5 <script language="javascript" th:src="@{/lib/jquery/jquery-3.6.0.min.js}" src="/lib/jquery/jquery-3.6.0.min.js"></script> 6 <script language="javascript" th:src="@{/lib/bootstrap-4.2.1-dist/js/bootstrap.min.js}" src="/lib/bootstrap-4.2.1-dist/js/bootstrap.min.js"></script> 7 </div> 8 9 <div th:fragment="content-header" class="container" id="content-header-id"> 10 <nav class="navbar navbar-light bg-light"> 11 <a class="navbar-brand" href="#"> 12 <img th:src="@{/images/bootstrap-solid.svg}" src="/images/bootstrap-solid.svg" width="30" height="30" class="d-inline-block align-top" alt=""> 13 Thymeleaf Demo 14 </a> 15 16 <a class="nav-link" th:href="@{/logout}" th:if="${session.loginUser} != null">Logout</a> 17 </nav> 18 </div> 19 20 <div th:fragment="content-footer(var)" class="container" id="content-footer-id"> 21 <p th:text="${var}">Content Footer</p> 22 </div>
2) 创建 src/main/resources/templates/product/index.html 文件
1 <!DOCTYPE html> 2 <html lang="en" xmlns:th="http://www.thymeleaf.org"> 3 <head th:include="common::header(var='Product Index')"> 4 </head> 5 <body> 6 <div th:replace="common::content-header"></div> 7 8 <div class="container" id="content" th:style="'min-height: 480px; padding: 10px;'"> 9 <h4>Product Index Page</h4> 10 11 <p> </p> 12 <div class="alert alert-info" role="alert" th:text="${message}" th:if ="${message} != null"></div> 13 14 <table class="table table-bordered"> 15 <thead> 16 <tr> 17 <th scope="col">#</th> 18 <th scope="col">Name</th> 19 <th scope="col">Description</th> 20 <th scope="col">Price</th> 21 <th scope="col">Create Time</th> 22 </tr> 23 </thead> 24 <tbody th:if="${pageData} != null"> 25 <tr th:each="row:${pageData}" > 26 <td th:object="${row}" th:text="*{id}"></td> 27 <td th:object="${row}" th:text="*{name}"></td> 28 <td th:object="${row}" th:text="*{description}"></td> 29 <td th:object="${row}" th:text="*{price}"></td> 30 <td th:object="${row}" th:text="*{createtime}"></td> 31 </tr> 32 </tbody> 33 <tbody th:if ="${pageData} == null"> 34 <tr> 35 <td colspan="5">No record</td> 36 </tr> 37 </tbody> 38 </table> 39 40 <nav aria-label="Page navigation example" th:if="${pageData.getTotalPages() gt 1}"> 41 <ul class="pagination justify-content-end"> 42 43 <li th:if="${pageData.hasPrevious()}" class="page-item"> 44 <a th:href="@{/product?index=1}" href="/product?index=1" class="page-link"><<</a> 45 </li> 46 <li th:if="${pageData.hasPrevious()}" class="page-item"> 47 <a th:href="@{/product?index={v}&step={s}(v=${pageData.getNumber()},s=${pageData.getSize()})}" href="/product?index=&step=" class="page-link"><</a> 48 </li> 49 50 <li th:unless="${pageData.hasPrevious()}" class="page-item disabled"><a href="#" class="page-link"><<</a></li> 51 <li th:unless="${pageData.hasPrevious()}" class="page-item disabled"><a href="#" class="page-link"><</a></li> 52 53 <li th:if="${pageData.getTotalPages() le 7}" th:each="i:${#numbers.sequence(1, pageData.getTotalPages())} " th:class="${i eq pageData.getNumber()+1}?'page-item active':'page-item'"> 54 <a th:href="@{/product?index={v}&step={s}(v=${i}, s=${pageData.getSize()})}" href="/product?index=&step=" th:text="${i}" class="page-link"></a> 55 </li> 56 57 <li th:if="${pageData.getTotalPages() gt 7}" th:each="i:${#numbers.sequence(1, 3)}" th:class="${i eq pageData.getNumber()+1}?'page-item active':'page-item'"> 58 <a th:href="@{/product?index={v}&step={s}(v=${i}, s=${pageData.getSize()})}" href="/product?index=&step=" th:text="${i}" class="page-link"></a> 59 </li> 60 <li th:if="${pageData.getTotalPages() gt 7}" th:class="'page-item disabled'"> 61 <a href="#" class="page-link">...</a> 62 </li> 63 <li th:if="${pageData.getTotalPages() gt 7}" th:each="i:${#numbers.sequence(pageData.getTotalPages()-2, pageData.getTotalPages())}" th:class="${i eq pageData.getNumber()+1}?'page-item active':'page-item'"> 64 <a th:href="@{/product?index={v}&step={s}(v=${i}, s=${pageData.getSize()})}" href="/product?index=&step=" th:text="${i}" class="page-link"></a> 65 </li> 66 67 <li th:if="${pageData.hasNext()}" class="page-item"> 68 <a th:href="@{/product?index={v}&step={s}(v=${pageData.getNumber()+2},s=${pageData.getSize()})}" href="/product?index=&step=" class="page-link">></a> 69 </li> 70 <li th:if="${pageData.hasNext()}" class="page-item"> 71 <a th:href="@{/product?index={v}(v=${pageData.getTotalPages()})}" href="/product?index=" class="page-link">>></a> 72 </li> 73 74 <li th:unless="${pageData.hasNext()}" class="page-item disabled"><a href="#" class="page-link">></a></li> 75 <li th:unless="${pageData.hasNext()}" class="page-item disabled"><a href="#" class="page-link">>></a></li> 76 77 </ul> 78 </nav> 79 80 </div> 81 82 <div th:replace="common::content-footer(var='Copyright © 2020')"></div> 83 84 <script type="text/javascript"> 85 $(document).ready(function(){ 86 console.log("jQuery is running"); 87 }); 88 </script> 89 </body> 90 </html>
注:本示例,页数小于等于 7 时,显示全部页码。大于 7 时,只显示前3个页码和后3个页码,其余用省略号替代。
3) 创建 src/main/java/com/example/controller/ProductController.java 文件
1 package com.example.controller; 2 3 import javax.servlet.http.HttpServletRequest; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.ui.Model; 7 import org.springframework.stereotype.Controller; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 import org.springframework.web.bind.annotation.RequestMethod; 10 import org.springframework.util.StringUtils; 11 12 import org.springframework.data.domain.Page; 13 import org.springframework.data.domain.PageRequest; 14 import org.springframework.data.domain.Pageable; 15 import org.springframework.data.domain.Sort; 16 17 import com.example.entity.Product; 18 import com.example.dao.ProductJpaRepository; 19 20 @Controller 21 public class ProductController { 22 @Autowired 23 private ProductJpaRepository productJpaRepository; 24 25 @RequestMapping(value = "/product", method = RequestMethod.GET) 26 public String product(HttpServletRequest request, Model model) { 27 28 Integer index = 1; 29 Integer step = 5; 30 String strIndex = request.getParameter("index"); 31 if (StringUtils.hasText(strIndex)) { 32 index = Integer.valueOf(strIndex); 33 if (index < 1) 34 index = 1; 35 } 36 String strStep = request.getParameter("step"); 37 if (StringUtils.hasText(strStep)) { 38 step = Integer.valueOf(strStep); 39 if (step < 5) 40 step = 5; 41 } 42 43 Sort sort = Sort.by(Sort.Direction.ASC, "id"); 44 Pageable pageable= PageRequest.of(index-1, step, sort ); 45 Page<Product> pageData = productJpaRepository.findAll(pageable); 46 47 model.addAttribute("pageData", pageData); 48 49 return "product/index"; 50 } 51 }
注:PageRequest 的 page 参数是从 0 开始的,所以 index-1,对应 pageData.getNumber() 取到的当前页也是从 0 开始的。
运行并访问 http://localhost:9090/product
--------------------------------------
示例代码:https://gitee.com/slksm/public-codes/tree/master/demos/springboot-series/SpringbootExample05
浙公网安备 33010602011771号