Spring Boot整合druid
整合druid的步骤主要为:
- 导入依赖druid-spring-boot-starter,mysql-connector-java依赖
- 在在配置文件中配置数据库驱动
- 就可以进行测试了
Spring Boot整合Mybatis-plus
Spring Boot整合Mybatis-Plus的基本步骤为:
-
导入依赖Mybatis-Plus-boot-starter,因为需要利用数据库,所以还需要导入druid数据库源以及connector-java依赖,如下所示:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
-
因为利用到了数据库,所以在配置文件中需要配置数据库驱动
spring: datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssmp?serverTimezone=Asia/Shanghai username: root password: root
-
对应的mapper接口继承了BaseMapper<T>,这样这个mapper接口可以不需要再写任何的代码,然后可以执行对应的操作了,而如果利用的是mybatis的时候,需要再方法上面利用对应的注解来执行响应的操作,或者需要通过映射文件,创建代理对象来操作。如下所示:
/* 通过mybatis-plus来实现的时候,它是通过定义mapper,然后 让这个mapper接口来继承BaseMapper<T>,然后什么代码都不需要 写,因为BaseMapper中已经定义了各种操作数据库的方法了,然后 查询到的数据返回的就是T。 */ @Mapper public interface BookDao extends BaseMapper<Book> { }
BaseMapper<T>的部分源码如下所示:
public interface BaseMapper<T> extends Mapper<T> { int insert(T entity);//插入操作,如果没有配置,那么id并不支持自增操作,所以需要配置文件中进行配置 int deleteById(Serializable id);//根据id进行删除操作 int updateById(@Param("et") T entity);//根据id进行编辑 int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper); T selectById(Serializable id);//根据id来查询 T selectOne(@Param("ew") Wrapper<T> queryWrapper);//查询单个数据 Integer selectCount(@Param("ew") Wrapper<T> queryWrapper); List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);//查询多条数据,如果queryWrraper不为null,那么就是根据这个参数进行条件查询 }
-
进行测试
但是进行测试查询等操作的时候,发现数据库表找不到,这是因为mybatis-plus中默认是根据实体类的名字作为数据库表名,而我们的数据库表的名字则是在实体名的前面添加了tb_
前缀,从而发生数据库表找不到。为了解决这个问题,需要在配置文件中设置数据库表的前缀。
mybatis-plus:
global-config:
db-config:
table-prefix: tb_ # 配置数据库表的前缀
但是如果执行插入操作的时候,发现了报错,提示Could not set property 'id' of 'class com.example.domain.Book' with value '1557236544425811970',Cause: java.lang.IllegalArgumentException: argument type mismatch
,因为mybatis-plus中id默认并不是自增操作的,所以我们同样需要在配置文件中配置id-Type,使其支持自增。
mybatis-plus:
global-config:
db-config:
table-prefix: tb_ # 配置数据库表的前缀
id-type: auto # 配置id是支持自增的
但是这时候如果我们需要通过日志来调试,那么需要看到执行的sql语句,这时候我们同样需要在配置文件中进行配置,使得它是标准输出,如下所示:
mybatis-plus:
global-config:
db-config:
table-prefix: tb_ # 配置数据库表的前缀
id-type: auto # 配置id是支持自增的
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置日志输出为标准输出,方便看到执行的sql语句
mybatis-plus实现分页操作时,那么这时候需要通过调用selectPage(IPage,QueryWrapper),这时候第一个参数设置查询的是第几页的记录,QueryWrapper封装的是条件,如果不为null,那么就是在条件查询的基础上,进行分页操作。
当调用selctPage操作之后,返回值是一个IPage对象,也可以用传递的参数IPage来存放数据。这时候通过这个对象调用以下方法来获取对应的信息:
- getPages() : 获取总页数
- getTotal(): 获取总记录数
- getCurrent(): 获取当前时第几页
- getSize(): 获取每一页有多少行
- getRecords(): 获取当前页的所有记录
所以对应的代码为:
测试类:
package com.example.domain;
public class Book {
private Integer id;
private String name;
private String description;
public Book() {
}
public Book(Integer id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
@SpringBootTest
public class BookDaoTest {
@Autowired
private BookDao bookDao;
/*
利用mybatis-plus来是实现分页操作,首先需要
创建MybatisPlusInterceptor,在这个拦截器
中添加一个内部拦截器PageInnerInterceptor,用于分页操作的。
如果没有这一步,那么执行下面代码的的时候,sql语句不会出现limit子句。
完成上面的操作之后,通过bookDao调用selectPage方法来执行分页操作,但是
我们需要知道要查询的是第几页,并且每一页由多少条记录。所以需要传递参数
IPage(他是一个接口),它已经封装了查询了第几页,每一页由多少条记录。
selectPage(IPage,QueryWrapper<T>),其中第二个参数用于条件查询的。
*/
@Test
public void testPage(){
IPage page = new Page(2,5);//获取第1页的数据,每一页有5行
bookDao.selectPage(page,null);//将执行selectPage操作,将查询到的数据存放到page中,第二个参数表示的是条件
System.out.println("当前是第 " + page.getCurrent() + " 页");
System.out.println("每一页有 " + page.getSize() + " 行");
System.out.println("总共有 " + page.getTotal() + " 行,总页数有 " + page.getPages() + " 页");
System.out.println("当前页的记录分别为: ");
List<Book> records = page.getRecords();
for(Book record : records){
System.out.println(record);
}
}
}
但是运行的时候,就会通过日志看到,并没有看到LIMIT
子句,所以查询的就是所有的数据:
所以在进行分页操作之前,首先我们需要添加一个MyBatisPlus拦截器,然后再这个拦截器的内部添加一个PaginationInnerInterceptor进行拦截操作,从而实现分页操作,对应的代码为:
@Configuration //Mybatis-Plus的配置类
public class MPConfig {
@Bean //将方法返回值添加到IOC容器中
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//mybatis-plus添加拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//拦截器内部再次添加拦截器,用于分页操作,所以需要添加分页操作的拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
再次执行测试类的方法时,日志输出的sql语句就含有limit字句了,如下所示:
通过上面可以知道,如果需要实现条件查询,那么这时候我们再调用对应的方法的时候,传递参数QueryWrapper或者LambdaQueryWrapper对象即可,通过这个对象调用对应的方法来设置条件,代码如下所示:
/*
测试Mybatis-plus的条件查询,只需要添加QueryWrapper参数即可,也可以是
LambdaWrapper参数,然后通过这个对象调用对应的方法来设置对应的条件。
*/
@Test
public void testGetBy(){
//查询名字为book888,并且description为玄幻小说的书
QueryWrapper<Book> qw = new QueryWrapper<>();
qw.eq("name","book888");//第一个参数表示数据库表中的字段名
qw.eq("description","玄幻小说");
Book book = bookDao.selectOne(qw);
System.out.println(book);
}
/*
在条件查询中,如果利用的是参数QueryWrapper来设置条件,那么需要
注意第一个参数数据库的字段名没有写错。所以这时候为了防止写错
的情况,所以就利用LambdaQueryWrapper,通过lambda表达式来获取字段名即可
*/
@Test
public void testGetBy2(){
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper();
/*
lqw.eq(Book::getDescription,"玄幻小说");//获取description字段值为玄幻小说的Book
但是这样写有弊端,这样通过设置日志之后,来看到它的sql语句,发现如果传递的参数是null
也即lqw.eq(Book::getDescription, null)的时候,那么这时候执行的sql语句是查询
description字段中为null的Book。也即null变成了它的值。
所以为了避免这种情况的发生,我们需要定义一个变量来定义条件.
第一种做法为:在执行这个操作之前,判断这个变量是否为null即可.
第二种做法:因为queryWrapper对象调用对应的方法设置条件的时候,在字段名
前面还有一个boolean参数,如果是true,那么添加这个条件,否则不添加。
所以我们根据字段名是否为null即可。
*/
String description = null;
lqw.eq(description != null,Book::getDescription,description);//变量description不为null时添加条件
List<Book> books = bookDao.selectList(lqw);
for(Book book : books){
System.out.println(book);
}
}
但是建议传递的参数是LambdaQueryWrapper对象,这是因为QueryWrapper对象调用对应的方法来设置条件的时候,方法中字段名必须要和数据库表的一致,很容易写错,而在LambdaQueryWrapper对象中,通过实体来获取对应的属性,就可以获取到条件对应的值了。这是因为实体类中的属性必然和数据库表的字段名相同,所以安全性更加高。
值得一提的是,如果第二个参数的值为null的话,也即lqw.eq(Book::getDescription,description)中的description为null的话,那么在日志中可以看到,他查询的是数据库表中description字段值为"null"的数据,但是事实上我们可能没有传递这个参数,如下所示:
测试代码:
@Test
public void testGetBy(){
//查询名字为book888,并且description为玄幻小说的书
QueryWrapper<Book> qw = new QueryWrapper<>();
qw.eq("name",null);//第一个参数表示数据库表中的字段名
qw.eq("description","玄幻小说");
Book book = bookDao.selectOne(qw);
System.out.println(book);
}
所以一种做法是在设置条件之前判断变量description是否为null,如果为null,那么不会进行条件查询,否则进行条件查询。
但是还有第二种做法,LambdaQueryWrapper或者QueryWrapper对象中还有xxx(boolean,column,value)方法,其中第一个参数boolean的值如果为true,表示会设置条件进行条件查询,否则如果为false,表示不会设置条件。所以一般建议采用的是第二种方法。
代码如下所示:
@Test
public void testGetBy(){
//查询名字为book888,并且description为玄幻小说的书
QueryWrapper<Book> qw = new QueryWrapper<>();
String name = null;
qw.eq(name != null,"name",name);//第一个参数表示数据库表中的字段名
qw.eq("description","玄幻小说");
List<Book> books = bookDao.selectList(qw);
for(Book book : books) {
System.out.println(book);
}
}
同时也可以看到,如果有多个条件的时候,那么这时候我们只需要通过QueryWrapper或者LambdaQueryWrapper对象调用多个方法来设置条件即可。
所以利用spring boot来整合第三方技术的时候,基本步骤为:导入xxx-starter依赖,如果这个依赖并没有在spring中配置,那么就需要自己手动来配置它的版本,负责不需要配置版本了。
ssmp进行简单开发
我们利用spring + spring mvc + mybatis-plus来进行开发一个简单图书管理,主要是来实现它的增删改查。
准备工作:
-
导入相关坐标:mybatis-plus-boot-starter,druid-spring-boot-starter,mysql-connector-java,spring-boot-starter-thymeleaf.之所以需要导入thymeleaf-spring-boot-starter,是因为我们需要进行渲染,并且在获取数据之后,我们可以通过thymeleaf来进行相应的操作,例如循环遍历等,否则,如果没有导入thymeleaf的话,那么就没有办法跳转到templates目录下面的html文件中。
<!--导入mybatis-plus以及druid坐标, 由于spring中没有配置这些坐标,所以需要手动配置版本--> <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>2.6.6</version> </dependency>
-
配置文件中配置数据库驱动,以及mybatis-plus
-
建立数据库表tb_book,如果直接通过mybatis-plus来实现数据库的操作,很容易发生报错,例如查询操作的时候,会发生报错,提示数据库表book不存在,这是因为我们的数据库表前面还有前缀
tb_
,或者执行插入操作的时候,发生错误,这是因为mybatis-plus中默认id不是支持自增的,所以我们在建立数据库表之后,还需要在配置文件中通过mybatis-plus来配置数据库表的前缀,id的类型。此外还需要配置日志,从而方便调试。 -
创建数据库表对应的实体类,以及controller,service,dao层
public class Book { private Integer id; private String name; private String description; public Book() { } public Book(Integer id, String name, String description) { this.id = id; this.name = name; this.description = description; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Book{" + "id=" + id + ", name='" + name + '\'' + ", description='" + description + '\'' + '}'; } }
/* 通过mybatis-plus来实现的时候,它是通过定义mapper,然后 让这个mapper接口来继承BaseMapper<T>,然后什么代码都不需要 写,因为BaseMapper中已经定义了各种操作数据库的方法了,然后 查询到的数据返回的就是T。 */ @Mapper public interface BookDao extends BaseMapper<Book> { }
/* 在mybatis-plus中,service层的实现类也可以不写代码, 可以通过定义一个IBookService,使得这个接口继承了IService<T> 然后定义这个接口的实现类IBookServiceImpl,使得在实现 这个接口的同时,继承了ServiceImpl<M,T>,其中这个M就是对应的Mapper接口, T就是对应的实体. 此时的service实现类就已经有了操作的各种方法了,如果需要添加功能的 时候,就需要手动在这个实现类中追加即可 */ public interface IBookService extends IService<Book> { IPage<Book> getPage(int currentPage, int size); }
@Service public class IBookServiceImpl extends ServiceImpl<BookDao,Book> implements IBookService { //手动追加方法,实现分页操作 @Autowired private BookDao bookDao; public IPage<Book> getPage(int currentPage, int size){ IPage<Book> page = new Page(currentPage, size); bookDao.selectPage(page,null); return page; } }
在上面的操作完毕之后,可以进行开发了:
-
查询所有的图书
当我们查询图书的时候,将通过IBookService调用list()方法来返回一个List<Book>对象,这时候我们需要将这个数据封装到视图中,然后跳转到对应的界面。但是这时候如果数据太多,影响美观,所以需要进行分页操作,因此进入首页中查询的是第一页的数据,封装到页面中的数据是一个IPage<Book>对象。但是如果在前端中怎么显示数据呢?因为有多个数据,所以必然是要用到了循环来实现,因此需要通过thymeleaf中来进行循环操作。对应的代码为:
@GetMapping //获取所有的数据,然后来到首页 public ModelAndView index(ModelAndView modelAndView){ //获取首页的数据,因为需要实现分页查找 IPage<Book> page = new Page(1, 5); iBookService.page(page,null); //将r添加到前端中 modelAndView.addObject("page",page); //设置跳转的页面 modelAndView.setViewName("/pages/books.html");//html文件放到了templates/pages下面 return modelAndView; }
-
编辑图书
首先当我们点击要编辑哪一本书的时候,我们需要获取这个书的信息,然后跳转到编辑页面,而在这个编辑页面中显示的是这个图书原来的信息。当编辑完毕之后,再次提交,当编辑成功之后,直接重定向到首页中。对应的代码为:
@GetMapping("/modifyById/{id}") public ModelAndView modifyById(@PathVariable("id")Integer id){ Book book = iBookService.getById(id); ModelAndView modelAndView = new ModelAndView(); //设置属性 modelAndView.addObject("book",book); //设置跳转的页面 modelAndView.setViewName("/pages/update.html"); return modelAndView; } @PostMapping("/modifyById") /* 通过这个注解@ResponseBody,告诉spring,这个返回值并不是用来页面跳转的,而是回写数据 如果没有这个注解,那么下面的代码就会进行页面跳转,这时候由于方法返回值是void,那么就会 再次来到当前的页面中. */ @ResponseBody public void modifyById(Book book,HttpServletResponse response) throws IOException { //注意的是这里并没有在Book的前面使用注解@RequestBody,否则就会发生报错 System.out.println("编辑后的书为: " + book); iBookService.updateById(book); response.sendRedirect("/books3");//为了避免当编辑完成之后,当来到首页的时候,url依旧没有发生变化,所以需要进行重定向 }
编辑页面:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>编辑页面</title> </head> <body> <form action="/books3/modifyById" method="post"> <input type="hidden" name="id" th:value="${book.id}"> 图书名字<input type="text" name="name" th:placeholder="${book.name}"><br> 图书描述<input type="text" name="description" th:placeholder="${book.description}"><br> <input type="submit" value="提交"> </form> </body> </html>
在看到上面的代码之后,可能会疑惑,为什么不可以在编辑完成之后,不可以通过注解
@RequestBody
来表示编辑之后的Book呢?我们尝试给这个参数前面添加这个注解,即代码如下所示:@ResponseBody public void modifyById(@RequestBody Book book,HttpServletResponse response)
然后运行之后,结果如下所示:
编辑操作点击提交之后,变成了:
数据库中数据没有编辑成功,重新回到IDEA,发现控制台中打印这一串数据:
2022-08-11 16:08:26.361 WARN 13940 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException:Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported]
重点看
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
这句,提示application/x-www-form-urlencoed;charset=UTF-8
不支持,这是因为什么呢?这是因为
@RequestBody
注解导致的。可以看一下这个文章:@RequestBody 的使用方法和注意事项,查看之后可以发现,原来是因为@RequestBody
支持的是application/json格式,当发送请求之后,他会将json格式的字符串绑定到对应的bean中。而在一些表单form提交,**类型为application/x-www-form-urlencoded;charset=UTF-8
**的情况下,并不能使用这个注解了。所以此时我们可以直接删除这个注解,就可以了,这就涉及到了spring中的POJO了。所以这就是为什么不可以在Book参数前面添加注解@RequestBody
了. -
删除图书
点击删除的超链接,当点击之后,就需要进行删除操作,删除完毕之后,需要重新重定向到首页,对应的代码如下所示:
@GetMapping("/deleteById/{id}") @ResponseBody public void deleteById(@PathVariable("id")Integer id, HttpServletResponse response) throws IOException { iBookService.removeById(id); response.sendRedirect("/books3"); }
-
添加图书
这个操作和编辑类似,所以代码也是相似的:
@GetMapping("/save") public String save(){ return "/pages/add.html"; } @PostMapping("/save") @ResponseBody public void save(Book book, HttpServletResponse response) throws IOException { System.out.println("添加的图书: " + book); iBookService.save(book); //重定向到首页 response.sendRedirect("/books3"); }
-
分页操作
因为我们来到首页的时候,封装的数据是IPage对象,这个对象可以通过对应的方法来获取需要的信息,如下所示:
- getCurrent() : 获取当前页面是第几页
- getTotal(): 获取一共有多少行
- getPages(): 获取一共有多少页
- getSize(): 获取每一页有多少行
- getRecords(): 获取每一页的数据,返回的是List<T>对象。
所以这时候我们需要通过循环来遍历这个List<T>对象,此时需要利用thymeleaf中的each用法。同时,要实现分页操作,还需要进行if判断,如果是第一页的话,那么上一页就会失效,同理,如果是最后一页,下一页就会失效。而if判断如果为true,那么修饰的标签就会显示,否则不会出现(并不是隐藏,而是真的没有这个标签)。
同时,为了点击下一页的时候,我们可以重新渲染页面,还需要再conroller中添加方法来实现分页操作,需要传递的参数是currentPage, size,表示第几页,以及每一页有多少行.
@GetMapping("/getPage/{currentPage}/{size}") public ModelAndView getPage(@PathVariable("currentPage")Integer currentPage, @PathVariable("size")Integer size, ModelAndView modelAndView){ //查询currentPage的记录,并且每页有size页 IPage page = new Page(currentPage, size); iBookService.page(page,null); modelAndView.addObject("page",page); //设置跳转的视图 modelAndView.setViewName("/pages/books.html"); return modelAndView; }
books.html的代码为:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>books页面</title>
</head>
<body>
<h1>ssmp练习</h1>
<div>
<a th:href="'/books3/save'">添加</a>
</div>
<div>
<table>
<thead>
<td>图书id</td>
<td>图书名字</td>
<td>图书描述</td>
<td>操作</td>
</thead>
<tr th:each="d:${page.records}">
<td th:text="${d.id}"></td>
<td th:text="${d.name}"></td>
<td th:text="${d.description}"></td>
<td>
<a th:href="'/books3/modifyById/' + ${d.id}">编辑</a>
<a th:href="'/books3/deleteById/' + ${d.id}">删除</a>
</td>
</tr>
</table>
<div>
<!--实现分页操作-->
<a th:href="'/books3/getPage/1/' + ${page.size}">首页</a>
<a th:href="'/books3/getPage/' + ${page.current - 1} + '/' + ${page.size}" th:if="${page.current ne 1}">上一页</a>
<a th:href="'#'" th:if="${page.current eq 1}">上一页</a>
<a th:href="'/books3/getPage/' + ${page.current + 1} + '/' + ${page.size}" th:if="${page.current ne page.pages}">下一页</a>
<a th:href="'#'" th:if="${page.current eq page.pages}">下一页</a>
<a th:href="'/books3/getPage/' + ${page.pages} + '/' + ${page.size}">尾页</a>
</div>
</div>
</body>
</html>
前端页面如下所示(每一页有2行):