SSM框架-MyBatis(三)

目录

1 MyBatis参数处理

1.1 单个简单类型参数

1.2 Map参数

1.3 实体类传值

1.4 多参数

1.5 @Param(命名注解)

2 MyBatis查询语句

2.1 返回Car

2.2 返回List

2.3 返回Map

2.4 返回List

2.5 返回Map(String,Map)

2.6 resultMap结果映射

2.7 返回总记录条数

3 动态SQL

3.1 if标签

3.2 where标签

3.3 trim标签

3.4 set标签

3.5 choose when otherwise

3.6 foreach标签

4 MyBatis的高级映射及延迟加载

4.1 多对一

4.1.1 级联属性映射

4.1.2 association

4.1.3 分步查询

4.2 多对一延迟加载

4.3 一对多 

4.3.1 collection

4.3.2 分步查询

5 MyBatis的缓存

5.1 一级缓存

5.2 二级缓存

5.3 MyBatis集成EhCache

6 MyBatis的逆向工程

6.1 逆向工程配置与生成

6.2 测试逆向工程是否好用

7 MyBatis使用PageHelper

7.1 limit分页

7.2 PageHelper插件

8 MyBatis的注解式开发


1 MyBatis参数处理

先准备环境和数据

准备数据库表:

然后配置idea环境:配置依赖,核心配置文件,SQL映射文件等等,基本上都是和之前一样的,先展示pojo类:

package com.itzw.mybatis.pojo;

public class Student {
    private Long id;
    private String name;
    private Integer age;
    private Double height;
    private Character sex;

    public Student(){}

    public Student(Long id, String name, Integer age, Double height, Character sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.height = height;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", height=" + height +
                ", sex=" + sex +
                '}';
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Double getHeight() {
        return height;
    }

    public void setHeight(Double height) {
        this.height = height;
    }

    public Character getSex() {
        return sex;
    }

    public void setSex(Character sex) {
        this.sex = sex;
    }
}

1.1 单个简单类型参数

单个简单类型参数有哪些:

  • byte,short,int,long,float,double,char
  • Byte,Short,Integer,Long,Float,Double,Character
  • String
  • java.util.Date
  • java.sql.Date

这种简单的参数我们之前经常使用,比如根据id查询信息等等,这里就不演示了。经过以前的使用我们得知,mybatis可以自动识别类型,因为在我们给SQL语句传值的时候,我们是没有指定数据类型的。但是我们在学JDBC的时候,我们在给SQL语句传值的时候是明确指定每个值得类型的。比较完整的写法如下:

我们之前不指定传值类型也没问题,所以其实这个类型是可以完全不写的。

如果参数只有⼀个的话,#{} ⾥⾯的内容就随便写了。对于 ${} 来说,注意加单引号

1.2 Map参数

这种方式传值我们在最开始就用过,大概就是先在map集合存储数据,它们有key和对应的value,我们在SQL语句中的#{}大括号中填写key,mybatis就会把对应值传过去。

这种⽅式是⼿动封装Map集合,将每个条件以key和value的形式存放到集合中。然后在使⽤的时候通过# {map集合的key}来取值。

1.3 实体类传值

这个我们更是用过很多,在插入数据和修改数据中我们的参数都是一个实体类

这⾥需要注意的是:#{} ⾥⾯写的是属性名字。这个属性名其本质上是:set/get⽅法名去掉set/get之后 的名字。

1.4 多参数

这个是我们之前没有遇到过的

List<Student> selectByNameAndSex(int age,char sex);

以前都是一个参数我们知道如何填#{}中的参数,但是多个参数怎么办呢,我们先将SQL语句中的 #{}参数写为对应的类属性,本次案例也就是age和sex

