一、整合Mybatis
1、数据库准备
2、创建对应的SpringBoot项目
3、编写与数据库对应的实体类(set和get方法省略)
4、编写SpringBoot的配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db
username: root
password: root
5、配置文件方式整合Mybatis
5,1、创建course数据库对应的操作接口CourseMapper
@Mapper
public interface BookDao {
public Book getById(Integer id);
}
5.2、创建Mapper对应的XML映射文件
resources目录下创建一个统一管理映射文件的包mapper,并在该包下编写与CourseMapper接口方应的映射文件CourseMapper.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.wang.demo.dao.BookDao">
<select id="getById" resultType="Book">
select * from tbl_book where id = #{id};
</select>
</mapper>
5.3、配置XML映射文件路径
在项目中编写的XML映射文件,SpringBoot并不知道,所以无法扫描到自定义编写的XML配置文 件,还必须在全局配置文件application.yml中添加MyBatis映射文件路径的配置,同时需要添加 实体类别名映射路径,示例代码如下
mybatis:
#配置MyBatis的xml配置文件路径
mapper-locations: classpath:mybatis/mapper/*.xml
#配置XML映射文件中指定的实体类别名路径
type-aliases-package: com.wang.demo.pojo
5.4、编写单元测试进行接口方法测试
@SpringBootTest
class SpringbootDemo2ApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void testGetById() {
Book book = bookDao.getById(1);
System.out.println(book.toString());
}
}
6、注解方式整合Mybatis
@Mapper
public interface BookDao {
@Select("select * from user where id = #{id}")
public Book getById(Integer id);
}
二、整合mybatis-plus
1、倒入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
2、修改配置文件
端口、数据库、mybatis-plus日志输出、驼峰映射、xml位置等
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
mapper-locations: mybatis/mapper/*.xml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
type-aliases-package: com.wang.demo.pojo
global-config:
db-config:
table-prefix: tbl_
目前数据库的表名定义规则是tbl模块名称,为了能和实体类相对应,需要做一个配置 #设置所有表的通用前缀名称为tbl _
驼峰命名法:true
3、Mapper
只要继承BaseMapper<~>即可、其他方法和xml可以删掉,IUserMapper继承BaseMapper<~>后就可以不用编写XML来实现了。
@Mapper
public interface BookDao extends BaseMapper<Book> {
}
4、mybatis-plus的api
4.1.、selectList(Wrapper queryWrapper) 条件查询/查询所有
selectList(Wrapper<User> queryWrapper) List<User> 批量查询/按条件批量查询
/*
注意点1:Wrapper<User>是一个条件构造器,让我们可以按照条件查询;如果我们传入一个null就表示是查询整张表
注意点2:Wrapper<User>中的泛型由你注入的哪个mapper决定,假如你注入的是UserMapper,而UserMapper又是继承的BaseMapper<User>,那么当你在使用userMapper.selectList(Wrapper<User> queryWrapper)时,Wrapper<User>泛型就默认是User了;
注意点3:我们可以传一个null进去 ,那么此时selectList(null)就是查询User这整张表的数据了;
*/
4.2、 insert() 插入数据
4.3、 删除数据
4.3.1、按照条件删除
delete(Wrapper<User> queryWrapper) int //返回值int
4.3..2、批量删除
List<Integer> ids = new ArrayList<>();
ids.add(5);
ids.add(6);
ids.add(7);
int i = userMapper.deleteBatchIds(ids);
4.3.3、通过id删除
deleteById(Serialization id) int *//返回值int*
int i = userMapper.deleteById(1);*//按照id删除*
4.3.4、通过Map删除
通过Map删除:什么意思呢?其实就是按照条件删除
deleteByMap(Map<String,Object> columnMap) int //返回值int
private UserMapper userMapper;
//这个方法需要一个Map,所以你必须先创建一个map
Map<String,Object> map = new HashMap<>();
map.put("name","许海");
map.put("age",28);
/* 注意:这里Map的泛型一定是<String,Object>,因为你要指定字段名,字段名肯定是String,后面的是字段的值,各个字段有不同的类型,所以要用Object */
int i = userMapper.deleteByMap(map);//意思就是将name等于许海,并且年龄等于28的user删掉;
4.4、 修改数据
4.4.1.、根据id修改
这样写就能达到一个根据id修改的目的:你必须要传一个user对象,并且这个user对象的id是有值的,否则修改不了;
updateById(User entity)
User user = new User();
user.setId(1L);
userr.setAge(18);
userMapper.updateById(user);
//但是这种方式就不太灵活,因为只能更新user对象里id对应的那一行的数据,假如我要实现UPDATE user SET age = 99 WHERE id > 1这样一条语句,上面的语句就满足不了了,所以需要按照条件修改;
4.4.2.、按照条件修改
update(User entity, Wrapper<User> updateWrapper) int
//如果把Wrapper<User> updateWrapper设置为null,就是没有条件
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.gt("id",1);
wrapper.set("age",99); //这里不能用wrapper.eq("age",99,因为eq是用来构建where条件的,我们这里是需要修改,要用set
questionMapper.update(null,wrapper); //这里本来要传一个User实体类对象,但是可以设置为null;
4.5.、条件构造器Wrapper
AbstractWrapper中提供了很多用于构造where条件的方法; QueryWrapper,UpdateWrapper都继承于AbstractWrapper; QueryWrapper额外提供了select方法 UpdateWrapper额外提供了set方法
4.6、 AbstractWrapper
4.6.1、gt,lt,eq等涵义
英文缩写 英文全拼 含义
eq equal 等于
ne not equal 不等于
gt greater than 大于
lt less than 小于
ge greater than or equal 大于等于
le less than or equal 小于等于
sql语句如下:
SELECT id,user_name,password,name,age,address FROM user
Where age > 18 AND address = '重庆'
//假如要用Wrapper实现上面的sql语句应该怎样书写呢?
@Autowired
private UserMapper userMapper;
@Test
public void testWrapper01(){
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.gt("age",18);
wrapper.eq("address","重庆");
userMapper.selectList(wrapper)
//这里为什么要采用selectList方法呢?因为通过条件查询,很可能查出来是多个结果,所以要selectList(Wrapper<User> queryWrapper);
}
4.6.2、select; 一定要注意,select跟selectList不是平级的,它跟eq,lt,gt这些是平级的
select有三种重载形式:
第一种:只查询某几个字段
现在有一个User表,里面有很多字段,我只想查询出user_name,password这两个字段,其他字段不查,就可以用select的第一种重载形式: select(String… columns)
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.select("user_name","password");
List<User> list = userMapper.selectList(wrapper );
第二种:查询除某几个字段外的所有字段
同样是这个User表,但是这个User表的字段实在是太多,我又不想把所有字段都查出来,我只想查除了age字段外的所有字段,如果是用上面的第一种重载形式select(String… columns) ,那么这么多字段我们很难写;所以可以用第二种重载形式;
QueryWrapper<User> wrapper = new QueryWrapper<User>();
//首先要指定User.class,告诉Mp是操作哪一个类; 然后还要new Predicate<TableFieldInfo>要采用匿名内部类的写法,MP将User表的所有属性封装到了TableFieldInfo对象中,所以Predicate这个函数式接口的泛型是TableFieldInfo;
!"age".equals(TableFieldInfo.getColunm()) 这段代码中,MP会将User表的所有字段拿出来跟"age"做对比,如果字段名不等于"age",test方法就返回为true,意思就是:只要是User表中名称不等于"age"的字段,我都给你查出来;
wrapper.select(User.class,tableFieldInfo->!"age".equals(TableFieldInfo.getColunm()));
List<User> list = userMapper.selectList(wrapper );
第三种:
第三种跟第二种其实是一样的,都是达到过滤字段的目的,只不过入参少了一个User.class,就是不用在入参处执行domain类的字节码对象了;但是你必须在new QueryWrapper()时传一个User对象进去,否则mp也不知道你到底是操作哪个表,因为MP会根据User.class找到@TableName,然后再找到数据库中对应的表;
QueryWrapper<User> wrapper = new QueryWrapper<>(new User()); //这里就多写一个new User()
wrapper.select(tableFieldInfo->!"age".equals(TableFieldInfo.getColunm())); //这里就少写一个User.class
List<User> list = userMapper.selectList(wrapper );
4.7、 lambda条件构造器
用旧的条件构造器存在的问题:
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.gt("age",18);
wrapper.eq("address","重庆");
userMapper.selectList(wrapper );
以上面这段代码为例,里面的"age","address"都是我们自己手写的字符串,我们手写是很可能出错的,并且只有在运行时才会报错,编译时是不会报错的;但是只要我们使用lambda条件构造器,如果出现这种问题,编译时就会报错,就更方便;
//使用lambda条件构造器,这里就要用LambdaQueryWrapper,同理还有LambdaUpdateWrapper;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>();
//这里就不再是使用wrapper.gt("age",18)了
//这里的gt里面的参数就变成了一个函数式接口,所以可以用这种方法引用的方式来写,getAge是要省略小括号的
//这里也一样表示age>18
wrapper.gt(User::getAge,18);
wrapper.eq(User::getAddress,"重庆");
userMapper.selectList(wrapper);
4.8、 自定义方法,自定义SQL语句
4.8.1 第一步:在mapper接口中添加自定义方法;
4.8.2 第二步:在yml中指定mapper.xml的位置;
mybatis-plus:
mapper-locations: classpath*:mybatis/mapper/*.xml
4.8.3、第三步:在resources目录下创建mapper.xml
5、设置表的映射规则
另外还需要注意:在实际的开发中,数据库表名跟实体类名很可能是不一致的,比如你的实体类是User,但是表名却是tb_user;此时,如果你还是写一个UserMapper继承BaseMapper,BaseMapper的泛型写User,那么MP就会默认到你的数据库中去找一个名为User的表,但是你数据库表名是tb_user,很显然是找不到的,所以就会报下图中的错:表user不存在;
5.1 单独设置表名 @TableName --->在实体类上添加注解
@TableName(“tb_user”)
public class User{
…
}
通过@TableName(“tb_user”)注解指定后,MP首先会通过BaseMapper中的泛型User找到User这个实体类,然后找到@TableName(“tb_user”),它会以这个注解里面的tb_user为表名到数据库中去查找;
5.2、全局设置表名前缀
全局配置表名前缀的用途是:上面我们一个个的在实体类上加@TableName指定表名显得太麻烦了,当你的表们都是以同一个前缀开头时,你就可以在yaml中将这个前缀配置进去,以后MP在操作表时,就会自动把前缀加到BaseMapper里指定的泛型前面作为表名,在公司中表名为了规范命名,很可能是加了统一的前缀的,这个配置就有用处了;但是我个人感觉用处不大,不如我自己一个个单独设置来的准确;
global-config:
db-config:
table-prefix: tbl_
6、设置主键的自增策略
6.1、全局设置主键生成策略
(1)、比如如果你的主键是整数类型,直接就可以使用最普通的主键自增策略,但是这种方式在分布式的环境下会存在问题;
(2)、还可以使用基于UUID的主键生成策略,生成的就是随机的UUID,且是字符串类型,但是使用UUID作为主键也会有问题,因为数据库的索引需要进行排序才能提交查询效率,但是UUID是随机的,没有大小,就无法排序,所以使用UUID作为主键时,索引就会有问题,也不推荐使用;
(3)、在分布式的环境下,还可以使用基于雪花算法生成的自增id,这个比较好用,在分布式下表现良好,但是它生成出来id都比较长;如果你的项目是分布式,推荐使用这一种;
//值得注意的是:MP中的主键自增策略,模式就是基于雪花算法生成的自增id;
mybatis-plus:
mapper-locations: classpath*:mybatis/mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
type-aliases-package: com.wang.demo.pojo
global-config:
db-config:
table-prefix: tbl_
id-type: auto #设置主键id字段的生成策略为参照数据库设定的策略,当前数据库设置id生成策略为自增
6.2、单独设置主键生成策略
@Table(TYPE = IdType.Auto)
7、设置字段和列名的驼峰映射
7.1、注意事项以及@TableField("address_str")
// 在MP中默认是开启驼峰命名的,开启驼峰命名会造成一个后果是:数据库表字段名与类属性名称不一致就会无法映射。
在上面的情况中,你的数据库字段是username,实体类属性又是userName,此时MP映射就会出现问题,但是如果你公司中这些数据库表又设计好了的,你无法对表属性做修改,那么此时你就只有关闭MP的驼峰映射了;
// @TableField(“address_str”) 设置字段与属性之间的映射关系
当你的类属性是address,表字段是address_str,此时这两个单词都不同,是不能映射过去的;这会导致从数据库中查出字段address_str的数据了,没有办法把数据传递给实体类的address属性;
@TableName("tb_user") //表名称映射--也可以使用全局设置表名前缀 例:table-prefix: tbl_
public class User{
@TableId(type = IdType.Auto) //主键自增 --也可以使用注解
private Long id;
private String userName;
private String password;
private String name;
private Integer age;
@TableField("address_str") //此设置后,类属性address跟表字段addressStr就产生映射了,前提需要关闭驼峰映射
private Integer address;
}
8、开启日志
如果你想看到MP执行的每句sql记录,你就可以开启日志,MP已经提供好了日志功能,实际上它的日志功能就是写了很多不同的实现类,我们采用不同的实现类,可以达到不同的日志效果; 比如最简单的StdOutImpl就是每次在执行sql后将sql语句打印在控制台;
mybatis-plus:
configuration:
# 加上这句配置后,才每次执行sql时,MP都会在控制台输出执行的sql语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
9、mp的分页查询
9.1、添加分页插件
如果要使用mp的分页功能,需要添加插件,其实这个插件就是个配置类: 这个配置类里面有两个配置,根据你的mp版本保留一个就可以了;
@Configuration
public class MybatisPlusConfig {
//如果你的mp是3.4.0以前的版本,你要用这种写法;
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setOverflow(false);
paginationInterceptor.setLimit(500);
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
//如果你的mp是3.4.0及以后的版本,你要用这种写法;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
//上述代码第一行是创建MP的拦截器栈,这个时候拦截器栈中没有具体的拦截器,第二行 是初始化了分页拦截器,并添加到拦截器栈中。如果后期开发其他功能,需要添加全新的拦 截器,按照第二行的格式继续add进去新的拦截器就可以了。
9.2、分页查询的方法:selectPage
selectPage入参解析
selectPage(IPage<T> page, Wrapper<T> queryWrapper) IPage<T> , 返回值是IPage<T>
/*
第一个参数IPage<T> page是用来定义你要分页查询的信息:比如每页显示多少条,第几页等;
IPage<T>这是一个mp提供的接口;
第二个参数queryWrapper是用来定义分页查询的条件;
使用selectPage方法
IPage<User> page = new Page<>();
//IPage<Book> page = new Page<>(1,5); 也可以使用构造函数,参数1:显示第几页 参数2:每页几条数据
//设置查询第几页,这里就是查询第一页
page.setCurrent(1);
//设置每页条数,这里就是每页只有2条数据
page.setSize(2);
IPage<User> page1 = userMapper.selectPage(page,null);//设置为null就表示没有查询条件
System.out.println(page.getRecords()); //获取查询出来的User集合
System.out.println("当前页数: "+ page.getCurrent());//获取当前页数
System.out.println("获取总条数 "+ page.getTotal());//获取总条数
System.out.println("每页显示数 "+ page.getSize()); //每页显示数
System.out.println("总页数 "+ page.getPages()); //总页数
注意:实际上这里返回值page1对象就是上面new出来的page对象,它们的地址值都是一样的,不过page对象在经过selectPage方法后,mp把查出来的User集合,当前页,总条数 封装到了page对象的records属性,current属性,total属性中;所以我们一般在调用selectPage方法时,我们都不会用变量接收它;有page对象就够了;
9.3 、多表分页查询
上面的分页查询并不是适应所有情况,当我们要进行联表查询再分页时,上面的分页查询就失效了;
举个例子,现在有一个需求,我们要去查询订单表,要求除了把订单表中的字段查出来,还要把每个订单的下单用户的用户也查出来;
因为要操作新的订单表,所以我们要新建一个OrderMapper,同时由于mp的BaseMapper中只提供了单表查询的方法,所以要实现联表查询,我们只能自定义方法;
1、
public interface OrderMapper extends BaseMapper<Order>{
List<Order> findAllOrders(); //这就是我们自定义的查询所有订单,并且把user_name也查出来的方法;
}
------------------xml文件添加sql语句
2、
<select id="findAllOrders" resultType="com.wang.demo.pojo.Order">
select o.* , u.user_name from tbl_order o left join tbl_user u on o.user_id = u.id
//或者写成select o.* , u.user_name from tbl_order o , tbl_user u where o.user_id = u.id 也可以
</select>
我们可以看到上面两部分实际上是没有做分页的,因为SQL语句里根本没有limit关键字;
所以要进一步调整;如何根据mp的习惯对上面的方法进行改造呢?
我们知道单表分页查询中有一个很关键的接口IPage,
//只要我们给findAllOrders方法传入Page<Order> page作为入参,并且让方法的返回值类型是IPage<Order>,mp就会自动帮我们把查出来的数据封装到IPage<Order>返回值中,然后我们再根据getRecords(),getTotal(),getCurrent()来取就可以了;
那为什么不以List<Order>作为返回值呢?因为我们做分页时,不仅仅只是要一个Order的集合,还需要总条数与当前所处页数,前端才能做分页;
所以
List<Order> findAllOrders();就要被改造为下面这样:
3、
public interface OrderMapper extends BaseMapper<Order>{
IPage<Order> findAllOrders(Page<Order> page);
//这就是我们自定义的查询所有订单,并且把user_name也查出来的方法;
}
然后xml中的sql就不用管了,不需要给sql加上limit,mp会自动在sql后面加上limit
10、mp的service接口(慎用)
10.1、mp的service层写法
写一个接口继承IService接口,泛型填对应的实体类,IService中已经封装好了常用方法;
public interface UserService extends IService<User>{
}
再写一个类继承ServiceImpl类,ServiceImpl的第一个泛型是UserMapper,第二个泛型是实体类User,实现UserService;
@Service
public Class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
}
10.2、ServiceImpl的api
查询所有:
List<User> list = userService.list();
System.out.println(list);//这样就查询出了user表中的所有记录;
按照条件查询:
List<T> list(Wrapper<T> queryWrapper);
插入: 批量插入:
Page<User> page = new Page<>();
page.setSize(2);
page.setCurrent(2);
userService.page(page); //注意,mapper层中是调用selectPage方法,service层是调用page方法;
System.out.println(page.getRecords());
10.3、自定义Service层的方法
同样的,实际开发过程中,IService中提供的方法并不能满足我们的要求,所以我们需要自己定义一些更加复杂的方法;
如何自定义Service层方法呢?实际上只需要在UserService中自己加方法就可以了,你想怎么加就怎么加;
比如现在我往UserService接口中添加了一个自定义的test方法,返回值是User;
public interface UserService extends IService<User>{
User test();
}
然后需要在UserServiceImpl 类中重写test方法;
并且假如我需要在UserServiceImpl 中用到userMapper这个对象,我不需要再通过@Autowired注入,mp自带的ServiceImpl类中已经封装好了一个getBaseMapper()方法,我们直接调用它,就能获取到userMapper对象;
我们自己写的UserServiceImpl 继承了ServiceImpl,所以可以直接用方法名调;
@Service
public Class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
@Autowired
private OrderMapper orderMapper; //假如UserServiceImpl类中还要用到其他Mapper,就没有办法了,只能自己注入;因为只有ServiceImpl<UserMapper,User>泛型中的UserMapper可以通过getBaseMapper获取到UserMapper对象;
@Override
public User test(){
UserMapper userMapper = getBaseMapper();// 那这里mp怎么就知道返回值是UserMapper呢? 这是因为上面ServiceImpl的第一个泛型指定了是UserMapper,所以mp知道;
List<Order> list = orderMapper.selectList(null);
return null;
}
}
11、代码生成器
mp提供了代码生成器,让我们可以一键生成实体类,Service,Controller等全套代码;使用方式如下:
添加依赖
<!--mybatisplus代码生成器的依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--模板引擎的依赖-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
创建模板类