MyBatis入门

jefxff 153,662 2020-05-04

1. MyBatis 认识

  • MyBatis 是 ORM 的一种实现; 使用MyBatis可以简化JDBC的操作, 实现数据的持久化, 代替的就是三层模型种的Dao层

  • ORM : Object Relational Mapping : 对象关系映射; MyBatis的实现就是将数据库中的一张表映射成一个JavaBeen对象; 通过 ORM 就可以像操作对象一样操作数据库中对应的表


2. 配置 MyBatis 及测试

  • 下载 MyBatis3.4.6

  • 下载后需要将 mybatis3.4.6.jar 引入需要使用的项目中

  • MyBatis 是实现数据库表和JavaBeen的映射关系, 这种映射关系的实现就体现在每个JavaBeenMapper.xml; 在映射文件中通过标签定义的增删改查就会实际作用到数据库表中

  • 如: personMapper.xml示例及参数说明

    • namespace: 该javaBeenMapper.xml 映射文件的全限定路径; (在面向接口开发中: namespace的值就是对应接口的全限定类名)

    • id : SQL语句的唯一标识符

    • parameterType : 接收参数的类型

      • 输入参数parameterType和输出参数resultType, 在形式上只能有一个(即如果需要传递多个参数, 就需要将多个参数封装起来)

      • 如果输入/输出参数是简单类型(8个基本类型+String): 则在#{}中可以使用任何占位符; 如果是对象类型, 则#{}中必须是对象的属性

    • resultType : 结果的类型, 即查完返回值类型;

      • 如果返回值类型是一个对象(如: Student), 则无论返回一个, 还是返回多个, 在resultType中都写成 "xyz.xmcs.Entity.Student" 的形式(返回多个只需要在接口中定义, 如:List)
  <!-- studentMapper.xml: 用于映射 Student.java 和 MySQL的student表 -->
  <?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="xyz.xmcs.Entity.studentMapper">

      <!-- id: 该标签的唯一标识; resultType:结果的类型, 即查完返回值类型; parameterType:接收参数的类型 -->
      <!-- #{} 就相当于 pstmt 中SQL语句里面的 ? -->
      <!-- parameterType 的参数类型是基本类型, 所以#{} 中可以是任何类型 -->
      <select id="queryStudentByStuNo" resultType="xyz.xmcs.Entity.Student" parameterType="int">
          select * from student where stuNo = #{stuNo}
      </select>

      <!-- 增加 -->
      <!-- parameterType 的参数类型是对象类型, 所以#{} 中可以是对象的属性 -->
      <insert id="addStudent" parameterType="xyz.xmcs.Entity.Student" >
          insert into student(stuNo, stuName, stuAge, graName) values(#{stuNo}, #{stuName}, #{stuAge}, #{graName})
      </insert>

      <!-- 删除 -->
      <delete id="deleteStudentByStuNo" parameterType="int">
          delete from student where stuNo = #{stuNo}
      </delete>

      <!-- 修改 -->
      <update id="updateStudentByStuNo" parameterType="xyz.xmcs.Entity.Student">
          update student set stuName=#{stuName}, stuAge=#{stuAge}, graName=#{graName} where stuNo=#{stuNo}
      </update>

      <!-- 查询全部 -->
      <select id="queryAllStudent" resultType="xyz.xmcs.Entity.Student">
          select * from student
      </select>
  </mapper>
  • 除上述实现映射的配置外, 还需要配置数据库信息以及需要加载的映射文件; 即MyBatis的总配置文件; 在src同级目录下配置 conf.xml; conf.xml 重要标签说明

conf.xml 重要标签说明

  • configuration : 配置信息区域, 所有的配置信息写在 configuration 标签体中

  • properties : 用于引入动态文件, 通过 resource 指定文件名来引入 (如将数据库链接信息写在配置文件中, 通过propertie引入)

  • settings : 用于设置全局参数; 可设置 二级缓存, 开启日志, 延迟加载, 以及其他配置

  • typeAliases : 用于设置单个/多个别名

  • typeHandlers : 用于配置类型转换器, (JDBC类型和JAVA类型之间的转换)

  • plugins : 通过<\plugin>标签配置自定义的插件

  • environments : 用于环境配置, environments里面可以配置多个environment, 并且在environment中的id指明数据库环境, 在environments里的default中选择对应的id来指定MyBatis运行时的数据库环境; (也可以在SSqlSessionFactoryBuilder().build(reader, "environment的id")来强行指定运行时的环境)

    • environment: 具体的环境配置, 可配置多个

      • dataSource: (在environment中配置)指明environment使用的数据源类型, 有如下三种类型, 一般选择 POOLED

        • POOLED: 使用数据库连接池

        • UNPOOLED: 使用传统的JDBC模式(每次打开关闭, 消耗性能)

        • JNDI: 从tomcat里面获取一个内置的数据库连接池

        • dataSource内的property数据库连接用的信息, 可以直接写在conf.xml 中, 也可以将信息用KV对的形式写在properties配置文件中, 在configuration标签里面增加 properties标签并指定其resource属性来引入配置文件, 在 property 的值中就可以通过类似于EL表达式的方式来引入

    • transactionManager: (在environment中配置)指明事务的提交方式, 有如下两种, 一般选择 JDBC

      • JDBC : 利用JDBC方式处理事务(需要手工的 commit, rollback, close)

        • 手工提交就是在代码中通过SqlSession的connit()方法手工提交
      • MANAGED : 将事务交由其他组件去托管(如: spring, jobss), 默认会关闭连接, 不关闭的话需要在transactionManager下面配置: (<\property name="closeConnection" value="false" />)

  • databaseIdProvider : 配置数据库支持类: 大小写不能乱写; 通过 type 来指定, 一般是 DB_VENDOR; 标签体内部通过property指定数据库

  • mappers : 用于指定映射文件, 可以单独映射, 也可以通过package批量映射

  • 注意的一个细节

    • 使用mysql8后, URL信息在xml和properties文件中需要注意(&)的细微区别; xml中(&)应该写成& 而在properties中可以直接写 &

    • URL在properties中, 后面要增加一句: &allowPublicKeyRetrieval=true 否则MyBatis会报错

  • 如 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="db.properties" />

      <!-- 设置全局参数-->
      <settings>

          <!-- true: 开启二级缓存 false: 关闭二级缓存; 再具体的Mapper.xml 中需要通过<cache/>标签声明-->
          <setting name="cacheEnabled" value="true" />

          <!-- 开启日志, 并指定使用的具体日志 -->
          <setting name="logImpl" value="LOG4J"/>

          <!-- 开启延迟加载 -->
          <setting name="lazyLoadingEnabled" value="true"/>

          <!-- 关闭立即加载 -->
          <!-- 下面的配置没错, 但是运行就报 Error creating lazy proxy.  Cause: java.lang.NullPointerException
               的异常, 看了网上的解决方案是将 MyBatis的jar包更新到3.5.1
               aggressive: 积极的, 好斗的
          -->
          <setting name="aggressiveLazyLoading" value="false"/>

          <!-- 配置全局的NULL, 在遇到数据库(oracle)不能解析时, 就使用NULL -->
          <setting name="jdbcTypeForNull" value="NULL"/>

      </settings>


      <!-- 设置单个/多个别名 Aliases:别名 -->
      <typeAliases>
          <!-- 单个别名
          <typeAlias type="xyz.xmcs.Entity.Student" alias="Student" /> -->

          <!-- 批量设置别名 -->
          <package name="xyz.xmcs.Entity" />
      </typeAliases>


      <!-- 配置类型转换器: JDBC类型和JAVA类型之间的转换
      <typeHandlers> -->
          <!-- 指定转换器的全类名, 并且说明java 和 jdbc 转换的类型
          <typeHandler handler="xyz.xmcs.converter.BooleanAndIntConverter" javaType="Boolean" jdbcType="INTEGER"/>
      </typeHandlers>  -->

      <!-- 配置插件: 在这里配置自定义的插件 -->
      <!--<plugins>-->
      <!--
      <plugin interceptor="xyz.xmcs.my.interceptors.MyInterceptor">
          <property name="name" value="zs" />
          <property name="age" value="23"/>
      </plugin>
      -->
      <!--
      <plugin interceptor="xyz.xmcs.my.interceptors.MyInterceptor2">
          <property name="name" value="zs22222" />
          <property name="age" value="33"/>
      </plugin>
      -->
      <!--</plugins>-->

      <!-- 该标签中可配置数据库信息, 如连接oracle, mysql 等 -->
      <environments default="devMySQL">
          <!-- MySQL 数据库 -->
          <environment id="devMySQL">
              <transactionManager type="JDBC"/>
              <dataSource type="POOLED">
                  <!-- 配置数据库信息; 可以直接在value中写死, 也可以将值以kv对的形式写在properties文件中,
                  以类似于EL表法是的方式动态引入(需要先加入 properties标签, 引入properties文件)-->
                  <property name="driver" value="${mysql.driver}"/>
                  <property name="url" value="${mysql.url}"/>
                  <property name="username" value="${mysql.username}"/>
                  <property name="password" value="${mysql.password}"/>
              </dataSource>
          </environment>

          <!-- Oracle 数据库 -->
          <environment id="devOracle">
              <transactionManager type="JDBC"/>
              <dataSource type="POOLED">
                  <!-- 配置数据库信息; 可以直接在value中写死, 也可以将值以kv对的形式写在properties文件中,
                  以类似于EL表法是的方式动态引入(需要先加入 properties标签, 引入properties文件)-->
                  <property name="driver" value="${oracle.driver}"/>
                  <property name="url" value="${oracle.url}"/>
                  <property name="username" value="${oracle.username}"/>
                  <property name="password" value="${oracle.password}"/>
              </dataSource>
          </environment>
      </environments>

      <!-- 配置数据库支持类: 大小写必能乱写 -->
      <databaseIdProvider type="DB_VENDOR">
          <property name="MySQL" value="mysql"/>
          <property name="Oracle" value="oracle"/>
          <!--<property name="SQL Server" value=""/>-->
      </databaseIdProvider>

      <mappers>
          <!-- 加载映射文件
          <mapper resource="xyz/xmcs/Entity/personMapper.xml"/>  -->

          <!-- 这种是批量引入mapper; 可以将 xyz.xmcs.mapper包中的注解接口(即接口中的方法上带注解)和xml全部一次性引入-->
          <package name="xyz.xmcs.Mapper"/>

          <!-- 这是使用注解的方式, 并单独引入; 不推荐使用
          <mapper class="xyz.xmcs.Mapper.StudentMapper"/>-->

          <!--
          <mapper resource="xyz/xmcs/Mapper/studentMapper.xml"/>
          <mapper resource="xyz/xmcs/Mapper/studentCardMapper.xml"/>
          <mapper resource="xyz/xmcs/Mapper/studentClassMapper.xml"/> -->

      </mappers>
  </configuration>

实现MyBatis基础方式的增删改查

  • 测试类和传统的JDBC一样, 也有固定的步骤:

    1. 加载MyBatis配置文件: 通过Resources.getResourceAsReader("conf.xml")获取配置文件的输入流对象

    2. 通过new SqlSessionFactoryBuilder().build("输入流对象") 获得 SqlSession 对象; SqlSession对象就相当于Connection

    3. 通过获得的SqlSession对象调用其方法实现查询并返回结果: sqlSession.selectOne("需要查询的SQL的namespace.id", "SQL的参数值");

    4. 手动提交事务: sqlSession.commit();

    5. 关闭 SqlSession 对象: sqlSession.close();

  • 示例:

  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.Reader;
  import java.util.List;

  public class TestStudent {
    // 提取获得SqlSession的工具类
      private static SqlSession getSqlSession() throws IOException {
          // 1. 加载MyBatis配置文件为Reader对象
          Reader reader = Resources.getResourceAsReader("conf.xml");

          // 2. 通过 Reader 对象获取SqlSessionFactory 对象
          SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

          // 3. 通过 SqlSessionFactory的对象获取SqlSession, 并通过调用SqlSession对象的方法实现数据库操作并拿到操作结果
          SqlSession sqlSession = sqlSessionFactory.openSession();
          return sqlSession;
      }
      // 按stuNo查询一个学生
      public static void queryStudentByStuNo() throws IOException {
          SqlSession sqlSession = getSqlSession();
          String stmt = "xyz.xmcs.Entity.studentMapper.queryStudentByStuNo";
          Student student = sqlSession.selectOne(stmt,1);
          System.out.println(student);
          // 4. 手动提交事务
          sqlSession.commit();
          // 5. 关闭SqlSession对象
          sqlSession.close();
      }
      // 增加学生
      public static void addStudent() throws IOException {
          SqlSession sqlSession = getSqlSession();
          String stmt = "xyz.xmcs.Entity.studentMapper.addStudent";
          Student student = new Student(2, "ls", 20, "class1");
          int insert = sqlSession.insert(stmt, student);
          sqlSession.commit();
          System.out.println("Insert Result:" + insert);
          sqlSession.close();
      }
      // 删除学生
      public static void deleteStudent() throws IOException {
          SqlSession sqlSession = getSqlSession();
          String stmt = "xyz.xmcs.Entity.studentMapper.deleteStudentByStuNo";
          int delete = sqlSession.delete(stmt, 3);
          System.out.println("Delete Result:" + delete);
          sqlSession.commit();
          sqlSession.close();
      }
      // 查询全部学生
      public static void queryAllStudent() throws IOException {
          SqlSession sqlSession = getSqlSession();
          String stmt = "xyz.xmcs.Entity.studentMapper.queryAllStudent";
          List<Student> students = sqlSession.selectList(stmt);
          for (Student student : students) {
              System.out.println(student);
          }
          sqlSession.commit();
          sqlSession.close();
      }
      // 根据 stuNo 修改学生
      public static void updateStudentByStuNo() throws IOException {
          SqlSession sqlSession = getSqlSession();
          String stmt = "xyz.xmcs.Entity.studentMapper.updateStudentByStuNo";
          Student student = new Student(2, "LS", 14, "class2");
          int update = sqlSession.update(stmt, student);
          System.out.println("Update Result:" + update);
          sqlSession.commit();
          sqlSession.close();
      }

      public static void main(String[] args) throws IOException {
          queryStudentByStuNo();
          addStudent();
          queryAllStudent();
          updateStudentByStuNo();
          deleteStudent();
      }
  }

3. 实现mapper动态代理方式的CRUD (MyBatis接口开发)

  • 原则: 约定优于配置

  • 配置方式: 就是在一个 xxxMapper.xml 文件中指定配置参数

    • 如: 在 xxxMapper.xml 中指定: MyBatisTest
  • 硬编码方式: 就是在xxx.java中: 通过 new Configuration().setName("MyBatisTest")


具体实现步骤

  • 基础环境: mybatis.jar, mysql-connection.jar, conf.xml, mapper.xml 都和上述基础查询时配置的一样

  • 与基础查询的不同之处:

    • 约定的目标: 省略掉stmt, 即根据约定直接可以定位处SQL语句

    • 习惯将mapper.xml 和 接口放在同一个包中

    • 约定:(即面向接口开发, 约定的就是对接口以及接口中的方法; 说到底就是让接口中的抽象方法和对应的mapper.xml中的SQL标签有关联)

      • mapper.xml 中namespace的值就是接口的全类名(即: 实现接口和mapper.xml的一一对应)

      • 接口中的方法名和mapper.xml中SQL标签的id一致

      • 方法的输入参数和mapper.xml中parameterType的类型一致;(如果mapper.xml中没有parameterType, 则说明没有输入参数, 或者输入多个参数)

      • 返回值和mapper.xml中的resultType类型一致; (如果mapper.xml中没有resultType, 则说明没有输出参数(也可以在接口抽象方法中直接定义返回值类型))

  • 匹配的过程: (约定的过程)

    • 根据 接口名 找到 mapper.xml 文件; (根据的是namespace=接口的全类名)

    • 根据接口的 方法名 找到mapper.xml文件中的SQL标签; (根据的是: 接口方法名=SQL标签的Id值)

    • 基于以上两种约定: 当我们调用接口中的方法时, 程序能自动定位到某一个mapper.xml文件中的SQL标签

  • 面向接口的执行步骤:

    1. 获取 SqlSession 对象

    2. 调用 SqlSession 对象的 getMappr(接口.class) 方法获取接口, 返回值类型就是接口类型

    3. 再通过调用该接口的对应的方法来执行即可完成SQLCRUD操作

    4. 手动commit, 并关闭SqlSession对象


4. MyBatis 优化

可以将配置信息单独放入 properties 文件中, 然后动态引入

  • 在 properties 中以 K=V 的形式保存配置信息

  • 在 <\configuration>标签中通过 <\properties resource="db.properties" />来引入配置文件

  • 在需要引用的地方通过: $ 即可引入需要的V值


设置MyBatis全局参数

  • 在conf.xml 中的<\configuration>标签中通过<\settings>标签来设定

  • 如:

  <settings>
    <setting name="cacheEnabled" value="false" />
    <setting name="lazyLoadingEnabled" value="false" />
  </settings>

设置别名

  • 设置别名主要是解决一个全类名很长的问题, 通过设置别名, 就可以使用一个别名来代替全类名; 并且不区分大小写

  • 单个别名还是批量设置别名, 都是在 conf.xml 中的 <\configuration> 标签中通过 <\typeAliases> 标签来设置的

  • 设置单个别名: 就是将一个类单独起一个别名, 且别名是忽略大小写的

  • 批量设置别名: 就是将包下的所有类批量设置别名, 别名就是类名(不带包名), 也是忽略大小写的

    <!-- 设置单个/多个别名 -->
    <typeAliases>
        <!-- 单个别名 
        <typeAlias type="xyz.xmcs.Entity.Student" alias="Student" /> -->
        <!-- 批量设置别名 -->
        <package name="xyz.xmcs.Entity" />
    </typeAliases>

MyBatis自带的别名

_byte=byte    _long=long    _short=short    _int-int    _integer=int    long=Long    int=Integer    integer=Integer    boolean=Boolean
decimal=BigDecimal    object=Object    hashmap=HashMap    arraylist=ArrayList    iterator=Iterator    _double=double    _float=float
_boolean=boolean    string=String    byte=Byte    short=Short    double=Double   float=Float    date=Date    bigdecimal=BigDecimal
map=Map    list=List    collection=Collection  

5. MyBatis类型处理器(类型转换器)

  • MyBatis 自带一些常见的类型处理器

自定义MyBatis类型处理器

  • 类型转换就是Java代码和JDBC类型之间的转换

  • 以例子来学习: 如Java实体类Student.java里true代表男生, false代表女生; 而在student表中以int类型来代表男女: 1代表男生, 0代表女生; 而需要做的就是实现让true转换为1, 而将false转换为0

  • 自定义类型转换器(将 boolean --> int)步骤

    • 创建转换器(需要实现TypeHandler接口或者继承TypeHandler的实现类BaseTypeHandler)

    • 在conf.xml中的configuration标签里面的typeHandlers标签中配置

  • 示例

  // BooleanAndIntConverter.java 实现的转换器将JAVA的Boolean转换为JDBC的int
  package xyz.xmcs.converter;
  import org.apache.ibatis.type.BaseTypeHandler;
  import org.apache.ibatis.type.JdbcType;
  import java.sql.CallableStatement;
  import java.sql.PreparedStatement;
  import java.sql.ResultSet;
  import java.sql.SQLException;

  public class BooleanAndIntConverter extends BaseTypeHandler<Boolean> {
      // java(boolean) --> DB(int)
      /**
       * @param preparedStatement: preparedStatement对象
       * @param i : preparedStatement对象操作参数的位置
       * @param aBoolean: java的值
       * @param jdbcType : JDBC操作的数据库类型
       * @throws SQLException
       */
      @Override
      public void setNonNullParameter(PreparedStatement preparedStatement, int i, Boolean aBoolean, JdbcType jdbcType) throws SQLException {
          // 思路: 就是判断 aBoolean 是true还是false; 如果true, 就通过preparedStatement的setInt()方法将i位置设为1; 否则设为0
          if(aBoolean) {
              // 1
              preparedStatement.setInt(i, 1);
          } else {
              // 0
              preparedStatement.setInt(i, 0);
          }
      }
      // DB(int) --> java(boolean) 按照列名拿值
      @Override
      public Boolean getNullableResult(ResultSet resultSet, String s) throws SQLException {
          int sexNum = resultSet.getInt(s);
          return sexNum == 1? true : false;
      }
      // DB(int) --> java(boolean) 按列数拿值
      @Override
      public Boolean getNullableResult(ResultSet resultSet, int i) throws SQLException {
          int sexNum = resultSet.getInt(i);
          return sexNum == 1? true : false;
      }
      // DB(int) --> java(boolean) 存储过程或存储函数拿值
      @Override
      public Boolean getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
          int sexNum = callableStatement.getInt(i);
          return sexNum == 1 ? true : false;
      }
  }
    <!-- conf.xml:配置转换器 -->
    <typeHandlers>
        <!-- 指定转换器的全类名, 并且说明java 和 jdbc 转换的类型 
      需要注意: jdbcType类型 需要全大写 INTEGER
      -->
        <typeHandler handler="xyz.xmcs.converter.BooleanAndIntConverter" javaType="Boolean" jdbcType="INTEGER"/>
    </typeHandlers>

