5.ssm之mybaties

Mybaties

1 基本概述

1.1 简单介绍

​ MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及结果集封装。MyBatis 可以使用简单的 XML 或注解来配置和映射信息,将Java 的 POJO(Plain Ordinary Java Object,8普通的 Java对象)映射成数据库中的记录。底层使用到了orm的技术。

  • orm:Object Relational Mapping对象关系映射。
1.2 特点:
  1. 小巧,易上手
  2. 轻量级:核心jar不到1M
  3. sql和java代码的分离
  4. 实现复杂的映射关系-resultmap
  5. 面向配置编程
  6. 支持动态sql(标签实现sql的流程控制)
1.3 jdbc和mybaties的对比
  1. 连接的创建

    1. jdbc需要频繁的创建和销毁连接,连接效率很低。
    2. Mybaties由内置连接池:实现连接的复用。
  2. 维护性

    1. jdbc的sql和java代码混合在一起,可读性差,学习成本高。
    2. mybaties实现sql和java代码的分离:将sql集中写到mapper文件中去。
  3. 占位符赋值

    1. jdbc给占位符赋值比较繁琐。
    2. mybaties实现对象到表记录的自动映射。
  4. 结果集处理

    1. jdbc解析结果集比较麻烦。
    2. mybaties实现对象的自动封装。

2 初步使用

2.1 环境的搭建

1 创建项目

​ 这里选择maven项目,使用的idea的开发工具。

2 在pom文件中添加依赖

<!--添加依赖信息-->
<dependencies>
    <!--log4j的jar包,日志包-->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.37</version>
    </dependency>
    <!--mybaties的依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.3</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
        <scope>test</scope>
    </dependency>
</dependencies>

3 创建数据库及表Student

image-20201029140504844

添加几条记录,用于测试。

4 创建实体类

package com.zhiyou.entity;
import java.io.Serializable;

public class Student implements Serializable {
    private int sid;
    private String sname;
    private int sage;

    public Student() {
    }

    public Student(int sid, String sname, int sage) {
        this.sid = sid;
        this.sname = sname;
        this.sage = sage;
    }

    public Student(String sname, int sage) {
        this.sname = sname;
        this.sage = sage;
    }

    public int getSid() {
        return sid;
    }

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

    public String getSname() {
        return sname;
    }

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

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

    public int getSage() {
        return sage;
    }

    public void setSage(int sage) {
        this.sage = sage;
    }
}

注意实体类和表字段的映射关系。

5 配置保存连接信息的jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/java26
jdbc.username=root
jdbc.password=19971001

如果是8.0+的mysql驱动需要添加时区

6 配置日志信息

有两种方式:

方式1:配置一个log4j的配置文件:log4j.properties,放在resource的根目录下。

### 设置###
log4j.rootLogger = debug,stdout,D

### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
### 输出目的地
log4j.appender.stdout.Target = System.out
#日志的格式
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n

### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = E://logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG 
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

方式2:在核心配置文件中添加配置信息,使用默认的日志配置

<settings>
    <setting name="logImpl" value="STDOUT_logging"/>
</settings>

注:需要注意配置的位置,要在environments之前配置,且依赖于log4j的jar包。

7 配置mybaties-conf.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<!-- 配置文件的根元素 -->
<configuration>
    <!--引入数据连接的四大参数文件-->
    <properties resource="jdbc.properties"/>
    <!-- 环境:配置mybatis的环境 -->
    <environments default="development">
        <!-- 环境变量:可以配置多个环境变量,比如使用多数据源时,就需要配置多个环境变量 -->
        <environment id="development">
            <!-- 事务管理器 -->
            <transactionManager type="JDBC"/>
            <!-- 数据源 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    
</configuration>

可选项:起别名

<!--起别名:可以直接在sql语句中进行使用-->
<typeAliases>
    <typeAlias type="student" alias="com.zhiyou.entity.Student"/>
</typeAliases>

