【Java进阶篇】Mybatis框架

MYBATIS

关于Mybatis框架

Mybatis框架解决了数据库编程相关的问题,主要是简化了数据库编程。

当使用Mybatis框架实现数据库编程时,只需要:

  • 定义数据操作功能的抽象方法(此抽象方法必须在接口中)
  • 配置以上抽象方法映射的SQL语句

Mybatis框架在实现过程中,会自动生成各接口的代理对象,所以,开发人员并不需要关注接口的实现问题。

使用Mybatis框架

在Spring Boot项目中,当需要使用Mybatis框架实现数据库编程时,需要添加:

  • mybatis-spring-boot-starter
  • 数据库的依赖,例如mysql-connector-java

所以,在pom.xml中添加:

<!-- Mybatis框架 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<!-- MySQL依赖项,仅运行时需要 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

由于添加以上依赖后,Spring Boot在启动时就会读取连接数据库的配置信息,如果未配置,则启动报错且失败,需要在src/main/resources下的application.properties中添加必要的配置:

spring.datasource.url=jdbc:mysql://localhast:8888

提示:以上配置中,属性名称是固定,以上示例值是错误值,但是,启动Spring Boot只是加载以上配置,并不会执行连接,所以,配置值错误并不影响启动项目。

连接数据库的配置

在Spring Boot项目中,src/main/resources下的application.properties是默认的配置文件,项目在启动时,Spring Boot会自动从此文件中读取相关的配置信息。

在许多配置过程中,需要在application.properties中的配置的属性的名称是固定的!

在配置数据库的连接信息时,至少需要配置spring.datasource.urlspring.datasource.usernamespring.datasource.password这3个属性,分别表示连接数据库的URL、登录数据库的用户名、登录数据库的密码

spring.datasource.url=jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai

spring.datasource.username=root

spring.datasource.password=root

为了检验配置值是否正确,可以在src/test/java下默认的包下创建DatabaseConnectionTests测试类,在测试类中编写测试方法,以尝试连接数据库,即可检验:

package com.unkeer.csmall.server;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;

// 测试类必须:
// 1. 在组件扫描的包下(默认的包及其子孙包)
// 2. 只有添加了@SpringBootTest注解才会加载整个项目的运行环境(包括Spring、读取配置等)
@SpringBootTest
public class DatabaseConnectionTests {

    // Spring Boot自动读取application.properties的配置,并创建了数据源对象
    @Autowired
    DataSource dataSource;

    // 连接数据库,可用于检查application.properties中的连接参数值是否正确
    @Test
    public void testConnection() throws Exception {
        // 获取与MySQL的连接,此方法的调用会实质的连接数据库
        dataSource.getConnection();
        System.out.println("当你看到这句话时,表示在application.properties中的数据库连接参数的值是正确的");
    }

}

关于设计数据表

关于id

阿里巴巴的建议是:每张表都应该有id字段,且是bigint unsigned类型,其中,bigint对应Java中的long类型,unsigned表示“无符号位”,将使得此字段值不会出现负数,且取值区间是原正数的2倍……以tinyint为例,没有添加unsigned时,取值区间是[-128, 127],添加unsigned以后,取值区间是[0, 255]。

当把id的类型设置为bigint时,理论上id值足够使用,即使不添加unsigned也不会出现id值不够用的情况,但仍推荐添加,其目的是为了表现语义。

关于编码

应该在创建表时指定编码,创建库时可以不指定编码。

在MySQL / MariaDB强烈推荐使用utf8mb4

关于字符串的字段类型

如果某个字符串类型的字段的值的长度变化可能较大,应该使用varchar类型,例如用户名,如果某个字符串类型的字段的值的长度变化不大,应该使用char类型。

注意:某些数据可能是纯数字的,但并不具备算术运算含义,也应该使用字符串类型,不要使用数值类型。

在使用varchar时,指定的长度一定是“大于必要长度”的标准,例如,现行的标准是“用户名最多15个字符”,则建议设计为varchar(25)或比25更大的值,但是,也不要过于夸张,避免影响语义。

使用Mybatis时定义的抽象方法

使用Mybatis时,定义的抽象方法都必须在接口中,通常,接口会使用Mapper作为名称的最后一个单词,例如命令为BrandMapper等。

关于抽象方法的声明原则:

  • 返回值类型:如果需要执行的SQL是增、删、改类型的,统一使用int作为返回值类型,表示“受影响的行数”,其实也可以使用void,但并不推荐,如果需要执行的SQL是查询类型的,如果查询最多只返回1个结果,则只需要保证返回值类型可以装得下所需的查询结果即可,如果查询返回的结果可能超过1条,则必须使用List集合进行封装,且集合的元素类型依然只需要保证可以装得下所需的查询结果即可
  • 方法名称:自定义
    • 获取单个对象的方法用 get 做前缀
    • 获取多个对象的方法用 list 做前缀
    • 获取统计值的方法用 count 做前缀
    • 插入的方法用 save/insert 做前缀
    • 删除的方法用 remove/delete 做前缀
    • 修改的方法用 update 做前缀
  • 参数列表:如果需要执行的SQL语句中的参数数量较多,推荐将多个参数封装到自定义类中

关于@Mapper和@MapperScan

Mybatis框架只要求开发人员编写接口和抽象方法,不需要开发人员编写实现类,是因为Mybatis会通过代理模式自动生成接口的实现对象,但是,它需要明确哪些接口是需要生成代理对象的。

可以在各接口上添加@Mapper注解,在启动项目时,Mybatis会对整个项目进行扫描,对于已经添加此注解的接口,就会生成代理对象。

也可以在配置类上添加@MapperScan注解,用于指定各接口所在的包,则Mybatis会扫描此包及其子孙包下的所有接口,并生成这些接口的代理对象。

关于@Mapper@MapperScan这2种注解,只需要选择其中1种使用即可,通常推荐@MapperScan

注意:使用@MapperScan时,一定只指向Mapper接口所在的包,并确保此包下无其它接口!

