MybatisPlus 的使用

目录

介绍 

特性

 使用

配置日志

CRUD操作

添加操作

雪花算法生成的id

使用数据库的主键自增

解释原因

​编辑

修改操作

 删除操作

Wrapper 的使用

插件与扩展

自动填充

乐观锁

分页查询


是什么?

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

官网地址:MyBatis-Plus (baomidou.com)

为什么要用?

因为在mybatis的基础上增加了很多功能。能够进一步的简化我们的开发。

怎么用?

接下来给大家演示

介绍 

 

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

 使用

创建一个SpringBoot工程,导入依赖:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

 写配置文件:application.properties

# 四大金刚
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=用户名
spring.datasource.password=密码

创建启动类:

/**
 * SpringBoot 的启动类
 */
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args){
        SpringApplication.run(MainApplication.class,args);
    }
}

创建表和插入数据的SQL语句:

DROP TABLE IF EXISTS user;

CREATE TABLE user
(
    id BIGINT(20) NOT NULL auto_increment 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)
);

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');

SELECT * FROM user;

创建实体类:

/**
 * 用户实体类
 */
@Data
@NoArgsConstructor
public class User {
    @TableId(type = IdType.AUTO)
    //主键
    private Long id;
    //名字
    private String name;
    //年龄
    private Integer age;
    //邮箱
    private String email;

    public User(Long id, String name, Integer age, String email) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

创建dao接口:需要继承 BaseMapper<T> 接口

/**
 * 用户dao接口
 */
@Mapper
public interface UserDao extends BaseMapper<User> {
}

测试:

/**
 * 用户的dao的测试类
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserDaoTest {
    @Resource
    private UserDao userDao;

    /**
     * 查询全部用户
     */
    @Test
    public void testListAll(){
        List<User> users = userDao.selectList(null);
        //输出
        users.forEach(System.out::print);
    }
}

结果:

可以看到我们什么dao里面什么也没写,可是结果已经出来了! 这只是最简单的演示,后面还有。

配置日志

在application.propeties 文件里面加一行这个就行了

# 打印日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

CRUD操作

添加操作

雪花算法生成的id

代码:

    /**
     * 测试添加
     */
    @Test
    public void testSave(){
        //返回受影响的函数
        int count = userDao.insert(new User(null,"zhangsan", 18, "zhangsan@qq.com"));
        System.out.println("count = " + count);
    }

结果:

可以看到此时我们并没有指定id,但是这里面却有id,这是它内部给我们生成的,是使用雪花算法算出来的。这个id几乎不会重复。可以去看下面这篇文章。

雪花算法的由来:

  • 一:Twitter使用scala语言开源了一种分布式 id 生成算法——SnowFlake算法,被翻译成了雪花算法。
  • 二:因为自然界中并不存在两片完全一样的雪花的,每一片雪花都拥有自己漂亮独特的形状、独一无二。雪花算法也表示生成的ID如雪花般独一无二。(有同学问为什么不是树叶,美团的叫树叶——Leaf)

雪花算法:雪花算法(详解) - 知乎 (zhihu.com) 

使用数据库的主键自增

在 User 实体类上的 id 属性加一行这个就可以了:

    @TableId(type = IdType.AUTO)

再去测试:

解释原因

 我们点进 TableId 这个注解的源码可以发现 IdType 这个类就是指定主键的类型,我们再点进去 :可以发现实现雪花算法的是这个 ASSIGN_ID 而它的默认实现类是:DefaultIdentifierGenerator

@Getter
public enum IdType {
    /**
     * 数据库ID自增
     */
    AUTO(0),
    /**
     * 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
     */
    NONE(1),
    /**
     * 用户输入ID
     * <p>该类型可以通过自己注册自动填充插件进行填充</p>
     */
    INPUT(2),

    /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
    /**
     * 分配ID (主键类型为number或string),
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
     *
     * @since 3.3.0
     */
    ASSIGN_ID(3),
}

DefaultIdentifierGenerator 类:

    @Override
    public Long nextId(Object entity) {
        return sequence.nextId();
    }

可以看到它点进去了一个 nextId() 的方法:Sequence 类中,想看的小伙伴可以去看一下

/**
 * 分布式高效有序ID生产黑科技(sequence)
 * <p>优化开源项目:https://gitee.com/yu120/sequence</p>
 *
 * @author hubin
 * @since 2016-08-18
 */
