文章目录

MyBatisPlus学习笔记前言1、MyBatisPlus概述2、快速体验3、CRUD接口3.1 Mapper层CRUD接口3.1.1 Insert3.1.2 Delete3.1.3 Update3.1.4 Select

3.2 Service层CRUD接口3.2.1 Save3.2.2 Remove3.2.3 Update3.2.4 Get

3.3 自定义SQL接口4、常用注解和配置4.1 @TableId4.2 @TableField4.3 @TableLogic

5、条件构造器和常用接口5.1 QueryWrapper5.2 UpdateWrapper5.3 使用Condition动态组装条件5.4 LamdaQueryWrapper5.5 LamdaUpdateWrapper

6、插件6.1 MyBatisPlus分页插件6.2 悲观锁与乐观锁6.3 MyBatisX插件

7、通用枚举8、代码生成器8.1 快速生成8.2 交互式生成

9、多数据源

MyBatisPlus学习笔记

前言

  本文是参考MyBatisPlus官网对MyBatisPlus的一个学习笔记,主要是对MyBatisPlus的一个简单的入门学习,大致对MyBatisPlus有一个整体认知,熟悉使用MyBatisPlus提供的各种API(比如MyBatisPlus提供的增删改查接口),以及各种便利的特性和插件(比如自动生成代码、MyBatisPlus分页插件)。当然主要目的是想利用费曼学习方法将学到的知识进行一个输出,这样能够加深我对知识有一个更加深刻的认知,因为这些知识在官方是都能够查看到的,我用自己的认知、自己的排版对这些知识进行重复输出。

1、MyBatisPlus概述

MyBatisPlus是什么?

MyBatisPlus(简称MP)是MyBatis的增强版,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。和MyBatis一样是一款数据访问层的框架,可以大大简化对SQL的操作

官方文档地址:MyBatis-Plus Gitee开源地址:mybatis-plus: mybatis GitHub开源地址:baomidou/mybatis-plus 创始人:青苗

MyBatisPlus的t特点:

无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作 支持的数据库

国外:MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift国内:达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库 框架结构

2、快速体验

#mermaid-svg-wEwO7RlaQNDn36ZG {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG .error-icon{fill:#552222;}#mermaid-svg-wEwO7RlaQNDn36ZG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wEwO7RlaQNDn36ZG .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-wEwO7RlaQNDn36ZG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wEwO7RlaQNDn36ZG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wEwO7RlaQNDn36ZG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wEwO7RlaQNDn36ZG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wEwO7RlaQNDn36ZG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wEwO7RlaQNDn36ZG .marker.cross{stroke:#333333;}#mermaid-svg-wEwO7RlaQNDn36ZG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wEwO7RlaQNDn36ZG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG .cluster-label text{fill:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG .cluster-label span{color:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG .label text,#mermaid-svg-wEwO7RlaQNDn36ZG span{fill:#333;color:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG .node rect,#mermaid-svg-wEwO7RlaQNDn36ZG .node circle,#mermaid-svg-wEwO7RlaQNDn36ZG .node ellipse,#mermaid-svg-wEwO7RlaQNDn36ZG .node polygon,#mermaid-svg-wEwO7RlaQNDn36ZG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wEwO7RlaQNDn36ZG .node .label{text-align:center;}#mermaid-svg-wEwO7RlaQNDn36ZG .node.clickable{cursor:pointer;}#mermaid-svg-wEwO7RlaQNDn36ZG .arrowheadPath{fill:#333333;}#mermaid-svg-wEwO7RlaQNDn36ZG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wEwO7RlaQNDn36ZG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wEwO7RlaQNDn36ZG .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-wEwO7RlaQNDn36ZG .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-wEwO7RlaQNDn36ZG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wEwO7RlaQNDn36ZG .cluster text{fill:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG .cluster span{color:#333;}#mermaid-svg-wEwO7RlaQNDn36ZG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wEwO7RlaQNDn36ZG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

搭建环境

编写配置文件

编写实体类

编写Mapper

编写测试类

扫描Mapper

Step1:搭建环境 1)建表(使用的是MySQL) 建立一张 user 表 DROP TABLE IF EXISTS user;

CREATE TABLE user

(

id BIGINT(20) NOT NULL COMMENT '主键ID',

name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',

age INT(11) NULL DEFAULT NULL COMMENT '年龄',

email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',

PRIMARY KEY (id)

);

往表中添加数据: DELETE FROM user;

INSERT INTO user (id, name, age, email)

VALUES (1, 'Jone', 18, 'test1@baomidou.com'),

(2, 'Jack', 20, 'test2@baomidou.com'),

(3, 'Tom', 28, 'test3@baomidou.com'),

(4, 'Sandy', 21, 'test4@baomidou.com'),

(5, 'Billie', 24, 'test5@baomidou.com');

2)导入依赖

mysql

mysql-connector-java

org.springframework.boot

spring-boot-starter

org.springframework.boot

spring-boot-starter-test

test

org.projectlombok

lombok

com.baomidou

mybatis-plus-boot-starter

3.5.2

3)编写配置文件 application.yml server:

port: 8080

spring:

datasource:

driver-class-name: com.mysql.cj.jdbc.Driver

url: jdbc:mysql://localhost:3306/mybatis-plus_study?serverTimezone=UTC

username: root

password: 32345678

mybatis-plus:

configuration:

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启标准日志

Step2:编写实体类 @Data

public class User {

private Long id;

private String name;

private Integer age;

private String email;

}

Step3:编写Mapper package com.hhxy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.hhxy.entity.User;

import org.apache.ibatis.annotations.Mapper;

@Mapper

public interface UserMapper extends BaseMapper {

/*

说明: BaseMapper

BaseMapper是MP封装了通用CRUD的接口,Mapper只需要集成BaseMapper

T为任意实体类,只要让Mapper集成BaseMapper并且关联对应的实体类,就能使用Mapper对象操作实体类对应的表了

*/

}