提示:Mybatis框架的@MapperScan,与Spring框架的@ComponentScan没有任何关系,且不会互相影响!

使用Mybatis实现插入数据

以实现“插入品牌数据”为例,需要执行的SQL语句大致是:

insert into pms_brand (name, pinyin, logo, description, keywords, sort, sales, product_count, comment_count, positive_comment_count, enable, gmt_create, gmt_modified) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

先在项目的默认包下创建pojo.entity.Brand类,类中的属性应该与数据表对应:

public class Brand implements Serializable {

    private Long id;
    private String name;
    private String pinyin;
    private String logo;
    private String description;
    private String keywords;
    private Integer sort;
    private Integer sales;
    private Integer productCount;
    private Integer commentCount;
    private Integer positiveCommentCount;
    private Integer enable;
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
 
    // 按照POJO规范补充后续代码
    
}

接下来,准备接口和抽象方法,在项目的默认包下创建mapper.BrandMapper接口,并在接口中添加抽象方法:

package com.unkeer.csmall.server.mapper;

public interface BrandMapper {

    /**
     * 插入品牌数据
     * @param brand 品牌数据
     * @return 受影响的行数,成功插入数据时,将返回1
     */
    int insert(Brand brand);

}

关于SQL语句,可以使用@Insert等注解进行配置,但不推荐!

推荐使用XML文件配置SQL语句!

关于此文件的配置:

  • 根节名必须是<mapper>
  • 根节点必须配置namespace属性,取值为对应的接口的全限定名
  • 在根节点内部,根据需要执行的SQL语句的类型不同,使用<insert><delete><update><select>节点
  • <insert>等节点上,必须配置id属性,取值为抽象方法的名称(不包含括号及参数)
  • <insert>等节点内部,配置SQL语句,SQL语句不需要使用分号结束

例如配置为:

<?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.unkeer.csmall.server.mapper.BrandMapper">

    <!-- int insert(Brand brand); -->
    <insert id="insert">
        insert into pms_brand (
            name, pinyin, logo, description, keywords,
            sort, sales, product_count, comment_count, positive_comment_count,
            enable, gmt_create, gmt_modified
        ) values (
            #{name}, #{pinyin}, #{logo}, #{description}, #{keywords},
            #{sort}, #{sales}, #{productCount}, #{commentCount}, #{positiveCommentCount},
            #{enable}, #{gmtCreate}, #{gmtModified}
        )
    </insert>

</mapper>

最后,还需要补充一个配置,用于告诉Mybatis框架这类XML文件的位置!在application.properties中添加:

mybatis.mapper-locations=classpath:mapper/*.xml

另外,在插入数据时,还可以配置,得到自动编号的ID值,具体做法是在<insert>节点上添加配置:

<!-- int insert(Brand brand); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    暂不关心此处的SQL语句
</insert>

练习

目标:实现向pms_album表中插入数据,并要求可以获取自动编号的id。

开发步骤:

  • entity包创建Album实体类,类中的属性与pms_album表保持一致,且符合POJO规范
  • mapper包中创建AlbumMapper接口
  • AlbumMapper接口中添加抽象方法:int insert(Album album);(记得添加注释)
  • src/main/resourcesmapper文件夹下,通过复制粘贴得到AlbumMapper.xml文件
  • 配置AlbumMapper.xml中的根节点的namespace
  • AlbumMapper.xml中,在根节点内部添加<insert>节点,并配置iduseGeneratedKeyskeyProperty属性,在<insert>节点内部配置SQL语句
  • src/test/java下的cn.tedu.csmall.server.mapper包(你的包名可能不同)下创建AlbumMapperTests测试类,在类中自动装配AlbumMapper对象,并编写、执行测试方法

使用Mybatis实现删除数据

目标:根据id删除某个品牌

需要执行的SQL语句大致是:

delete from pms_brand where id=?

BrandMapper接口中添加抽象方法:

/**
 * 根据品牌id,删除品牌数据
 * @param id 期望删除的品牌数据的id
 * @return 受影响的行数,当删除成功时,将返回1,如果无此id对应的数据,将返回0
 */
int deleteById(Long id);

BrandMapper.xml中配置SQL:

<!-- int deleteById(Long id); -->
<delete id="deleteById">
    delete from pms_brand where id=#{id}
</delete>

BrandMapperTests中编写并执行测试:

@Test
public void testDeleteById() {
    Long id = 1L;
    int rows = mapper.deleteById(id);
    System.out.println("删除完成,受影响的行数=" + rows);
}

练习

目标:实现根据id删除pms_album表中的数据。

使用Mybatis实现修改数据

目标:实现根据id修改pms_brand表中某条数据的name字段值。

需要执行的SQL语句大致是:

update pms_brand set name=? where id=?

BrandMapper接口中添加抽象方法:

/**
 * 根据id修改品牌的名称
 * @param id 品牌id
 * @param name 新的品牌名称
 * @return 受影响的行数,当修改成功时,将返回1,如果无此id对应的数据,将返回0
 */
int updateNameById(@Param("id") Long id, @Param("name") String name);

BrandMapper.xml中配置SQL:

<!-- int updateNameById(@Param("id") Long id, @Param("name") String name); -->
<update id="updateNameById">
    update pms_brand set name=#{name} where id=#{id}
</update>

BrandMapperTests中编写并执行测试:

@Test
public void testUpdateNameById() {
    Long id = 3L;
    String name = "白萝卜";
    int rows = mapper.updateNameById(id, name);
    System.out.println("修改品牌名称完成,受影响的行数=" + rows);
}

练习

目标:实现根据id修改pms_album表中某条数据的name字段值。

使用Mybatis实现批量删除数据

在Mybatis中,有“动态SQL”的机制,它允许根据调用方法时传入的参数值不同,来生成不同的SQL语句。

目标:根据若干个id一次性删除多个品牌。

需要执行的SQL语句大致是:

delete from pms_brand where id=? or id=? or id=?;

或者:

delete from pms_brand where id in (?, ?, ?);

注意:以上SQL语句中的id的数量是不确定的。

BrandMapper接口中,抽象方法可以是:

int deleteByIds(Long... ids); // 注意:可变参数在处理时,本质上就是数组

或者:

int deleteByIds(Long[] ids);

或者:

int deleteByIds(List<Long> ids);

BrandMapper.xml中配置SQL语句:

<!-- int deleteByIds(Long... ids); -->
<delete id="deleteByIds">
    delete from pms_brand where id in (
    	<foreach collection="array" item="id" separator=",">
            #{id}
    	</foreach>
    )
</delete>

由于需要对参数ids(若干个id)进行遍历,需要使用到动态SQL中的<foreach>节点,此节点可以对数组或集合进行遍历!关于<foreach>的配置:

  • collection属性:表示被遍历的参数对象,当抽象方法的参数只有1个,且没有添加@Param注解时,当参数值的类型是数组时,此属性值为array,当参数值的类型是List时,此属性值为list;否则,此属性值为@Param注解中的参数值
  • item属性:表示被遍历到的元素的名称,是自定义的名称,在<foreach>内部,使用#{}格式的占位符时,也使用此属性来表示每个元素
  • separator属性:表示遍历过程中各元素值之间的分隔符号

最后,在BrandMapperTests中编写并执行测试:

@Test
public void testDeleteByIds() {
    int rows = mapper.deleteByIds(1L, 3L, 5L, 7L, 9L);
    System.out.println("批量删除完成,受影响的行数=" + rows);
}

使用Mybatis实现动态SQL的修改数据

在动态SQL机制中,可以使用<if>标签,可用于对某参数值进行判断,从而生成不同的SQL语句片段,常用于设计更新数据的操作。

目标:使用1个方法,实现多种不同的数据更新(想更新哪些字段就更新哪些字段,不想更新的字段值将保持不变)

需要执行的SQL语句大致是:

update pms_brand set name=?, pinyin=?, logo=? where id=?

注意:以上SQL语句的修改的字段列表应该不是固定的,应该根据传入的参数值来决定。

先在BrandMapper接口中添加抽象方法:

int updateById(Brand brand);

然后,在BrandMapper.xml中进行配置:

<!-- int updateById(Brand brand); -->
<update id="updateById">
	UPDATE
    	pms_brand
    <set>
    	<if test="name != null">
            name=#{name},
    	</if>
        <if test="pinyin != null">
        	pinyin=#{pinyin},
	    </if>
    	<if test="logo != null">
        	logo=#{logo},
	    </if>
    </set>
    WHERE
    	id=#{id}
</update>

需要注意的是,在Mybatis的动态SQL中,<if>并没有对应的<else>,如果一定要实现类似Java中的if...else效果,需要使用<choose>标签,其基本格式是:

<choose>
	<when test="条件">
    	满足条件时的SQL片段
    </when>
    <otherwise>
    	不满足条件时的SQL片段
    </otherwise>
</choose>

或者,也可以使用2个条件完全相反的<if>标签来实现类似效果(但是执行效率偏低),例如:

<if test="pinyin != null">
    某代码片段
</if>
<if test="pinyin == null">
    某代码片段
</if>

使用Mybatis实现查询数据

统计查询

目标:统计品牌表中的数据的数量

需要执行的SQL语句大致是:

select count(*) from pms_brand;

BrandMapper接口中添加抽象方法:

int count();

BrandMapper.xml中配置SQL:

<!-- int count(); -->
<select id="count" resultType="int">
    SELECT count(*) FROM pms_brand
</select>

注意:所有查询节点(<select>)必须配置resultTyperesultMap这2个属性中的其中1个。

当使用resultType声明封装结果的数据类型时,取值与抽象方法的返回值对应,如果是基本类型,直接写类型名称即可,例如resultType="int",如果是引用数据类型,在java.lang包下的可以直接写类名,其它包下的写全限定名。

指定条件的单一结果查询

目标:根据id查询品牌详情

需要执行的SQL语句大致是:

select id, name, pinyin ... from pms_brand where id=?

由于不推荐使用星号表示字段列表,并且,在实际查询时,可能有部分字段是不需要体现在查询结果中的,推荐的做法是针对所需的查询字段,另外创建类型进行结果的封装。

例如,在pojo.vo下创建BrandDetailVO类:

public class BrandDetailVO implements Serializable {

    /**
     * 记录id
     */
    private Long id;

    /**
     * 品牌名称
     */
    private String name;

    /**
     * 品牌名称的拼音
     */
    private String pinyin;

    /**
     * 品牌logo的URL
     */
    private String logo;

    /**
     * 品牌简介
     */
    private String description;

    /**
     * 关键词列表,各关键词使用英文的逗号分隔
     */
    private String keywords;

    /**
     * 自定义排序序号
     */
    private Integer sort;

    /**
     * 销量(冗余)
     */
    private Integer sales;

    /**
     * 商品种类数量总和(冗余)
     */
    private Integer productCount;

    /**
     * 买家评论数量总和(冗余)
     */
    private Integer commentCount;

    /**
     * 买家好评数量总和(冗余)
     */
    private Integer positiveCommentCount;

    /**
     * 是否启用,1=启用,0=未启用
     */
    private Integer enable;
    
    // 参考POJO规范添加其它代码
    
}

BrandMapper接口中添加抽象方法:

BrandDetailVO getById(Long id);

然后,在BrandMapper.xml中配置SQL:

<select id="getById" resultType="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
    SELECT 
    	id, name, pinyin, logo, description, 
    	keywords, sort, sales, product_count, comment_count, 
    	positive_comment_count, enable 
    FROM 
    	pms_brand
    WHERE
    	id=#{id}
</select>

另外,在查询时,一定要明确几个概念:

  • 字段(Field):在创建数据表时指定的名称(后续也能修改表结构时改名)
  • 列(Column):查询的结果集中的每一竖排,列名默认情况下是字段名,如果查询时指定了别名,则列名就是指定的别名
  • 属性( Property):类中的属性