public class Sequence {    
    /**
     * 获取下一个 ID
     *
     * @return 下一个 ID
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        //闰秒
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }
        }

        if (lastTimestamp == timestamp) {
            // 相同毫秒内,序列号自增
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 同一毫秒的序列数已经达到最大
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒内,序列号置为 1 - 3 随机数
            sequence = ThreadLocalRandom.current().nextLong(1, 3);
        }

        lastTimestamp = timestamp;

        // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
        return ((timestamp - twepoch) << timestampLeftShift)
            | (datacenterId << datacenterIdShift)
            | (workerId << workerIdShift)
            | sequence;
    }
}

这大概就是那个id的由来了。而 IdType.AUTO 使用的就是 数据库自增的主键,那里面的注释写的也很清楚。

修改操作

我们修改最后一条记录的数据:

代码:

    /**
     * 测试修改
     */
    @Test
    public void testUpdate(){
        int count = userDao.updateById(new User(1529446296992210946L, "wangwu", null, "wangwu@qq.com"));
        System.out.println("count = " + count);
    }

 结果:

 删除操作

删除最后一行的数据

代码:

    /**
     * 测试删除
     */
    @Test
    public void testDelete() {
        int count = userDao.deleteById(1529446296992210946L);
        System.out.println("count = " + count);
    }

结果:

Wrapper 的使用

Wrapper  条件构造抽象类
    -- AbstractWrapper 查询条件封装,用于生成 sql 中的 where 语句。
        -- QueryWrapper Entity 对象封装操作类,用于查询。
        -- UpdateWrapper Update 条件封装操作类,用于更新。
        -- AbstractLambdaWrapper 使用 Lambda 表达式封装 wrapper
            -- LambdaQueryWrapper 使用 Lambda 语法封装条件,用于查询。
            -- LambdaUpdateWrapper 使用 Lambda 语法封装条件,用于更新。

QueryWrapper

    /**
     * 测试 queryWrapper
     */
    @Test
    public void testQueryWrapper(){
        QueryWrapper<User> queryWrapper=new QueryWrapper<>();
        //模糊查询  %值%
        queryWrapper.like("name", "zhang");
        //大于
        queryWrapper.gt("age", "18");

        List<User> users = userDao.selectList(queryWrapper);
        users.forEach(System.out::println);
    }

结果:

插件与扩展

自动填充

介绍

阿里巴巴Java开发手册泰山版上这样写:

【强制】 表必备三字段:id, gmt_create, gmt_modified。
说明: 其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。gmt_create, gmt_modified

的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。

gmt_create :添加记录时的时间 gmt_modified : 修改此记录的时间

方式一:在数据库上设置(不建议直接更改数据库)

实体类:

    //添加时间 这里需要标记为填充字段  添加的时候填充
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;
    //修改时间  添加/修改的时候填充
    @TableField(fill=FieldFill.INSERT_UPDATE)
    private Date gmtModified;

试了一下图形化工具,我使不明白,还是用代码吧

 SQL :

ALTER TABLE user ADD gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE user ADD gmt_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP;

结果:

 执行修改代码之后:值已经改了

Java 代码层面(建议使用)

 首先去除这两个字段的默认值: 

ALTER TABLE user ALTER COLUMN gmt_create DROP DEFAULT;
ALTER TABLE user ALTER COLUMN gmt_modified DROP DEFAULT;

然后创建一个类:记得实现 MetaObjectHandler 接口

/**
 * 元对象处理器
 */
@Component//注册到 IOC 容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 新增时的填充
     *
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "gmt_create", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
    }

    /**
     * 修改时的填充
     *
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
    }
}

测试:发现可以成功

乐观锁

介绍

乐观锁:在操作数据时非常乐观,认为被人不会同时修改数据。因此乐观锁不会上锁,只是在更新的时候判断一下在此期间别人是否修改了数据;如果别人修改了数据则放弃操作,否则执行操作
悲观锁:在操作数据时非常悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才释放锁;上锁期间其他人不能修改数据。

实现方式

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时,set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败

解释:

# 线程 A
update user set name = 'zhaoliu',version = version+1 where id = 1 and version = 1;
# 线程 B
update user set name = 'tianqi',version = version+1 where id = 1 and version = 1;

如果线程 A 在修改此条数据的时候,线程 B 突然进来了把数据给修改了,并把 version 的值加了1,此时线程 A 再拿着 version=1 去更新此数据就不会成功。这就是乐观锁

 实现

实体类:记得在属性上添加 @Version 注解

    //乐观锁
    @Version
    private Integer version;

数据库添加一个 version 字段:

ALTER TABLE user ADD version DEFAULT 1;

创建一个 MybaitsPlus 的配置类:

/**
 * MybatisPlus 的配置类
 */