Step4:扫描Mapper 在SpringBoot的启动类上添加**@MapperScan("com.hhxy.mapper")**注解 PS:如果我们在Mapper接口上添加了@Mapper注解,则这个注解可以省略 Step5:编写测试类 @SpringBootTest

public class SampleTest {

@Autowired

private UserMapper userMapper;

@Test

public void testSelect() {

System.out.println(("----- selectAll method test ------"));

List userList = userMapper.selectList(null);

Assertions.assertEquals(5, userList.size());

userList.forEach(System.out::println);

}

}

知识拓展:关于MP中的SQL是如何确定要操作的表是谁

我们在使用MP时,我们自己编写的Mapper必须继承BaseMapper,通过这一步,我们MP底层会自动将T映射为SQL操作表,举个例子吧:当我们的T是User时,那么SQL操作的表就是user

那如果数据库中的表是tb_user,如何确保SQL操作的表是tb_user呢?

方案一:最直接的方法肯定是修改entity的格式,比如可以将entity修改成tb_user、TbUser、tbUser、Tb_User、Tb_User。 方案二:通过@TableName注解映射表名,比如@TableName("tb_user") 方案二:通过编写配置文件,给user添加一个前缀 mybatis-plus:

global-config:

db-config:

table-prefix: tb_

需要注意的是,通过配置文件是全局有效的,也就是所有的实体类都会增加一个前缀tb_

3、CRUD接口

MP在MyBatis的基础上做了增强,底层封装了大量通用的SQL,主要有BaseMapper和IService两个CRUD接口,其中IService的实现类是ServiceImpl,BaseMapper中的方法以insert、delete、update、select开始,IService中的方法以save、remove、get开始

insert、delete、update的返回值都是int类型,返回值为0说明无数据更新,此时SQL执行失败save、remove的返回值都是boolean类型,返回值为false说明SQL执行失败

3.1 Mapper层CRUD接口

参数说明:

类型参数名描述Tentity实体对象Wrapperwrapper实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)CollectionidList主键 ID 列表(不能为 null 以及 empty)Serializableid主键 ID(可以是任何实现了序列化接口的数据类型)MapcolumnMap表字段 map 对象IPagepage分页查询条件(可以为 RowBounds.DEFAULT)

备注:基本数据类型的包装类都实现了序列化接口,String也实现了序列化接口

3.1.1 Insert

int insert(T entity):插入一条记录,返回值为更新数据的条数

@Test

public void testInsert(){

User user = new User("张三", 23, "123@qq.com");

// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? );

int result = userMapper.insert(user);

System.out.println("insert = " + result); // result = 1

// MP很贴心,它会将Insert后自动生成的id,重新赋值给user对象(不用向MyBatis还要进行配置才能获取生成id)

System.out.println("MP自动生成的id为: "+user.getId());

}

备注:不止是BaseMapper的Insert方法能够通过getId直接获取自动生成的Id,IService的save方法也能够通过getId直接获取自动生成的Id。总的来讲MP真的简化了MyBatis,让开发变得更简单

3.1.2 Delete

int deleteById(Serializable id):根据id删除一条记录,返回值为更新数据的条数 int deleteById(T entity):根据entity的id删除一条记录,返回值为更新数据的条数 注意事项:entity必须具有getId()这个方法,否则会报错 PS:其实可以通过@TableId配置自定义的主键,详情键第四节【常用注解和配置】 int deleteByMap(Map columnMap):以map的value作为条件进行删除 注意事项:map的key必须要与表中的字段进行对应(不区分大小写) int deleteBachIds(Cellection idList):批量删除 int delete(Wrapper queryWrapper):条件删除

int result = 0;

// DELETE FROM user WHERE id=?;

result = userMapper.deleteById(1613825708092678146L);

User user = new User(1L,"张三", 23, "123@qq.com");

// DELETE FROM user WHERE id=?;

result = userMapper.deleteById(user);

Map map = new HashMap<>();

map.put("name", "张三");

map.put("age", 23);

// DELETE FROM user WHERE name=? AND age=?;

result = userMapper.deleteByMap(map);

List idList = Arrays.asList(1L, 2L, 3L);

// DELETE FROM user WHERE id IN (?, ?, ?);

result = userMapper.deleteBatchIds(idList);

3.1.3 Update

int updateById(T entity):根据id进行修改 entity必须具有getId()这个方法,否则会报错 int update(T entity, Wrapper updateWrapper):根据条件进行修改

int result = 0;

User user = new User(1L,"张三", 23, "123@qq.com");

// UPDATE user SET name=?, age=?, email=? WHERE id=?;

result = userMapper.updateById(user);

3.1.4 Select

T selectById(Serializable id):根据id进行查询 List selectBatchIds(Collection idList):批量查询 List selectByMap(Map columnMap):根据map的value进行条件查询 注意事项:map的key必须要与表中的字段进行对应(不区分大小写) List selectList(Wrapper queryWrapper):条件查询

// SELECT id,name,age,email FROM user WHERE id=?;

User user = userMapper.selectById(1L);

List idList = Arrays.asList(1l, 2l, 3l);

// SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? );

List users = userMapper.selectBatchIds(idList);

Map map = new HashMap<>();

map.put("name", "张三");

map.put("age", 23);

// SELECT id,name,age,email FROM user WHERE name = ? AND age = ?;

List users = userMapper.selectByMap(map);

// SELECT id,name,age,email FROM user;

List users1 = userMapper.selectList(null);

3.2 Service层CRUD接口

类型参数名描述intbatchSize插入批次数量Tentity实体对象WrapperupdateWrapper实体对象封装操作类 UpdateWrapperCollectionentityList实体对象集合CollectionidListidListFunctionmapper转换函数

3.2.1 Save

boolean save(T entity):新增一条记录 boolean saveBatch(Collection entityList):批量添加 温馨提示:

使用saveBatch,最好在数据库连接的url中添加一个rewriteBatchedStatements=true参数,实现高性能的批量插入 使用saveBatch,底层使用了事务,执行多条新增只会提交一次事务;但是如果在for循环中使用,会提交多次事务(不建议在循环中使用saveBatch方法) saveBatch(Collection entityList)底层就是调用saveBatch(Colection entityList, int batchSize),只是前一个他设置了默认值batchSize=1000,也就是一个批次会插入1000条,超过一千条需要等待下一批次执行 备注:saveBatch底层是通过JDBC的executeBatch实现的。批次只针对增删改操作,它是指将执行SQL的语句的请求先存起来,等到达到一定数量,就当作一批发送给MySQL服务器,让MySQL服务器一次将这些SQL执行完,这样做可以避免频繁和MySQL数据库交互,造成效率低下

参考文章:

MyBatis-plus批量写入数据方法saveBatch速度很慢原因排查 Mybatis-Plus批量插入的简单自测Mybatis Plus saveBatch批量插入如何高效Java-Mysql之批处理_veejaLiu的博客-CSDN博客什么是批处理?

boolean saveBatch(Colection entityList, int batchSize):批量添加 当batchSize=1时,都是一条一条执行单一的insert语句 当batchSize>=list.size()时,底层通过executeBatch方法将list中的数据拼接成一条SQL,最终执行性一条SQL boolean saveOrUpdate(T entity): boolean saveOrUpdate(T entity, Wrapper updateWrapper): boolean saveOrUpdateBatch(Collection entityList): boolean saveOrUpdateBatch(CollectionentityList, int batchSize):

// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? );

User user = new User("张三", 22, "123@qq.com");

boolean result = userService.save(user);

List users = new ArrayList<>();

for (int i = 0; i < 3; i++) {

User user = new User("张三", 18, "123@qq.com");

users.add(user);

}

// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? ),( ?, ?, ?, ? ),( ?, ?, ?, ? );

boolean result = userService.saveBatch(users);

// 执行三次: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? );

boolean result = userService.saveBatch(users, 1);

3.2.2 Remove

boolean removeById(Serializable id):根据id删除数据boolean removeById(T entity):根据id删除数据boolean removeById(Serializable id, boolean useFill):boolean removeBatchByIds(Collection list):更具id进行批量删除boolean removeBatchByIds(Collection list, int batchSize):boolean removeBatchByIds(Collection list, boolean useFill):boolean removeBatchByIds(Collection list, int batchSize, boolean useFill):

3.2.3 Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset

boolean update(Wrapper updateWrapper);

// 根据 whereWrapper 条件,更新记录

boolean update(T updateEntity, Wrapper whereWrapper);

// 根据 ID 选择修改

boolean updateById(T entity);

// 根据ID 批量更新

boolean updateBatchById(Collection entityList);

// 根据ID 批量更新

boolean updateBatchById(Collection entityList, int batchSize);

3.2.4 Get

// 根据 ID 查询

T getById(Serializable id);

// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")

T getOne(Wrapper queryWrapper);

// 根据 Wrapper,查询一条记录

T getOne(Wrapper queryWrapper, boolean throwEx);

// 根据 Wrapper,查询一条记录

Map getMap(Wrapper queryWrapper);

// 根据 Wrapper,查询一条记录

V getObj(Wrapper queryWrapper, Function mapper);

// 查询所有

List list();

// 查询列表

List list(Wrapper queryWrapper);

// 查询(根据ID 批量查询)

Collection listByIds(Collection idList);

// 查询(根据 columnMap 条件)

Collection listByMap(Map columnMap);

// 查询所有列表

List> listMaps();

// 查询列表

List> listMaps(Wrapper queryWrapper);

// 查询全部记录

List listObjs();

// 查询全部记录

List listObjs(Function mapper);

// 根据 Wrapper 条件,查询全部记录

List listObjs(Wrapper queryWrapper);

// 根据 Wrapper 条件,查询全部记录

List listObjs(Wrapper queryWrapper, Function mapper);

// 无条件分页查询

IPage page(IPage page);

// 条件分页查询

IPage page(IPage page, Wrapper queryWrapper);

// 无条件分页查询

IPage> pageMaps(IPage page);

// 条件分页查询

IPage> pageMaps(IPage page, Wrapper queryWrapper);

3.3 自定义SQL接口

这个和MyBatis中是高度重合的,我就不在这里继续赘述了,如果想了解可以参考这篇文章:初识MyBatisPlus

MP充分践行了它的理念:只做增强,不做修改

4、常用注解和配置

此外还有@TableName注解比较常用,这个已经在前面学习过了

4.1 @TableId

@TableId:用于映射主键 MP默认将id作为注解,如果数据库中主键非id,会报错。比如我们数据库中的注解为uid,实体类的字段也为uid,此时需要在实体类的uid上添加一个@TableId注解,告诉MP uid是主键

value属性 前面是数据库的主键为uid,实体类中的属性也是uid。但如果数据库中主键是uid,实体类中的主键是id呢?此时就需要使用@TableId注解的value属性了。具体使用方式: 给实体类的uid属性上添加@TableId(value = "uid"),只有value一个属性,还可以省略为@TableId(“uid”) type属性

IdType说明AUTO使用id自增策略(前提是数据库也要开启id自增)NONE不手动设置主键值,MP将默认给出一个 Long 类型的字符串作为主键INPUT手动设置主键值(不设置就没有)ASSIGN_ID使用雪花算法生成主键值ASSIGN_UUID使用uuid生成主键值MP的id生成策略默认是雪花算法,如果我们不想使用雪花算法,需要使用@TableId注解的type属性进行配置 给实体类的id属性上添加@TableId(type = idType.AUTO) 注意:手动设置id的优先级要高于MP的id生成策略

知识拓展:

拓展一:通过配置文件配置主键生成策略 mybatis-plus:

global-config:

db-config:

id-type: auto # assign_id、assign_uuid、input、none