测试自定义类型转换

  • 在写好自定义类型转换类以及在conf.xml配置的基础上测试自定义类型转换

  • 在MySQL数据库MyBatisTest的 student 表中增加 stuSex 列来记录学生的性别: alter table student add stuSex int(2);

  • 在 Entity 包下的Student.java 中增加 Boolean stuSex 属性, 并增加setter和getter以及修改构造器

  • 查询学生的SQL标签来测试自定义类型转换

    • 在 StudentMapper.xml 中增加id=queryStudentByStuNoWithConverter的 select标签; 需要注意的是接收的返回值需要(int --> boolean)类型转换, 所以不能使用resultType而是使用resultMap; 并且需要单独定义 <\resultMap> 标签; 并且在 resultMap标签许需要转换的标签中添加 jdbcType 和 javaType; 然后在queryStudentByStuNoWithConverter 中指定此 resultMap 的 id 即可

    • 在 StudentMapper.java 接口中增加方法: Student queryStudentByStuNoWithConverter(int stuNo);

    • 在测试类中实现此方法来测试

  • 增加学生的SQL标签来测试自定义类型转换

    • 在 StudentMapper.xml 中增加id=addStudentWithConverter的 insert标签, 只是在 sql语句中需要类型转换的参数后面增加 javaType=java类型, jdbcType=INTEGER(JDBC类型)

    • 在 StudentMapper.java 接口中增加方法:void addStudentWithConverter(Student student);

    • 在测试类中实现此方法来测试