8 写一个mapper配置文件,用来写sql

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhiyou.mapper.StudentMapper">
    <!--id:是所有标签的提示符
        parameterType:占位符的类型
        resultType:结果集被封装的类型
    -->
    <select id="getStudentById" parameterType="int" resultType="com.zhiyou.entity.Student">
        select * from student where sid=#{sid}
    </select>
    <insert id="addStudent" parameterType="com.zhiyou.entity.Student" >
        insert into student(sname,sage) values(#{sname},#{sage})
    </insert>
    <select id="getAllStudent" resultType="com.zhiyou.entity.Student">
        select * from student;
    </select>
    <delete id="deleteOneById" parameterType="int">
        delete from student where sid=#{sid};
    </delete>
    <update id="updateOneById" parameterType="com.zhiyou.entity.Student">
        update student set sname=#{sname},sage=#{sage} where sid=#{sid}
    </update>
</mapper>

注:

  1. 当我们存在多个mapper文件的时候最好将namespace补充完整,否则如果出现多个id值一致的时候会报错。
  2. 如果使用接口的方式,namespace必须是对应接口的全类名。

9 配置mappers

在mybaties-conf.xml中将我们的配置的mapper文件配置进其中

<!-- 映射器:指定映射文件或者映射类 -->
<mappers>
	<mapper resource="mapper/StudentMapper.xml"/>
</mappers>

10 测试

写一个util类,创建SqlSession

package com.zhiyou.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;

public class SqlSessionUtil {
    public static SqlSession getSqlSession() throws IOException {
       	//方式1:使用Resource的静态方法
        InputStream resourceAsStream = Resources.getResourceAsStream("mybaties-config.xml");
        //方式2:使用类加载器
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = build.openSession();
        return sqlSession;
    }
}

测试方式1:

@Test
public void test01() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    Student student = (Student)sqlSession.selectOne("getStudentById", 1);
    System.out.println(student);
    sqlSession.close();
}

测试方式2:此方式依赖接口

1)创建一个接口

public interface StudentMapper {
   //方法的名字要和mapper的对应sq的id保持一致
   Student getStudentById(int sid);
}

2)测试代码

@Test
public void test02() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    Student studentById = mapper.getStudentById(1);
    System.out.println(studentById);
}
@Test
public void test03() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    int count = mapper.addStudent(new Student("赵六",18));
    System.out.println(count);
    sqlSession.commit();
    sqlSession.close();
}
@Test
public void test04() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    List<Student> allStudent = mapper.getAllStudent();
    System.out.println(allStudent);
    sqlSession.close();
}
@Test
public void test05() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    int i = mapper.deleteOneById(3);
    System.out.println(i);
    sqlSession.commit();
    sqlSession.close();
}
@Test
public void test06() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    int count = mapper.updateOneById(new Student(1,"赵六",18));
    System.out.println(count);
    sqlSession.commit();
    sqlSession.close();
}

注:对于增,删除,更改的时候需要进行提交。

  1. 手动提交:sqlSession.commit();
  2. 设置自动提交:openSession(true);
2.2 细节
1 两种测试方式我们应该如何进行选择?

使用接口绑定的优点:

  • 不依赖于字符串字面值,他会变得更加安全一点,防止我们写错。
  • ide工具可以帮我们快速的映射好sql语句。
2 命名空间的作用
  • 可以利用更长的全限定名来将不同的语句隔离开来。
  • 实现了接口绑定。
3 命名的解析规则
  • 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。
  • 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。
4 作用域和生命周期
  1. SqlSessionFactoryBuilder:一旦创建好SqlSessionFactory,该对象就没有作用了,所以最佳作用域是方法作用域。
  2. SqlSessionFactory:一旦创建就应该在应用运行期间一直存在,最好不要多次创建,所以应该是应用作用域,可以使用单例模式,或者静态单例模式。
  3. SqlSession:每个线程拥有属于自己的SqlSession。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它–方法级别。
  4. 映射器实例:映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域,sqlSession.getMapper。
5 占位符的区别

​ 在Mybties中存在两种占位符:#{ },${ }。

1 #{ }根据值的名称获取值,可以是3种类型的值的填充。

  1. 参数是基本数据类型。那么在映射的语句中可以不写 parameterType, #{} 中的参数名也可以随意些。
  2. 参数是自定义类型。必须填写 parameterType , #{} 中名称是自定义类型的属性名,该属性有对应的 get 方法。如果没有get方法,那么会根据反射去获取该类型的值,如果找不到,那么报 ReflectionException 异常。
  3. 参数可以是 map 类型。#{} 中的名称 map中的 key 值即可。

#{ }存在的问题:

  1. 不能作为表名,也不能作为列名。

2 ${} 占位符是字符串连接符,可以动态获取相关值。

  1. 能从 properties 文件中获取值,也可以作为表名,列名等值。
  2. ${} 占位符参数只能使用自定义类型和 map 类型。
    1. 无法避免sql的注入。

3 总结:

​ 当我们的占位符是筛选的字段的时候使用${ },其他时候尽量使用#{ },