拓展二:雪花算法 简介:雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的 主键的有序性 特点:全局唯一性(生成的id与当前时间相关)、递增性、高可用性(能够确保任何时候都能生成正确的id)、高性能(特别适合高并发场景) 核心思想: 长度共64bit(一个long型)。 首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负 数是1,所以id一般是正数,最高位是0。 41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。 10bit作为机器的ID(5个bit是数据中心,5bit的机器ID,可以部署在1024个节点)。 12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID) 为了应对数据规模增长导致的压力,我们需要对数据库进行扩展。常见的扩展方式有:业务分库、主从复制、数据库分表 优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高 1)数据库分表 将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务 继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据, 如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进 行拆分。 单表数据拆分有两种方式:垂直分表和水平分表。示意图如下: 垂直分表:拆分字段。垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。 例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用 户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展 示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外 一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。 水平分表:拆分记录。水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以 作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。 但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性 能瓶颈或者隐患。 水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理 体验分表:以主键id为例 1)方式一:通过分段的方式分表。 选取适当的分段范围,比如可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中, 1000000 ~ 1999999 放到表2中,以此类推。 复杂点:分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会 导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适 的分段大小。 优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万, 只需要增加新的表就可以了,原有的数据不需要动 缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而 另外一个分段实际存储的数据量有 1000 万条 2)方式二:通过取模的方式分表。假如我们一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来 表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号 为 6 的子表中。 复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题 优点:表分布比较均匀 缺点:扩充新的表很麻烦,所有数据都要重分布

4.2 @TableField

前面我们遇到主键不一致,我们可以使用@TableId注解进行映射,如果我们的其它属性和表的字段不一致,我们则需要使用@TableField注解进行映射

情况一:MP自动映射驼峰和下划线

若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格。例如实体类属性userName,表中字段user_name ,此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格(这个在MyBatis中需要手动配置,而MP中则默认配置了)

情况二:使用@TableField注解映射字段

若实体类中的属性和表中的字段不满足情况一,例如实体类属性name,表中字段username,此时需要在实体类属性上使用@TableField("username")设置属性所对应的字段

情况三:使用@TableField注解自动填充

在项目开发时,对于一些字段(比如:create_time、update_time……),每次执行添加、修改操作时,都需要去填充,如果使用注入的方式会显得比较麻烦,所以我们需要设置字段自动填充,当进行该操作时,就自动为这些字段进行赋值

字段自动填充主要使用到了@TableField注解的fill属性,该属性有以下取值:

Step1:环境搭建 …… Step2:创建实体类,并在实体类需要填充的字段添加上@TableField @TableField(fill = FieldFill.INSERT_UPDATE) // 当进行更新(插入、删除、修改)操作时,就会进行自动填充

private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT) // 当进行插入操作时,就会进行自动填充

private LocalDateTime createTime;

Step3:编写元数据处理器,用于设置填充的数据 @Component

public class MyMetaObjectHandler implements MetaObjectHandler {

/**

* 插入时填充字段

* @param metaObject

*/

@Override

public void insertFill(MetaObject metaObject) {

if (metaObject.hasSetter("createTime")){

metaObject.setValue("createTime", LocalDateTime.now());

/* this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); */ //也可以使用这个,这是官方给出的自动填充方式

}

if (metaObject.hasSetter("updateTime")){

metaObject.setValue("updateTime", LocalDateTime.now());

}

}

/**

* 更新时填充字段(包括修改、删除)

* @param metaObject

*/

@Override

public void updateFill(MetaObject metaObject) {

if(metaObject.hasSetter("updateTime")){

metaObject.setValue("updateTime", LocalDateTime.now());

}

}

}

备注:如果实体类中只存在updateTime一个属性时,会报错,必须要同时具有updateTime和createTime两个属性(在实体类中添加@TableField注解进行自动填充,SpringBoot会自动调用MyMetaObjectHandler中所有的方法)。为了解决这个问题,我们可以添加一个if判断,判断该类是否具有该属性,然后再进行赋值操作。 注意事项

填充的数据类型要和实体类的数据类型保持一致,否则填充结果为null 注意区别官方给出的三个填充的方法 // 这个是通用的,插入和更新都可以使用 但是当字段存在值 的时候不进行填充

this.fillStrategy(metaObject, "createTime", LocalDateTime.now());

// 这个是insert的时候用的,插入的时候时候强制进行填充

this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());

// update的时候使用,更新的时候强制进行填充

this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());

备注:以上方法都需要MyBatisPlus3.3.0版本才能进行使用,值得一提的是fillStrategy再3.3.0版本中有bug,需要使用更高的版本(PS:反正我现在使用的是MP3.5问题不大)

4.3 @TableLogic

逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库 中仍旧能看到此条数据记录物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据

如果我们想要被删除的数据能够恢复,就需要使用到逻辑删除

示例:

Step1:创建一个逻辑删除字段 is_deleted,同时设置默认值为0(0表示未删除,1表示删除) Step2:实体类中也添加一个逻辑删除的属性isDeleted Step3:给实体类的逻辑删除属性上添加一个@TableLogic注解 Step4:测试 int result = userMapper.deleteById(1613825708092678146L);

可以看到此时,删除的SQL并不是以前的DELETE FROM user WHERE id=?;了,而是换成了UPDATE tb_user SET is_deleted=1 WHERE id=? AND is_deleted=0; 要恢复逻辑删除的数据也能简单,直接在数据库中将is_deleted的值改成0就好了,需要注意的是逻辑删除后的数据,查询语句是无法查到的,同时update语句也是无法进行修改的

5、条件构造器和常用接口

Wrapper : 条件构造抽象类,最顶端父类

AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件

QueryWrapper : 查询条件封装UpdateWrapper : Update 条件封装AbstractLambdaWrapper : 使用Lambda 语法

LambdaQueryWrapper :用于Lambda语法使用的查询WrapperLambdaUpdateWrapper : Lambda 更新封装Wrapper

5.1 QueryWrapper

QueryWrapper 示例一:使用QueryMapper进行查询 @Test

public void test1() {

// 查询姓张的,年龄在20~30之间,并且email字段不为空的用户信息

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.like("name", "张")

.between("age", 20, 30)

.isNotNull("email");

/*

SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND

(name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)

*/

List users = userMapper.selectList(queryWrapper);

users.forEach(System.out::println);

}

进行指定字段查询: @Test