Mybatis在执行查询时,会尝试自动的将结果集中的数据封装到返回结果类型的对象中,但是,它只能自动处理列名与属性名相同的部分,如果列名与属性名不同,默认并不能自动封装!

可以在查询的SQL语句中,自定义别名,使得列名与属性名相同,则可以实现自动封装,例如:

<select id="getById" resultType="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
    SELECT
        id, name, pinyin, logo, description,
        keywords, sort, sales, 
    
    	product_count AS productCount, 
    
    	comment_count,
        positive_comment_count, enable
    FROM
        pms_brand
    WHERE
        id=#{id}
</select>

以上的product_count AS productCount自定义别名可以保证product_count字段的值可以被自动封装!

更推荐使用<resultMap>来指导Mybatis如何封装结果,它将与<select>标签的resultMap属性一起使用,例如:

<select id="xx" resultMap="DetailResultMap">
</select>

<resultMap id="DetailResultMap" type="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
</resultMap>

然后,在<resultMap>内部,使用<result>来配置列名与属性名的对应关系,例如:

<resultMap id="DetailResultMap" type="com.unkeer.csmall.server.pojo.vo.BrandDetailVO">
    <result column="product_count" property="productCount" />
    <result column="comment_count" property="commentCount" />
    <result column="positive_comment_count" property="positiveCommentCount" />
</resultMap>

提示:在使用<resultMap>配置时,从规范的角度出发,每个列与属性的关系都需要显式的配置出来(即使从功能实现的角度来看可能并不需要),另外,还应该使用<id>节点对主键进行配置。

查询列表

目标:查询品牌列表

需要执行的SQL语句大致是(暂时使用星号表示字段列表):

select * from pms_brand order by id

通常,查询列表时,所需要查询的字段与查单个数据可能是不同的,所以,可能需要自定义新的VO类作为List中的元素类型,为了避免写完代码后发现某个VO不能复用于查询单个数据和查询列表这2个功能,推荐一开始就定义用于封装列表项结果的VO类,例如:

public class BrandListItemVO implements Serializable {
    // 可暂时与BrandDetailVO使用完全相同的属性
}

BrandMapper接口中添加抽象方法:

List<BrandListItemVO> list();

然后,在BrandMapper.xml中配置SQL:

<select id="list" resultMap="ListItemResultMap">
    SELECT
		id, name, pinyin, logo, description,
		keywords, sort, sales, product_count, comment_count,
		positive_comment_count, enable
	FROM
		pms_brand
	ORDER BY id
</select>

<resultMap id="ListItemResultMap" type="com.unkeer.csmall.server.pojo.vo.BrandListItemVO">
	<id column="id" property="id" />
    <result column="product_count" property="productCount" />
    <result column="comment_count" property="commentCount" />
    <result column="positive_comment_count" property="positiveCommentCount" />
</resultMap>

关于<sql><resultMap>

在使用XML配置SQL语句时,可以使用<sql>封装SQL语句片段,并使用<include>进行引用,例如:

<select id="list" resultMap="ListItemResultMap">
    SELECT
        <include refid="ListItemQueryFields"/>
    FROM
        pms_brand
    ORDER BY id
</select>

<sql id="ListItemQueryFields">
    id, name, pinyin, logo, description,
    keywords, sort, sales, product_count, comment_count,
    positive_comment_count, enable
</sql>

通过,使用<sql>封装字段列表,而<resultMap>通常与之对应,所以,这2个节点的id命名通常会使用相同的关键词,例如<sql>配置为id="ListItemQueryFields",并且<resultMap id="ListItemResultMap">,甚至,在编码时,会把这2个节点放在相邻的位置。

提示:使用<sql>封装字段列表时,IntelliJ IDEA可能会误判为错误的语法,将字段列表使用<if test="true>"框住(或将此<if>添加在之前)即可避免出现这样的错误提示。

练习