2.3 传递多个参数
2.3.1 原始的dao方式

​ 调用mapper的sql语句直接sqlsession的方法。

优点:比较容易理解其执行过程。

缺点:

  • 存在硬编码的问题,需要使用一个字符串来指定sql标签的id。
  • 传递多个参数的时候,方式比较单一
    • 传递对象:对象的属性和占位符内的值保持一致。
    • 传递map:map的key和占位符内的值保持一致。
2.3.2 采用接口代理的方式

​ 传递的参是有多种方式

  • 传递对象:对象的属性和占位符内的值保持一致。

  • 传递map:map的key和占位符内的值保持一致。

  • 顺序传参:sql种使用#{arg0},#{arg1}或者#{param1},#{param2}的方式来指定参数,index从0开始。

  • @Param参数绑定:要求pname必须和占位符名称保持一致。

SQL和接口如下所示:

image-20201029150702203

image-20201029150710419

2.4 解析结果集

1 列名和表的字段一致

  • 直接封装即可,返回值resultType写对应的javaBean。

2 列名和表的字段不一致

  • sql中使用别名
  • 自定义resultMap

自定义resultMap如下所示:

<!--resultMap用于自定义结果集-->
<resultMap id="studentMap" type="Student1">
    <!--id为主键列
       column是列名
       property是javabean的属性
    -->
    <id property="id" column="sid" javaType="Integer" />
    <result property="name" column="sname" javaType="String" />
    <result property="age" column="sage" javaType="Integer" />
</resultMap>

<select id="getStudent3" resultMap="studentMap">
    select * from student where sid=#{0} and sname=#{1}
</select>
2.5 表之间的关系的处理

1 1对1和N对1

​ 存在外键,对于从表来说就是一对一,也可能是一对N。sql的resultMap如下:

<!--方式1:使用多次查询的方式
        字段和属性的名字相同的时候可以省略
 -->
<resultMap id="resultMap1" type="Student10">
    <association property="teacher10" column="tid" javaType="Teacher10" select="getTeacher10">
    </association>
</resultMap>
<select id="getAllStudent10" resultMap="resultMap1">
    select * from student10 where sid=#{sid}
</select>
<select id="getTeacher10" resultType="Teacher10">
    select * from teacher10 where tid=#{tid}
</select>
<!--方式2:连表查询:以下两个map都可以使用
        字段不可以进行省略:想要的字段都要写。
-->
<resultMap id="resultMap2" type="Student10">
    <id property="sid" column="sid" javaType="Integer" />
    <result property="sname" column="sname" javaType="String" />
    <result property="sex" column="sex" javaType="String" />
    <result property="sage" column="sage" javaType="Integer" />
    <association property="teacher10" column="tid" javaType="Teacher10">
        <id property="tid" column="tid" javaType="Integer"/>
        <result property="tname" column="tname" javaType="String" />
        <result property="tsex" column="tsex" javaType="String" />
    </association>
</resultMap>
<resultMap id="resultMap3" type="Student10">
    <id property="sid" column="sid" javaType="Integer" />
    <result property="sname" column="sname" javaType="String" />
    <result property="sex" column="sex" javaType="String" />
    <result property="sage" column="sage" javaType="Integer" />
    <result property="teacher10.tid" column="tid" javaType="Integer"/>
    <result property="teacher10.tname" column="tname" javaType="String" />
    <result property="teacher10.tsex" column="tsex" javaType="String" />
</resultMap>
<select id="getAllStudents" resultMap="resultMap3">
    select * from student10 s,teacher10 t where s.sid=t.tid;
</select>

结果展示:

image-20201029150644317

2 1对N

​ 对于主表来说,主表中一条记录对应多个从表的记录,如一个老师有多名学生。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.zhiyou.mapper.Teacher10Mapper">
    <!--方式1:使用多次查询的方式
            字段和属性的名字相同的时候可以省略
     -->
    <resultMap id="resultMap1" type="Teacher10">
        <id column="tid" property="tid" javaType="Integer" />
        <collection property="list" column="tid" ofType="Student10" select="getTeacher10">

        </collection>
    </resultMap>

    <select id="getAllStudent10" resultMap="resultMap1">
        select * from teacher10
    </select>
    <select id="getTeacher10" resultType="Student10">
        select * from student10 where tid=#{tid}
    </select>
    <!--方式2:连表查询:以下两个map都可以使用
            字段不可以进行省略:想要的字段都要写。
    -->
    <resultMap id="resultMap2" type="Teacher10">
        <id property="tid" column="tid" javaType="Integer" />
        <result property="tname" column="tname" javaType="String" />
        <result property="tsex" column="tsex" javaType="String" />
        <collection property="list" column="tid" ofType="Student10">
            <id property="sid" column="sid" javaType="Integer" />
            <result property="sname" column="sname" javaType="String" />
            <result property="sex" column="sex" javaType="String" />
            <result property="sage" column="sage" javaType="Integer" />
        </collection>
    </resultMap>
    <select id="getAllStudents" resultMap="resultMap2">
        select * from student10 s right join teacher10 t on s.sid=t.tid
    </select>