测试代码示例

  • StudentMapper.xml 中增加的标签
  <resultMap type="Student" id="studentResult">
        <!-- 分为主键和非主键: id 指定主键, result 指定非主键 -->
        <id property="stuNo" column="stuNo"/>
        <result property="stuName" column="stuName"/>
        <result property="stuAge" column="stuAge"/>
        <result property="graName" column="graName"/>
        <!--  在需要类型转换的地方, 通过 jdcbType 和 JavaType 来指定类型  -->
        <result property="stuSex" column="stuSex" jdbcType="INTEGER" javaType="boolean"/>
    </resultMap>
    
    <!-- 查询: 使用了类型转换器
    如果类中的属性和表中的字段类型能够合理识别(String - varchar), 则可以使用resultType; 否则(boolean - int)使用resultMap
    如果类中的属性名和表中的字段名能够一一对应(stuNo - stuno), 则可以使用resultType; 否则(stuNo - id)使用resultMap
    -->
    <select id="queryStudentByStuNoWithConverter" resultMap="studentResult" parameterType="int">
        select * from student where stuNo = #{stuNo}
    </select>

    <!-- 增加: 带类型转换 -->
    <insert id="addStudentWithConverter"  parameterType="Student" >
        insert into student(stuNo, stuName, stuAge, graName, stuSex) values(#{stuNo}, #{stuName}, #{stuAge}, #{graName}, #{stuSex,  javaType=boolean, jdbcType=INTEGER})
    </insert>
  • StudentMapper.java 接口中新增的方法
  // 查询一个学生, 使用了类型转换
    Student queryStudentByStuNoWithConverter(int stuNo);

    // 增加学生 带类型转换
    void addStudentWithConverter(Student student);
  • TestStudent.java 中的测试方法
  // 测试类型转换查询
  public static Student queryStudentByStuNoWithConverter(int stuNo) throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Student student = mapper.queryStudentByStuNoWithConverter(stuNo);
        System.out.println(student);
        sqlSession.commit();
        sqlSession.close();
        return student;
    }
    // 测试类型转换增加
    public static void addStudentWithConverter() throws IOException {
        SqlSession sqlSession = getSqlSession();
        Student student1 = new Student(3, "ju", 20, "class3", false);
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        mapper.addStudentWithConverter(student1);
        sqlSession.commit();
        sqlSession.close();
    }