完成以下查询功能:

  • 统计相册的数量
  • 统计类别的数量
  • 根据id查询相册详情
    • 除了gmt_creategmt_modified字段的值
  • 根据id查询类别详情
    • 除了gmt_creategmt_modified字段的值
  • 查询相册的列表
    • 除了gmt_creategmt_modified字段的值
  • 根据父级类别,查询子级类别列表(条件:where parent_id=?
    • 除了gmt_creategmt_modified字段的值

关于异常

BindingException

绑定异常,异常提示信息如下:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.unkeer.csmall.server.mapper.BrandMapper.insert

出现此异常是因为找不到与抽象方法对应的SQL语句,原因可能有:

  • 在XML中配置的接口名有误
    • <mapper>节点的namespace属性值有误
  • 在XML中配置的抽象方法名称有误
    • <insert>或类似节点的id属性值有误
  • 在配置文件中指定的XML路径,此项配置有误
    • application.properties中配置的mybatis.mapper-locations属性有误
  • 如果十分确实以上代码都没有问题,则只可能是依赖项出错
    • 先检查pom.xml中的依赖代码是否正确,如果无误,则删除本地仓库并重新下载

其它:关于#{}${}格式的占位符

提示:在开发实践中,并不推荐使用${}格式的占位符。

假设需要实现: 分页查询品牌数据。

需要执行的SQL语句大致是:

SELECT * FROM pms_brand ORDER BY id LIMIT ?, ?

以上SQL语句中,需要使用到2个参数,分别表示“跳过几条记录”、“查询几条记录”。

BrandMapper中添加抽象方法:

List<BrandListItemVO> listPage(
    @Param("offset") Integer offset, @Param("count") Integer count);

提示:offset表示“偏移量”。

BrandMapper.xml中配置SQL:

<select id="listPage" resultMap="ListItemResultMap">
    SELECT
        <include refid="ListItemQueryFields" />
    FROM
        pms_brand
    ORDER BY id
    LIMIT #{offset}, #{count}
</select>

完成后,在BrandMapperTests中测试:

@Test
public void testListPage() {
    Integer offset = 5;
    Integer count = 3;
    List<BrandListItemVO> list = mapper.listPage(offset, count);
    System.out.println("查询列表完成,结果集中的数据的数量=" + list.size());
    for (BrandListItemVO item : list) {
        System.out.println(item);
    }
}

经过测试,可以发现,在配置SQL时,2个参数无论使用#{}还是${}都可以正常运行!

假设需要实现:根据名称查询品牌详情

BrandMapper中添加抽象方法:

BrandDetailVO getByName(String name);

BrandMapper.xml中配置SQL:

<select id="getByName" resultMap="DetailResultMap">
    SELECT
        <include refid="DetailQueryFields" />
    FROM
        pms_brand
    WHERE
    	name=#{name}
</select>

完成后,在BrandMapperTests中测试:

@Test
public void testGetByName() {
    String name = "华为";
    BrandDetailVO brandDetailVO = mapper.getByName(name);
    System.out.println("根据name=" + name + "查询完成,结果=" + brandDetailVO);
}

经过测试,可以发现,在配置SQL时,参数使用#{}可以正常运行,但是使用${}会出错!

其实,#{}${}这2种占位符的处理机制是不同的!

当占位符是#{}时,是预编译的,会先将SQL语句中的参数使用?表示,当编译通过后,再将参数值代入并执行。

当占位符是${}时,不是预编译的,会将各参数值先拼接到SQL语句中,然后再执行编译流程,完成后,再执行SQL语句。

以分页查询为例,当使用#{}时,其大致SQL是:

SELECT * FROM pms_brand ORDER BY id LIMIT ?, ?

以上SQL会先执行编译流程,完成后,再将参数值代入并执行SQL语句。

当使用${}时,需要先将参数值代入,假设offset的值是5count的值是3,则其SQL大致是:

SELECT * FROM pms_brand ORDER BY id LIMIT 5, 3

然后,会将以上SQL语句进行编译流程,最终执行SQL语句。

换成根据名称来查询时,当#{}占位符时,其SQL语句大致是:

SELECT * FROM pms_brand WHERE name=?

当使用${}占位符时,依然是先使用参数值替换占位符,得到的SQL语句大致是:

SELECT * FROM pms_brand WHERE name=华为

由于华为这2个字并没有添加引号表示这是一个字符串,所以,会被视为字段名,最终执行时的错误就是:

Cause: java.sql.SQLSyntaxErrorException: Unknown column '华为' in 'where clause'

所以,当使用${}格式的占位符,且参数值是字符串类型时(其实,其它非数值型都是如此),需要考虑数据类型的问题,例如,以上出错时,将测试数据的华为改成'华为'即可。

结论:使用${}格式的占位符,需要自行考虑数据类型的问题,但是,使用#{}时,并不需要。

另外,如果不使用预编译的做法,由于参数值可以改变语义,所以,还存在SQL注入的风险!

所以,为了保证SQL语句不会被注入,不应该使用${}格式的占位符!

提示:即便${}格式的占位符是有SQL注入风险的,但是,也并不是不能解决的,可以在执行SQL语句之前,对参数值使用正则表达式检查,如果存在SQL注入的关键字时,不执行SQL语句即可。

Mybatis的缓存机制

缓存:是一种临时存储数据的机制,甚至,使用这种机制存储的数据也只是临时使用而已。

通常,使用缓存存储数据时,都会比其它的某种处理机制更快(访问数据的速度更快,效率更高)!

当使用Mybatis实现增删改查的数据访问时,本质上,程序是运行在APP服务器上,而数据库通常都在另一台服务器上,则访问数据的效率非常低(需要在2台电脑之间进行互相通信,另外,还可以处理SQL语句、查询结果等),特别是查询数据非常多的数据表,在没有索引等机制的情况下,每次查询耗时都非常久!

通常,会使用缓存来解决查询效率低下的问题(与增删改无关),Mybatis内置的缓存机制就是将查询到的结果临时存储到APP服务器上,下次执行相同的查询时,直接将APP服务器上的结果进行返回即可,并不会真实的查询MySQL数据库服务器中的数据。

在这里插入图片描述

Mybatis在处理缓存时,区分为一级缓存和二级缓存(是2种不同的缓存机制,但可同时存在)。

Mybatis的一级缓存又被称之为“会话缓存”,是默认开启的,且无法关闭,其能够缓存数据的条件是:

  • 必须是同一个会话
  • 必须是通过同一个Mapper接口的对象执行的查询
  • 必须是执行完全相同的SQL语句
  • 必须是完全相同的SQL参数

当满足以上条件时,查询到的每个数据都会被Mybatis缓存下来,并且,下次查询的还是此数据时,将直接返回此前缓存的结果。

一级缓存会在关闭Session、通过SqlSession清空缓存、通过此会话的Mapper使得表中的数据发生修改时自动清空缓存数据!

Mybatis的二级缓存又称之为"namespace缓存",是作用于每一个XML配置的缓存,也就是说,只要执行的是同一个XML中的同一个查询,且参数相同,即使使用不同的会话,也可以共用缓存数据!

在整合Spring的Mybatis工程中(包括Spring Boot),二级缓存默认是全局开启,但各namespace默认未开启的,如果需要开启各namespace缓存,需要在XML文件中添加<cache/>节点(此节点直属于根节点,与其它同级节点不区分先后顺序)。

注意:二级缓存的数据必须在sqlSession提交(commit)或关闭(close)后才会产生。

注意:Mybatis在执行查询时,会先查找二级缓存,如果命中,直接返回结果,如果未命中,则查询一级缓存,如果命中则返回一级缓存中的结果,如果仍未命中,则执行实际的查询(连接到数据库服务器查询数据)。

提示:使用二级缓存后,输出中的日志中将包含Cache Hit Ratio信息,此信息表示的是“缓存数据的命中率”,将在日志的尾部通过0.00.5类似的数值表示。

另外,在每个查询的<select>上还可以配置useCache属性,取值为true / false,表示此查询是否使用缓存,默认值为true

另外,当开启二级缓存后,封装查询结果的类型必须实现Serializable接口,否则就会出现不可序列化的异常!

二级缓存也会因为当前namespace中执行了增删改操作而清空缓存数据!

【总结】

Mybatis内置的缓存机制有一级缓存和二级缓存这2种,并且,在执行查询时,会先查找二级缓存,再查找一级缓存。

其中,一级缓存是会话缓存,必须是同一个会话、同一个Mapper、执行同样的SQL、且使用同样的SQL参数,才会应用缓存,且一级缓存会因为关闭会话、在会话中主动清除缓存、使用此会话的Mapper执行了任何写操作后自动清除缓存数据,一级缓存是默认开启的,一定程度上人为不可控;二级缓存是Namespace缓存,即使不同的会话也可以使用到缓存数据,默认是全局开启、各Namespace未开启的状态,当需要使用Namespace缓存时,需要在XML文件中添加<cache/>节点以开启当前Namespace的二级缓存,另外,还可以在各<select>节点上配置useCache属性,以配置某个查询功能是否使用二级缓存,并且,使用二级缓存时,需要封装结果的类型实现了Serializable接口,二级缓存也会因为写数据而被清除。

由于无论是一级缓存还是二级缓存,都会因为发生了数据的写操作后清除缓存数据,以保证缓存数据的准确性,但是,这种机制并不一定符合开发实践中的应用需求,在开发实践中,可能并不需要每时每刻关注每个数据的准确性,例如微博的热搜、头条的热榜的机制是每10分钟或15分钟更新一次缓存数据,通过Mybatis的缓存机制是做不到这一点的,所以,通常会使用自定义策略的缓存做法,例如使用Redis来处理缓存。

MyBatis小结

关于Mybatis的使用,你需要:

  • 了解使用Mybatis时需要添加的依赖
    • mybatis-spring-boot-starter
    • mysql-connector-java
  • 了解使用Mybatis时的一次性配置(每个项目中只做1次)
    • application.properties中配置数据库的连接参数
    • 使用@MapperScan配置接口文件所在的包
    • application.properties中配置XML文件的位置
  • 掌握抽象方法的声明原则
    • 返回值类型:如果SQL是增删改类型的,则返回int,如果SQL是查询类型的,当查询结果是单个数据时,只需要保证足以封装结果即可,如果查询结果是多条数据,使用List,且元素类型依然是保证足以封装结果(每一条数据)即可
    • 方法名称:参考阿里的建议,不要使用重载
    • 参数列表:事先分析需要执行的SQL语句(或事先打草稿),在SQL语句中的参数将是抽象方法的参数,如果参数数量只有1个,则方法参数列表就指定对应的参数,如果参数超过1个,可以声明为多个参数,各参数添加@Param注解来配置参数名称,或将多个参数封装到自定义的POJO中
  • 掌握在XML中配置各抽象方法映射的SQL
    • 这类XML文件应该有固定的声明(顶部的2句),所以,这类XML通常是复制粘贴得到,或使用工具来创建、生成
    • 必须在根节点<mapper>namespace指定对应的接口
    • 应该根据需要执行的SQL语句来选择<insert>等4种节点中的某1个
    • <insert>等4种节点中,必须配置id属性,取值为抽象方法名称
    • 在配置<insert>实现插入数据时,应该还配置useGeneratedKeyskeyProperty属性,以获取自动编号的id(如果此表的id不是自动编号,则不配置)
    • <select>节点中,必须配置resultTyperesultMap中的其中1个属性
    • 使用<sql>节点可以封装SQL语句片段,当需要使用这段SQL语句时,可以使用<include>节点来引用
    • 使用<resultMap>节点可以指导Mybatis封装查询结果,例如可以配置列与属性的对应关系
    • 掌握动态SQL中的<foreach>,通常用于实现批量删除、批量插入等
    • 掌握动态SQL中的<if>,通常结合<set>处理更新数据
  • 掌握规范的SQL语句
    • insert语句中,应该显式的指定字段列表
      • 正例:insert into user (username, password) values ('root', '1234')
      • 反例:insert into user values (null, 'root', '1234')
    • 在统计查询时,使用count(*)进行统计
    • 在查询表数据时,不要使用星号表示字段列表
    • 当查询结果可能超过1条时,必须显式的使用order by指定结果集的排序规则
    • 当查询结果可能超过1条时,应该考虑是否需要进行分页处理

补充:自动更新gmt_create和gmt_modified的Mybatis拦截器

MybatisConfiguration.java

// 补充

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

@PostConstruct
public void addInterceptor() {
    Interceptor interceptor = new InsertUpdateTimeInterceptor();
    for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
        sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
    }
}