@Configuration//声明此类为一个配置类
public class MybatisPlusConfig {
        /**
     * 设置拦截器
     *
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //乐观锁的拦截器
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

然后测试:

代码:

    /**
     * 测试乐观锁
     */
    public void testOptimisticLock(){
        User user1 = userDao.selectById(1529443905827876866L);
        user1.setEmail("tianqi@qq.com");

        User user2 = userDao.selectById(1529443905827876866L);
        user2.setName("zhaoliu");
        
        userDao.updateById(user2);
        userDao.updateById(user1);
    }

结果:发现 user2 被执行成功,而 user1 没有

 原因:当第一次拿着 version 去更新时,version=5,更新完毕。然后第二条 SQL 语句还拿着 5 去更新,此时数据库中 version=6 ,所以就更新不成功。

==>  Preparing: UPDATE user SET name=?, age=?, email=?, gmt_create=?, gmt_modified=?, version=? WHERE id=? AND version=?
==> Parameters: zhaoliu(String), 18(Integer), zhaoliu@qq.com(String), 2022-05-25 21:18:42.0(Timestamp), 2022-05-26 01:34:45.0(Timestamp), 6(Integer), 1529443905827876866(Long), 5(Integer)
<==    Updates: 1
==>  Preparing: UPDATE user SET name=?, age=?, email=?, gmt_create=?, gmt_modified=?, version=? WHERE id=? AND version=?
==> Parameters: zhaoliu(String), 18(Integer), tianqi@qq.com(String), 2022-05-25 21:18:42.0(Timestamp), 2022-05-26 01:34:45.0(Timestamp), 6(Integer), 1529443905827876866(Long), 5(Integer)

分页查询

在 MybatisPlusConfig 中的 mybatisPlusInterceptor 方法中增加几行代码即可:

        //分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        //可以不写
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);

测试:

代码:

    /**
     * 测试分页
     */
    @Test
    public void testPage() {
        //参数1:为第几页  参数2:一页显示多少条
        Page<User> page = new Page<>(2, 5);
        //调用mp分页查询的方法
        //在调用mp分页查询过程中,底层将分页所有数据封装到page对象里面
        page = userDao.selectPage(page, null);
        System.out.println("page = " + page);
        page.getRecords().forEach(System.out::println);
    }

结果:

可以设置不查询总条数: Page<User> page = new Page(2,5,false); 加一个 false 的参数即可

但是一般都会查询总条数,都会有这个需求。   

# 先查询出总条数
==>  Preparing: SELECT COUNT(1) FROM user
==> Parameters: 
<==    Columns: COUNT(1)
<==        Row: 10
<==      Total: 1
# 再分页查询数据
==>  Preparing: SELECT id,name,age,email,gmt_create,gmt_modified,version FROM user LIMIT ?,?
==> Parameters: 5(Long), 5(Long)
<==    Columns: id, name, age, email, gmt_create, gmt_modified, version
<==        Row: 1529443905827876865, zhangsan, 19, zhangsan@qq.com, 2022-05-25 21:18:42.0, 2022-05-26 01:31:02.0, 3
<==        Row: 1529443905827876866, zhaoliu, 18, zhaoliu@qq.com, 2022-05-25 21:18:42.0, 2022-05-26 01:34:45.0, 8
<==        Row: 1529443905827876867, lisi, 18, lisi@qq.com, 2022-05-25 21:18:42.0, 2022-05-25 21:32:05.0, 1
<==        Row: 1529443905827876868, wangwu, 18, wangwu@qq.com, 2022-05-25 21:18:42.0, 2022-05-25 21:18:40.0, 1
<==        Row: 1529446296992210947, zhangsan, 18, zhangsan@qq.com, 2022-05-25 23:13:38.0, 2022-05-25 23:13:38.0, 1
<==      Total: 5
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值