SQL 标签中 resultType 和 resultMap 的区别

  • 如果类中的属性和表中的字段类型能够合理识别(String - varchar), 则可以使用resultType; 否则(boolean - int)使用resultMap

  • 如果类中的属性名和表中的字段名能够一一对应(stuNo - stuno), 则可以使用resultType; 否则(stuNo - id)使用resultMap

  • 总结: resultMap 可以实现类型转换和 属性与字段的映射关系两个功能

  • resultMap 标签示例

  <resultMap type="Student" id="studentResult">
    <!-- 当Been中的属性名和表中的字段名不一致的时候, 就要通过下面这样的方式来指定(property: Been中的属性名, column:表中的字段名)
    <id property="id" column="stuNo"/>
    -->
        <!-- 分为主键和非主键 -->
        <id property="stuNo" column="stuNo"/>
        <result property="stuName" column="stuName"/>
        <result property="stuAge" column="stuAge"/>
        <result property="graName" column="graName"/>
        <!-- stuSex 需要类型转换, 则需要在标签内注明 -->
        <result property="stuSex" column="stuSex" jdbcType="INTEGER" javaType="boolean"/>
    </resultMap>

6. 输入参数: parameterType

parameterType 类型为简单类型: (8个基本类型+String)

  • #{任意值} 中 {} 花括号里面在标签中可以是任意值; 但是在接受实参时 #{} 会自动给String类型加上 ('') 单引号(自动类型转换; 这样可以防止SQL注入)

  • $ 中 {} 花括号的标识符只能是 value; ${} 是原样输出(传递什么值就是什么值), 所以String字段的话, 需要手工加上单引号, 但是适合于动态排序(动态字段)


parameterType 类型为对象类型

  • #{} 和 ${} 花括号里面只能是属性名

#{} 和 ${} 的相同之处

  • 都可以获取对象的值, (甚至是嵌套类型的对象)

    • 如使用like进行模糊查询: 使用#{}时, 传值时需要传入"%值%"; 而使用${}时, 在配置时就配置成 '%$%', 所以传值时可以直接传递字符串

    • 如通过级联属性来查询: 使用#{}时, 如 # ; 使用${}时, 如 '$'


parameterType 类型为 HashMap

  • 原理就是用Map中的key匹配占位符#{}中的字段, (所以就要求key的值和占位符得到内容要一致)

  • StudentMapper.xml 中只需要修改 parameterType 的类型为 HashMap

  • 重写相关的接口方法, 将接受参数类型改为 Map<Stirng, Object> 类型

  • 在实际使用中就是创建Map实例, 添加需要的参数, 参数的 key 就是 SQL语句中需要的字段, 而 value 就是需要查询的字段


MyBatis 调用存储过程(输入参数HashMap)

  • 在数据库编写两个存储过程
-- 查询某个年级的学生总数
delimiter $ -- 将结束符改成$
CREATE procedure queryCountByGradeWithProcedure(OUT sCount int(10), IN gName varchar(255))
BEGIN
select count(1) INTO sCount from student where graName = gName;
END$

-- 根据学号删除学生
CREATE procedure deleteStudentByStuNo(In stuno int(10))
begin 
  delete from student where stuNo = stuno;