InsertUpdateTimeInterceptor.java

package com.unkeer.mall.product.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>基于MyBatis的自动更新"最后修改时间"的拦截器</p>
 *
 * <p>需要SQL语法预编译之前进行拦截,则拦截类型为StatementHandler,拦截方法是prepare</p>
 *
 * <p>具体的拦截处理由内部的intercept()方法实现</p>
 *
 * <p>注意:由于仅适用于当前项目,并不具备范用性,所以:</p>
 *
 * <li>拦截所有的update方法(根据SQL语句以update前缀进行判定),无法不拦截某些update方法</li>
 * <li>所有数据表中"最后修改时间"的字段名必须一致,由本拦截器的FIELD_MODIFIED属性进行设置</li>
 *
 * @see cn.tedu.mall.product.config.InsertUpdateTimeInterceptorConfiguration
 */
@Slf4j
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
)})
public class InsertUpdateTimeInterceptor implements Interceptor {
    /**
     * 自动添加的创建时间字段
     */
    private static final String FIELD_CREATE = "gmt_create";
    /**
     * 自动更新时间的字段
     */
    private static final String FIELD_MODIFIED = "gmt_modified";
    /**
     * SQL语句类型:其它(暂无实际用途)
     */
    private static final int SQL_TYPE_OTHER = 0;
    /**
     * SQL语句类型:INSERT
     */
    private static final int SQL_TYPE_INSERT = 1;
    /**
     * SQL语句类型:UPDATE
     */
    private static final int SQL_TYPE_UPDATE = 2;
    /**
     * 查找SQL类型的正则表达式:INSERT
     */
    private static final String SQL_TYPE_PATTERN_INSERT = "^insert\\s";
    /**
     * 查找SQL类型的正则表达式:UPDATE
     */
    private static final String SQL_TYPE_PATTERN_UPDATE = "^update\\s";
    /**
     * 查询SQL语句片段的正则表达式:gmt_modified片段
     */
    private static final String SQL_STATEMENT_PATTERN_MODIFIED = ",\\s*" + FIELD_MODIFIED + "\\s*=";
    /**
     * 查询SQL语句片段的正则表达式:gmt_create片段
     */
    private static final String SQL_STATEMENT_PATTERN_CREATE = ",\\s*" + FIELD_CREATE + "\\s*[,)]?";
    /**
     * 查询SQL语句片段的正则表达式:WHERE子句
     */
    private static final String SQL_STATEMENT_PATTERN_WHERE = "\\s+where\\s+";
    /**
     * 查询SQL语句片段的正则表达式:VALUES子句
     */
    private static final String SQL_STATEMENT_PATTERN_VALUES = "\\)\\s*values?\\s*\\(";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 检查方法的注解,如果方法包含 @IgnoreGmtTimeField 则不进行拦截
        // if(invocation.getMethod().getDeclaredAnnotation(IgnoreGmtTimeField.class)!=null){
        //    return invocation.proceed();
        // }