</mapper>

执行结果如下所示:

image-20201029144559983

3 N对N

​ 对于N对N来说,需要存在一个中间关系表来记录两张表之间的关系,可以通过中间表的存在,将其他的两张表连接起来,创建javaBean的时候不需要创建中间表的javabean,双向一对多。

3 XML的配置

​ MyBatis的配置文件包含了深深影响MyBatis行为的设置和属性信息,因此我们配置的时候,需要注意配置的内容在配置文件中位置。配置文档的顶层结构如下:

image-20201029144621740

详情见:https://mybatis.org/mybatis-3/zh/configuration.html

4 动态sql

4.1 简答介绍

​ 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

常见的标签如下所示:

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach
4.2 IF

​ 使用动态 SQL 最常见情景是根据条件查询包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

​ 这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。

​ 如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?

​ 首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>
4.3 choose

​ 有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

​ 还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员挑选的 Blog)。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>
4.4 trim

​ 前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。

MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 setset 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的,后置的逗号)。

来看看与 set 元素等价的自定义 trim 元素吧:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意,我们覆盖了后缀值设置,并且自定义了前缀值。

4.5 foreach

​ 动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

提示 你可以将任何可迭代对象( 如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

5 缓存

5.1 什么是缓存?

​ 缓存的重要性不言而喻,使用了缓存可以避免我们频繁的和数据库进行交互,降低数据库的压力,提高我们的查询效率。MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制我们的缓存配置。

5.2 一级缓存

1 一级缓存

​ 默认情况,mybatis开启一级缓存,在同一个SqlSession的情况下会,使用同一条查询语句,第二次查询的时候会直接去内存中将结果拿出,而不是直接去数据库查找。

代码测试LocalCache:

@Test
public void test01() throws IOException {
    SqlSession sqlSession = SqlSessionUtil.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    Student student1 = mapper.getStudentById(1);
    Student student2 = mapper.getStudentById(1);
    System.out.println(student1==student2);
}

image-20201029145720808

​ 如果关闭sqlsession再次开启之后,我们会发现会查询两次数据库,且获取到的对象是两个对象。

2 一级缓存的工作流程

  1. 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果
  2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
  3. 如果命中,则直接将缓存结果返回;
  4. 如果没命中:
    1. 去数据库中查询数据,得到查询结果;
    2. 将key和查询到的结果分别作为key,value对存储到Cache中;
    3. 将查询结果返回;

3 一级缓存的不足之处

​ 使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:

  1. session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
  2. statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存。
5.3 二级缓存配置及特点

​ 默认情况下,只启用了一级缓存,即本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 为了解决一级缓存不能跨SqlSession的问题,我们可以使用二级缓存,是一个namespace级别的缓存。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

<settings>
  <!--在mybatis的配置文件中全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 -->
  <setting name="cacheEnabled" value="true"/>
</settings>

<!--设置到环境中-->
<cache/>

image-20201029145941019

​ 一旦我们配置了二级缓存,那么执行select查找的时候。顺序为:二级缓存–>一级缓存–>数据库查询。

基本上就是这样。二级缓存的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
5.4 cache的属性

​ cache存在一些默认行为,我们可以通过修改属性的方式来修改其默认行为,如下:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

​ 这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

注意:二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

5.5 自定义缓存

​ 除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache"/>

​ 这个示例展示了如何使用一个自定义的缓存实现。type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

为了对你的缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如,下面的例子将在你的缓存实现上调用一个名为 setCacheFile(String file) 的方法:

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

你可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。 你也可以使用占位符(如 ${cache.file}),以便替换成在配置文件属性中定义的值。

从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。 如果想要使用这个特性,请在你的自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
  void initialize() throws Exception;
}

上一节中对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。

请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

5.6 cache-ref