public void test6(){

// 查询指定字段(用户名、年龄、邮箱)

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.select("name", "age", "email");

// SELECT name,age,email FROM user

List> maps = userMapper.selectMaps(queryWrapper);

maps.forEach(System.out::println);

}

使用QueryMapper实现子查询: @Test

public void test7(){

// 查询id小于100的用户信息

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.inSql("id","select id from user where id < 100");

// SELECT id,name,age,email,is_deleted FROM user WHERE (id IN (select id from user where id < 100))

List> maps = userMapper.selectMaps(queryWrapper);

maps.forEach(System.out::println);

}

示例二:使用QueryMapper进行条件删除 @Test

public void test2(){

// 查询用户信息,按照年龄降序排序,如果年龄相同则按照id升序排序

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.orderByDesc("age")

.orderByAsc("id");

// SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 ORDER BY age DESC,id ASC

List users = userMapper.selectList(queryWrapper);

users.forEach(System.out::println);

}

示例三:使用QueryMapper进行条件修改 @Test

public void test4(){

// 修改(年龄大于20并且姓张)或邮箱为null的用户信息

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 20)

.like("name", "张")

.or() // QueryMapper的连接条件默认是 AND

.isNull("email");

User user = new User("张三", 22, "123@qq.com");

// UPDATE user SET name=?, age=?, email=? WHERE (age > ? AND name LIKE ? OR email IS NULL)

userMapper.update(user, queryWrapper);

}

@Test

public void test5(){

// 修改年龄大于20并且(姓张或邮箱为null)的用户信息

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 20)

.and(qw->qw.like("name", "张")

.or()

.isNull("email"));

User user = new User("张三", 22, "123@qq.com");

// UPDATE user SET name=?, age=?, email=? WHERE (age > ? AND (name LIKE ? OR email IS NULL))

userMapper.update(user, queryWrapper);

}

5.2 UpdateWrapper

示例一:

@Test

public void test8(){

// 修改 用户名中含有张并且(年龄大于20或者邮箱null)的用户信息

UpdateWrapper updateWrapper = new UpdateWrapper<>();

updateWrapper.like("name", "张")

.and(uw -> uw.gt("age", 20)

.or()

.isNull("email"));

updateWrapper.set("name", "李四").set("age", 22);

// UPDATE user SET name=?,age=? WHERE (name LIKE ? AND (age > ? OR email IS NULL))

int result = userMapper.update(null, updateWrapper);

System.out.println("result = " + result);

}

某个字段自增或自减,MyBatiPlusm目前只有两种方式实现

使用UpdateWrapper拼接 // 使用UpdateWrapper

UpdateWrapper wrapper = new UpdateWrapper();

wrapper.eq("id",【实体类对象】.getBid());

wrapper.setSql("'自增字段' = '自增字段' + 1"); // 注意SQL中没有 += 这个符号

【service对象】.update(wrapper);

// 直接调用update方法

【service对象】.update().

setSql("stock = stock -1")

.eq("voucher_id", voucherId)

.update();

直接写原生sql到xml中 略……

方式三:自定义一个自增自减字段的方法,参考文章:mybatis-plus 自定义UpdateWrapper(一)实现列自增

// 库存充足,秒杀券库存减一

// 方式一:使用Service对象的update方法

/*boolean flag = seckillVoucherService.update().

setSql("stock = stock -1")

.eq("voucher_id", voucherId)

.update();*/

// 方式二:使用LambdaUpdateWrapper的setSql方法

UpdateWrapper updateWrapper = new UpdateWrapper();

updateWrapper.eq("voucher_id", voucherId);

updateWrapper.setSql("stock = stock -1");

boolean flag = seckillVoucherService.update(updateWrapper);

// 方式三:使用自定义的LambdaUpdateWrapper

/*CustomLambdaUpdateWrapper updateWrapper = new CustomLambdaUpdateWrapper<>();

updateWrapper.descField(SeckillVoucher::getStock, 1)

.eq(SeckillVoucher::getVoucherId, voucherId);

boolean flag = seckillVoucherService.update(updateWrapper);*/

// 方式四:在xml中编写SQL语句

备注:相关完整代码请参考博主Gitee仓库中的hmdp项目

5.3 使用Condition动态组装条件

前面我们学习了QueryWrapper的常用方法,在这些方法的基础上,我们可以都可以通过添加一个condition条件,进行判断 是否需要添加改条件,这个类似于MyBatis中的动态SQL的标签,只有满足condition条件,才能够将改条件组装到SQL上

@Test

public void test9(){

// 模拟从前端接收参数

String username = "";

Integer ageBegin = 20;

Integer ageEnd = 30;

// 模糊查询姓某某的人,同时20

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.like(StringUtils.isNotBlank(username), "name", username)

.ge(ageBegin != null, "age", ageBegin)

.le(ageEnd != null, "age", ageEnd);

// SELECT id,name,age,email,is_deleted FROM user WHERE (age >= ? AND age <= ?)

List users = userMapper.selectList(queryWrapper);

users.forEach(System.out::println);

}

5.4 LamdaQueryWrapper

前面我们使用QueryWrapper和UpdateWrapper时,字段都是自己来编写的,如果你尝试过就一定知道,自己通过字符串的形式编写是没有一点提示的,这样很容易出现字段名写错,导致SQL执行失败的情况。而MP的开发者也是知道这一点的,于是就提供了LamdaQueryWrapper,这样我们可以通过lamda表达式的形式确定字段名,这在IDEA中是有提示的,基本上可以杜绝字段写错的问题。这个完全可以替代QueryWrapper,前提是你的JDK必须大于1.8,因为Lamda表达式是JDK1.8引入的