end$
  • 在 studentMapper.xml 中编写 select 以及 delete 标签; 需要注意的是存储过程的调用方法以及显式的声明statementType="CALLABLE"; 输入参数使用Map传递
  <!-- 调用[存储过程]实现查询  statementType默认是STATEMENT, 如果是存储过程, 就特别指定;
        存储过程的输入参数, 在MyBatis中用Map(HashMap)来传递
    -->
    <select id="queryCountByGradeWithProcedure" statementType="CALLABLE" parameterType="HashMap">
        {call queryCountByGradeWithProcedure(#{gName, jdbcType=VARCHAR, mode=IN},#{sCount, jdbcType=INTEGER, mode=OUT})}
    </select>

    <delete id="deleteStudentByStuNoWithProcedure" parameterType="HashMap" statementType="CALLABLE">
        {call deleteStudentByStuNo(#{stuno mode=IN jdbcType=INTEGER})}
    </delete>
  • 编写StudentMapper.java 接口的方法
  // 根据存储过程查询某个年级的学生总数
    void queryCountByGradeWithProcedure(Map<String, Object> map);
    // 根据存储过删除某个stuno学号的学生
    void deleteStudentByStuNoWithProcedure(Map<String, Object> map);
  • 编写测试类测试上述存储过程; 通过map的put传入输入参数,通过map的get方法获取输出参数的值
    // 根据存储过程查询某个年级的学生总数
    public static void queryCountByGradeWithProcedure() throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Map<String, Object> map = new HashMap<>();
        map.put("gName", "class1");
        mapper.queryCountByGradeWithProcedure(map); // 调用存储过程并传入输入参数
        Object sCount = map.get("sCount");
        System.out.println(map.size());
        System.out.println(map);
        System.out.println(map.get("gName") + " 年级的学生总数是: " + sCount);
        sqlSession.commit();
        sqlSession.close();
    }

    // 根据存储过程删除某个stuno的学生
    public static void deleteStudentByStuNoWithProcedure() throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Map<String, Object> map = new HashMap<>();
        map.put("stuno", 9);
        mapper.deleteStudentByStuNoWithProcedure(map);
        sqlSession.commit();
        sqlSession.close();
    }
  • 注意:

    • 如果报错: No enum constant org.apache.ibatis.type.JdbcType.xx: 说明MyBatis不支持xx类型, 需要检查类型

    • 存储过程无论输入参数是什么值, 语法上都需要用map 来传递该值

    • 主配置文件 conf.xml 中 transactionManager 配置的是 JDBC 的话, 增删改都需要调用SqlSession对象的commit()方法手工提交事务


7. 输出参数: resultType

  • resultType 为简单类型(8基本+String)

    • 在xxxMapper.xml 的SQL标签中定义 resultType为简单类型或者字符串; 接口中增加抽象方法, 其返回值为resultType定义的简单类型, 编写测试方法测试
  • resultType 为对象/对象集合类型

    • 注意: 不管输出的是对象还是对象类型的的集合, resultType 中只写对象, 不写集合类型的对象

    • 使用方法类似于使用基本类型

  • resultType 为HashMap

    • 指定resultType输出类型为 HashMap, 并给SQL语句的每个字段加上别名

    • 接口的抽象方法中, 返回值类型为 Map<String, Object>

    • 测试方法中, 根据 Mapper.xml 中SQL语句中定义的别名作为 Map 的 key 来获取对应的 value

    • 注意1: 一个 HashMap 对应一个学生的多个元素(也就是多个属性); 如果要查询多个学生, 就需要多个HashMap

    • 注意2: 一般情况下使用 resultType, 但是实体类属性和数据表的字段存在类型, 名字不一致时, 使用 resultMap

    • 注意3: 当属性名和字段名不一致时, 除了使用resultMap 之外, 还可以受用 resultType + HashMap

    • 注意4: 如果发现获取的值中某个字段始终是默认值, 则可能是表的字段和属性的值类型或名字不一致导致的, 需要解决类型不一致或名字不一致问题

  • 使用 resultMap 解决属性和字段不一致问题以及指定 resultMap 标签

  <!-- type:指定返回值的类型 id:唯一标识符
    区分主键和非主键, 主键通过id指定, 非主键通过result指定
  先将数据库字段的字段名改了: 
    alter table student rename column stuNo to id;
    alter table student rename column stuName to name;
   -->
  <resultMap id="resultMap01" type="Student">
        <id property="stuNo" column="id"/>
        <result property="stuName" column="name"/>
        <result property="stuAge" column="StuAge"/>
    </resultMap>

    <!-- 使用 resultMap: 需要指定resultMap 标签 -->
    <select id="queryStudentByIdWithResultMap" parameterType="int" resultMap="resultMap01">
        select id, name, stuAge from student where id = #{id}
    </select>

    <!--接着:写接口, 写接口的测试类 -->
  • 使用 resultType + HashMap 解决 属性和字段不一致问题
    <!-- 使用resultType + HashMap 解决属性和字段不一致问题; 原理就是给SQl的字段加别名-->
    <select id="queryStudentByIdWithHashMap" parameterType="int" resultType="HashMap">
        select id 'stuNo', name 'StuName', stuAge 'age' from student where stuNo = #{stuNo}
    </select>
    <!-- 
    1. 接口中的返回值需要设置成Map 或者 List<Map<String, Map>> 的形式
    2. 获取值的时候, 通过 别名获取
     -->

8. 动态SQL

if, where 以及 trim 标签

  • <\where> 标签会自动处理拼接SQL中第一个<\if>标签中的and, 但不会处理之后<\if>中的and

  • <\trim> 可以处理拼接SQL中第一个和最后一个and; 如: <\trim prefix="where" prefixOverrides="and">

    • prefix="where" 表示给拼接SQL加 where

    • prefixOverrides="and" 表示处理拼接SQL中第一个and

    • suffixOverrides="and" 表示处理拼接SQL中最后结尾多出来的and

  • if 标签中 test 测试的就是parameterType中指定的属性

  <!-- SQL动态标签: 根据名字或年龄来查询学生信息 -->
    <!-- SQL 语句中采用条件判断, 根据测试方法传入的值来决定后面跟那句话 -->
    <select id="queryStuByNaOrAgWithTag" parameterType="Student" resultType="Student">
        select stuNo, stuAge, stuName from student 
        <where>
            <if test="stuName != null and stuName != ''">
                and stuName = #{stuName} 
          </if>
          <if test="graName != null and graName != ''">
              and graName = #{graName} 
          </if>
          <if test="stuAge != null and stuAge != ''">
              and stuAge = #{stuAge}
          </if>
        </where>
    </select>

    <!-- 
    <trim> 标签可以处理拼接SQL中第一个和最后一个and; 也可以添加前缀
     -->
    <!-- prefixOverrides: 会自动处理多出来的第一个and -->
    <trim prefix="where" prefixOverrides="and">
      <if test="stuName != null and stuName != ''">
            and stuName = #{stuName} 
        </if>
        <if test="graName != null and graName != ''">
            and graName = #{graName} 
        </if>
        <if test="stuAge != null and stuAge != ''">
            and stuAge = #{stuAge}
        </if>
    </trim>

    <!-- suffixOverrides: 会自动处理多出来的最后一个and -->
    <trim prefix="where" suffixOverrides="and">
      <if test="stuName != null and stuName != ''">
            stuName = #{stuName} and 
        </if>
        <if test="graName != null and graName != ''">
           graName = #{graName} and 
        </if>
        <if test="stuAge != null and stuAge != ''">
           stuAge = #{stuAge} and 
        </if>
    </trim>

trim 处理update语句的示例

  • .mapper.xml示例
  <!-- 修改: 使用trim实现可以随意修改 -->
    <update id="updateStudentByStuNoTrim" parameterType="Student">
        update student
            <trim prefix="set" suffixOverrides=",">
                <if test="stuName != null and stuName != ''">
                    stuName=#{stuName} ,
                </if>
                <if test="stuAge != null and stuAge != ''">
                    stuAge=#{stuAge} ,
                </if>
                <if test="graName != null and graName != ''">
                    graName=#{graName} ,
                </if>
                <if test="stuSex != null and stuSex != ''">
                    stuSex=#{stuSex} ,
                </if>
            </trim>
        where stuNo=#{stuNo}
    </update>
  • 接口及测试方法
    // 接口: 修改: 使用trim实现可以随意修改
    Boolean updateStudentByStuNoTrim(Student student);

    // 测试方法: trim: 实现所以的修改
    public static void updateStudentByStuNoTrim() throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Student student = new Student();
        student.setStuNo(2);
//        student.setStuName("ls");
//        student.setStuAge(10);
        student.setGraName("class2");
        Boolean aBoolean = mapper.updateStudentByStuNoTrim(student);
        if(aBoolean){
            System.out.println("修改成功!");
        }
        sqlSession.commit();
        sqlSession.close();
    }

foreach 标签

  • foreach 可以迭代的类型: 数组, 对象数组, 集合, 属性(Grade类, 类里面的属性的类型就是一个List)

foreach 迭代属性

  • foreach 标签迭代属性时, 是将属性的类型作为集合来迭代

  • foreach 标签属性:

    • collection: 要迭代的属性的集合

    • open: 即要迭代的语句的前部分, 左双引号内的左边需要加空格(加了空格才能组成SQL语句, 否则两个单词连起来的, 就错了)

    • close: 即要迭代的语句的后半部分 也是双引号内的右边需要加空格

    • item: 就是给每次迭代出来的值起的名, 在方法体中需要#来获取值

    • separator: 就是分隔符, 没有分隔符的话两次迭代的item的值会拼接在一起, 所以必须指定分隔符

  <!-- 将多个元素放在对象的属性中 -->
  <select id="queryStudentsWithNoInGrade" parameterType="Grade" resultType="Student">
        select * from student
        <where>
            <if test="array != null and array.size >0">
                <foreach collection="array" open=" and stuNo in (" close=") " item="stuNo" separator=",">
                    #{stuNo}
                </foreach>
            </if>
        </where>
    </select>

foreach 迭代简单类型的数组

  • 无论编写代码时, 传递的是什么参数名, 在 mapper.xml 中必须使用 array 代替该简单数组

  • 当使用 foreach 遍历数组的时候, 在if条件, foreach 中, 参数名固定写法是 array

    <!-- 将多个元素放在数组中 -->
    <select id="queryStudentsWithArray" parameterType="int[]" resultType="Student">
        select * from student
        <where>
            <if test="array != null and array.length >0">
                <foreach collection="array" open=" and stuNo in (" close=")" item="stuNo" separator=",">
                    #{stuNo}
                </foreach>
            </if>
        </where>
    </select>

foreach 迭代集合

  • 无论编写代码时, 传递的是什么参数名, 在 mapper.xml 中必须使用 list 代替该集合
  <!-- 将多个元素放在集合中 List<Integer> -->
    <select id="queryStudentsWithList" parameterType="list" resultType="Student">
        select * from student
        <where>
            <if test="list != null and list.size >0">
                <foreach collection="list" open=" and stuNo in (" close=")" item="stuNo" separator=",">
                    #{stuNo}
                </foreach>
            </if>
        </where>
    </select>

foreach 迭代对象的数组

  • 无论编写代码时, 传递的是什么参数名, 在 mapper.xml 中必须使用 object[] 代替该对象的数组

  • 在使用时使用的是对象, 所以在具体的#{}中,使用的是对象.属性

  <!-- 将多个元素放在对象数组中 -->
    <select id="queryStudentsWithObjectArray" parameterType="object[]" resultType="Student">
        select * from student
        <where>
            <if test="array != null and array.length >0">
                <foreach collection="array" open=" and stuNo in (" close=")" item="student" separator=",">
                    #{student.stuNo}
                </foreach>
            </if>
        </where>
    </select>

SQL片段(将相似的代码提取出来)

  • 第一步: 提取相似代码, 写在 标签中, 通过 id 来让需要的地方引用

  • 第二部: 引用: 在SQL标签中通过标签的refid来引用即可; 跨文件引用需要加上前缀 namespace

  <!-- 提起代码片段 -->
    <sql id="baseSql" >
        select * from student
    </sql>

    <!-- 引用代码片段 -->
    <select id="testBaseSql" resultType="Student">
        <include refid="baseSql" />
    </select>

9. 关联查询

  • MyBatis主要学习两个, 一对一和一对多, (MyBatis认为多对多的本质就是一对多的变化)

一对一

  • 一对一通过类型扩展类和resultMap来实现一对一

业务扩展类实现一对一(不适用于大型项目)

  • 业务扩展类实现一对一的核心就是通过一个扩展类实现两个类的属性(其实也就是两个表中的全部字段)

  • 这样做的原因是resultType所代表的返回值类型不能写两个实体类

  • 先设计数据库表以及实现表的映射类

  -- 创建 cardinfo 表
  create table studentCard (
    cardid int(10) primary key,
    cardinfo varchar(20)
    );
  -- 增加数据
  insert into studentCard values(1, 'zs info ...');
  insert into studentCard values(2, 'ls info ...'); 
  -- student 表增加cardid 栏
  alter table student add column cardid int(10);
  -- 增加外键关联
  alter table student add constraint fk_student_stundetCard_cardid foreign key (cardid) references studentCard (cardid);
  -- Student 表增加cardid 数据
  update student set cardid = 1 where stuNo = 1;
  update student set cardid = 2 where stuNo = 2;
  • studentMapper.xml

    • 因为是要返回多个类的数据, 但是 resultType 只能写一个类, 所以就想办法, 将两个类的属性整合到一个类当中; (实现办法就是继承一个属性多的类, 而重写一个属性少的类)
<!-- 利用业务扩展类实现一对一 -->
<select id="queryStudentByOneToOne" resultType="StudentBusiness" parameterType="int">
        select s.*, c.* from student s inner join studentcard c
            on s.cardid = c.cardid
            where s.stuNo = #{stuNo}
    </select>
  • 创建 StudentBusiness 类继承Student类, 自然就继承了Student的所有属性, 并在此类中重写 StudentCard 类的属性; 这样StudentBusiness 类就有了两个类的属性, 也就相当于有了两个数据表的字段
  public class StudentBusiness extends Student {
      private int cardId;
      private String cardInfo;

      public int getCardId() {
          return cardId;
      }

      public void setCardId(int cardId) {
          this.cardId = cardId;
      }

      public String getCardInfo() {
          return cardInfo;
      }

      public void setCardInfo(String cardInfo) {
          this.cardInfo = cardInfo;
      }

      @Override
      public String toString() {
          return "StudentBusiness{" +
                  "cardId=" + cardId +
                  ", cardInfo='" + cardInfo + '\'' +
                  "} " + super.toString();
      }
  }
  • 写测试方法测试该接口

resultMap 实现一对一(重点在于 resultMap 标签)

  • 业务扩展类是通过一个扩展类包含两个表的字段来完成一对一

  • resultMap 是通过两个类互相持有对方为属性成员来将两个类关联, 就相当于数据表中通过外键将两个表关联

  • 使用 resultMap 来指定返回值类型是, 需要单独指定resultMap标签(type代表返回值实体类, 该实体类需要包含两张表的所有字段), 并且主键使用 id, 非主键使用 result, 对象成员使用 association, 其中 association 的 javaType 指定该属性的类型, property 指定属性名

  • studentMapper.xml

  <resultMap id="student_card_map" type="Student">
        <!-- 学生的信息 -->
        <id property="stuNo" column="stuNo"/>
        <result property="stuName" column="stuName" />
        <result property="stuAge" column="stuAge"/>
        <result property="stuSex" column="stuSex"/>
        <result property="graName" column="graName"/>
        <!-- 一对一时, 对象成员使用 association映射, javaType指定该属性的类型 -->
        <association property="studentCard" javaType="StudentCard">
            <id property="cardId" column="cardId"/>
            <result property="cardInfo" column="cardInfo"/>
        </association>
    </resultMap>

    <!-- resultMap 来完成一对一, 两个类互相持有对方的成员变量 -->
    <select id="queryStudentByRMap" parameterType="int" resultMap="student_card_map">
        select s.stuNo, s.stuName, s.stuAge, s.stuSex, s.graName, c.cardid, c.cardinfo  from student s
          inner join studentcard c on s.cardid = c.cardid
          where s.stuNo = #{stuNo}
    </select>
  • 接口的方法中, 返回值是 Student, 测试时, 通过 this.studentCard.getCardId 就能获取关联表的属性

一对多

  • 先设计数据库表; 一个班级可以对应多个学生, 品一下昨天想返回多个resultMap; 两张表是通过 classId 的外键来关联的
  -- 创建 StudentClass 表
  create table studentClass (
    classId int(10) primary key,
    className varchar(20)
    );
  -- 增加数据
  insert into studentClass values(1, 'g1');
  insert into studentClass values(2, 'g2'); 
  -- student 表增加classId 栏
  alter table student add column classId int(10);
  -- 增加外键关联
  alter table student add constraint fk_student_stundetClass_classId foreign key (classId) references stundetClass (classId);
  -- Student 表增加classId数据
  update student set classId = 1 where stuNo = 1;
  update student set classId = 2 where stuNo = 2;
  • 创建 StudentClass.java 的实体类, 通过List students来完成一个班级对应多个学生的映射关系

  • studentMapper.xml 中, 通过resultMap 指定返回的类型

    • 注意1: tpye 描述的是返回的类型, 所以type类型写的是谁, resultMap就把谁当作主类来配置

    • 注意2: 一对多的成员属性的配置标签是 collection, 区别于一对一使用 association 来配置(记住)

    • 注意3: 成员属性的类型是 List, 但是 javaType 不可以这样配置, 所以就使用 ofType来配置该属性的元素的类型

    • 注意4: 元素的类型使用javaType, 元素的属性的类型使用 ofType

  <!-- 下面是一对多的表关联, 一对多通过resultMap 来完成关联
        类 - 表的对应关系
     -->
    <resultMap id="student_class_map" type="StudentClass">
        <!-- 因为type返回的主类是 studentClass, 所以先配置 StudentClass -->
        <id property="classId" column="classId"/>
        <result property="className" column="className"/>
        <!-- 配置成员属性, 一对一是association, 一对多是 collection -->
        <!-- ofType: 描述该属性的元素类型; javaType: 描述该属性的类型
                    这里的属性类型是 List<Student>, 但是 javaType里面不能这样写, 所以这里使用 ofType来写该属性的元素的类型
        -->
        <collection property="students" ofType="Student">
            <id property="stuNo" column="stuNo"/>
            <result property="stuName" column="stuName"/>
            <result property="stuAge" column="stuAge"/>
            <!-- 这里还可以接着配 Student 内的 studentCard的信息, 使用的标签是 association -->
        </collection>
    </resultMap>
    <!-- 查询g1班的信息, 和g1班的所有学生的信息 -->
    <select id="queryClassAndStudents"   resultMap="student_class_map" parameterType="int">
        select c.*, s.* from student s inner join studentclass c
                on s.classId = c.classId
                where c.classId = #{classId}
    </select>
  • 接口抽象方法及测试类
    // 表关联, 一对多, 通过resultMap de collection 子标签
    StudentClass queryClassAndStudents(int classId);

     // 查询班级和班级对应的学生
    public static StudentClass queryClassAndStudents(int classId) throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        StudentClass studentClass = mapper.queryClassAndStudents(classId);
        System.out.println(studentClass);
        List<Student> students = studentClass.getStudents();
        for (Student student : students) {
            System.out.println(student.getStuNo() + " - "+ student.getStuName() +" - "+ student.getStuAge());
        }
        sqlSession.close();
        return studentClass;
    }

