数据访问
JDBC 场景
-
导入 JDBC 场景启动器依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency>
-
JDBC 场景启动器中没有导入数据库驱动,需要手动导入依赖。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
-
在 application.yaml 中配置数据库连接信息。
(1)数据源相关的配置:spring.datasource
,当我们没有自定义数据库连接池时,SpringBoot 默认使用 HikariDataSource 数据库连接池。
(2)JDBC 场景启动器中已将 JdbcTemplate 对象保存到 IOC 容器中了,修改 JdbcTemplate 相关的配置:spring.jdbc
#数据库 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book?serverTimezone=UTC&rewriteBatchedStatements=true username: root password: 密码 #指定使用的数据库连接池 type: com.zaxxer.hikari.HikariDataSource #JdbcTemplate jdbc: template: #超时时间 query-timeout: 5
-
测试。
@Slf4j @SpringBootTest class WebApplicationTests { @Autowired private JdbcTemplate jdbcTemplate; @Test void contextLoads() { String sql = "select count(*) from book"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); log.info("总记录数为:{}", count); } }
Druid
使用 Druid 数据库连接池
官方Github地址,中文版Document地址
- 当我们没有自定义数据库连接池时,SpringBoot 默认使用 HikariDataSource 数据库连接池;
- 当我们向容器中添加了自定义的数据库连接池后,SpringBoot 就会使用我们自定义的数据库连接池。
- 当我们自定义了多个数据库连接池时,可以使用
spring.datasource.type=
指定要使用的数据库连接池。
实现步骤:
-
引入 Druid 数据库连接池依赖。
<properties> <druid-version>1.1.23</druid-version> </properties> <!--druid数据库连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid-version}</version> </dependency>
-
在配置类中使用 @Bean 将 druid 对象添加到 IOC 容器中,使用
@ConfigurationProperties(prefix = "spring.datasource")
,将配置文件中的 url 等属性,与 druid 对象绑定。@Configuration public class DruidDataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource(){ DruidDataSource druidDataSource= new DruidDataSource(); return druidDataSource; } }
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book?serverTimezone=UTC&rewriteBatchedStatements=true username: root password: 密码 #指定使用的数据库连接池 # type: com.alibaba.druid.pool.DruidDataSource
-
测试。
@Slf4j @SpringBootTest class WebApplicationTests { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private DataSource dataSource; @Test void contextLoads() { String sql = "select count(*) from book"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); log.info("总记录数为:{}", count);//总记录数为:25 log.info("dataSource: {}", dataSource.getClass());//dataSource: class com.alibaba.druid.pool.DruidDataSource } }
开启 Druid 的监控功能
使用 Druid 的内置监控页面
向 IOC 容器中添加:com.alibaba.druid.support.http.StatViewServlet
,并设置处理的请求路径为:/druid/*
@Bean
public ServletRegistrationBean<StatViewServlet> statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> servletRegistrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
return servletRegistrationBean;
}
为监控页添加用户名和密码:
@Bean
public ServletRegistrationBean<StatViewServlet> statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> servletRegistrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
//为监控页添加用户名和密码
servletRegistrationBean.addInitParameter("loginUsername","admin");
servletRegistrationBean.addInitParameter("loginPassword","admin");
return servletRegistrationBean;
}
打开 Druid 的 SQL 监控统计功能
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
//打开 Druid 的 SQL 监控统计功能
druidDataSource.setFilters("stat");
return druidDataSource;
}
打开 Druid 的 Web、URI、Session 监控功能
@Bean
public FilterRegistrationBean<WebStatFilter> webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
打开 Druid 的 SQL 防火墙功能
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
//打开 Druid 的 SQL 监控统计功能
//打开 Druid 的 SQL 防火墙功能
druidDataSource.setFilters("stat,wall");
return druidDataSource;
}
使用场景启动器实现 Druid
druid-spring-boot-starter官方文档地址
SQL防火墙配置项都有哪些
-
引入 Druid 场景启动器。
引入之后,会自动在 application.yaml 中查找spring.datasource
信息,创建 DataSource 数据库连接池对象,并放进 IOC 容器中,不需要我们自己编写配置类、创建连接池对象、再放到容器中了。<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency>
-
可以修改的配置项:
(1)spring.datasource.druid
:Druid 的拓展配置。
(2)spring.datasource.druid.aop-patterns
:配置 Spring 监控。
(3)spring.datasource.druid.stat-view-servlet
:配置监控页。
(4)spring.datasource.druid.filter
:配置 SQL 统计、防火墙等。
(5)spring.datasource.druid.web-stat-filter
:配置 Web应用、URI监控、Session监控。 -
在配置文件中进行配置,开启功能。
spring: #数据库 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book?serverTimezone=UTC&rewriteBatchedStatements=true username: root password: 密码 #指定使用的数据库连接池 #type: com.alibaba.druid.pool.DruidDataSource # 配置druid数据源的拓展配置 druid: # 开启监控页 stat-view-servlet: enabled: true login-username: admin login-password: admin url-pattern: /druid/* reset-enable: false # 开启 SQL 统计和防火墙 filters: stat,wall # 配置 SQL 统计 filter: stat: enabled: true # 慢查询毫秒数 slow-sql-millis: 2000 # 记录慢查询 log-slow-sql: true # 配置 SQL 防火墙 wall: enabled: true config: # 不允许删表 drop-table-allow: false # 配置 Web应用、URI监控、Session监控 web-stat-filter: enabled: true url-pattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' # 配置 Spring 监控 aop-patterns: com.mcc.springboot.* # 监控改包下的所有SpringBean
整合 MyBatis
引入 MyBatis 场景启动器、配置环境
-
引入 mybatis-spring-boot-starter。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency>
-
在 application.yaml 中,使用
mybatis
配置相关属性。其中,mybatis.config_location
:配置核心配置文件位置。mybatis.mapper-locations
:配置映射文件位置。
注意:核心配置文件中的相关配置,都被整合到mybatis
的相关属性中了,只需要配置这些属性,即可实现核心配置文件的全部功能,因此,在 SpringBoot 整合 Mybatis 时,不需要编写核心配置文件,只需编写映射文件即可。# 配置Mybatis mybatis: # 映射文件位置 mapper-locations: classpath:/mapper/*.xml # 开启驼峰命名 configuration: map-underscore-to-camel-case: true # 为JavaBean起别名 type-aliases-package: com.mcc.springboot.pojo
-
编写 Mapper 接口,并在接口上标注
@Mapper
,也可以在配置类上使用@MapperScan
指明 Mapper 接口所在的包,两种方式选择一种即可。package com.mcc.springboot.mapper; @Mapper public interface BookMapper { }
@MapperScan(basePackages = "com.mcc.springboot.mapper") @SpringBootApplication public class WebApplication { public static void main(String[] args) { SpringApplication.run(WebApplication.class, args); } }
xml 配置文件实现
BookMapper.java
@Mapper
public interface BookMapper {
/**
* 根据 id 查询书籍
* @param id 编号
* @return Book
*/
Book getBookById(Integer id);
/**
* 插入book
* @param book book
* @return 受影响行数
*/
Integer insertBook(Book book);
}
BookMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mcc.springboot.mapper.BookMapper">
<!--Book getBookById(Integer id);-->
<!--
插入时不插入自增的主键,使用useGeneratedKeys开启获取自增主键功能,
使用keyProperty指定主键。开启之后,mybatis会将自增的主键信息保存到
刚刚的插入对象中并返回。(同一个对象,修改一个另一个也修改,因此原对象中也多了主键内容)
-->
<select id="getBookById" resultType="Book">
select id,bookname,author,price,sales,stock,img_path from book where id = #{id}
</select>
<!--Integer insertBook(Book book);-->
<insert id="insertBook" useGeneratedKeys="true" keyProperty="id">
insert into book
(bookname,author,price,sales,stock)
values
(#{bookname},#{author},#{price},#{sales},#{stock})
</insert>
</mapper>
测试:
@Slf4j
@SpringBootTest
class WebApplicationTests {
@Autowired
private BookMapper bookMapper;
@Test
void testGetBookById(){
//测试 Book getBookById(Integer id);
Book book = bookMapper.getBookById(2);
log.info("id为3的book: {}",book);
//id为3的book: Book(id=2, bookname=数据结构与算法, author=严敏君,
//price=78.5, sales=7, stock=12, imgPath=static/img/default.jpg)
}
@Test
void testInsertBook(){
//测试 Integer insertBook(Book book);
Book book = new Book();
book.setBookname("六级写作与翻译");
book.setAuthor("王江涛");
book.setPrice(22);
book.setSales(10000);
book.setStock(500);
bookMapper.insertBook(book);
log.info("添加的book为: {}",book);
//添加的book为: Book(id=41, bookname=六级写作与翻译, author=王江涛,
//price=22.0, sales=10000, stock=500, imgPath=null)
}
}
注解实现
注解方式整合 MyBatis 时,连映射文件都不需要了。
实现方式:
- 在 Mapper 接口的方法上使用
@Insert, @Delete, @Update, @Selete
,实现增删改查功能。 - 使用
@Option
,实现操作中的一些其他功能。类似的注解还有@ResultMap
等等。
BookMapper.java
@Mapper
public interface BookMapper {
/**
* 根据 id 查询书籍
* @param id 编号
* @return Book
*/
@Select("select id,bookname,author,price,sales,stock,img_path from book where id = #{id}")
Book getBookById(Integer id);
/**
* 插入book
* @param book book
* @return 受影响行数
*/
@Insert("insert into book\n" +
" (bookname,author,price,sales,stock)\n" +
" values\n" +
" (#{bookname},#{author},#{price},#{sales},#{stock})")
@Options(useGeneratedKeys = true,keyProperty = "id")
Integer insertBook(Book book);
}
组合实现
简单 SQL 语句使用注解实现,复杂 SQL 语句使用映射文件实现。
@Mapper
public interface BookMapper {
@Select("select id,bookname,author,price,sales,stock,img_path from book where id = #{id}")
Book getBookById(Integer id);
Integer insertBook(Book book);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mcc.springboot.mapper.BookMapper">
<!--Integer insertBook(Book book);-->
<insert id="insertBook" useGeneratedKeys="true" keyProperty="id">
insert into book
(bookname,author,price,sales,stock)
values
(#{bookname},#{author},#{price},#{sales},#{stock})
</insert>
</mapper>
整合 MyBatis-Plus
原理
- 在 application.yaml 中使用:
mybatis-plus=
,对 mybatis-plus 进行配置 。 - xml 映射文件有默认存放位置,
mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
-
@Mapper
标注的接口会被自动扫描,建议在配置类上使用@MapperScan()
批量扫描。
实现
- 导入 MyBatis-Plus 场景启动器。
MP-starter 已经引入了 MyBatis 和 MyBatis-Spring,因此不需要重复引入后两个依赖了。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
- 建库建表。
- JavaBean 和 Mapper 接口。
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private String id;
private String username;
private String password;
private String email;
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
- 测试。
@Autowired
private UserMapper userMapper;
@Test
public void testMP(){
User user = userMapper.selectById("4");
log.info("user: {}",user.toString());
}
CRUD
- Mapper 接口继承 BaseMapper。
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
- Service 接口继承 IService,Service 实现类继承 ServiceImpl,并实现对应的 Service 接口。
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
- 控制层注入 Service 对象,并进行操作即可。
@Autowired
private UserService userService;
- 如果要丰富 Mapper 接口的功能,可以自定义 xml 映射文件,绑定对应的 Mapper 接口即可。
- 如果要丰富 Service 层的功能,可以在 Service 接口中自定义方法,在 Service 实现类中注入 Mapper 对象,并实现方法即可。
分页
- 在配置类内注入分页对象,启动分页插件。
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
}
- thymeleaf 发送带参数的请求。
?
类型的参数,在路径最后,使用(name=${value})
,发送?name=xxx
请求。
rest 风格时,用{name}
占位,在路径最后用(name=${})
,为同名占位符赋值。
<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html"
th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
控制层
@GetMapping("/dynamic_table/{pageNum}")
public String dynamicTable(@PathVariable("pageNum") Integer pageNum, Model model){
Page<User> userPage = userService.page(new Page<User>(pageNum, 3), null);
model.addAttribute("userPage",userPage);
return "table/dynamic_table";
}
页面
<tr class="gradeX" th:each="user,status : ${userPage.records}">
<td th:text="${status.count}"></td>
<td th:text="${user.id}">Trident</td>
<td th:text="${user.username}">Trident</td>
<td th:text="${user.password}">Internet Explorer 4.0</td>
<td th:text="${user.email}">Trident</td>
</tr>
<!-- 分页 -->
<div class="row-fluid">
<div class="span6">
<div class="dataTables_info" id="hidden-table-info_info">
第 [[${userPage.current}]] 页,共 [[${userPage.pages}]] 页,总计 [[${userPage.total}]] 条记录
</div>
</div>
<div class="span6">
<div class="dataTables_paginate paging_bootstrap pagination">
<ul>
<li class="prev disabled">
<a href="#" th:if="${userPage.hasPrevious()}" th:href="@{/dynamic_table/{num}(num=${userPage.current}-1)}">← Previous</a>
</li>
<li class="active" th:each="num:${#numbers.sequence(1,userPage.pages)}" th:class="${userPage.current==num?'active':''}">
<a href="#" th:href="@{/dynamic_table/{num}(num=${num})}">[[${num}]]</a>
</li>
<li class="next disabled">
<a href="#" th:if="${userPage.hasNext()}" th:href="@{/dynamic_table/{num}(num=${userPage.current}+1)}">Next → </a>
</li>
</ul>
</div>
</div>
</div>
分页后的删除
删除时的两种情况:
(1)删除某一页中的数据后,跳转回该页面。
(2)若删除的数据是该页的最后一个数据,要跳转到上一页面。
<tr class="gradeX" th:each="user,status : ${userPage.records}">
<td th:text="${status.count}"></td>
<td th:text="${user.id}">Trident</td>
<td th:text="${user.username}">Trident</td>
<td th:text="${user.password}">Internet Explorer 4.0</td>
<td th:text="${user.email}">Trident</td>
<td>
<a class="btn btn-danger btn-sm" type="button"
th:href="@{/deleteUser/{userId}/{pageNum}(userId=${user.id},pageNum=${userPage.current})}">删除</a>
</td>
</tr>
/**
* 删除dynamic_table中的数据
* @return 跳转回删除页面
*/
@GetMapping("/deleteUser/{userId}/{pageNum}")
public String deleteUser(
@PathVariable("userId") String userId,
@PathVariable("pageNum") String pageNum,Model model){
//删除数据
userService.removeById(userId);
//检查删除的数据,是否是该页面最后一条数据,如果是最后一条,则要跳转到上一页
Page<User> userPage = userService.page(new Page<User>(1, 3), null);
//删除数据后的总页码
long totalPages = userPage.getPages();
long currentNum = Long.parseLong(pageNum);
currentNum = currentNum>totalPages?currentNum-1:currentNum;
return "redirect:/dynamic_table/"+currentNum;
}