#mermaid-svg-7Y106tXSNVAHn4hK {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-7Y106tXSNVAHn4hK .error-icon{fill:#552222;}#mermaid-svg-7Y106tXSNVAHn4hK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7Y106tXSNVAHn4hK .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-7Y106tXSNVAHn4hK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7Y106tXSNVAHn4hK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7Y106tXSNVAHn4hK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7Y106tXSNVAHn4hK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7Y106tXSNVAHn4hK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7Y106tXSNVAHn4hK .marker.cross{stroke:#333333;}#mermaid-svg-7Y106tXSNVAHn4hK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7Y106tXSNVAHn4hK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7Y106tXSNVAHn4hK .cluster-label text{fill:#333;}#mermaid-svg-7Y106tXSNVAHn4hK .cluster-label span{color:#333;}#mermaid-svg-7Y106tXSNVAHn4hK .label text,#mermaid-svg-7Y106tXSNVAHn4hK span{fill:#333;color:#333;}#mermaid-svg-7Y106tXSNVAHn4hK .node rect,#mermaid-svg-7Y106tXSNVAHn4hK .node circle,#mermaid-svg-7Y106tXSNVAHn4hK .node ellipse,#mermaid-svg-7Y106tXSNVAHn4hK .node polygon,#mermaid-svg-7Y106tXSNVAHn4hK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7Y106tXSNVAHn4hK .node .label{text-align:center;}#mermaid-svg-7Y106tXSNVAHn4hK .node.clickable{cursor:pointer;}#mermaid-svg-7Y106tXSNVAHn4hK .arrowheadPath{fill:#333333;}#mermaid-svg-7Y106tXSNVAHn4hK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7Y106tXSNVAHn4hK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7Y106tXSNVAHn4hK .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-7Y106tXSNVAHn4hK .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-7Y106tXSNVAHn4hK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7Y106tXSNVAHn4hK .cluster text{fill:#333;}#mermaid-svg-7Y106tXSNVAHn4hK .cluster span{color:#333;}#mermaid-svg-7Y106tXSNVAHn4hK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7Y106tXSNVAHn4hK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

创建条件构造器

添加条件

执行SQL

@Test

public void test10(){

// 模拟从前端接收参数

String username = "";

Integer ageBegin = 20;

Integer ageEnd = 30;

// 模糊查询姓某某的人,同时20

LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.like(StringUtils.isNotBlank(username), User::getName, username)

.ge(ageBegin != null, User::getAge, ageBegin)

.le(ageEnd != null, User::getAge, ageEnd);

// SELECT id,name,age,email,is_deleted FROM user WHERE (age >= ? AND age <= ?)

List users = userMapper.selectList(lambdaQueryWrapper);

users.forEach(System.out::println);

}

5.5 LamdaUpdateWrapper

同样的这个也可以完全替代UpdateWrapper,推荐使用这个

#mermaid-svg-mrPqQrpGQo7yCVIc {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc .error-icon{fill:#552222;}#mermaid-svg-mrPqQrpGQo7yCVIc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mrPqQrpGQo7yCVIc .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-mrPqQrpGQo7yCVIc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mrPqQrpGQo7yCVIc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mrPqQrpGQo7yCVIc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mrPqQrpGQo7yCVIc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mrPqQrpGQo7yCVIc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mrPqQrpGQo7yCVIc .marker.cross{stroke:#333333;}#mermaid-svg-mrPqQrpGQo7yCVIc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mrPqQrpGQo7yCVIc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc .cluster-label text{fill:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc .cluster-label span{color:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc .label text,#mermaid-svg-mrPqQrpGQo7yCVIc span{fill:#333;color:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc .node rect,#mermaid-svg-mrPqQrpGQo7yCVIc .node circle,#mermaid-svg-mrPqQrpGQo7yCVIc .node ellipse,#mermaid-svg-mrPqQrpGQo7yCVIc .node polygon,#mermaid-svg-mrPqQrpGQo7yCVIc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mrPqQrpGQo7yCVIc .node .label{text-align:center;}#mermaid-svg-mrPqQrpGQo7yCVIc .node.clickable{cursor:pointer;}#mermaid-svg-mrPqQrpGQo7yCVIc .arrowheadPath{fill:#333333;}#mermaid-svg-mrPqQrpGQo7yCVIc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mrPqQrpGQo7yCVIc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mrPqQrpGQo7yCVIc .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-mrPqQrpGQo7yCVIc .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-mrPqQrpGQo7yCVIc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mrPqQrpGQo7yCVIc .cluster text{fill:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc .cluster span{color:#333;}#mermaid-svg-mrPqQrpGQo7yCVIc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mrPqQrpGQo7yCVIc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

创建条件构造器

添加条件

执行SQL

@Test

public void test11(){

// 修改 用户名中含有张并且(年龄大于20或者邮箱null)的用户信息

LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper<>();

lambdaUpdateWrapper.like(User::getName, "张")

.and(uw -> uw.gt(User::getAge, 20)

.or()

.isNull(User::getEmail));

lambdaUpdateWrapper.set(User::getName, "李四").set(User::getAge, 22);

// UPDATE user SET name=?,age=? WHERE (name LIKE ? AND (age > ? OR email IS NULL))

int result = userMapper.update(null, lambdaUpdateWrapper);

System.out.println("result = " + result);

}

6、插件

分页插件和乐观锁插件都是内置再MP中的,只需要配置以下就能使用了,而MyBatisX插件需要到插件商店中进行下载

6.1 MyBatisPlus分页插件

Step1:搭建环境 请参考第二节【快速体验】 Step2:创建MP配置类,配置拦截器 @Configuration

public class MyBatisPlusConfig {

@Bean

public MybatisPlusInterceptor mybatisPlusInterceptor(){

// 1、创建MP拦截器对象

MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 2、将MP的分页插件添加到拦截器对象中

interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return interceptor;

}

}

备注:如果没有加@Configuration注解,则需要在启动类上添加@MapperScan注解扫描该类 Step3:测试 @Test

public void testPage(){

// 创建分页构造器

Page page = new Page<>(2,3);

// 执行SQL:分页查询所有条件(还可以使用QueryWrapper对象进行条件过滤)

userMapper.selectPage(page, null);

// 这里和前面获取生成id是一样的,MP将查询的数据重新赋值给了Page对象

System.out.println("当前页码 = " + page.getCurrent());

System.out.println("当前页展示的记录条数 = " + page.getSize());

System.out.println("当前页展示的记录: ");

page.getRecords().forEach(System.out::println);

System.out.println("分页查询的总页数 = " + page.getPages());

System.out.println("分页查询的总记录数 = " + page.getTotal());

System.out.println("是否存在上一页: " + page.hasPrevious());

System.out.println("是否存在下一页: " + page.hasNext());

}