10. 使用 log4j 日志

  • 导入 log4j-1.2.17.jar (下载的MyBatis压缩包的lib目录中已有)

  • 在 conf.xml 通过配置开启日志; 如果不开启就会按照 (SJF4J -> Apache CommonsLogging -> Log4j 2 -> log4j -> JDK logging) 的顺序来寻找日志

    <!-- 设置全局参数-->
    <settings>
        <!-- 开启日志, 并指定使用的具体日志 -->
        <setting name="logImpl" value="LOG4J"/>
    </settings>
  • 编写日志输出文件 log4j.properties

    • log4j日志级别(从高到低): ERROR, WARN, INFO, DEBUG

    • 开发阶段使用 DEBUG, 在生产阶段使用 WARN 或者更高的级别

  ### 设置Logger输出级别和输出目的地 ###
  log4j.rootLogger=DEBUG, Console

  ### 生产环境使用
  # log4j.rootLogger=WARN, FILE

  #### Console 日志输出到控制台
  log4j.appender.Console=org.apache.log4j.ConsoleAppender
  log4j.appender.Console.layout=org.apache.log4j.PatternLayout
  ## 带时间的
  # log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
  log4j.appender.Console.layout.ConversionPattern=[%t] %-5p - %m%n
  log4j.appender.Console.encoding=UTF-8

  #### File  日志输出到文件; 需要再 rootLogger 后面增加: , File
  #log4j.appender.File = org.apache.log4j.FileAppender
  #log4j.appender.File.File = D://File//Java//JAVA_WEB//Log4J_Log
  #log4j.appender.File.layout = org.apache.log4j.PatternLayout
  #log4j.appender.File.layout.ConversionPattern =%d [%t] %-5p [%c] - %m%n 
  • 使用: 使用log4j日志的好处是可以观察到MyBatis实际执行SQL语句的过程以及SQL执行时的参数及返回的结果等信息