        SqlSession sqlSession = SqlSessionUtil.openSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        List<Student> students = mapper.selectByNameAndSex(21, '男');
        students.forEach(student -> System.out.println(student));
        sqlSession.close();

idea提示我们,age找不到,让我们使用arg1,arg0,param1,param0

那我们就使用这个参数试一下

改为上面参数果然成功了 

实现原理:实际上在mybatis底层会创建一个map集合,以arg0/pram1为key,以方法上的参数为value

例如:map.put("arg0",age)        map.put("arg1",sex)

1.5 @Param(命名注解)

以上参数可读性太差,根本不知道传的什么值,我们可以使用Param注解

接口改为如下:

SQL语句如下:

经测试没有问题

核心理解:@Param("这里其实就是map集合的key")

2 MyBatis查询语句

还是用之前的t_car数据库表,那么idea配置信息 也差不多

2.1 返回Car

当查询的结果,有对应的实体类,并且查询结果只有⼀条时。这个我们在之前就经常使用,当我们想查询一条语句的时候就可以使用返回Car。当然也可以使用返回List集合。这就不展示了。

2.2 返回List<Car>

当查询的记录条数是多条的时候,必须使⽤集合接收。如果使⽤单个实体类接收会出现异常。这个和上一条类似,只不过这是查询多条信息,那我们就必须使用集合接收了。

2.3 返回Map

当返回的数据,没有合适的实体类对应的话,可以采⽤Map集合接收。字段名做key,字段值做value。 查询如果可以保证只有⼀条数据,则返回⼀个Map集合即可。
    /**
     * 根据id查询,返回的是map集合
     * @param id
     * @return
     */
    Map<String,Object> selectByIdRetMap(long id);
    <select id="selectByIdRetMap" resultType="map">
        select id,
               car_num,
               brand,
               guide_price,
               produce_time,
               car_type
        from
            t_car
        where id = ${id}
    </select>

这里的resultType可以直接写map,因为mybatis内置了很多别名,可以查看mybatis帮助文档。并且这里的SQL语句不用重命名甚至直接写select * from t_car就可以

2.4 返回List<Map>

上面的Map集合只能返回一条数据,那我们就可以使用List<Map>

查询结果条数⼤于等于 1 条数据,则可以返回⼀个存储Map集合的List集合。List<Map>等同于List<Car>
    /**
     * 查找所有信息,返回的类型是List<Map>
     * @return
     */
    List<Map<String,Object>> selectAllRetListMap();
    <select id="selectAllRetListMap" resultType="map">
        select *
        from t_car;
    </select>
    @Test
    public void selectAllRetListMap(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        List<Map<String, Object>> maps = mapper.selectAllRetListMap();
        maps.forEach(map ->{
            System.out.println(map);
        });
        sqlSession.close();
    }

2.5 返回Map(String,Map)

拿Car的id做key,以后取出对应的Map集合时更⽅便。
    /**
     * 获取所有信息返回一个map集合
     * map集合的key是Car的id
     * Map集合的value是对应的Car
     * @return
     */
    @MapKey("id")
    Map<Long,Map<String,Object>> selectAllRetMap();
    <select id="selectAllRetMap" resultType="map">
        select * from t_car
    </select>
    @Test
    public void testSelectAllRetMap(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Map<Long, Map<String, Object>> maps = mapper.selectAllRetMap();
        System.out.println(maps);
        sqlSession.close();
    }

2.6 resultMap结果映射

查询结果的列名和java对象的属性名对应不上怎么办?

  • 第一种方式:as给列名起别名
  • 第二种方式:使用resultMap进行结果映射
  • 第三种方式:是否开启驼峰命名自动映射(配置settings)

第一种方式就是我们之前用的,非常的愚蠢。

下面我们演示第二种方式:

在写SQL语句之前我们要写一个resultMap结果映射,就是把SQL中的列名与java中的属性名一一对应。

    /**
     * 查询所有信息
     * @return
     */
    List<Car> selectAll();
    <!--结果映射-->
    <!--其中id为这个结果映射的唯一标识,作为select标签中的resultMap属性的值
        type是结果映射要映射的类,可以使用别名-->
    <resultMap id="carResultMap" type="car">
        <!--其中对象的唯一标志id,也就是主键,我们用下面方式对应。官方解释:提高mybatis的性能,建议写上-->
        <id property="id" column="id"></id>
        <!--以下就是将列名和java属性名对应起来的方式,其中列名和属性名一样就不需要对应了,可以省略-->
        <result property="carName" column="car_num"></result>
        <result property="guidePrice" column="guide_price"></result>
        <result property="produceTime" column="produce_time"></result>
        <result property="carType" column="car_type"></result>
    </resultMap>
    <select id="selectAll" resultMap="carResultMap" >
        select * from t_car
    </select>
    @Test
    public void testSelectAll(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        List<Car> cars = mapper.selectAll();
        cars.forEach(car -> System.out.println(car));
        sqlSession.close();
    }

下面演示第三种:

使用驼峰命名自动映射的前提是:属性名遵循java的命名规范,数据库表的列名遵循SQL的命名规范。

Java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式

SQL命名规范:全名小写,单词之间采用下划线分割

比如下面:

如何启用该功能,在mybatis-config核心配置文件中:

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
 List<Car> selectAllByMapUnderscoreToCamelCase();

第三种方式虽然方便,但是要求也比较严格,看情况选择,肯定都比第一种方便就对了

2.7 返回总记录条数

    /**
     * 返回总条数
     * @return
     */
    Long selectTotal();
    <select id="selectTotal" resultType="long">
        select count(*) from t_car
    </select>
    @Test
    public void testSelectTotal(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Long total = mapper.selectTotal();
        System.out.println(total);
        sqlSession.close();
    }

3 动态SQL

有些业务场景需要我们使用SQL语句动态拼接,比如批量删除,批量插入,多条件查询等等。

我们创建一个模块演示这些动态SQL

3.1 if标签

需求:多条件查询

可能的条件包括:品牌,价格,汽车类型

if标签的注意事项:

  • if标签中的test属性是必须的
  • if标签中test属性的值是false或者是true
  • 如果test时true,if标签中的SQL语句就会拼接,如果是false则不会
  • test属性中可以使用的是:当使用了@Param注解,那么test中主要出现的是注解指定的参数名;如果没使用注解那就是param1或者arg0这种;当使用了pojo,那test中出现的是pojo类的属性名
  • 在mybatis的动态SQL中,不能使用&&,只能使用and
    List<Car> selectByMultiCondition(@Param("brand") String brand,@Param("guidePrice")
            Double guidePrice,@Param("carType") String carType);
    <select id="selectByMultiCondition" resultType="car">
        select * from t_car where
        <!-- test里面填的是判断条件 -->
        <if test="brand != null and brand != ''">
            brand like #{brand}"%"
        </if>
        <if test="guidePrice != null and guidePrice != ''">
            and guide_price >= #{guidePrice}
        </if>
        <if test="carType != null and carType != ''">
            and car_type = #{carType}
        </if>
    </select>

SQL语句如上,但是这样我们也发现了一个问题就是假如最后一个或者中间一个条件为空那么程序正常运行,但是如果第一个条件为空,那么就出出现这样一个SQL语句 select * from t_car where and guide_price >= #{guidePrice},这显然是一条非法的SQL语句,这怎么解决呢,只要在where后面加上1=1这样的恒成立语句就可以了。但是这样第一个条件执行的话又少了一个and,我们加上这个and即可,如下:

3.2 where标签

where标签的作用:让where子句更加动态智能,听到这就知道前面的知识点白学了

