文章目录
一、MyBatis Plus基础
1.1 相关概念介绍
1.1.1 持久化、持久层
内存数据是易失的(断电就没了),而磁盘数据是持久的(断电后不丢失),将内存中的数据保存到磁盘中的过程就是数据持久化,项目中实现数据持久化的代码层就是持久层。
1.1.2 ORM(Object Relational Mapping)
ORM指对象关系映射,是一种实现持久化类与数据库表之间映射的技术。使用了该技术的框架也被称为ORM框架,通过映射可以实现持久化类的对象和数据库表的记录之间的相互转换。
使用前需要先完成持久化类与数据库表的映射,即指明实体类与哪个表对应、实体类的属性和表字段的具体映射规则。使用时由ORM框架自动生成SQL语句,并将执行结果自动封装到实体类对象中。
ORM框架的主要优点是可以省去数据库操作中的一些重复工作,免去写SQL和数据绑定的过程,提高了开发效率。此外,ORM框架一般还会提供持久层可以用到的常用功能。主要缺点是会降低程序性能。
常见的ORM框架有:Hibernate,iBatis,MyBatis,MyBatis Plus
1.1.3 DAO(Data Access Object)
DAO指数据访问对象,是一种设计模式。业务逻辑代码直接调用DAO的方法就能完成数据库操作,完全感觉不到数据库表的存在。DAO通过隔离数据访问代码和业务逻辑代码,降低了耦合性,提高了可复用性。
在典型的MVC三层架构中,DAO层属于持久层的一部分。
1.1.4 正向工程和逆向工程
正向工程:由开发人员创建实体类,再使用框架根据实体类生成数据库表
逆向工程:由开发人员创建数据库表,再由框架根据数据库表生成实体类、Mapper接口、Service类、Controller类等代码
(Hibernate支持正向工程,而MyBatis和MyBatis Plus支持逆向工程)
1.2 MyBatis Plus与MyBatis的关系
MyBatis Plus是MyBatis的一个增强版本,在MyBatis的基础上只做增强不做改变,具有无侵入性且损耗小的特点。因此,我们应该尽可能使用MyBatis Plus,而不是MyBatis。
无侵入性:对于使用了MyBatis的项目,将MyBatis依赖换成MyBatis Plus,项目不会受到影响,因为MyBatis Plus兼容MyBatis的一切功能和用法。
损耗小:MyBatis-Plus是基于MyBatis的增强版本,增强所带来的额外开销可忽略不计。
1.3 MyBatis Plus的主要功能
(先知道有哪些主要功能,在针对每个功能学习具体如何使用)
- 提供了丰富的CRUD方法:内置了通用Mapper和Service,仅仅通过少量配置即可实现针对单表的大部分CRUD操作
- 支持主键自动生成:有4种主键策略
- 支持ActiveRecord模式:实体类继承Model类后可以进行CRUD操作
- 提供了乐观锁功能:支持版本号管理,用于处理并发情况下的数据一致性问题
- 提供了条件构造器(Wrapper):借助Wrapper可以灵活地构造SQL中的条件子句
- 提供了代码生成器:可以用于生成各层代码和实现逆向工程,以提高开发效率
- 内置全局拦截插件:提供全表delete、update操作智能分析阻断,也可自定义拦截规则,预防误操作
- 支持自定义全局通用操作:支持全局通用方法注入,实现代码复用
- 内置分页插件:配置好插件后,分页查询等同于普通List查询
- 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 支持多种数据库:包括MySQL、Oracle、SQL Server等(底层数据库类型不会影响MyBatis Plus的使用方式)
1.4 Spring Boot集成MyBatis Plus
- 添加Maven依赖
- 配置:数据源,SQL日志,分页插件,乐观锁等
- 在Spring Boot启动类中添加@MapperScan(basePackages = “xxx.yyy.mapper”)注解,扫描Mapper文件夹
// 如下通过配置类的方式来配置分页插件和乐观锁,不配置无法使用
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加:分页插件
//参数:new PaginationInnerInterceptor(DbType.MYSQL),是专门为mysql定制实现的内部的分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//添加:乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
1.5 实体类(entity)
实体类的分类:
- VO(View Object):视图对象,用于封装来自/发给页面或组件的数据
- DTO(Data Transfer Object):数据传输对象,用于封装展示层与服务层之间传输的数据,分为展示层调用服务层的方法时所需的数据和服务层完成服务后返回给视图层的数据
- DO(Domain Object):领域对象,指从现实世界中抽象出来的业务实体,服务层会根据DTO构造(或重建)DO传给业务方法,通过调用业务方法来完成具体业务
- PO(Persistent Object):持久化对象,指数据库表的映射实体
实体类的规范(就是POJO):
- 实体类里面的属性都是私有的,即修饰词为private
- 私有属性使用公开的set和get方法操作(可使用lombok的@Data注解生成)
- 实体类属性建议不使用基本数据类型,而使用基本数据类型对应的包装类
二、使用MyBatis Plus完成简单的CRUD
2.1 基于MyBatis Plus完成CRUD的流程
- 创建持久化对象,完成数据库表的映射
- 实现CRUD(基于BaseMapper或IService实现)
2.2 创建持久化对象,完成数据库表的映射
2.2.1 命名规范
若表名为xxx_yyy_zzz
,那么PO命名为XxxYyyZzzPO
(一些项目前缀可以省)
2.2.2 MyBatis Plus中相关的常用注解
- @TableName:标识实体类对应的表
- @TableId:标识某个字段为主键,可以使用type属性指定自增类型
- @TableField:标识某个字段对应的列(如字段名和列名一致,则可忽略)
- @TableLogic(value = “0”,delval = “1”):标识某个字段是逻辑删除标志
2.2.3 lombok中相关的常用注解
(以下三个都作用在类上)
- @Data:生成get、set、equals、hashCode、toString等方法
- @EqualsAndHashCode(callSuper = false):生成equals和hashCode方法,默认仅使用非静态的属性,可以通过exclude参数排除不需用到的属性,或通过of参数来指定需要用到的属性
- @Accessors(chain = true):设置属性chain为true是,调用字段的setter方法后返回当前对象
2.3 基于BaseMapper接口实现CRUD
2.3.1 使用方式
- mapper接口继承自
BaseMapper<XxxYyyZzzPO>
- 使用mapper对象直接调用对应的方法完成CRUD
2.3.2 Create(以insert为前缀的方法)
(没有直接提供批量插入)
int insert(T entity)
:插入一条记录,返回受影响的行数
2.3.3 Read(以select为前缀的方法)
T selectById(Serializable id)
:根据主键ID查询一条记录List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList)
:根据ID集合批量查询多条记录T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
:根据Wrapper条件查询一条记录,如果有多个结果则抛出异常T selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
:根据Wrapper条件查询多条记录Page<T> selectPage(Page<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
:根据Page和Wrapper条件进行分页查询Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
:根据Wrapper条件查询记录条数List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap)
:根据columnMap条件(等值条件)查询多条记录List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
:根据Wrapper条件查询多条记录,结果为Map列表List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper)
:根据Wrapper条件查询多条记录,结果为Object列表
2.3.4 Update
int updateById(@Param(Constants.ENTITY) T entity)
:根据ID更新一条记录,返回受影响的行数int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> wrapper)
:根据wrapper条件更新一条记录,返回受影响的行数
2.3.5 Delete
int deleteById(Serializable id)
:根据ID删除一条记录,返回受影响的行数int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap)
:根据columnMap条件删除记录,返回受影响的行数int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper)
:根据Wrapper条件删除记录,返回受影响的行数int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList)
:根据ID集合批量删除记录,返回受到影响的行数
2.3.6 Mapper提供的CRUD方法概述
Mapper提供的CRUD方法只是最基本、最常用的CRUD方法,对于复杂业务,如需要多表查询、批量操作等场景,还是需要手动写SQL。
Mapper中提供的CRUD方法都有相应的前缀,记住前缀,使用时点出来选择即可,非常方便。前缀规则如下:
- Create操作的前缀:insert
- Read操作的前缀:select
- Update操作的前缀:update
- Delete操作的前缀:delete
2.4 基于IService接口实现CRUD
2.4.1 使用方式
- 服务层接口继承
IService<XxxYyyZzzPO>
- 服务层接口的实现类继承
ServiceImpl<XxxYyyZzzMapper, XxxYyyZzzPO>
- 使用Service对象直接调用对应的方法完成CRUD
2.4.2 IService提供的方法
2.4.2.1 CRUD
T get(Serializable id)
:根据ID查询单条记录T getOne(Wrapper<T> queryWrapper)
:根据Wrapper查询单条记录List<T> list()
:查询所有记录List<T> listByIds(Collection<? extends Serializable> idList)
:根据ID列表查询多条记录List<T> list(Wrapper<T> queryWrapper)
:根据Wrapper查询多条记录boolean save(T entity)
:保存(根据ID判断,无则新增,有则更新)一条记录boolean saveBatch(Collection<T> entityList, int batchSize)
:批量保存记录boolean update(T entity)
:更新一条记录boolean update(Wrapper<T> updateWrapper)
:根据条件更新记录boolean updateBatchById(Collection<T> entityList, int batchSize)
:批量根据ID更新记录(给出对象集合,根据ID判断更新哪条记录,根据其他属性来更新)boolean remove(Serializable id)
:根据ID删除一条记录boolean removeByIds(Collection<? extends Serializable> idList)
:根据ID集合批量删除记录boolean removeLogicById(Serializable id)
:逻辑删除一条记录boolean remove(Wrapper<T> queryWrapper)
:根据条件删除记录
2.4.2.2 其他(计数,存在性,分页查询)
long count()
:查询记录总条数long count(Wrapper<T> queryWrapper)
:根据Wrapper查询记录数量boolean exists(Wrapper<T> queryWrapper)
:根据Wrapper判断记录是否存在IPage<T> page(IPage<T> page)
:无条件的分页查询IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper)
:有条件的分页查询IPage<T> pageOrderBy(IPage<T> page, Wrapper<T> queryWrapper, boolean isAsc)
:有条件的分页查询并排序
三、使用Wrapper实现复杂条件的CRUD
3.1 常用Wrapper
查询Wrapper:用于构建WHERE
子句,可在查询和删除时使用,常见的查询Wrapper有QueryWrapper,QueryChainWrapper,LambdaQueryWrapper,LambdaQueryChainWrapper
更新Wrapper:用于构建SET
子句和WHERE
子句,仅在更新时使用,常见的更新Wrapper有UpdateWrapper,LambdaUpdateWrapper
LambdaXxxxWrapper:支持使用Lambda表达式构建查询条件的Wrapper,如LambdaQueryChainWrapper和LambdaUpdateWrapper
XxxChainWrapper:仅查询Wrapper有,支持链式操作,如QueryChainWrapper和LambdaQueryChainWrapper
EntityWrapper:早期版本中提供的Wrapper,已弃用,推荐使用QueryWrapper和UpdateWrapper代替
3.2 Wrapper的使用流程(非ChainWrapper)
- 创建Wrapper对象
- 调用Wrapper对象的方法来构建条件语句
- 配合BaseMapper或IService中的条件操作来使用
// 1. 创建Wrapper对象
QueryWrapper<UserPO> wrapper = new QueryWrapper<>();
// 2. 调用Wrapper对象的方法来构建条件语句
wrapper.eq("name", "Tom");
// 3. 使用BaseMapper或IService中提供的方法来执行该Wrapper对应的操作
UserPO user = userMapper.selectOne(wrapper);
3.3 Wrapper常用方法介绍
(图取自MyBatis-Plus:条件构造器Wrapper)
3.4 lambda表达上的使用
对于不带Lambda的Wrapper,调用的方法一般涉及两个参数,第一个参数是列名(和表的列名一致,而不是实体类的字段名),第二个参数是构成对应条件所需的数据。
wrapper.eq("name", "Tom");
对于带Lambda的Wrapper,调用的方法一般涉及两个参数,第一个参数通过方法调用"XxxYxxPO::getField"来指定列,即该字段所映射的列。
lambdaWrapper.eq(User::getName, "Tom");
不带Lambda的Wrapper可以通过先调用lambda()方法来支持lambda表达式,之后追加调用的方法所需的参数同带Lambda的Wrapper一致。
wrapper.lambda().eq(User::getName, "Tom");
使用Lambda的方式来指定列名的好处:
- 通过方法调用来指定列,而不是手动输入列名,避免输入错误
- 使用方法调用时对重构很友好,如当修改了表的列名时,只需要在实体类那里修改即可,而手写列名字符串的方式则需要一个一个去改
3.5 链式操作(可读性差,不建议使用)
LambdaQueryChainWrapper的不同之处:
- 中间调用,得到的是LambdaQueryChainWrapper对象本身
- 终端操作,得到的是查询结果(而不需要配合BaseMapper或IService使用)
// 链式操作
List<UserPO> users = new LambdaQueryChainWrapper<>(userMapper)
.eq(user::getId, id)
.list();
// 对比非链式的操作
LambdaQueryChainWrapper<UserPO> wrapper= new LambdaQueryWrapper<>();
wrapper.eq(user::getId, id);
List<UserPO> users = userMapper.selectList(wrapper);
四、一些特殊的CRUD操作
4.1 逻辑删除
(逻辑删除仅对自动注入的SQL起效(原生SQL失效))
使用@TableLogic(value = "0",delval = "1")
作用于逻辑删除字段Integer isDel
4.2 新增后返回主键值
(注意:使用前提是在对应实体类的主键上使用了@TableId
注解)
方式一:使用MyBatis Plus提供的insertAndGetId()
方法来插入记录并返回主键值
方法二:直接使用userMapper.insert(user)
来插入记录,之后再从user中区主键值
4.3 执行原生SQL的流程
- 开启SqlRunner(否则会报错)
enableSqlRunner: true
- 使用SqlRunner.db()调用对应的方法,并传入SQL语句字符串(链式方式)
List<Object> result = SqlRunner.db().selectList.selectList(sql);
4.4 自动填充功能
有些默认值,或者如日期这类有更新规则的值,可以使用自动填充实现。自动填充功能可以在数据库实现,但不推荐,这里介绍MyBatis Plus提供的自动填充功能。
- 实用
@TableField
注解,设置自动填充的字段及其填充时机
// 在新增时自动填充日期时间
@TableField(fill = FieldFill.INSERT)
private Data createTime;
// 在新增和更新时自动填充日期时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private Data updateTime;
- 配置字段填充处理规则(对加了
@TableField
、且为指定字段名的字段进行填充)
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
}
// 更新时的填充策略
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
- 配置MyBatis Plus的自动填充
@Configuration
public class MyBatisPlusConfig {
@Autowired
private MyMetaObjectHandler myMetaObjectHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new MetaObjectHandlerInterceptor(myMetaObjectHandler));
return interceptor;
}
}
五、分页查询
5.1 前端分页与后端分页
前端分页:后端将所有数据全部查出来一起给前端,由前端负责数据的分页。在数据量较小时是可行的,但是,随着数据量的增大,这将占用大量的内存和带宽
后端分页:后端将分页数据找出来并发送给前端,具体实现又可分为逻辑分页和物理分页
5.2 逻辑分页与物理分页
逻辑分页:在后端先查询出所有的数据,再在后端进行分页操作,将目标页面的数据发送给前端。逻辑分页需要占用大量的内存和带宽
物理分页:在MySQL服务器完成分页查询,给到后端的数据就是分页后的数据,由后端再将其发送给前端。显然,物理分页是最优的方案
5.3 使用BaseMapper和IService实现分页查询
- 创建Page对象
Page<XxxDO> doPage = new Page<>(pageNum, pageSize)
- 使用BaseMapper/IService的分页查询方法,并将doPage传入
// BaseMapper的例子
xxxMapper.selectPage(doPage, queryWrapper);
// IService的例子
xxxService.page(doPage, queryWrapper);
5.4 使用PageHelper
对于某些场景,如需要手写SQL时,若需要分页,就可以使用PageHelper来实现(底层通过拦截器在SQL中拼接上对应的分页子句实现)。使用时,在执行需要分页的SQL语句之前使用PageHelper.startPage(pageNum, pageSize)
来完成分页,还需要在上述SQl语句执行之后立刻接上PageHelper.clear()
,避免因为SQL执行失败或其他原因,导致分页操作未清除,从而会影响到后续SQL。
六、手写SQL
MyBatis支持注解和XML文件两种方式来手写SQL,这里以XML文件方式为例。
6.1 定义CRUD语句的标签
CRUD四种操作对应的标签分别是<insert>
,<select>
,<update>
和<delete>
。
它们的常用属性如下:(后两个属性仅<select>
使用)
id
:SQL语句的唯一标识符,在mapper映射文件通过此ID引用该SQL,还通过此ID与对应的接口方法绑定(接口方法的全限定名与ID相同)parameterType
:若参数是一个Java类的类型,此时需要使用该属性指定传入的参数的类型,可以是Java类型(全限定名)或者别名。如果参数类型是基本数据类型,可以省略此属性,MyBatis会自动识别resultType
:Java类的全限定名,在自动映射结果集时使用,根据结果集中的列名与Java对象的属性名来完成映射(不区分大小,相同则匹配)resultMap
:ResultMap
的ID,在手动映射结果集时使用,根据ResultMap
中设置的映射规则来完成映射
6.2 ResultMap标签
6.2.1 使用场景
- 简单映射:查询结果集中的列名与Java类的属性名不一致时
- 复杂映射:用到了关联查询时(一对一关联,一对多关联)
6.2.2 简单映射
首先新建一个ResultMap
标签,属性id
用于引用该标签,配合SQL标签中的resultMap
属性使用,属性type
用于指定映射到的Java类,需要用该类的全限定名称。
然后使用ResultMap
标签的子标签定义从列名到属性名的映射。主键列的映射使用子标签<id>
实现,而非主键列的映射使用子标签<result >
实现。两个标签中都使用property
和column
来指定此映射对应的属性和列名。
使用时,只需要在SQL标签中使用resultMap="userResultMapID"
来指定指定映射方式即可。
<select id="queryUserAll" resultMap="userResultMap">
SELECT
user_id,
user_name
FROM
user
</select>
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="name" column="user_name" />
</resultMap>
6.2.3 复杂映射
6.2.3.1 背景
业务背景:查询业务中常涉及多表的关联查询。以订单表和用户表关系为例,从查询订单信息出发关联查询用户信息时,属于一对一关联查询。而从查询用户信息出发关联查询订单信息时,属于一对多关联查询。
实体类与关联关系:每张表会对应一个实体类,实体类除了用于列到属性的映射外,还可以用于表示关联关系。如下所示,每个Order对应一个用户,存在一对一关联查询,所以需要在Order中增设一个User user来表示这种关联查询关系。对个用户对应多个订单,存在一对多关联查询,所以需要在User中增设一个List<Order> orders俩表示这种关联查询关系。
@Data
public class Order {
private Long id;
private String description;
private User user;
}
---
@Data
public class User{
private Long id;
private String name;
private List<Order> orders;
}
问题:数据库查询结果是个二维表,我们需要将二维表数据绑定到上述类中,由于结果集中某些数据需要映射到二级属性中,然而自动映射无法支持这种操作,这时候就要使用ResultMap
来实现。
6.2.3.2 一对一查询的映射
借助ResultMap
标签的<association>
子标签可以完成一对一关联查询的映射。该子标签的属性以及内部映射的定义方式和ResultMap
大体一致,不过新增了property
来指标对应的一级属性,指定映射实体类的属性名从type
改成了javaType
。
<select id="queryOrderUser" resultMap="orderUserResultMap">
SELECT
o.id as id,
o.description as description,
o.id as user_id,
u.name as user_name
FROM
order o LEFT JOIN user u ON o.user_id = u.id
</select>
<resultMap id="orderUserResultMap" type="Order">
<id property="id" column="id" />
<result property="description" column="description" />
<association property="user" javaType="User">
<id property="id" column="user_id" />
<result property="name" column="user_name" />
</association>
</resultMap>
6.2.3.3 一对多查询的映射
借助ResultMap
标签的<collection>
子标签可以完成一对一关联查询的映射。该子标签的属性以及内部映射的定义方式和ResultMap
大体一致,不过新增了property
来指标对应的一级属性,新增的属性javaType
值为list
,使用ofType
属性来指定集合元素对应的实体类。
<select id="queryUserOrder" resultMap="userOrderResultMap">
SELECT
u.id as id,
u.name as name,
o.id as order_id,
o.description as order_description
FROM
user u LEFT JOIN order o ON u.id = o.user_id
</select>
<resultMap id="userOrderResultMap" type="User">
<id property="id" column="id" />
<result property="name" column="name" />
<collection property="user" javaType="list" ofType="Order">
<id property="id" column="order_id" />
<result property="description" column="order_description" />
</collection>
</resultMap>
6.3 动态SQL的标签
6.3.1 if 标签
作用:满足test条件时才会将相应的SQL片段拼接到SQL语句中
<if test="name != null and name != ''">
and NAME = #{name}
</if>
6.3.2 foreach 标签
作用:遍历集合或数组,并将每个元素的值插入到SQL语句的指定位置
属性:
- collection:告诉MyBatis从哪里获取数据来进行循环遍历。它指定了一个集合或数组作为数据源
- item:迭代过程中当前元素的别名,用于在SQL语句中引用当前遍历的元素
- index:如果迭代的是集合或数组,这个属性代表当前迭代的位置索引;如果是Map,则表示当前元素的键名
- open:指定第一次迭代产生的SQL片段之前添加的字符,通常在构建IN语句时放置一个左括号在最前面
- close:指定最后一次迭代产生的SQL片段之后添加的字符,通常在构建IN语句时放置一个右括号在最后面
- separator:每次迭代生成的SQL片段之间的分隔符
<select id="selectUsersByStatuses" parameterType="java.util.List" resultType="com.example.User">
SELECT * FROM users
<where>
<foreach item="status" collection="statuses" separator=" OR ">
status = #{status}
</foreach>
</where>
</select>
6.3.3 choose-when-otherwise 标签
choose-when-otherwise标签类似于Java中的if…else if…else语句,<choose>
仅用于引导开始和结束,每个<when>
标签对应一个条件分支,<otherwise>
对应最后的那个else的处理。
<choose>
<when test="Name!=null and student!='' ">
AND name LIKE CONCAT(CONCAT('%', #{student}),'%')
</when>
<when test="hobby!= null and hobby!= '' ">
AND hobby = #{hobby}
</when>
<otherwise>
AND AGE = 15
</otherwise>
</choose>
6.3.4 where 标签
使用<if>
标签构建where子句时,需要以where 1 = 1
开头,而且需要自己在每个条件前加上连接词AND
或者OR
。使用<where>
标签时,若内部没有生成内容,则不会生成where,故不用加where 1 = 1
开头。且内部可以都用AND Condition
或OR Condition
,不用担心where后面紧跟AND的情况,该标签会自动删除在最前面的连接词。
6.3.5 set标签
在<update>
标签使用,用于动态生成SQL中的SET子句。在<set>
标签内部使用<if>
、<choose>
、<when>
等动态标签来构建更新条件时,可以大胆地在后面加上逗号,因为<set>
标签会自动删除最后的那个逗号问题,避免多余的逗号产生语法错误。
此外,<set>
标签还可以配合<if>
标签使用,解决参数为空时产生的问题。
<set>
<if test="username != null">username = #{username},</if>
</set>
6.3.6 其他标签
sql标签:定义复用的sql片段
include标签:引用定义的sql片段
七、乐观锁和悲观锁
7.1 乐观锁和悲观锁的概念
乐观锁:不加锁,而是在每次操作数据时去查看是否冲突,冲突则按一定策略处理。乐观锁在冲突少时性能好、并发性高。但在冲突频繁时,处理冲突会浪费很多资源,会严重影响性能。
悲观锁:通过加锁来解决并发问题,使用锁的开销较大,但是能保证数据的独占性和正确性。
7.2 乐观锁的原理:基于版本号的冲突判断
(类似MySQL内部的乐观锁机制,只不过这里基于自己加的字段和SQL实现)
- 取出记录时,获取当前version
- 更新时,带上这个version
- 执行更新时,使用
set version = newVersion where version = oldVersion
来检测冲突 - 如果version不对,说明发生了冲突,就会更新失败(可以在代码层面实现冲突处理:重试或放弃)
7.3 乐观锁的实现
- 在MyBatis Plus的配置类中新增乐观锁拦截器(即开启乐观锁),并且要开启自动管理事务
@EnableTransactionManagement
- 在表中增加一个int(10)字段version,初始值设置为1
- 在需要同步的实体类(PO)中的version字段上加上
@Version
注解
八、代码生成器
MyBatis-Plus提供的代码生成器可以快速生成 Entity、Mapper、Mapper XML、Service、Controller等各个模块的代码,极大的提升了开发效率,MyBatis Plus从3.0.3之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖,才能实现代码生成器功能。
// to do (看似方便,但不实用,先了解有哪些功能,需要用时再自行了解即可)
参考博客推荐
ORM框架
MyBatis Plus详细教程
MyBatis-Plus 使用详解
Mybatis-Plus详解
IService接口和ServiceImpl实现类(Mybatis-Plus对service层的封装)
BaseMapper接口的使用
MyBatis-Plus:条件构造器Wrapper