        // 日志
        log.debug("准备拦截SQL语句……");
        // 获取boundSql,即:封装了即将执行的SQL语句及相关数据的对象
        BoundSql boundSql = getBoundSql(invocation);
        // 从boundSql中获取SQL语句
        String sql = getSql(boundSql);
        // 日志
        log.debug("原SQL语句:{}", sql);
        // 准备新SQL语句
        String newSql = null;
        // 判断原SQL类型
        switch (getOriginalSqlType(sql)) {
            case SQL_TYPE_INSERT:
                // 日志
                log.debug("原SQL语句是【INSERT】语句,准备补充更新时间……");
                // 准备新SQL语句
                newSql = appendCreateTimeField(sql, LocalDateTime.now());
                break;
            case SQL_TYPE_UPDATE:
                // 日志
                log.debug("原SQL语句是【UPDATE】语句,准备补充更新时间……");
                // 准备新SQL语句
                newSql = appendModifiedTimeField(sql, LocalDateTime.now());
                break;
        }
        // 应用新SQL
        if (newSql != null) {
            // 日志
            log.debug("新SQL语句:{}", newSql);
            reflectAttributeValue(boundSql, "sql", newSql);
        }

        // 执行调用,即拦截器放行,执行后续部分
        return invocation.proceed();
    }

    public String appendModifiedTimeField(String sqlStatement, LocalDateTime dateTime) {
        Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_MODIFIED, Pattern.CASE_INSENSITIVE);
        if (gmtPattern.matcher(sqlStatement).find()) {
            log.debug("原SQL语句中已经包含gmt_modified,将不补充添加时间字段");
            return null;
        }
        StringBuilder sql = new StringBuilder(sqlStatement);
        Pattern whereClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_WHERE, Pattern.CASE_INSENSITIVE);
        Matcher whereClauseMatcher = whereClausePattern.matcher(sql);
        // 查找 where 子句的位置
        if (whereClauseMatcher.find()) {
            int start = whereClauseMatcher.start();
            int end = whereClauseMatcher.end();
            String clause = whereClauseMatcher.group();
            log.debug("在原SQL语句 {} 到 {} 找到 {}", start, end, clause);
            String newSetClause = ", " + FIELD_MODIFIED + "='" + dateTime + "'";
            sql.insert(start, newSetClause);
            log.debug("在原SQL语句 {} 插入 {}", start, newSetClause);
            log.debug("生成SQL: {}", sql);
            return sql.toString();
        }
        return null;
    }

    public String appendCreateTimeField(String sqlStatement, LocalDateTime dateTime) {
        // 如果 SQL 中已经包含 gmt_create 就不在添加这两个字段了
        Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_CREATE, Pattern.CASE_INSENSITIVE);
        if (gmtPattern.matcher(sqlStatement).find()) {
            log.debug("已经包含 gmt_create 不再添加 时间字段");
            return null;
        }
        // INSERT into table (xx, xx, xx ) values (?,?,?)
        // 查找 ) values ( 的位置
        StringBuilder sql = new StringBuilder(sqlStatement);
        Pattern valuesClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_VALUES, Pattern.CASE_INSENSITIVE);
        Matcher valuesClauseMatcher = valuesClausePattern.matcher(sql);
        // 查找 ") values " 的位置
        if (valuesClauseMatcher.find()) {
            int start = valuesClauseMatcher.start();
            int end = valuesClauseMatcher.end();
            String str = valuesClauseMatcher.group();
            log.debug("找到value字符串:{} 的位置 {}, {}", str, start, end);
            // 插入字段列表
            String fieldNames = ", " + FIELD_CREATE + ", " + FIELD_MODIFIED;
            sql.insert(start, fieldNames);
            log.debug("插入字段列表{}", fieldNames);
            // 定义查找参数值位置的 正则表达 “)”
            Pattern paramPositionPattern = Pattern.compile("\\)");
            Matcher paramPositionMatcher = paramPositionPattern.matcher(sql);
            // 从 ) values ( 的后面位置 end 开始查找 结束括号的位置
            String param = ", '" + dateTime + "', '" + dateTime + "'";
            int position = end + fieldNames.length();
            while (paramPositionMatcher.find(position)) {
                start = paramPositionMatcher.start();
                end = paramPositionMatcher.end();
                str = paramPositionMatcher.group();
                log.debug("找到参数值插入位置 {}, {}, {}", str, start, end);
                sql.insert(start, param);
                log.debug("在 {} 插入参数值 {}", start, param);
                position = end + param.length();
            }
            if (position == end) {
                log.warn("没有找到插入数据的位置!");
                return null;
            }
        } else {
            log.warn("没有找到 ) values (");
            return null;
        }
        log.debug("生成SQL: {}", sql);
        return sql.toString();
    }


    @Override
    public Object plugin(Object target) {
        // 本方法的代码是相对固定的
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        // 无须执行操作
    }

    /**
     * <p>获取BoundSql对象,此部分代码相对固定</p>
     *
     * <p>注意:根据拦截类型不同,获取BoundSql的步骤并不相同,此处并未穷举所有方式!</p>
     *
     * @param invocation 调用对象
     * @return 绑定SQL的对象
     */
    private BoundSql getBoundSql(Invocation invocation) {
        Object invocationTarget = invocation.getTarget();
        if (invocationTarget instanceof StatementHandler) {
            StatementHandler statementHandler = (StatementHandler) invocationTarget;
            return statementHandler.getBoundSql();
        } else {
            throw new RuntimeException("获取StatementHandler失败!请检查拦截器配置!");
        }
    }

    /**
     * 从BoundSql对象中获取SQL语句
     *
     * @param boundSql BoundSql对象
     * @return 将BoundSql对象中封装的SQL语句进行转换小写、去除多余空白后的SQL语句
     */
    private String getSql(BoundSql boundSql) {
        return boundSql.getSql().toLowerCase().replaceAll("\\s+", " ").trim();
    }

    /**
     * <p>通过反射,设置某个对象的某个属性的值</p>
     *
     * @param object         需要设置值的对象
     * @param attributeName  需要设置值的属性名称
     * @param attributeValue 新的值
     * @throws NoSuchFieldException   无此字段异常
     * @throws IllegalAccessException 非法访问异常
     */
    private void reflectAttributeValue(Object object, String attributeName, String attributeValue) throws NoSuchFieldException, IllegalAccessException {
        Field field = object.getClass().getDeclaredField(attributeName);
        field.setAccessible(true);
        field.set(object, attributeValue);
    }

    /**
     * 获取原SQL语句类型
     *
     * @param sql 原SQL语句
     * @return SQL语句类型
     */
    private int getOriginalSqlType(String sql) {
        Pattern pattern;
        pattern = Pattern.compile(SQL_TYPE_PATTERN_INSERT, Pattern.CASE_INSENSITIVE);
        if (pattern.matcher(sql).find()) {
            return SQL_TYPE_INSERT;
        }
        pattern = Pattern.compile(SQL_TYPE_PATTERN_UPDATE, Pattern.CASE_INSENSITIVE);
        if (pattern.matcher(sql).find()) {
            return SQL_TYPE_UPDATE;
        }
        return SQL_TYPE_OTHER;
    }

}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
完整全套资源下载地址:https://download.csdn.net/download/qq_27595745/70761177 【完整课程列表】 完整版Java web开发教程PPT课件 Java开发进阶教程 第01章 JavaScript简介以及基本语法(共15页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第02章 Dom模型,控制客户端元素(共15页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第03章 表单验证,常用函数(共15页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第04章 自定义web服务器(共14页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第06章 SERVLET(共15页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第07章 JSP语法及运行过程(共13页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第08章 JSP内置对象(共22页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第09章 jstl、el、java bean(共18页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第10章 过滤器、监听器、自定义标签(共19页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第11章 AJAX实现(共11页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第12章 自定义mvc框架(共11页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第13章 spring ioc aop(共18页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第14章 spring mvc介绍,原理以及搭建(共15页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第15章 spring mvc核心对象拦截器(共26页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第17章 自定义orm框架(共11页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第18章 mybatis框架的搭建以及应用(共13页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第19章 mybatis高级应用(共21页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第20章 数据库连接池,缓存(共15页).pptx 完整版Java web开发教程PPT课件 Java开发进阶教程 第22章 常用框架的介绍以及环境搭建(共16页).pptx JS课程案例ebookHTML\网上书店需求文档.doc
手把手视频详细讲解项目开发全过程,需要的小伙伴自行百度网盘下载,链接见附件,永久有效。 课程简介 MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。使用原生的Mybatis编写持久层逻辑时,所需要的代码是比较繁琐的,需要定义Mapper接口和Mapper.xml文件,每一个方法都需要编写对应的sql语句,会存在很多大量的重复工作,使用MP之后,对通用的方法做了高度的抽取,避免了很多重复工作,可以非常快速的实现了单表的各种增、删、改、查操作。除此之外,MP还提供了其他的高级功能,如:枚举、插件、ActiveRecord、SQL注入等。 本课程全面讲解了Mybatis-Plus框架的使用,从快速入门到原理分析再到插件的应用。每一个知识点都有案例进行演示学习,最终通过学习你将全面掌握MP的使用,从而使Mybatis的的开发更加的高效,达到事半功倍的效果。 适应人群 有一定的Java以及Mybatis框架的基础。 从0开始全面讲解Mybatis-Plus框架 l 快速入门 n Mybatis + MP 整合 n Spring + Mybatis + MP 整合 n SpringBoot + Mybatis + MP 整合 n 通用CRUD的全面讲解 n 配置 l 高级用法 n 条件构造器 n Oracle 主键Sequence n 通用枚举n ActiveRecord n 逻辑删除 l 插件 n 执行分析插件 n 性能分析插件 n 乐观锁插件 主讲内容 章节一:快速入门 1. Mybatis-Plus简介 2. 快速入门 3. 通用CRUD 4. 配置 5. 条件构造器 章节二:进阶 1. ActiveRecord 2. Oracle 主键Sequence 3. 插件 章节三:高级应用 1. Sql 注入器 2. 自动填充功能 3. 逻辑删除 4. 通用枚举 5. 代码生成器 6. MybatisX 快速开发插件

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员阿谷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值