知识拓展:XML自定义分页

如果在我们像自定义一个分页查询的SQL,并且需要使用MP内置的分页插件

UserMapper:

/**

* 通过年龄查询用户信息并进行分页

* @param page 如果想使用MP内置的分页插件,则分页对象必须放在第一个

* @param age

* @return

*/

Page selectPageByAge(@Param("page") Page page, @Param("age") Integer age);

备注:这里配置了实体类的别名

测试结果:

6.2 悲观锁与乐观锁

在学习悲观锁和乐观锁之前,我们先模拟一个场景:

一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小 李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太 高,可能会影响销量。又通知小王,你把商品价格降低30元。 此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王 也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据 库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就 完全被小王的覆盖了。 现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1 万多。

像上面那样,不加锁,就会导致操作并发时出现严重故障,所以我们对于这种事务性操作是一定要进行枷锁校验的,这样才能保证数据的一致性,不然系统的使用者蒙受巨大损失

乐观锁:对数据的处理持乐观态度,乐观的认为数据一般情况下不会发生冲突,只有提交数据更新时,才会对数据是否冲突进行检测。乐观锁的实现不依靠数据库提供的锁机制,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号,一种是通过时间戳。悲观锁:对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定。通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update来实现悲观锁。需要注意的是使用悲观锁时,需要关闭数据库自动提交功能,即:set autocommit = 0;

如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过 了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。 如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证 最终的价格是120元。

示例一:

模拟数据冲突

数据冲突包括:丢失修改、不可重复读、脏读

Step1:环境搭建 1)建表 2)插入数据 3)导入依赖 4)编写配置文件 Step2:编码 1)编写实体类 2)编写Mapper Step3:测试 @Test

public void testDataConflict(){

// 小李查看价格

Product p1 = productMapper.selectById(1L);

System.out.println("小李取出的价格:" + p1.getPrice());

// 小王查看价格

Product p2 = productMapper.selectById(1L);

System.out.println("小王取出的价格:" + p2.getPrice());

// 小李将价格加了50元,存入了数据库

p1.setPrice(p1.getPrice() + 50);

int result1 = productMapper.updateById(p1);

System.out.println("小李修改结果:" + result1);

// 小王将商品减了30元,存入了数据库

p2.setPrice(p2.getPrice() - 30);

int result2 = productMapper.updateById(p2);

System.out.println("小王修改结果:" + result2);

// 获取商品最终的结果

Product p3 = productMapper.selectById(1L);

System.out.println("最后的结果:" + p3.getPrice());

}

示例二:

使用悲观锁解决数据冲突

Step1:搭建环境 略…… Step2:给表添加一个version字段。 Step3:在编写实体类时,使用@Version注解标记version属性。取出记录时,获取当前version;更新时,version + 1,如果where语句中的version版本不对,则更新失败 Step4:编写配置类 @Configuration

public class MyBatisPlusConfig {

@Bean

public MybatisPlusInterceptor mybatisPlusInterceptor(){

MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 添加乐观锁插件

interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

return interceptor;

}

}

Step5:测试 测试代码和示例一相同,略…… 小李进行操作时,此时小王还没进行更新操作,version字段不变;当小王操作时,因为前面小李进行了更新操作,所以version+1了,和小王获取的version不同,更新操作失败!

示例三:

使用悲观锁解决数据冲突

Step1:搭建环境 略…… Step2:

6.3 MyBatisX插件

MyBatisX插件主要具有以下功能

通过Mapper接口快速在Mapper配置文件中生成statement快速生成代码

7、通用枚举

表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举 来实现

Step1:搭建环境 1)建表(将sex的类型设置为int) 2)插入数据 3)导入依赖 4)编写配置文件 Step2:编码 1)编写枚举类 @Getter // 需要提供get方法,让外界能够获取到枚举对象

public enum SexEnum {

MALE(1, "男"),

FEMALE(2, "女");

@EnumValue // 将注解所标识的值存储到数据库中(不添加这个注解,则添加的值是MALE或FEMALE)

private Integer sex;

private String sexName;

SexEnum(Integer sex, String sexName) {

this.sex = sex;

this.sexName = sexName;

}

}

备注:MP3.5.2版本以前(3.5.2开始就已经废弃了,不需要配置就能直接使用通用没觉了),还需要再配置文件中进行配置,才能成功使用通用枚举 2)编写实体类 @Data

@NoArgsConstructor

@AllArgsConstructor

public class User {

private Long id;

private String name;

private Integer age;

private String email;

private SexEnum sex;

}

3)编写Mapper Step3:测试 @Test

public void testEnum(){

User user = new User("张三", 23, "123@qq.com", SexEnum.MALE);

int result = userMapper.insert(user);

System.out.println("result = " + result);

}

8、代码生成器

在MyBatis中我们可以通过逆向工程快速生成代码,从而节省大量的时间,但是MyBatis的逆向工程配置起来较为麻烦,所以MP简化了配置,让代码生成变得更加简单,堪称码农神器。让我们不必过多地重复哪些CRUD操作,只专注于核心业务。

代码生成器模板结构:

FastAutoGenerator.create("url", "username", "password")

//2、全局配置

.globalConfig(...)

//3、包配置

.packageConfig(...)

//4、策略配置

.strategyConfig(...)

//5、模板引擎配置

.templateEngine(...)

//6、执行

.execute();

8.1 快速生成

示例:

Step1:搭建环境 1)建库、建表 2)创建SpringBoot项目 3)引入依赖

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

com.github.xiaoymin

knife4j-spring-boot-starter

3.0.3

mysql

mysql-connector-java

com.baomidou

mybatis-plus-boot-starter

3.5.2

com.baomidou

mybatis-plus-generator