11. 延迟加载(懒加载)

  • 如果不采用延迟加载(就是立即加载), 查询时会将 一 和 多 都查询, 如班级, 班级中的所有学生; 如果想要暂时只查询 一, 而多的一方先不查询, 而是在需要的时候再去查询, 这个过程就是延迟加载

  • 理解: 延迟加载就是关联查询时, 通过配置实现按需查询, 并不是将所有关联的数据一次性都查出来, 而是在需要的时候再查询(可以通过DEBUG来查看)


一对一延迟加载例子

  • 延迟加载, 需要将延迟查询的表单独放在一个Mapper.xml 中, 在 resultMap 的 association 标签内通过 select属性指定 namespace.id; 以及在column两个表之间关联的外键
  <!-- studentCardMapper.xml -->
  <?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="xyz.xmcs.Mapper.StudentCardMapper">

      <!-- 查询学生证信息 -->
      <select id="queryCardById" parameterType="int" resultType="StudentCard">
          <!-- 查询学生对应的学生证
              根据cardId 查询学生证的SQL在: xyz.xmcs.Mapper.StudentCardMapper.queryCardById
          -->
          select * from studentcard where cardid = #{cardId}
      </select>
  </mapper>

  <!-- StudentMapper.xml 中延迟加载一对一的resultMap 及 select 标签 -->
  <resultMap id="student_card_lazyLoad_map" type="Student">
        <!-- 学生的信息 -->
        <id property="stuNo" column="stuNo"/>
        <result property="stuName" column="stuName" />
        <result property="stuAge" column="stuAge"/>
        <result property="stuSex" column="stuSex"/>
        <result property="graName" column="graName"/>
        <!-- 一对一时, 对象成员使用 association映射, javaType指定该属性的类型
            采用延迟加载, 在查询学生是, 并不立即加载学生证
        -->
        <!-- 学生证 在需要的时候通过select查询学生证信息-->
        <association property="studentCard" javaType="StudentCard" select="xyz.xmcs.Mapper.StudentCardMapper.queryCardById" column="cardId">
          <!--  <id property="cardId" column="cardId"/>
            <result property="cardInfo" column="cardInfo"/>-->
        </association>
    </resultMap>
    <!-- 下面的例子是延迟加载, 一对一的延迟加载 -->
    <select id="queryStudentOtOWithLazyLoad" parameterType="int" resultMap="student_card_lazyLoad_map">
        select * from student
    </select>

  • 在 conf.xml 总配置文件中配置延迟加载
  <!-- 设置全局参数-->
    <settings>
        <!-- 开启延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 关闭立即加载 -->
        <setting name="aggressiveLazyLoading" value="false"/>
        <!-- 上面的配置没错, 但是运行就报 Error creating lazy proxy.  Cause: java.lang.NullPointerException
             的异常, 看了网上的解决方案是将 MyBatis的jar包更新到3.5.1
        -->
    </settings>

    <!-- 将 StudentMapper.xml 添加进来 -->
    <mapper resource="xyz/xmcs/Mapper/studentCardMapper.xml"/>
  • 编写接口方法及实现测试方法
    // 延迟加载, 一对一
    List<Student> queryStudentOtOWithLazyLoad();

  // 延迟加载, 一对一, 查询全部学生, 并延迟加载学生的学生证
    public static List<Student>  queryStudentOtOWithLazyLoad() throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        List<Student> students = mapper.queryStudentOtOWithLazyLoad();
        for (Student student : students) {
            System.out.println(student.getStuNo() + " - "+student.getStuName()+ " - "+student.getStuAge());
            // 根据学生查询学生证
             StudentCard card = student.getStudentCard();
             System.out.println(card.getCardId() + " - "+ card.getCardInfo());
        }
        sqlSession.close();
        return students;
    }

一对多延迟加载

  • 一对多和一对一的原理是一样的, 首先确保再总配置文件中开启了延迟加载, 然后在配置一的一方的select以及resultMap, 并且在resultMap中配置 collection子标签

  • 配置多的一方的 select 标签; 在一的一方的 connection 标签的 select 属性中通过 namespace.id 引入多的一方的 select 标签; 在 collection 标签中, 通过 property指定多的一方的属性名, 通过 column指定关联的外键名, 通过ofType指定多的一方属性的元素的类型 (应为JavaType 不能指定集合类型)

  • studentClassMapper.xml 以及 studentMapper.xml 中的 select 标签

  <?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">
            <!-- 这里的namespace 指的是 StudentClassMapper 接口的全类名 -->
  <mapper namespace="xyz.xmcs.Mapper.StudentClassMapper">

      <resultMap id="student_class_lazyLoad_map" type="StudentClass">
        <!-- 因为type返回的主类是 studentClass, 所以先配置 StudentClass -->
        <id property="classId" column="classId"/>
        <result property="className" column="className"/>
        <collection property="students" ofType="Student" select="xyz.xmcs.Mapper.StudentMapper.queryAllStudentLazyByStuNo" column="classId">
            <!-- 再记一遍, 一对一: association; 一对多: collection
                 因为是对多, 所以类中的属性是 List<Xxx> 类型的, 所以collection中不能用javaType; 而只能使用表示元素的类型的 ofType
                 select中指定需要延迟加载的SQL语句的 namespace.id; column指定外键栏; property 指定的是多的属性名(类文件中的)
                -->
        </collection>
    </resultMap>
     <!-- 延迟加载: 一对多 -->
    <select id="queryClassAndStudentsLazy"   resultMap="student_class_lazyLoad_map">
      <!-- 先查班级, 以及全部班级的学生 -->
      select * from studentclass
    </select>
  </mapper>

  <!-- studentMapper.xml select 标签 -->
   <!-- 一对多: 延迟加载需要的: 查询班级中的所有学生 -->
    <select id="queryAllStudentLazyByStuNo" resultType="Student"  parameterType="int">
        select * from student where classId = #{classId}
    </select>
  • 接口以及测试类
    // 延迟加载: 一对多; StudentClassMapper.java
    List<StudentClass> queryClassAndStudentsLazy();

    // 测试一对多延迟加载; TestStudent.java
    public static List<StudentClass> queryClassAndStudentsLazy() throws IOException {
        SqlSession sqlSession = getSqlSession();
        StudentClassMapper mapper = sqlSession.getMapper(StudentClassMapper.class);
        List<StudentClass> studentClasses = mapper.queryClassAndStudentsLazy();
        // 班级信息
        for (StudentClass aClass : studentClasses) {
            System.out.println(aClass);
            // 学生信息
            for(Student student : aClass.getStudents()){
                System.out.println(student.getStuNo() + " -- " + student.getStuName() + " -- " + student.getStuAge());
            }
        }
        sqlSession.close();
        return studentClasses;
    }

12. 查询缓存

  • Mybatis 框架提供了缓存策略,通过缓存策略可以减少查询数据库的次数,提升系统性能; 在 Mybatis 框架中 缓存分为一级缓存和二级缓存