​ 对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>
5.6 什么情况下使用二级缓存
  1. 因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
  2. 如果多个namespace 中有针对于同一个表的操作,比如Blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用。
5.7 第三方缓存做二级缓存

​ 除了MyBatis 自带的二级缓存之外,我们也可以通过实现Cache 接口来自定义二级缓存。MyBatis 官方提供了一些第三方缓存集成方式,比如ehcache 和redis:https://github.com/mybatis/redis-cache ,这里就不过多介绍了。当然,我们也可以使用独立的缓存服务,不使用MyBatis 自带的二级缓存。

6 逆向工程

6.1 什么是逆向工程
6.2 如何使用逆向工程

1 导入jar包或者依赖

mybatis-generator-core-1.3.2.jar

<dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <version>1.3.7</version>
</dependency>

2 写配置文件

<?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>  
    <!-- 一个数据库一个context -->  
    <context id="infoGuardian">  
        <!-- 注释 -->  
        <commentGenerator >  
            <property name="suppressAllComments" value="true"/><!-- 生成代码的时候是否生成注释,true是取消注释,false会生成注释 -->
            <property name="suppressDate" value="true" /> <!-- 是否生成注释代时间戳-->  
        </commentGenerator>  
          
        <!-- jdbc连接 -->  
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"  
            connectionURL="jdbc:mysql://localhost:3306/java26" userId="root"
            password="19971001" />
          
        <!-- 类型转换 -->  
        <javaTypeResolver>  
            <!-- 默认为false,可以把数据库中的decimal以及numeric类型解析为Integer,为true时会解析为java.math.BigDecimal) -->  
            <property name="forceBigDecimals" value="false"/>  
        </javaTypeResolver>  
          
        <!-- 生成实体类地址 -->    
        <javaModelGenerator targetPackage="com.zhiyou100.entity"
            targetProject=".\02_mybatis_reverse\src\main\java" >
            <!-- 是否在当前路径下新加一层schema,
如果为fase路径com.shop.pojo, 为true:com.shop.pojo.[schemaName]  这个情况主要是oracle中有,mysql中没有schema -->  
            <property name="enableSubPackages" value="false"/>  
            <!-- 是否针对string类型的字段在set的时候进行trim调用 -->  
            <property name="trimStrings" value="true"/>  
        </javaModelGenerator>  
          
        <!-- 生成mapxml文件 -->  
        <sqlMapGenerator targetPackage="mapper"
            targetProject=".\02_mybatis_reverse\src\main\resources" >
            <!-- 是否在当前路径下新加一层schema,
如果为fase路径com.shop.dao.mapper, 为true:com.shop.dao.mapper.[schemaName]  这个情况主要是oracle中有,mysql中没有schema -->  
            <property name="enableSubPackages" value="false" />  
        </sqlMapGenerator>  
          
        <!-- 生成mapxml对应client,也就是接口dao -->      
        <javaClientGenerator targetPackage="com.zhiyou100.mapper"
            targetProject=".\02_mybatis_reverse\src\main\java" type="XMLMAPPER" >
            <!-- 是否在当前路径下新加一层schema,
如果为fase路径com.shop.dao.mapper, 为true:com.shop.dao.mapper.[schemaName]  这个情况主要是oracle中有,mysql中没有schema -->  
            <property name="enableSubPackages" value="false" />  
        </javaClientGenerator>  
          
        <!-- 配置表信息 -->      
        <table schema="" tableName="student" domainObjectName="Student" > </table>
  
    </context>  
</generatorConfiguration>  

3 写代码生成类

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class GeneratorSqlmap {
    public void generator() throws Exception {
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        // 指定配置文件E:\projects\ideacode\zhiyou\ssm\02_mybatis_reverse\src\main\resources\generatorConfig.xml
        File configFile = new File("02_mybatis_reverse\\src\\main\\resources\\generatorConfig.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);
    }

    // 执行main方法以生成代码
    public static void main(String[] args) {
        System.out.println("执行开始");
        try {
            GeneratorSqlmap generatorSqlmap = new GeneratorSqlmap();
            generatorSqlmap.generator();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("执行结束");
    }

}

对于idea来说:

​ 如果是main方法来写生成类,那么默认的相对路径是项目的src目录。

​ 对于test单元测试来说,默认的相对路径是模块的src目录。

参考

参考网站:http://www.mybatis.org/mybatis-3/zh/index.htm

参考文章:https://www.cnblogs.com/wuzhenzhao/p/11103043.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值