3.5.2

org.freemarker

freemarker

2.3.31

4)编写配置文件 # 配置端口

server:

port: 8080

# Spring相关配置

spring:

# 数据源相关配置

datasource:

driver-class-name: com.mysql.cj.jdbc.Driver

url: jdbc:mysql://localhost:3306/mybatis-plus_study?serverTimezone=UTC

username: root

password: 32345678

# MyBatisPlus相关配置

mybatis-plus:

configuration:

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启标准日志

type-aliases-package: com.hhxy.entity # 配置类型别名

Step2:编写生成器 注意:一定要使用绝对路径 public class FastAutoGeneratorTest {

public static void main(String[] args) {

// 1、配置数据源

FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis-plus_study?serverTimezone=UTC", "root", "32345678")

// 2、全局配置

.globalConfig(builder -> {

builder.author("ghp") // 设置作者

.enableSwagger() // 开启 swagger 模式

.fileOverride() // 覆盖已生成文件

.outputDir("/src/main/java"); // 指定输出目录

})

// 3、包配置

.packageConfig(builder -> {

builder.parent("com.hhxy") // 设置父包名

.moduleName("") // 设置父包模块名

.pathInfo(Collections.singletonMap(OutputFile.xml, "/src/main/resources/mapper")); // 设置mapperXml生成路径

})

// 4、策略配置

.strategyConfig(builder -> {

builder.addInclude("user") // 设置需要生成的表名(数据库中必须存在该表)

.addTablePrefix("tb_", "t_", "tbl_"); // 设置过滤表前缀

})

// 5、模板引擎配置

.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板

// 6、执行

.execute();

}

}

备注:MyBatisPlus5.2开始,fileOverride方法被弃用了 Step3:运行方法

使用MyBatisX插件生成自定的SQL

我们需要通过insert、delete、update、select等关键次开头,IDEA会有提示

8.2 交互式生成

public class InteractiveAutoGeneratorTest {

public static void main(String[] args) {

Scanner scan = new Scanner(System.in);

System.out.println("=====================数据库配置=======================");

System.out.println("请输入 URL");

String url = scan.next();

System.out.println("请输入 username");

String username = scan.next();

System.out.println("请输入 password");

String password = scan.next();

FastAutoGenerator.create(url, username, password)

// 全局配置

.globalConfig((scanner, builder) -> builder.author(scanner.apply("=====================全局配置=======================\n请输入作者名称"))

.outputDir(System.getProperty("user.dir") + "/src/main/java")

.commentDate("yyyy-MM-dd hh:mm:ss")

.dateType(DateType.TIME_PACK)

.enableSwagger()

.fileOverride()

.enableSwagger()

.disableOpenDir()

)

// 包配置

.packageConfig((scanner, builder) -> builder.parent(scanner.apply("=====================包配置=======================\n请输入包名?"))

.moduleName(scanner.apply("请输入父包模块名?"))

.entity("entity")

.service("service")

.serviceImpl("serviceImpl")

.mapper("mapper")

.xml("mapper")

.other("utils")

.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir")+"/src/main/resources/mapper"))

)

// 策略配置

.strategyConfig((scanner, builder) -> {

builder.addInclude(getTables(scanner.apply("=====================策略配置=======================\n请输入表名,多个参数使用逗号分隔")))

.serviceBuilder()

.formatServiceFileName("%sService")

.formatServiceImplFileName("%sServiceImpl")

.entityBuilder() //实体类策略配置

.enableLombok() //开启 Lombok

.disableSerialVersionUID()

.logicDeleteColumnName("deleted") //逻辑删除字段

.naming(NamingStrategy.underline_to_camel)

.columnNaming(NamingStrategy.underline_to_camel)

.addTableFills(new Column("create_time", FieldFill.INSERT), new Column("modify_time", FieldFill.INSERT_UPDATE))

.enableTableFieldAnnotation() // 开启生成实体时生成字段注解

.controllerBuilder()

.formatFileName("%sController")

.enableRestStyle()

.mapperBuilder()

.superClass(BaseMapper.class)

.formatMapperFileName("%sMapper")

.enableMapperAnnotation() //@mapper

.formatXmlFileName("%sMapper");

})

.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板

.execute();

}

// 处理 all 情况

protected static List getTables(String tables) {

return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));

}

}

9、多数据源

一个项目中经常会使用到多个数据库,比如主从复制,主库来进行更新操作,从库用来进行查询操作,从而有效降低数据库库的负担,一定程度提高系统的效率。

关于主从复制相关配置可以参考:MySQL学习笔记

多数据源主要存在以下三种配置方式:

# 多主多从 纯粹多库(记得设置primary) 混合配置

spring: spring: spring:

datasource: datasource: datasource:

dynamic: dynamic: dynamic:

datasource: datasource: datasource:

master_1: mysql: master:

master_2: oracle: slave_1:

slave_1: sqlserver: slave_2:

slave_2: postgresql: oracle_1:

slave_3: h2: oracle_2:

主从复制相关配置:

spring:

# 配置数据源信息

datasource:

dynamic:

# 设置默认的数据源或者数据源组, 默认值即为master

# primary: master

# 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源

# strict: false

datasource:

names:

master,slave # 设置别名,可以随便取,但要与后面的前缀对应(可以配置多个从库)

master:

url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false

driver-class-name: com.mysql.cj.jdbc.Driver

username: root

password: 123456

slave:

url: jdbc:mysql://localhost:3306/mybatis_plus_1?characterEncoding=utf-8&useSSL=false

driver-class-name: com.mysql.cj.jdbc.Driver

username: root

password: 123456

@Service

@DS("master") // 使用主库

public class UserServiceImpl implements UserService {

@Autowired

private JdbcTemplate jdbcTemplate;

public List selectAll() {

return jdbcTemplate.queryForList("select * from user");

}

@Override

@DS("slave") // 使用从库

public List selectByCondition() {

return jdbcTemplate.queryForList("select * from user where age >10");

}

}

好文推荐

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: 

发表评论

返回顶部暗黑模式