一级缓存

  • 一级缓存: 第一次查询后, 将查询的结果保存在SqlSession对象里放在内存中, 在接下来查询时, 会先在内存中的SqlSession对象查找是不是有缓存, 有则使用缓存, 没有则再查询数据库

  • 一级缓存是 sqlSession 范围的缓存,只能在同一个 sqlSession 内部有效; 使用commit会清空一级缓存

  • MyBatis 默认是开启一级缓存的, 如果用同一个SqlSession对象查询相同的数据, 则只会在第一次查询时向数据库发送SQL语句, 并将结果放在SqlSession中(作为缓存存在); 后续再次查询该同样的对象时, 则直接从缓存中查询该对象即可(即省略了数据库的访问, 从而提高性能)


二级缓存

MyBatis 自带二级缓存

  • 二级缓存: 同一个namespace生成的mapper对象; MyBatis 默认没有开启二级缓存, 需要再 settings 中手动配置打开

    • 手动打开二级缓存: <\setting name="cacheEnabled" value="true"/> 再具体的Mapper.xml 中声明开启二级缓存 <\cache/>

    • 回顾: namespace 的值就是接口的全类名(包名.类名); 通过接口可以产生代理对象(即接口的对象), 所以即namespace决定了接口产生对象的产生

  • 二级缓存是将对象缓存在硬盘当中(序列化:就是从内存到硬盘; 反序列化:就是从硬盘到内存); 所以准备缓存的对象必须实现序列化(并且该对象的父类, 级联属性都需要实现序列化)

  • 触发将对象写入二级缓存的时机: SqlSession 对象的 closee() 方法

  • 只要产生的XxxMapper对象来自于同一个namespace, 则这些对象共享二级缓存(即用来区分两个对象是否共享二级缓存: 只看是否来自于同一个namespace, 如果多个Mapper.xml的namespace值相同, 则它们之间也是共享二级缓存)

  • 如果同一个SqlSession对象进行多次相同的查询, 则直接进入一级缓存; 如果不是同一个SqlSession对象进行多次相同的查询(但均来自同一个nameSpace),则进入二级缓存


禁用, 清理二级缓存

  • 禁用某一条select的二级缓存, 只需要在相关的SQL语句中添加 usecache="false"

  • 禁用二级缓存示例

<!-- useCache: false表示该SQL语句禁用二级缓存, 不写默认就是true -->
    <select id="queryStudentByStuNo" resultType="Student" parameterType="int" useCache="false">
        select * from student where stuNo = #{stuNo}
    </select>
  • 清理缓存:

    • 第一种方式: 一级,二级缓存, 都是在 commit 的时候清理; 执行增删改的时候执行commit后, 会清理掉缓存(目的(这样设计的原因): 为了防止读取脏数据)

      • 注意: 这个 commit 不能说查询自身的 commit, 而是 增删改的 commit
    • 第二种方式: 在相关的select标签中添加 flushCache="true"

-命中率: 1:0.0 2:0.5 3:0.666 4:0.75

第三方二级缓存(ehcache)

  • 想要整个第三方(或者自定义)的二级缓存, 必须实现org.apache.ibatis.cache.Cache接口, 该接口的默认实现类是PerpetalCache

  • 整合 EhCahce 需要三个jar包: Ehcache-core.jar; mybatis-Ehcache.jar; slf4j-api.jar

  • 编写配置文件: Ehcache.xml

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
  <!-- 当二级缓存的对象超过内存限制时 (缓存对象的个数>maxElementsInMemory),  存放入硬盘文件 -->
  <diskStore path="D:\Ehcache"/>
  <!-- 
    maxElementsInMemory: 设置内存中缓存对象的个数
    maxElementsOnDisk: 设置在硬盘中缓存对象的个数
    eternal: 设置缓存是否永远不过期
    overflowToDisk: 当内存中缓存的对象个数超过 maxElementsInMemory 的时候, 是否转移到硬盘中
    timeToIdleSeconds: 当两次访问超过该值的时候, 将缓存对象失效
    timeTlLiveSeconds: 一个缓存对象做多存放的时间(生命周期)
    diskExpiryThreadIntervalSeconds设置每隔多长时间, 通过一个线程来清理硬盘中的缓存
    memoryStoreEvictionPolicy: 当内存中缓存对象达到最大数, 如果有新的对象加入缓存时, 移除已有缓存对象的策略;默认是LRU(最近最少使用); LFU(最不常使用); FIFO(先进先出)
   -->
  <defaultCache
    maxElementsInMemory="1000"
    maxElementsOnDisk="1000000"
    eternal="false"
    overflowToDisk="false"
    timeToIdleSeconds="100"
    timeTlLiveSeconds="100"
    diskExpiryThreadIntervalSeconds="120"
    memoryStoreEvictionPolicy="LRU">
  </defaultCache>
</ehcache>
  • 开启EhCahce二级缓存
<!-- StudentMapper.xml -->
<cache type="org.mybatis.caches.encache.EhcacheCache">
  <!-- 还可以设置自定义值, 覆盖 Ehcache.xml 中的值 -->
  <property name="maxElementsInMemory" value="2000" />
  <property name="maxElementsOnDisk" value="3000" />
</cache>

13. MyBatis逆向工程

  • 表, 类, 接口, mapper.xml 四者密切相关, 因此当知道一个的时候, 其他三个理论上应该可以自动给生成

由表 -> 生成其他的(实现步骤)

  • 需要下载: mybatis-generator-core.jar

  • 逆向工程配置文件: generator.xml

<!--from: 蓝桥学院/mybatiis/逆向工程 -->
<?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 id="DB2Tables" targetRuntime="MyBatis3">
      <commentGenerator>
        <!--
        suppressAllComments属性值:
          true:自动生成实体类、SQL映射文件时没有注释
          true:自动生成实体类、SQL映射文件,并附有注释
        -->
        <property name="suppressAllComments" value="true" />
      </commentGenerator>
    <!-- 数据库连接信息 : Oracle
    <jdbcConnection driverClass="oracle.jdbc.OracleDriver"
        connectionURL="jdbc:oracle:thin:@127.0.0.1:1521:XE" 
        userId="system"  password="sa">
    </jdbcConnection>-->
    <!-- 数据库链接信息: Mysql -->
    <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
      connectionURL="url=jdbc:mysql://127.0.0.1:3306/MyBatisTest"
      userId="root" password="root">
    </jdbcConnection>
      <!-- 
      forceBigDecimals属性值: 
        true:把数据表中的DECIMAL和NUMERIC类型,解析为JAVA代码中的java.math.BigDecimal类型 
        false(默认):把数据表中的DECIMAL和NUMERIC类型,解析为解析为JAVA代码中的Integer类型 
      -->
    <javaTypeResolver>
      <property name="forceBigDecimals" value="false" />
    </javaTypeResolver>
      <!-- 
        targetProject属性值:实体类的生成位置  
        targetPackage属性值:实体类所在包的路径
      -->
    <javaModelGenerator targetPackage="xyz.xmcs.entity"
                             targetProject=".\src">
      <!-- trimStrings属性值:
        true:对数据库的查询结果进行trim操作
        false(默认):不进行trim操作
       -->
    <property name="trimStrings" value="true" />
    </javaModelGenerator>
      <!-- 
        targetProject属性值:SQL映射文件的生成位置  
        targetPackage属性值:SQL映射文件所在包的路径
      -->
    <sqlMapGenerator targetPackage="xyz.xmcs.mapper" 
      targetProject=".\src">
    </sqlMapGenerator>
    <!-- 生成动态代理的接口  -->
    <javaClientGenerator type="XMLMAPPER" targetPackage="xyz.xmcs.mapper" targetProject=".\src">
    </javaClientGenerator>
    <!-- 指定数据库表  -->
    <table tableName="Student"> </table>
    <table tableName="studentCard"> </table>
    <table tableName="studentClass"> </table>
  </context>
</generatorConfiguration>
  • 编写测试类
  public class Test {
    public static void main(Stirng[] args) {
      File file = new File("src/generator.xml"); // 配置文件
      List<String> warnings = new ArrayList<>(); // 警告的集合
      ConfigurationParser cp = new ConfigurationParser(warnings);
      Configuration config = cp.parseConfiguration(file);
      DefaultShellCallback callBack = new DefaultShellCallback(true);
      // 逆向工程核心类
      MyBatisGenerator generator = new MyBatisGenerator(config, callBack, warnings);
      generator.generate(null);
    } 
  }
  // 逆向工程说实在的不实用

# Java # mysql