  • 所有条件都为空时,where标签保证不会生成子句
  • 自动去除某些条件前面多余的and或or

我们使用where标签修改上面的SQL语句

    List<Car> selectByMultiConditionWithWhere(@Param("brand") String brand,@Param("guidePrice")
            Double guidePrice,@Param("carType") String carType);
    <select id="selectByMultiConditionWithWhere" resultType="car">
        select * from t_car
        <where>
            <!-- test里面填的是判断条件 -->
            <if test="brand != null and brand != ''">
                and brand like #{brand}"%"
            </if>
            <if test="guidePrice != null and guidePrice != ''">
                and guide_price >= #{guidePrice}
            </if>
            <if test="carType != null and carType != ''">
                and car_type = #{carType}
            </if>
        </where>
    </select>

如上的SQL语句就可以解决上一小节的问题,注意它只能智能去掉and不能添加and,所以and我们必须写。还要注意的是只能去掉最前面的and,不能把and加到后面。or和and一样都可以智能去掉

3.3 trim标签

  • trim标签的各属性作用
  • prefix:加前缀
  • suffix:加后缀
  • prefixOverrides:删除前缀
  • suffixOverrides:删除后缀
  • prefix="where"表示是在trim标签所有内容的前面添加where
  • suffixOverrides="and|or"表示把trim标签中内容的后缀and或or去掉

从上面这些属性的作用看来,我们又要白学了,我们继续修改SQL

    <select id="selectByMultiConditionWithTrim" resultType="car">
        select * from t_car
        <trim prefix="where" suffix="" prefixOverrides="and|or" suffixOverrides="and|or">
            <if test="brand != null and brand != ''">
                brand like #{brand}"%" and
            </if>
            <if test="guidePrice != null and guidePrice != ''">
                guide_price >= #{guidePrice} and
            </if>
            <if test="carType != null and carType != ''">
                car_type = #{carType}
            </if>
        </trim>
    </select>

我们可以选择把and加到每个条件前面然后使用prefixOverrides删除前缀,我们当然也可以把and加到每个条件后面使用suffixOverrides删除后缀。

3.4 set标签

在之前我们修改数据的时候都是修改一行数据的全部信息,但是我若只想修改某一个可以吗?显然用之前的方法不可以,如果给其它数据都赋值为null不进行修改,那么结果就是把这些数据修改为了null,所以我们想指定某个数据修改,赋值为null或空的就原封不动。

set主要就使用在update语句中,用来生成set关键字,同时去掉最后多余的“,”

下面演示:

int updateWithSet(Car car);
    <update id="updateWithSet">
        update t_car
        <set>
            <if test="carName != null and carName != ''">car_num =  #{carName},</if>
            <if test="brand != null and brand != ''">brand = #{brand},</if>
            <if test="guidePrice != null and guidePrice != ''">guide_price = #{guidePrice},</if>
            <if test="produceTime != null and produceTime != ''">produce_time = #{produceTime},</if>
            <if test="carType != null and carType != ''">car_type = #{carType}</if>
        </set>
        where id = #{id}
    </update>
    @Test
    public void testUpdateWithSet(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Car car = new Car(33L,null,"特斯拉",null,null,null);
        int count = mapper.updateWithSet(car);
        System.out.println(count);
        sqlSession.commit();
        sqlSession.close();
    }

3.5 choose when otherwise

以上等同于以下:

只有一个分支会被选择

需求:先根据品牌查询,如果没有提供品牌就根据价格查询,如果没有提供价格就根据汽车类型查询:

    List<Car> selectWithChoose(@Param("brand") String brand,@Param("guidePrice")
            Double guidePrice,@Param("carType") String carType);
    <select id="selectWithChoose" resultType="CAR">
        select * from t_car
        <where>
            <choose>
                <when test="brand != null and brand != ''">brand like "%"#{brand}"%"</when>
                <when test="guidePrice != null and guidePrice != ''">guide_price >= #{guidePrice}</when>
                <otherwise>car_type = #{carType}</otherwise>
            </choose>
        </where>
    </select>

这样查询只会根据一个条件查询,若三个条件都有就根据最前面的条件查

3.6 foreach标签

foreach标签可以循环数组或集合,这样就可以批量删除或者添加:

foreach标签的属性:

  • collection:集合或数组
  • item:集合或数组的元素
  • separator:分隔符
  • open:foreach中所有内容的开始
  • close:foreach中所有内容的结束
int deleteBatchByForeach(@Param("ids") Long[] ids);
    <delete id="deleteBatchByForeach">
        delete from t_car where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>

以上删除的SQL语句是用的in来实现的,还可以使用or来实现:

    <delete id="deleteBatchByForeach2">
        delete from t_car where
        <foreach collection="ids" item="id" separator="or">
            id = #{id}
        </foreach>
    </delete>

批量添加:

int insertBatchByForeach(@Param("cars") List<Car> cars);
    <insert id="insertBatchByForeach">
        insert into t_car values
        <foreach collection="cars" item="car" separator=",">
        (#{car.id},#{car.carName},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType})
    </foreach>

注意这里#{这里属性前面要加car.}

    @Test
    public void testInsertBatchByForeach(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        List cars = new ArrayList();
        Car car1 = new Car(null,"123","比亚迪1",23.0,"2021-5-6","新能源");
        Car car2 = new Car(null,"124","比亚迪2",23.0,"2021-5-6","新能源");
        Car car3 = new Car(null,"125","比亚迪3",23.0,"2021-5-6","新能源");
        Car car4 = new Car(null,"126","比亚迪4",23.0,"2021-5-6","新能源");
        cars.add(car1);
        cars.add(car2);
        cars.add(car3);
        cars.add(car4);
        int count = mapper.insertBatchByForeach(cars);
        System.out.println(count);
        sqlSession.commit();
        sqlSession.close();
    }

4 MyBatis的高级映射及延迟加载

准备两个数据库表 t_stu和t_clazz,对应学生信息和班级信息,一个班级对应多个学生

配置idea环境,还是和以前一样的配置,不过因为有两个表所以配置两个mapper接口和两个class文件还有两个SQL映射文件

4.1 多对一

多的一方是Student,一的一方是Clazz、

怎么分主表和副表,原则:谁在前面谁是主表

多对一:多在前,那么多就是主表

一对多:一在前,那么一就是主表

多对一有多种方式,常见的有三种:

  • 第一种方式:一条SQL语句,级联属性映射
  • 第二种方式:一条SQL语句,association
  • 第三种方式:两条SQL语句,分步查询。(这种方式常用:优点是可复用和支持懒加载)

4.1.1 级联属性映射

pojo类Student类中添加 一个属性:Clazz clazz。表示学生关联的班级对象

pojo类如下:

package com.itzw.mybatis.pojo;

public class Clazz {
    private Integer cid;
    private String cname;

    public Clazz(Integer cid, String cname) {
        this.cid = cid;
        this.cname = cname;
    }

    public Clazz(){}

    @Override
    public String toString() {
        return "Clazz{" +
                "cid=" + cid +
                ", cname='" + cname + '\'' +
                '}';
    }

    public Integer getCid() {
        return cid;
    }

    public void setCid(Integer cid) {
        this.cid = cid;
    }

    public String getCname() {
        return cname;
    }

    public void setCname(String cname) {
        this.cname = cname;
    }
}
package com.itzw.mybatis.pojo;

public class Student {
    private Integer sid;
    private String sname;
    private Clazz clazz;

    public Student(){}

    public Student(Integer sid, String sname, Clazz clazz) {
        this.sid = sid;
        this.sname = sname;
        this.clazz = clazz;
    }

    @Override
    public String toString() {
        return "Student{" +
                "sid=" + sid +
                ", sname='" + sname + '\'' +
                ", clazz=" + clazz +
                '}';
    }

    public Integer getSid() {
        return sid;
    }

    public void setSid(Integer sid) {
        this.sid = sid;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public Clazz getClazz() {
        return clazz;
    }

    public void setClazz(Clazz clazz) {
        this.clazz = clazz;
    }
}

写SQL映射文件:

先说好到底我们要干嘛,我们想实现多表查询,根据外键可以查询到这个人对应的别的信息,我们可以先回顾一下之前学mysql写的多表查询:

这是多表查询的左连接,左连接是先查询出左表(即以左表为主),然后查询右表,右表中满足条件的显示出来,不满足条件的显示NULL,其中m1.id是外键,m2.id是主键

select * from Menu1 m1 left join Menu2 m2 on m1.id = m2.id;

这是右连接,右连接就是先把右表中所有记录都查询出来,然后左表满足条件的显示,不满足显示NULL。 

select * from Menu1 m1 right join Menu2 m2 on m1.id = m2.id;

这是内连接,查询结果必须满足条件,返回同时满足两个表的部分

select * from Menu1 m1 inner join Menu2 m2 on m1.id = m2.id;

我也可以如下查询

select * from Menu1 m1, Menu2 m2 where m1.id = m2.id;

就比如本次我的t_stu表中有学生信息,它有外键,而这个外键对应的就是班级信息表t_clazz的主键,我想一次就能得到学生的个人信息和班级信息。

下面看看SQL映射文件怎么写:

因为是两个表的信息,那么select标签中的resultType就不能简单的写Student了,我们要使用ResultMap,在resultMap中写对应关系,主要是clazz和数据库表中的列名对不上,其它可以不用对应,但是主键的对应我们还是如下写上,官方说这样可以提高效率。下面的SQL语句当然不止这一种写法。

    <resultMap id="studentResultMap" type="student">
        <id property="sid" column="sid"></id>
        <result property="clazz.cid" column="cid"></result>
        <result property="clazz.cname" column="cname"></result>
    </resultMap>
    
    <select id="selectById" resultMap="studentResultMap">
        select * from t_stu s left join t_clazz c on s.cid = c.cid
        where sid = #{sid}
    </select>

4.1.2 association

将原来的resultMap修改一下即可,如下:

    <resultMap id="studentResultMap" type="student">
        <id property="sid" column="sid"></id>
        <result property="sname" column="sname"></result>
        <association property="clazz" javaType="Clazz">
            <result property="cid" column="cid"></result>
            <result property="cname" column="cname"></result>
        </association>
    </resultMap>

注意:经测试,如果加上association这个标签那么sname这种即使属性和数据库表一样也要写上对应关系,否则查出结果为null,那association中的对应关系更是要写全。

association的意思为关联:就是学生对象关联班级对象

4.1.3 分步查询

我们要修改三处:

第一处:在ClazzMapper接⼝中添加⽅法,这里我们需要一个cid才能查询

    /**
     * 分步查询第二步
     * @param cid
     * @return
     */
    Clazz selectByCId(Integer cid);

第二处:在ClazzMapper.xml⽂件中进⾏配置。这里看我们还是需要一个cid

    <select id="selectByCId" resultType="clazz">
        select * from t_clazz where cid = #{cid}
    </select>

第三处:association中select位置填写sqlId。sqlId=namespace+id。其中column属性作为这条子sql语句的条件。这里我们就不需要再写clazz表的列名和属性的对应关系了,因为用不到了

    <resultMap id="studentResultMap" type="student">
        <id property="sid" column="sid"></id>
        <result property="sname" column="sname"></result>
        <association property="clazz"
                     select="com.itzw.mybatis.mapper.ClazzMapper.selectByCId"
                     column="cid">
        </association>
    </resultMap>
    
    <select id="selectById" resultMap="studentResultMap">
        select s.* from t_stu s where sid = #{sid}
    </select>

这里的column属性的值就是上两步我们需要的cid值

通过这个查询结果我们发现是通过两个SQL语句查到的,先是查询student表的信息,然后将得到的外键信息传给clazz表得到clazz信息。

分步查询的优点:

  • 复用性强,可以重复利用
  • 可以充分利用它们的延迟加载/懒加载机制

4.2 多对一延迟加载

什么是延迟加载(懒加载),有什么用:

  • 延迟加载核心原理:用的时候再执行查询语句,不用的时候不查询
  • 作用:提高性能,尽可能的不查,或者说尽可能的少查,来提高效率

在mybatis中怎么开启延迟加载呢?

association标签中添加fetchType="lazy"

这样当我们只想获取t_stu表中的内容时就不会执行t_clazz相关的SQL语句 

当我们想查询班级信息的时候,那班级信息相关的SQL语句就会执行:

注意:fetchType的设置只对association关联的SQL语句起作用

在实际开发中,大部分情况下是需要延迟加载的,所以建议开启全部的延迟加载机制,在mybatis核心配置文件中添加全局配置:lazyLoadingEnabled=true,false为关闭

    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>

这样我们就不用在association标签中写fetchType了。但是如果我就想某个语句跟着执行呢,我们可以单独在那个association标签中设置fetchType=eager

4.3 一对多 

一个班级对应多个学生,一是主表,也就是t_clazz是主表

一对多通常是在一的一方中有List集合属性,我们在Clazz类中添加List<Student> stus属性

一对多通常包括两种实现方式:

  • 第一种:collection
  • 第二种:分步查询

4.3.1 collection

    /**
     * 查询班级信息并且要查到班级对应的学生信息
     * @param cid
     * @return
     */
    Clazz selectByCollection(Integer cid);
    <resultMap id="clazzResultMap" type="clazz">
        <id property="cid" column="cid"></id>
        <result property="cname" column="cname"></result>
        <collection property="stus" ofType="student">
            <id property="sid" column="sid"></id>
            <result property="sname" column="sname"></result>
        </collection>
    </resultMap>

    <select id="selectByCollection" resultMap="clazzResultMap">
        select c.*,s.sid,s.sname from t_clazz c,t_stu s where
        c.cid = s.cid and c.cid = #{cid}
    </select>

这里要注意的是collection标签中指定属性的类型要使用ofType,而之前使用的是JavaType

4.3.2 分步查询

和多对一分布查询类似:

首先接口方法:

    /**
     * 分步查询
     * @param cid
     * @return
     */
    Clazz selectByStep1(Integer cid);
    /**
     * 分步查询2
     * @param cid
     * @return
     */
    List<Student> selectByStep2(Integer cid);

SQL映射:

    <resultMap id="clazzResultMapByStep" type="clazz">
        <id property="cid" column="cid"></id>
        <result property="cname" column="cname"></result>
        <collection property="stus"
                    select="com.itzw.mybatis.mapper.StudentMapper.selectByStep2"
                    column="cid"></collection>
    </resultMap>

    <select id="selectByStep1" resultMap="clazzResultMapByStep">
        select * from t_clazz where cid = #{cid}
    </select>
    <select id="selectByStep2" resultType="student">
        select * from t_stu where cid = #{cid}
    </select>

因为之前我们设置了全局延迟加载,所以只要我们只访问班级信息,SQL语句就只执行clazz相关的。

5 MyBatis的缓存

缓存:是提前把数据存放到缓存(内存)中,下一次用的时候,直接从缓存中拿,效率高。缓存对应的英语单词是:cache

目的:提高执行效率,缓存机制减少IO的方式来提高效率

缓存通常是我们程序开发中优化程序的重要手段,你听说过哪些缓存技术呢?

字符串常量池;整数型常量池;线程池;连接池等等

MyBatis缓存机制:执行DQL(select语句)的时候,将查询结果放到缓存中,如果下一次执行完全相同的dql语句就直接从缓存中拿数据,不再查数据库了,不再去硬盘找数据了,MyBatis缓存包括:

  • 一级缓存:将查询到的数据存储到SqlSession中
  • 二级缓存:将查询到的数据存储到SqlSessionFactory中
  • 或者集成其它第三方的缓存:比如EhCache(java语言开发的),Memcache(c语言开发的)等

注意:缓存值针对DQL语句,也就是只适用于select语句

5.1 一级缓存

一级缓存是默认开启的,不需要任何配置

原理:只要使用同一个SqlSession对象执行同一个SQL语句,就会走缓存

Car selectById(Integer id);
    <select id="selectById" resultType="car">
        select * from t_car where id = #{id}
    </select>
@Test
    public void testSelectById(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Car car1 = mapper.selectById(35);
        System.out.println(car1);
        Car car2 = mapper.selectById(35);
        System.out.println(car2);
        sqlSession.close();

查询结果:确实只查询了一次

我们使用不同的SqlSession对象试试:

    @Test
    public void testSelectById() throws IOException {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
        Car car1 = mapper1.selectById(35);
        System.out.println(car1);
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
        Car car2 = mapper2.selectById(35);
        System.out.println(car2);
        sqlSession1.close();
        sqlSession2.close();
    }

查询结果:确实查了两次 

什么情况下不走缓存:

  • SqlSession对象不是同一个,肯定不走缓存
  • 查询条件不一样也不走缓存

什么时候一级缓存失效:

  • 第一次DQL和第二次DQL之间做以下任意一件,都会让 一级缓存失效
  • 执行了sqlSession的clearCache()方法,这是手动清空缓存
  • 执行了Insert或delete或update语句,不管是操作的哪张表都会清空一级缓存

插入信息到t_clazz表中:确实查了两次

5.2 二级缓存

二级缓存的范围是SqlSessionFactory,使用二级缓存要具备以下几个条件:

  • <setting name="cacheEnabled",value="true">全局性的开启或关闭所有映射器配置文件中以配置的任何缓存。默认就是true,无需配置
  • 虽然不需要在MyBatis核心配置文件配置,但是需要在需要使用二级缓存的SqlMapper.xml文件中添加配置:<cache/>
  • 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
  • SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存中,此时二级缓存才可用。

下面演示:

    @Test
    public void testSelectById() throws IOException {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
        //代码执行到这个,实际上数据是缓存到一级缓存当中了
        Car car1 = mapper1.selectById(35);
        System.out.println(car1);
        //如果这里不关闭SqlSession对象的话,二级缓存中还是没有数据的
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
        //代码执行到这个,实际上数据是缓存到一级缓存当中了
        Car car2 = mapper2.selectById(35);
        System.out.println(car2);
        //程序执行到这里的时候,会将SqlSession1这个一级缓存的数据写入到二级缓存中
        sqlSession1.close();
        //程序执行到这里的时候,会将SqlSession2这个一级缓存的数据写入到二级缓存中
        sqlSession2.close();
    }

如上我们好像一切都都执行了但是查询结果仍然是查了两次,这是为啥?

因为在我们查第二次之前并没有执行close方法,也就是说数据还只存在一级缓存中,我们需要在执行第二次查询之前就将第一次查询中的sqlSession对象关闭,数据就会从第一次缓存中写入到二级缓存。我们改造一下:

此时查询结果就是只查询一次的。

二级缓存失效:只要两次查询之间出现了增删改查操作,二级缓存就会失效。一级缓存也会失效

5.3 MyBatis集成EhCache

集成EhCache是为了代替MyBatis自带的二级缓存,一级缓存是无法替代的

MyBatis对外提供了接口,也可以集成第三方缓存组件。因为EhCache是java写的,所以MyBatis集成EhCache较为常见,按照以下步骤操作就可以完成集成:

第一步:引入MyBatis整合ehcache的依赖:

<!--mybatis集成ehcache的组件-->
<dependency>
 <groupId>org.mybatis.caches</groupId>
 <artifactId>mybatis-ehcache</artifactId>
 <version>1.2.2</version>
</dependency>
<!--ehcache需要slf4j的⽇志组件,log4j不好使-->
<dependency>
 <groupId>ch.qos.logback</groupId>
 <artifactId>logback-classic</artifactId>
 <version>1.2.11</version>
 <scope>test</scope>
</dependency>
第⼆步:在类的根路径下新建echcache.xml⽂件,并提供以下配置信息。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <!--磁盘存储:将缓存中暂时不使⽤的对象,转移到硬盘,类似于Windows系统的虚拟内存-->
    <diskStore path="e:/ehcache"/>

    <!--defaultCache:默认的管理策略-->
    <!--eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有
   效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断-->
    <!--maxElementsInMemory:在内存中缓存的element的最⼤数⽬-->
    <!--overflowToDisk:如果内存中数据超过内存限制,是否要缓存到磁盘上-->
    <!--diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false-
   ->
    <!--timeToIdleSeconds:对象空闲时间(单位:秒),指对象在多⻓时间没有被访问就会失
   效。只对eternal为false的有效。默认值0,表示⼀直可以访问-->
    <!--timeToLiveSeconds:对象存活时间(单位:秒),指对象从创建到失效所需要的时间。
   只对eternal为false的有效。默认值0,表示⼀直可以访问-->
    <!--memoryStoreEvictionPolicy:缓存的3 种清空策略-->
    <!--FIFO:first in first out (先进先出)-->
    <!--LFU:Less Frequently Used (最少使⽤).意思是⼀直以来最少被使⽤的。缓存的元
   素有⼀个hit 属性,hit 值最⼩的将会被清出缓存-->
    <!--LRU:Least Recently Used(最近最少使⽤). (ehcache 默认值).缓存的元素有⼀
   个时间戳,当缓存容量满了,⽽⼜需要腾出地⽅来缓存新的元素的时候,那么现有缓存元素中时间戳
   离当前时间最远的元素将被清出缓存-->
    <defaultCache eternal="false" maxElementsInMemory="1000" overflowToDis
                  k="false" diskPersistent="false"
                  timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStor
                  eEvictionPolicy="LRU"/>
</ehcache>
第三步:修改SqlMapper.xml⽂件中的<cache/>标签,添加type属性。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

第四步:测试

之前的代码不变直接运行即可,发现也是只查一次

注意:如果使用这个EhCache,那么就不需要实现Serializable接口了

6 MyBatis的逆向工程

所谓的逆向工程是:根据数据库表逆向生成java的pojo类,SqlMapper.xml文件,以及Mapper接口类等。要完成这个工作需要借助别人写好的逆向工程插件 

思考:使用这个插件的话,需要给这个插件配置哪些信息?

  • pojo类名、包名以及生成位置
  • SqlMapper.xml文件名以及生成位置
  • Mapper接口以及生成位置
  • 连接数据库信息
  • 指定哪些表参与逆向工程
  • ...

6.1 逆向工程配置与生成

http://yuque.com/docs/share/82677b3a-e06a-427f-9b5c-c33baed33a3f?#cKAWR
https://www.yuque.com/dujubin/ltckqu/pozck9?#cKAWR

第一步:准备环境(数据库表和idea)

第二步:在pom文件中添加逆向工程插件

    <!--定制构建过程-->
    <build>
        <!--可配置多个插件-->
        <plugins>
            <!--其中的⼀个插件:mybatis逆向⼯程插件-->
            <plugin>
                <!--插件的GAV坐标-->
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.1</version>
                <!--允许覆盖-->
                <configuration>
                    <overwrite>true</overwrite>
                </configuration>
                <!--插件的依赖-->
                <dependencies>
                    <!--mysql驱动依赖-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.30</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
第三步:配置generatorConfig.xml
该⽂件名必须叫做:generatorConfig.xml
该⽂件必须放在类的根路径下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!--
    targetRuntime有两个值:
    MyBatis3Simple:⽣成的是基础版,只有基本的增删改查。
    MyBatis3:⽣成的是增强版,除了基本的增删改查之外还有复杂的增删改查。
    -->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!--防止生成重复代码-->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>

        <commentGenerator>
            <!--是否去掉⽣成⽇期-->
            <property name="suppressDate" value="true"/>
            <!--是否去除注释-->
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!--连接数据库信息-->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/powernode"
                        userId="root"
                        password="123456">
        </jdbcConnection>
        <!-- ⽣成pojo包名和位置 -->
        <javaModelGenerator targetPackage="com.itzw.mybatis.pojo" targetProject="src/main/java">
            <!--是否开启⼦包-->
            <property name="enableSubPackages" value="true"/>
            <!--是否去除字段名的前后空⽩-->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!-- ⽣成SQL映射⽂件的包名和位置 -->
        <sqlMapGenerator targetPackage="com.itzw.mybatis.mapper" targetProject="src/main/resources">
            <!--是否开启⼦包-->
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!-- ⽣成Mapper接⼝的包名和位置 -->
        <javaClientGenerator
                type="xmlMapper"
                targetPackage="com.itzw.mybatis.mapper"
                targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
        <!-- 表名和对应的实体类名-->
        <table tableName="t_car" domainObjectName="Car"/>
    </context>
</generatorConfiguration>

第四步:运行插件

运行成功发现生成的文件如下:注意本次逆向工程使用的是增强版

6.2 测试逆向工程是否好用

在测试之前我们还要手动引入那老四样依赖,还有MyBatis核心配置文件,logback配置文件还有工具类,这些是无法自动生成的,我们需要自己创建(复制粘贴)

package com.itzw.mybatis.test;

import com.itzw.mybatis.mapper.CarMapper;
import com.itzw.mybatis.pojo.Car;
import com.itzw.mybatis.pojo.CarExample;
import com.itzw.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.math.BigDecimal;
import java.util.List;

public class CarMapperTest {
    @Test
    public void testSelect(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        //查一个
        Car car = mapper.selectByPrimaryKey(35);
        System.out.println(car);
        System.out.println("==================================");
        //查所有,条件查询,赋值为null就是查所有
        List<Car> cars = mapper.selectByExample(null);
        cars.forEach(car1 -> System.out.println(car1));
        System.out.println("==================================");
        //多条件查询
        //这种查询是QBC风格的,比较面向对象,看不到sql语句
        CarExample carExample = new CarExample();
        carExample.createCriteria().andBrandEqualTo("比亚迪1").andGuidePriceGreaterThan(new BigDecimal(22));
        carExample.or().andBrandLike("比亚迪");
        List<Car> cars1 = mapper.selectByExample(carExample);
        cars1.forEach(car1 -> System.out.println(car1));
        sqlSession.close();
    }
}

我们发现这玩意非常的好用,连SQL语句都不用我们写了,直接面向对象查询信息。

7 MyBatis使用PageHelper

7.1 limit分页

我们先回顾mysql中是怎样使用分页的,使用limit关键字

limit语法格式:limit 开始下标,显示的记录条数

                        limit startIndex ,pageSize

比如我想查看t_car 表中的下标为0记录条数为3的信息:select * from t_car limit 0,3

假设已知页码pageNum,还有每页显示的记录条数为pageSize,第一个数字可以是动态的获取嘛

startIndex = (pageNum-1)*pageSize

所以我们想查看第pageNum页的内容就可以这样写:

select * from t_car limit (pageNum-1)*pageSize,pageSize

演示:

List<Car> selectByPage(@Param("startIndex") Integer startIndex, @Param("pageSize")Integer pageSize);
    <select id="selectByPage" resultType="car">
        select * from t_car limit #{startIndex},#{pageSize}
    </select>
    @Test
    public void testSelectByPage(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        int pageNum = 2;//页码
        int pageSize = 3;//一页条数
        int startIndex = (pageNum - 1) * pageSize;//开始下标
        List<Car> cars = mapper.selectByPage(startIndex, pageSize);
        cars.forEach(car -> System.out.println(car));
        sqlSession.close();
    }

我们可以直接使用最朴素的方式,也就是 上面这种直接用SQL语句查询。这样可以,但是我们有更简单的方式,而且可以获取分页相关的数据

7.2 PageHelper插件

第一步:引入依赖

<dependency>
 <groupId>com.github.pagehelper</groupId>
 <artifactId>pagehelper</artifactId>
 <version>5.3.1</version>
</dependency>

第二步:在mybatis-config.xml文中配置插件

<plugins>
 <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>

第三步:编写Java代码

使用PageHelper插件就不需要传参了

List<Car> selectAll();

使用PageHelper写SQL就不需要再SQL语句里限制页数和条数了 

    <select id="selectAll" resultType="car">
        select * from t_car
    </select>

 我们在查询前开启分页,这两个参数分别为页码和一页的条数

        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        //开启分页
        PageHelper.startPage(2,3);
        //执行查询语句
        List<Car> cars = mapper.selectAll();
        //cars.forEach(car -> System.out.println(car));
        //获取分页信息对象
        PageInfo<Car> carPageInfo = new PageInfo<>(cars, 5);
        System.out.println(carPageInfo);
        sqlSession.close();

我们还可以查看分页信息,使用PageInfo

        PageInfo{pageNum=2, pageSize=3, size=3, startRow=4, endRow=6, total=8, pages=3,
         list=Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=8, pages=3, reasonable=false, pageSizeZero=false}
         [Car{id=38, carName='null', brand='比亚迪4', guidePrice=23.0, produceTime='2021-5-6', carType='新能源'},
         Car{id=39, carName='null', brand='宝马', guidePrice=32.0, produceTime='2022-3-4', carType='燃油车'},
         Car{id=40, carName='null', brand='奥迪', guidePrice=31.0, produceTime='2012-3-4', carType='燃油车'}],
         prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=5,
         navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}

8 MyBatis的注解式开发

MyBatis中提供了注解开发,使用注解开发可以减少SQL映射文件的配置

当然,使用注解式开发的话,SQL语句是写在java程序中的,这种方式也会给SQL语句的维护带来成本。官方是这么说的:使用注解来映射简单的语句会使代码显得更加简洁,但是对于稍微复杂一点的语句,java注解不仅力不从心还会让你本来复杂的SQL语句变得更加混乱不堪。因此,如果你需要一些很复杂的操作,最好用XML来映射语句。所以这样看来我们之前的内容没有白学。

下面简单演示一下注解如何操作:

    /**
     * 使用注解方式删除信息
     * @param id
     * @return
     */
    @Delete("delete from t_car where id = #{id}")
    int deleteById(Integer id);

    /**
     * 使用注解查看所有信息
     * @return
     */
    @Select("select * from t_car")
    /*@Results({
            @Result(column = "id", property = "id", id = true),
            @Result(column = "car_num", property = "carName"),
            @Result(column = "brand", property = "brand"),
            @Result(column = "guide_price", property = "guidePrice"),
            @Result(column = "produce_time", property = "produceTime"),
            @Result(column = "car_type", property = "carType")
    })*/
    List<Car> selectAll();

这里只演示了删除和查询,在使用注解查询时依然要设置好java类的属性名和数据库表的列名的对应,这样写在注解上看起来挺麻烦的。当然我们也可以使用开启驼峰自动命名映射。