MyBatis


MyBatis

MyBatis 本是 apache 的一个开源项目 iBatis,2010年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis。2013年11月迁移到 Github。iBATIS 一词来源于 “internet” 和 “abatis” 的组合,是一个基于 Java 的持久层框架。iBATIS 提供的持久层框架包括 SQL Maps 和 Data Access Objects(DAOs)。

MyBatis 概述

框架

框架(framework)其实就是对通用代码的封装,提前写好了一堆接口和类,我们可以在做项目的时候直接引入这些接口和类(引入框架),基于这些现有的接口和类进行开发,可以大大提高开发效率。框架一般都以 jar 包的形式存在。(jar 包中有 class 文件以及各种配置文件等。)

三层架构

  1. 表示层(UI):直接和前端打交互(一是接收前端 ajax 技术,二是向前端返回 json 串)。
  2. 业务逻辑层(BLL):处理表示层转发过来的请求,并将从持久层获取的数据返回表现层。
  3. 持久化层(DAL):直接操作数据库完成 CRUD,并将获得的数据返回 BLL。

JDBC 的不足

JDBC 的 sql 语句是直接写在 java 程序中的,采用硬编码的方式,当功能需要增强、扩展的时候,会违背 OCP 原则。

JDBC 为了防止 SQL 注入,占位符、设置参数以及获取参数的编码方式太繁琐,代码冗余。

了解 MyBatis

MyBatis 中文网(戳我)。MyBatis 本质上就是对 JDBC 的封装,通过 MyBatis 完成 CRUD。MyBatis 在三层架构中负责持久层的,属于持久层框架。

MyBatis 涉及到一种思想叫 ORM 设计思想。ORM(对象关系映射)分为三个部分:

  1. O(Object):Java 虚拟机中的 Java 对象。
  2. R(Relational):关系型数据库。
  3. M(Mapping):将 Java 虚拟机中的 Java 对象映射到数据库中的一行记录,或是将数据库表中一行记录映射成 Java 虚拟机中的一个 Java 对象。

MyBatis 是一个半自动化的 ORM 框架,因为当中的 SQL 语句需要程序员自己编写。(Hibernate 框架就是一个全自动化的 ORM 框架)

MyBatis 入门

引入依赖:

<dependencies>
    
    <!-- mysql链接驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>

    <!-- mybatis依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.13</version>
    </dependency>
    
    <!-- 引入junit测试依赖 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>

     <!-- 引入logback依赖 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.4.11</version>
    </dependency>
    
</dependencies>

从 XML 中构建 SqlSessionFactory

mybatis-config.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>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="MySQL:040809"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 指定路径 -->
    <mappers>
        <!-- resource属性会自动从类的根路径下查找资源 -->
        <mapper resource="CarMapper.xml"/>
    </mappers>
</configuration>

编写 Mapper 文件

我们主要在这个文件中编写 sql 语句,通常一张表对应一个 Mapper 文件。在这里我们编写一个CarMapper.xml来处理t_car的数据:

<?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="hnuxcc21">

    <!-- insert语句,id是sql语句的唯一标识,id代表了这个sql语句 -->
    <insert id="insertCar">
        insert into t_car
        values (null, '1003', '丰田霸道', 30.00, '2000-10-11', '燃油车')
    </insert>

</mapper>

编写 MyBatis 程序

使用 MyBatis 类库,链接数据库,进行数据的增删改查。在 MyBatis 中,负责执行 sql 语句的对象叫SqlSession,是 Java 程序和数据库之间的一次会话。

要想获取SqlSession对象,需要先获取SqlSessionFactory对象,通过SqlSessionFactory来生产SqlSession对象。我们需要利用SqlSessionFactoryBuilder对象的build方法获取SqlSessionFactory对象。

一般一个数据库对应一个SqlSessionFactory对象。

public class MyBatisIntroductionTest {
    public static void main(String[] args) throws IOException {
        //获取SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

        //获取SqlSessionFactory对象,从类根路径下查找资源
        InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = builder.build(resourceAsStream);

        //获取SqlSession对象
        SqlSession sqlSession = factory.openSession();

        //执行sql语句,传入sql语句的id,返回影响数据库表中的记录条数
        int count = sqlSession.insert("insertCar");
        System.out.println(count);

        //手动提交
        sqlSession.commit();
    }
}

关于入门程序的一些细节

  • 在配置文件中的 sql 语句可以省略分号。

  • 如果涉及到 resource 这个单词,大部分情况下,这种加载是从类的根路径下开始加载的。除了使用ResourcesgetResourceAsStream方法,也可以使用自己创建的流:

    //使用类加载器创建文件流,加载类路径下的资源
    InputStream  resourceAsStream = ClassLoader
                    .getSystemClassLoader()
                    .getResourceAsStream("mybatis-config.xml");
  • Mapper 文件的名称和路径都不是固定的。Mapper 配置文件中,mapper标签的url属性是从绝对路径来加载文件的,并且路径前方需要加上file:///,该做法我们不推荐。

MyBatis 事务管理机制

mybatis-config.xml配置文件中,有一个transactionManager。语句为:

<transactionManager type="JDBC"/>

其中,type 的值有两个:JDBCMANAGED(不区分大小写)。也就是说,MyBatis 中提供了两种事务管理机制,分别是 JDBC 和 MANAGED 的事务管理器。

  • JDBC 事务管理器:MyBatis 框架自己采用原生 jdbc 代码管理事务。
  • MANAGED 事务管理器:MyBatis 不再负责事务,而是交给其他容器管理,例如 Spring。

JDBC 事务管理器的事务默认是手动提交的,可以在构造SqlSession的时候开启自动提交(不建议,因为没有开启事务):

//获取SqlSession对象
SqlSession sqlSession = factory.openSession(true);

完善入门程序

事务的提交与回滚

/**
 * 采用正规的方式写一个完整版的MyBatis程序
 * @author n70huihui
 * @version 1.0
 * @since 1.0
 */
public class MyBatisComplete {
    public static void main(String[] args) {

        SqlSession sqlSession = null;

        try {
            //创建builder
            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

            //创建factory
            SqlSessionFactory factory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"));

            //创建SqlSession
            sqlSession = factory.openSession();

            //执行sql语句
            sqlSession.insert("insertCar");

            //提交事务
            sqlSession.commit();

        } catch (IOException e) {
            //遇到异常,回滚事务
            if (sqlSession != null) {
                sqlSession.rollback();
            }
            e.printStackTrace();
        } finally {
            //关闭会话
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }
}

集成日志框架 logback

mybatis-config.xml配置文件的configuration标签中引入如下片段:

<!-- 开启标准日志STDOUT_LOGGING -->
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

STDOUT_LOGGING 是标准日志,MyBatis 已经实现了这种标准日志,只要配置了上述代码,就可以直接使用。

不过,这个标签在编写的时候要注意,其应该出现在environment标签之前。(dtd 文件有对标签的顺序进行约束)

也可以引入其他日志框架,例如 logback:

<!-- 引入logback依赖 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.11</version>
</dependency>

接下来引入 logback 所必须的配置文件,这个配置文件必须叫做logbacktest.xml,不能是其他名字(放在类路径下,此时我们便无需配置mybatis-config.xml):

<?xml version="1.0" encoding="UTF-8"?>

<configuration debug="false">

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!-- 输出日志记录格式 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- mybatis log configure -->
    <logger name="com.apache.ibatis" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>

    <!-- 日志输出级别,logback日志级别包括五个: TRACE < DEBUG < INFO < WARN < ERROR -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
    
</configuration>

MyBatis 工具类编写

在 MyBatis 中,一个SqlSessionFactory对应一个 environment,通常一个 environment 对应一个数据库。

/**
 * MyBatis工具类
 */
public class SqlSessionUtil {

    private SqlSessionUtil() {}

    private static final SqlSessionFactory FACTORY;

    private static final ThreadLocal<SqlSession> LOCAL;

    static {

        LOCAL = new ThreadLocal<>();

        try {
            FACTORY = new SqlSessionFactoryBuilder()
                    .build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static SqlSession openSession() {
        //保证同一线程使用同一个SqlSession
        SqlSession sqlSession = LOCAL.get();
        if (sqlSession == null) {
            sqlSession = FACTORY.openSession();
            LOCAL.set(sqlSession);
        }
        return sqlSession;
    }
    
    public static void close(SqlSession sqlSession) {
        //从当前线程中移除SqlSession对象
        if (sqlSession != null) {
            sqlSession.close();
            LOCAL.remove();
        }
    }

}

此时配置类中的代码便简洁一些了:

@Test
public void testInsertByUtil() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    int count = sqlSession.insert("insertCar");
    System.out.println(count);

    sqlSession.commit();
    sqlSession.close();
}

利用 MyBatis 完成 CRUD

完成 insert (使用 Map 集合传参)

在 jdbc 我们使用?当作占位符,在 mybatis 中我们使用#{}来作为占位符(二者作用相同)。

<?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="hnuxcc21">

    <!-- insert语句,id是sql语句的唯一标识,id代表了这个sql语句 -->
    <insert id="insertCar">
        insert into t_car
        values (null, #{k1}, #{k2}, #{k3}, #{k4}, #{k5});
    </insert>

</mapper>

使用 Map 集合添加对象:

@Test
public void testInsertCar() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    //执行sql语句,传入pojo对象,我们先使用map集合进行数据的封装
    Map<String, Object> map = new HashMap<>();
    map.put("k1", "102");
    map.put("k2", "比亚迪汉");
    map.put("k3", 10.0);
    map.put("k4", "2020-11-11");
    map.put("k5", "新能源");

    int count = sqlSession.insert("insertCar", map);
    System.out.println(count);

    sqlSession.commit();
    sqlSession.close();
}

如果占位符中的 key 不存在,则会填入 null。

完成 insert(使用 POJO 对象传参)

可以使用 Java 对象给 sql 语句传值,此时占位符中需要填入 POJO 类的属性名。MyBatis 通过寻找 POJO 类的get 方法,所以实际上填入的是get方法方法名去掉 get 后首字母小写。

<?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="hnuxcc21">

    <!-- insert语句,id是sql语句的唯一标识,id代表了这个sql语句 -->
    <insert id="insertCar">
        insert into t_car
        values (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType});
    </insert>

</mapper>
@Test
public void testInsertCarByPoJo() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    //封装数据
    Car car = new Car(null, "103", "比亚迪秦", 30.0, "2020-11-11", "新能源");

    //直接插入Car
    int count = sqlSession.insert("insertCar", car);
    System.out.println(count);

    sqlSession.commit();
    sqlSession.close();
}

完成 delete(简单删除)

配置文件:

<?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="hnuxcc21">

    <!-- delete语句,占位符如果只有一个,#{}当中可以随便写,但是最好见名知意 -->
    <delete id="deleteById">
        delete from t_car where id = #{id};
    </delete>

</mapper>
@Test
public void testDeleteById() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    //删除id为15
    int count = sqlSession.delete("deleteById", 15);
    System.out.println(count);

    sqlSession.commit();
    sqlSession.close();
}

完成 update

<?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="hnuxcc21">

    <!-- update语句,同样也最好填上属性名 -->
    <update id="updateCar">
        update
            t_car
        set
            car_num = #{carNum},
            brand = #{brand},
            guide_price = #{guidePrice},
            produce_time = #{produceTime},
            car_type = #{carType}
        where
            id = #{id};
    </update>

</mapper>
@Test
public void testUpdate() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    int count = sqlSession.update("updateCar", new Car(13, "104", "凯美瑞", 30.3, "2000-11-30", "燃油车"));
    System.out.println(count);

    sqlSession.commit();
    sqlSession.close();
}

完成 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">

<mapper namespace="hnuxcc21">

    <!-- 利用resultType指定要封装成什么对象 -->
    <select id="selectById" resultType="cn.hnu.mybatis.pojo.Car">
        select
            id,
            car_num carNum,
            brand,
            guide_price guidePrice,
            produce_time produceTime,
            car_type carType
        from 
        	t_car 
        where 
        	id = #{id};
    </select>

</mapper>
@Test
public void testSelectById() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    //调用selectOne查一个
    Car car = sqlSession.selectOne("selectById", 1);
    System.out.println(car);

    //此时无需提交,因为这个是一个查询语句
    //sqlSession.commit();

    sqlSession.close();
}

需要特别注意的是:select 标签中 resultType 属性用来告诉 MyBatis 查询结果集封装成什么类型的 Java 对象。并且,记得给查询出来的结果集当中的字段起别名,使得查询字段符合 POJO 类的属性名,这样才能够成功赋值。

完成 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">

<mapper namespace="hnuxcc21">

    <!-- resultType写的是List集合中的元素类型 -->
    <select id="selectAll" resultType="cn.hnu.mybatis.pojo.Car">
        select
            id,
            car_num carNum,
            brand,
            guide_price guidePrice,
            produce_time produceTime,
            car_type carType
        from
            t_car;
    </select>

</mapper>
@Test
public void testSelectAll() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    //调用selectList返回list集合
    List<Car> list = sqlSession.selectList("selectAll");

    list.forEach(System.out::println);

    sqlSession.close();
}

Mapper 中的 namespace

当多个 Mapper 文件拥有同名的 sql 语句时,可以使用 mapper 文件的 namespace 来进行区分。

@Test
public void testNameSpace() {
    SqlSession sqlSession = SqlSessionUtil.openSession();

    //加上命名空间hnuxcc21来防止id冲突
    List<Car> list = sqlSession.selectList("hnuxcc21.selectAll");

    list.forEach(System.out::println);

    sqlSession.close();
}

MyBatis 核心配置文件

多环境

<?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">
<!-- dtd约束,约束了该配置文件可以出现哪些标签以及这些标签的关系 -->

<!-- 根标签configuration,只能有一个 -->
<configuration>

    <!-- environments可以配置多个环境,default表示默认使用的环境 -->
    <environments default="development">
        <!-- 一般一个数据库(或者一个环境)会对应一个SqlSessionFactory对象 -->

        <!-- 其中的一个环境,id为development,链接了test这个数据库 -->
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="MySQL:040809"/>
            </dataSource>
        </environment>

        <!-- 另一个环境 -->
        <environment id="newsSystemDB">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/news_system"/>
                <property name="username" value="root"/>
                <property name="password" value="MySQL:040809"/>
            </dataSource>
        </environment>

    </environments>

    <!-- 指定路径 -->
    <mappers>
        <!-- resource属性会自动从类的根路径下查找资源 -->
        <mapper resource="CarMapper.xml"/>
    </mappers>

</configuration>
@Test
public void testEnvironment() throws IOException {
    //获取SqlSessionFactory对象
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    //使用环境id指定数据库
    SqlSessionFactory factory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"), "newsSystemDB");
    //...
}

数据源

数据源可以为程序提供 Connection 对象。数据源实际上是一套规范,一个接口,JDK中有这套规范(javax.sql.DataSource)。我们只要实现javax.sql.DataSource接口,就可以拥有自己的数据源(也叫数据库连接池)

指定 type 标签也就是在指定使用哪个数据库连接池,指定具体使用什么方式来获取 Connection 对象。

常见的数据源:Druid 连接池、c3p0、dbcp…

type标签有三种:

  1. POOLED:使用 MyBatis 自己的数据库连接池。

    POOLED 中几个比较重要的参数有:

    • poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10。
    • poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)。
    • poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
    • poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
  2. UNPOOLED:不使用连接池,来一个链接就创建一个新的 Connection 对象。

  3. JNDI:集成第三方数据连接池。

其中,JNDI 是 java 命名目录接口,是一套规范,很多 Web 容器(Tomcat、Jetty、WebLogic、WebSphere)都实现了 JNDI 规范。也就是说,数据源是配置在 Web容器里的,使用 JNDI 是为了让容器集成 MyBatis。

properties 标签

<?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可以给下方的属性赋值,使得属性赋值更加灵活 -->
    <!--
    <properties>
        <property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="jdbc.url" value="jdbc:mysql://localhost:3306/test"/>
        <property name="jdbc.username" value="root"/>
        <property name="jdbc.password" value="MySQL:040809"/>
    </properties>
	-->
    
    <!-- 利用resource引入外部properties配置文件(类路径下) -->
    <!-- 使用url是从绝对路径下引入资源,使用resource是从类根路径下引入资源 -->
    <properties resource="jdbc.properties"/>
    
    <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>

    <!-- 指定路径 -->
    <mappers>
        <!-- resource属性会自动从类的根路径下查找资源 -->
        <mapper resource="CarMapper.xml"/>
    </mappers>

</configuration>

MyBatis 三大对象作用域

  • SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
  • SqlSessionFactory: 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
  • SqlSession:每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。

WEB 应用中应用 MyBatis

在使用 MyBatis 处理 Web 业务的时候,我们需要考虑的到事务的异常情况。在之前的操作中,我们利用 SqlSession 的commit方法提交事务,为了保证每个线程的事务能够被正常关闭,所以我们需要利用 LocalThread 来处理线程与 SqlSession 之间的关系。

接下来,我们以银行转账的案例来展示 MyBatis 在 Web 项目中的实际应用。首先是 Controller 层:

@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {

    private final AccountService accountService = new AccountServiceImpl();

    @Override
    protected void service(HttpServletRequest req,
                           HttpServletResponse resp) throws ServletException, IOException {
        //获取表单数据
        String fromAction = req.getParameter("fromAction");
        String toAction = req.getParameter("toAction");
        int money = Integer.parseInt(req.getParameter("money"));

        //调用service层
        try {
            accountService.transfer(fromAction, toAction, money);
            //调用完展示页面最终结果
            resp.sendRedirect(req.getContextPath() + "/success.html");
        } catch (MoneyNotEnoughException e) {
            resp.sendRedirect(req.getContextPath() + "/MoneyNotEnoughException.html");
        } catch (Exception e) {
            resp.sendRedirect(req.getContextPath() + "/TransferException.html");
        }

    }
}

Service 层(在这里我们就要对事务进行提交,不能把提交放到 Dao 层中):

public class AccountServiceImpl implements AccountService {

    private final AccountDao accountDao = new AccountDaoImpl();

    @Override
    public void transfer(String fromAction, String toAction, int money)
            throws MoneyNotEnoughException, TransferException {

        //控制事务
        //此处的SqlSessionUtil内部自带了ThreadLocal,保证了SqlSession的线程安全
        SqlSession sqlSession = SqlSessionUtil.openSession();

        //判断转出账户的余额是否充足
        Account fromAccount = accountDao.selectByAccount(fromAction);
        if (fromAccount.getMoney() < money) {
            throw new MoneyNotEnoughException("对不起,余额不足");
        }
        //先更新内存余额
        fromAccount.setMoney(fromAccount.getMoney() - money);
        Account toAccount = accountDao.selectByAccount(toAction);
        toAccount.setMoney(toAccount.getMoney() + money);
        //余额写入数据库
        int count = accountDao.updateByAction(fromAccount);

        //模拟异常
        String s = null;
        s.toString();

        count += accountDao.updateByAction(toAccount);
        if(count != 2) {
            throw new TransferException("转账异常,未知原因");
        }

        sqlSession.commit();
        //调用close方法关闭线程中的sqlSession
        SqlSessionUtil.close(sqlSession);
    }
}

最后是 Dao 层代码:

public class AccountDaoImpl implements AccountDao {

    @Override
    public Account selectByAccount(String accountName) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        return sqlSession.selectOne("selectByAccountName", accountName);
    }

    @Override
    public int updateByAction(Account account) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        int count = sqlSession.update("updateByAction", account);
        return count;
    }
}

我们看出,在三层架构下,我们的 Dao 层代码变得很少。并且SqlSession的获取代码变得重复冗余,也没有业务代码。所以,我们需要一种新的技术来帮助我们解决这些问题。我们接下来会介绍 Javassist,当我们使用了这项技术之后,我们的 Dao 层实现类便不需要我们手动编写,我们只负责编写接口即可。

Javassist 动态生成类

<!-- javassist依赖 -->
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

基础使用

(如果运行出错, 在方法编辑中点击右上角“修改选项”勾选“添加虚拟机选项”,然后在方法的构建与运行中加入参数:--add-opens java.base/java.lang=ALL-UNNAMED,在环境变量中加入参数:--add-opens java.base/sun.util=ALL-UNNAMED

@Test
public void testGenerateFirstClass() throws Exception {
    //获取类池,这个类池就是用来生成class的
    ClassPool pool = ClassPool.getDefault();

    //制造类,需要传入全类名
    CtClass ctClass = pool.makeClass("cn.hnu.bank.dao.Impl.AccountDaoImpl");
    //制造方法,第一个参数传方法的代码,第二个参数传入对应的ctClass
    String methodCode = "public void showInfo() {System.out.println(\"hello world\");}";
    CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
    //向类中添加方法
    ctClass.addMethod(ctMethod);
    //在内存中生成class
    ctClass.toClass();

    //类加载,把上面生成的对象加载到虚拟机中,返回AccountDao的字节码
    Class<?> clazz = Class.forName("cn.hnu.bank.dao.Impl.AccountDaoImpl");
    //创建对象
    Object obj = clazz.getConstructor().newInstance();
    //获取新添加的showInfo方法
    Method method = clazz.getMethod("showInfo");
    //调用方法
    method.invoke(obj);
}

实现接口中的单个方法

//现有一个单方法的接口
public interface AccountDao {
    void delete();
}
@Test
public void testGenerateImpl() throws Exception {
    //获取类池
    ClassPool pool = ClassPool.getDefault();
    //制造类
    CtClass ctClass = pool.makeClass("cn.hnu.bank.dao.Impl.AccountDaoImpl");
    //制造接口
    CtClass ctInterface = pool.makeInterface("cn.hnu.bank.dao.AccountDao");
    //添加接口到类中,表示让上述的类去实现这个接口
    ctClass.addInterface(ctInterface);
    //实现接口中的方法,我们需要先制造方法出来
    String methodCode = "public void delete() {System.out.println(\"delete\");}";
    CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
    //将方法添加到类中
    ctClass.addMethod(ctMethod);
    //在内存中生成类,同时,将生成的类装载到JVM中
    Class<?> clazz = ctClass.toClass();

    //生成对象并调用delete方法
    AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
    accountDao.delete();
}

实现接口中的多个方法

//现有多个不同返回值,不同参数的方法
public interface AccountDao {
    void delete();
    int insert(String account);
    int update(String account, Double balance);
    String selectByAccountNo(String AccountNo);
}
@Test
public void testGenerateAccountDaoImpl() throws Exception {
    //获取类池
    ClassPool pool = ClassPool.getDefault();
    //制造类
    CtClass ctClass = pool.makeClass("cn.hnu.bank.dao.Impl.AccountDaoImpl");
    //制造接口
    CtClass ctInterface = pool.makeInterface("cn.hnu.bank.dao.AccountDao");
    //实现接口
    ctClass.addInterface(ctInterface);

    //实现接口中的所有方法
    //我们需要先获取接口中所有方法
    Method[] methods = AccountDao.class.getDeclaredMethods();
    Arrays.stream(methods).forEach(method -> {
        CtMethod ctMethod = null;
        try {
            StringBuilder methodCode = new StringBuilder("public ");
            //追加返回值类型(获得全类名后再获取实际名称)
            methodCode.append(method.getReturnType().getSimpleName());
            methodCode.append(" ");
            //追加方法名
            methodCode.append(method.getName());
            methodCode.append("(");
            //拼接参数,先获取方法的各个参数类型数组
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (int i = 0; i < parameterTypes.length; ++i) {
                //获取单个参数类型
                Class<?> parameterType = parameterTypes[i];
                //参数类型名称
                methodCode.append(parameterType.getName());
                methodCode.append(" ");
                //参数名称
                methodCode.append("arg" + i);
                if (i != parameterTypes.length - 1) {
                    methodCode.append(", ");
                }
            }
            methodCode.append(") {System.out.println(\"hello\");");
            //动态添加返回语句
            //获取参数类型(普通名称)
            String returnTypeSimpleName = method.getReturnType().getSimpleName();
            switch (returnTypeSimpleName) {
                case "int" -> methodCode.append("return 1;");
                case "String" -> methodCode.append("return \"hello\";");
            }
            methodCode.append("}");
            ctMethod = CtMethod.make(methodCode.toString(), ctClass);
            ctClass.addMethod(ctMethod);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

    //在内存中生成class并加载到JVM中
    Class<?> clazz = ctClass.toClass();
    //创建对象
    AccountDao accountDao = (AccountDao) clazz.getConstructor().newInstance();
    //调用方法
    accountDao.insert("aaa");
    accountDao.delete();
    accountDao.update("aaa", 100.0);
    accountDao.selectByAccountNo("aaa");
}

MyBatis 中的代理类

在 MyBatis 中,提供了相关的机制,也可以为我们动态生成代理类。此时注意当我们使用代理类时,namespace 必须是 Dao 接口的全限定类名,sql 语句的 id 必须是 Dao 接口的方法名。此时将全限定类名与 id 名拼接在一起,代理类才可以唯一确定一条 sql 语句。

//MyBatis也使用了类似的代理机制,自动创建接口的代理类
private final AccountDao accountDao = SqlSessionUtil.openSession()
            .getMapper(AccountDao.class);

MyBatis 小技巧

#{} 和 ${}

  • #{}:先编译 sql 语句,再给占位符传值,底层是 PreparedStatement 实现。可以防止 sql 注入,我们需要优先使用
  • ${}:先进行 sql 语句拼接,然后再编译 sql 语句,底层是 Statement 实现。存在 sql 注入现象。只有在需要进行 sql 语句关键字拼接的情况下才会用到。

可以理解为,#{}为了防止 sql 注入,拼接参数的时候会把参数当作一个字符串拼接(也就是在原本参数的基础上添加两个双引号)。而${}是直接拼接,在参数两侧是不带双引号的。

${} 的使用场景:

<!-- 需要直接拼接asc或desc字符串的时候,使用${} -->
<mapper namespace="cn.hnu.mybatis.mapper.CarMapper">

    <select id="selectByCarTypeAscOrDesc" resultType="cn.hnu.mybatis.pojo.Car">
        select
            id,
            car_num carNum,
            brand,
            guide_price guidePrice,
            produce_time produceTime,
            car_type carType
        from
            t_car
        order by
            guidePrice ${ascOrDesc}
    </select>

</mapper>

批量删除

批量删除,顾名思义就是一次性删除有多条记录。这个时候,sql 语句有两种写法:

  1. 使用 or 连接:delete from t_user where id = 1 or id = 2 ...
  2. 使用 in 关键字:delete from t_user where id in (1, 2, 3...)

我们可以考虑使用第二个方案,先利用${}拼接 id:

<delete id="deleteBatch">
    delete from t_car where car_num in (${ids});
</delete>

再传入 id 即可:

int count = carMapper.deleteBatch("105,106,107");

模糊查询

模糊查询也可以直接使用${}进行拼接:

<select id="selectByBrand" resultType="cn.hnu.mybatis.pojo.Car">
    select
        id,
        car_num carNum,
        brand,
        guide_price guidePrice,
        produce_time produceTime,
        car_type carType
    from
        t_car
    where
        brand like '%${brand}%';
</select>
<!--
	第二种方案
	... where brand like concat('%', #{brand}, '%');

	第三种方案(注意使用${}时需要在外面自己加上两个引号)
	... where brand like concat('%', '${brand}', '%');

	第四种方案
	... where brand like "%"#{brand}"%";
-->

别名机制

MyBatis 给我们提供了别名机制,让我们能够给resultType需要填入的参数起别别名,我们需要在配置文件mybatis-config中进行配置,当然,我们需要注意一下标签的顺序:

<?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>

    <typeAliases>
        <!--
            给type起别名,别名为alias
            <typeAlias type="cn.hnu.mybatis.pojo.Car" alias="MyCar"/>
        -->
        
        <!-- namespace不能起别名 -->
        
        <!-- 也可以省略alias,省略之后,别名就是类的简名,不区分大小写-->
        <typeAlias type="cn.hnu.mybatis.pojo.Car"/>
        
        <!-- 指定包的方式,包名下所有类自动起别名,别名就是类简名,不区分大小写 -->
        <package name="cn.hnu.mybatis.pojo"/>
        
    </typeAliases>


</configuration>

mapper 的配置

mapper 中有三个标签:

  • resource:这种方式是从类的根路径下开始查找资源,采用这种方式的话,配置文件需要放到类路径当中去。
  • url:这种方式是绝对路径的方式,不要求放入类的根路径。这种方式使用极少,因为移植性太差。
  • class:全限定接口名,带有包名。框架会在这个接口的同级目录下找这个文件。也就是说,如果使用这种方式,配置文件的 resource 目录结构需要和接口所在的包结构一致

获取自动生成的主键

如果生成后再查询主键,这会有点麻烦。MyBatis 给我们提供了一个更加方便的方法。

mapper 配置文件:

<!--
    useGeneratedKeys开启使用生成的主键
    keyProperty指定将自动生成的主键放入对象的哪个属性中
 -->
<insert id="insertCarAndUseKey" useGeneratedKeys="true" keyProperty="id">
    insert into
        t_car
    values
        (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType});
</insert>

测试程序:

@Test
public void testInsertAndUseKey() {
    SqlSession sqlSession = SqlSessionUtil.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
    //new对象,接收id数据
    Car car = new Car(null, "107", "凯美瑞", 30.01, "2022-01-01", "新能源");
    carMapper.insertCarAndUseKey(car);
    sqlSession.commit();
    sqlSession.close();
    //自动把主键值赋值给id属性
    System.out.println(car.getId());
}

MyBatis 参数处理

单个简单类型参数

<mapper namespace="cn.hnu.mybatis.mapper.StudentMapper">

    <!--
        parameterType可以指定单个参数的类型(可以不写,MyBatis会做自动类型推断)
        方便MyBatis底层jdbc代码调用set方法进行占位符赋值
        当然,MyBatis提供了很多参数类型别名,具体可以参阅官方手册
     -->
    <select id="selectById" resultType="Student" parameterType="Integer">
        select * from t_student where id = #{id};
    </select>

    <!-- 也可以在#{}中填入java和jdbc中的类型区别 -->
    <select id="selectByName" parameterType="Student">
        select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR};
    </select>

    <select id="selectByBirth" resultType="Student">
        select * from t_student where birth = #{birth};
    </select>

    <select id="selectByGender">
        select * from t_student where gender = #{gender};
    </select>

</mapper>

Map 集合

<!-- 使用map集合传参的时候,#{}当中需要存放map集合的key -->
<insert id="insertByMap">
    insert into t_student values (null, #{姓名}, #{年龄}, #{身高}, #{生日}, #{性别});
</insert>

POJO 类

<!-- 使用pojo对象传参,占位符当中填入的是pojo对象的属性名 -->
<insert id="insertStudentByPojo">
    insert into t_student values (null, #{name}, #{age}, #{height}, #{birth}, #{gender});
</insert>

多参数

如果是多个参数的话,MyBatis 框架底层会自动创建一个 map 集合,并且 map 集合的键是arg0, arg1, ...

<!-- 多参数传递,如果使用param的话需要从1开始索引 -->
<select id="selectByNameAndGender" resultType="Student">
    select * from t_student where name = #{arg0} and gender = #{arg1};
</select>

Param 注解

通过 Param 注解可以为多参数指定 map 集合键的名字。

public interface StudentMapper {
    /* 在方法中利用@Param注解为参数的key取名字 */
    List<Student> selectByNameAndGenderAnnotation(@Param("name") String name,@Param("gender") Character gender);

}
<!-- 在占位符中填入利用@Param取的名字 -->
<select id="selectByNameAndGenderAnnotation" resultType="Student">
    select * from t_student where name = #{name} and gender = #{gender};
</select>

使用了注解之后,arg 系列就失效了,但是 param 系列还是可以继续使用的。

MyBatis 查询语句专题

返回单个 POJO 对象

<!-- 如果数据库的字段名和pojo对象的字段名不一致,需要起别名 -->
<select id="selectById" resultType="Car">
    select
        id,
        car_num carNum,
        brand,
        guide_price guidePrice,
        produce_time produceTime,
        car_type carType
    from t_car where id = #{id};
</select>

返回多个 POJO 对象

写法同上,只不过要用 List 集合来接收。

返回 Map 集合

如果我们倒时候查询出来的结果没有合适的 Java 对象来接收数据,我们可以把数据放入一个 Map<String, Object> 集合当中。

<!-- resultType写成Map即可返回一个Map集合 -->
<select id="selectByIdReturnMap" resultType="Map">
    select
        id,
        car_num carNum,
        brand,
        guide_price guidePrice,
        produce_time produceTime,
        car_type carType
    from t_car where id = #{id};
</select>

返回多个 Map 集合

返回的时候使用List<Map<String, Object>>即可。

返回大 Map 集合

上述我们把返回的 Map 集合放入到一个 List 集合中,但是当数据量变大的时候,List 集合一般检索效率比较慢,我们可以把 Map 放入另一个 Map 中,形成一个大 Map(Map<Integer, Map<String, Object>>)。

/* 在方法上加上注解MapKey,将小Map的某个key作为大Map的key */
@MapKey("id")
Map<Integer, Map<String, Object>> selectAll();

查询结果映射

查询结果映射是用来解决数据库字段名称和 Java 对象属性名不一致的问题的,一共有三种解决方案:

  1. 使用查询语句的时候给字段起别名。

  2. 使用 resultMap 进行结果映射。

    <!-- 专门定义一个结果映射,专门指定java对象和数据库字段的对应关系 -->
    <!-- type标签用来指定pojo类的类名 -->
    <!-- id属性,指定resultMap的唯一标识,这个id将来要在select中使用 -->
    <resultMap id="carResultMap" type="Car">
        <!-- 数据库表有主键的话,需要额外配置一个id标签 -->
        <id property="id" column="id"/>
        <!-- property填写pojo类的属性名,column填写数据库表的字段名 -->
        <result property="carNum" column="car_num"/>
        <result property="brand" column="brand"/>
        <result property="guidePrice" column="guide_price"/>
        <result property="produceTime" column="produce_time"/>
        <result property="carType" column="car_type" javaType="String" jdbcType="VARCHAR"/>
    </resultMap>
    
    <!-- resultMap需要指定resultMap的id-->
    <select id="selectAllByResultMap" resultMap="carResultMap">
        select * from t_car;
    </select>
  3. 开启自动驼峰映射,但前提是 Java 类和数据库字段命名符合规范(配置 mybatis-config.xml)。

    <!-- 开启驼峰自动映射 -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

返回总记录条数

<!-- resultType填上Integer -->
<select id="selectTotal" resultType="Integer">
    select count(*) from t_car;
</select>

动态 SQL

当 sql 语句中的 where 条件等我们没办法写死的时候(例如批量删除,条件查询等),就需要使用动态 SQL。

if 标签

需求:多条件查询。

<!--
    if标签中的test属性是必须的
    test内容是个boolean类型,如果是true,就把if的内容拼接到sql语句中
        如果使用了@Param,那么test属性中需要填入@Param的value属性
        如果没有使用@Param,则需要使用arg0,arg1或者param1,param2...
        如果使用POJO类,则需要填入属性名
    在MyBatis的动态sql中,不能使用&&,需要使用and

    加上 1 = 1,符合sql语句查询,防止出现条件都不符合的情况下报出语法错误
    多条件查询记得在if标签中使用and连接
-->
<select id="selectByMultiCondition" resultType="Car">
    select
        *
    from
        t_car
    where
        1 = 1
    <if test="brand != null and brand != ''">
        and brand like '%${brand}%'
    </if>
    <if test="guidePrice != null">
        and guide_price > #{guidePrice}
    </if>
    <if test="carType != null and carType != ''">
        and car_type = #{carType}
    </if>
</select>

where 标签

让 where 子句更加动态智能。有两个作用:

  1. 当所有条件都为空的时候,where 标签能保证不会生成 where 子句。
  2. 自动去除某些条件前面多余的 and 或者 or。
<!-- 使用where子句使得sql更加动态灵活 -->
<select id="selectByMultiConditionUseWhere" resultType="Car">
    select
        *
    from
        t_car
    <where>
        <if test="brand != null and brand != ''">
            and brand like '%${brand}%'
        </if>
        <if test="guidePrice != null">
            and guide_price > #{guidePrice}
        </if>
        <if test="carType != null and carType != ''">
            and car_type = #{carType}
        </if>
    </where>
</select>

trim 标签

<!--
    prefix 加前缀
    suffix 加后缀
    prefixOverrides 去掉前缀
    suffixOverrides 去掉后缀
-->
<select id="selectByMultiConditionUseTrim" resultType="Car">
    select
        *
    from
        t_car
    <!-- 加上前缀where,并且去掉后缀and或者or,使用|连接多个参数 -->
    <trim prefixOverrides="" suffix="" prefix="where" suffixOverrides="and|or">
        <if test="brand != null and brand != ''">
            brand like '%${brand}%' and
        </if>
        <if test="guidePrice != null">
            guide_price > #{guidePrice} and
        </if>
        <if test="carType != null and carType != ''">
            car_type = #{carType} and
        </if>
    </trim>
</select>

set 标签

主要使用在 update 语句中,用来动态生成 set 关键字,同时去掉最后多余的,。考虑到一个实际问题,就是当我们使用 set 语句的时候,如果往里传入一个 null 参数,那么很有可能会把本来不为空的参数更新为 null,我们需要使用 set 标签来避免这种情况的发生,只提交不为空的字段。

<!-- 使用set标签动态拼接sql,同时自动去除末尾多余的, -->
<update id="updateCarBySet">
    update
        t_car
    <set>
        <if test="carNum != null and carNum != ''">
            car_num = #{carNum},
        </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>

choose 标签

choose、when、otherwise 这三个标签是一起使用的。类似于 if else 分支,也就是说,只有一个分支执行

需求:先根据条件1查,如果没有提供条件1,则根据条件2查…

<!--
    使用choose标签进行多分支查询
    注意此时我们不再需要用and来连接多个条件
    因为我们是多分支查询,一次只执行一个分支
    不存在多条件合并,所以不需要使用and或者or
 -->
<select id="selectByChoose" resultType="Car">
    select
        *
    from
        t_car
    <where>
        <choose>
            <when test="brand != null and brand != ''">
                brand like '%${brand}%'
            </when>
            <when test="guidePrice != null">
                guide_price = #{guidePrice}
            </when>
            <otherwise>
                car_type = #{carType}
            </otherwise>
        </choose>
    </where>
</select>

foreach 批量删除

//使用数组的形式入参,记得加上@Param
int deleteByIds(@Param("ids") Integer[] ids);
<!--
    collection 指定数组或者集合
    item 代表数组或集合中的元素
    separator 循环之间的分隔符
    open 以什么字符开始
    close 以什么字符结束
-->
<delete id="deleteByIds">
    delete from t_car where id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

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

foreach 批量插入

<!-- 利用foreach遍历集合,增加数据 -->
<insert id="insertBatch">
    insert into
        t_car
    values
    <foreach collection="cars" item="car" separator=",">
        (null, #{car.carNum}, #{car.brand}, #{car.guidePrice}, #{car.produceTime}, #{car.carType})
    </foreach>
</insert>

sql 和 include 标签

sql 标签用来声明 sql 片段,include 标签用来将声明的 sql 片段包含到某个 sql 语句中。可以让我们的代码复用性增强,易维护。

<!-- 抽取出相同sql代码片段 -->
<sql id="carColumnNameSql">
    id,
    car_num carNum,
    brand,
    guide_price guidePrice,
    produce_time produceTime,
    car_type carType
</sql>

<!-- 使用include标签使用重复代码 -->
<select id="selectAll" resultType="Map">
    select
        <include refid="carColumnNameSql"/>
    from t_car;
</select>

高级映射及延迟加载

在之前的学习中,数据库的一张表和一个 POJO 对象进行映射,一对一的映射我们称为基本映射。所谓高级映射指的是存在多张表并且表与表之间有关系的话,我们需要进行高级映射(解决多表连结查询问题的)。

我们接下来来研究多对一和一对多怎么进行映射。如果是多对一,则多的是主表,反之一是主表

多对一映射

例如有一张学生表(Student)和一张课程表(Clazz),学生表当中的课程编号是外键,同时是课程表的主键。这个时候,多对一指的是学生表对课程表。我们通过学生表当中的课程编号可以找到对应的课程类对象,那么我们应该在 Student 类当中添加一个 Clazz 属性,作为内部类

多对一的关系,常见有三种处理方式:

  1. 一条 sql 语句,级联属性映射。

    <!-- property是POJO对象的属性名,column是查询出来的字段名 -->
    <resultMap id="studentResultMap" type="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
        <result property="clazz.cid" column="cid"/>
        <result property="clazz.cname" column="cname"/>
    </resultMap>
    
    <!-- 利用级联查询进行多表联查 -->
    <select id="selectById" resultMap="studentResultMap">
         select
            s.sid,
            s.sname,
            c.cid,
            c.cname
        from
            t_stu s
        join
            t_clazz c on s.cid = c.cid
        where
            s.sid = #{sid}
    </select>
  2. 一条 sql 语句,association。

    <resultMap id="studentResultMapAssociation" type="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
        <!-- association-关联,表示一个Student对象关联一个Clazz对象 -->
        <!-- property提供要映射的POJO类的属性名,javaType提供要映射的类型 -->
        <association property="clazz" javaType="Clazz">
            <id property="cid" column="cid"/>
            <result property="cname" column="cname"/>
        </association>
    </resultMap>
    
    <select id="selectByIdAssociation" resultMap="studentResultMapAssociation">
        select
            s.sid,
            s.sname,
            c.cid,
            c.cname
        from
            t_stu s
        join
            t_clazz c on s.cid = c.cid
        where
            s.sid = #{sid}
    </select>
  3. 两条 sql 语句,分步查询。(这种方式常用,优点一是可复用,二是支持懒加载)

    <!-- ClazzMapper当中的sql语句 -->
    
    <!-- 分步查询第二步,根据cid查询课程信息 -->
    <select id="selectByIdStep2" resultType="Clazz">
        select * from t_clazz where cid = #{cid}
    </select>
    
    
    <!-- StudentMapper当中的sql语句 -->
    
    <!-- 两条sql语句完成多对一的分步查询 -->
    <resultMap id="studentResultMapByStep" type="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
    
        <!--
            这里需要指定第二步要执行的sql语句的id
            select放入要执行的第二步的sql语句
            column放入要传入到sql语句的参数
         -->
        <association property="clazz"
                     column="cid"
                     select="cn.hnu.mybatis.mapper.ClazzMapper.selectByIdStep2"/>
    </resultMap>
    
    <!-- 分步查询第一步,先查学生课程cid -->
    <select id="selectByIdStep1" resultMap="studentResultMapByStep">
        select sid, sname, cid from t_stu where sid = #{sid};
    </select>

多对一映射延迟加载

延迟加载,又叫懒加载。核心原理便是:用到的时候再加载。在这里便是用到的时候再执行查询语句,不用的时候不查,可以提升性能。MyBatis 中开启懒加载的方式:

<association property="clazz"
             column="cid"
             select="cn.hnu.mybatis.mapper.ClazzMapper.selectByIdStep2"
             fetchType="lazy"/>
<!-- 利用fetchType="lazy"去开启懒加载 -->

当然,上述的设置只是局部的懒加载设置,只对当前 association 关联的 sql 语句起作用。我们可以设置全局的懒加载方式,在mybatis-config中配置:

<settings>
    <!-- 所有但凡只要带了分步的,都采用懒加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

<!-- 如果有特殊需求,不需要延迟加载,则加上fetchType="eager"即可-->
<association property="clazz"
             column="cid"
             select="cn.hnu.mybatis.mapper.ClazzMapper.selectByIdStep2"
             fetchType="eager"/>

一对多映射

现在我们把课程表当作主表,很显然,一个班级对应多个学生,一对多。我们可以在 Clazz 类中添加一个 List 集合(或者数组)来存储多个 Student。

一对多的关系,通常有两种处理方式:

  1. collection。

    <resultMap id="clazzResultMap" type="Clazz">
        <id property="cid" column="cid"/>
        <result property="cname" column="cname"/>
        <!-- 一对多使用collection,ofType当中写入集合当中的元素类型 -->
        <collection property="stus" ofType="Student">
            <id property="sid" column="sid"/>
            <result property="sname" column="sname"/>
            <!-- 这里我们不能再给Student里面的Clazz赋值了,否则会导致赋值递归 -->
        </collection>
    </resultMap>
    
    <select id="selectByCollection" resultMap="clazzResultMap">
        select
            c.cid,
            c.cname,
            s.sid,
            s.sname
        from
            t_clazz c
        join
            t_stu s on c.cid = s.cid
        where
            c.cid = #{cid}
    </select>
  2. 分步查询。(常用)

    <!-- StudentMapper当中的sql语句 -->
    
    <!-- 分步查询第二步,根据cid查询学生信息集合 -->
    <select id="selectByCid" resultType="Student">
        select * from t_stu where cid = #{cid}
    </select>
    
    
    <!-- ClazzMapper当中的sql语句 -->
    
    <!-- 分步查询第一步,根据Clazz的cid获取班级信息-->
    <resultMap id="clazzResultMapStep" type="Clazz">
        <id property="cid" column="cid"/>
        <result property="cname" column="cname"/>
        <collection property="stus"
                    select="cn.hnu.mybatis.mapper.StudentMapper.selectByCid"
                    column="cid"/>
    </resultMap>
    
    <select id="selectByStep1" resultMap="clazzResultMapStep">
        select * from t_clazz where cid = #{cid}
    </select>

一对多映射延迟加载

同多对一映射的延迟加载,这里不再赘述。

MyBatis 缓存

缓存(cache)是我们开发当中一种常用的优化程序的重要手段,常见的缓存技术有:

  • 字符串常量池。
  • 整数型常量池。
  • 线程池。
  • 数据库连接池。

我们知道,计算机中的内存是用来临时存储数据的,这些数据会在断电后消失。而数据存放在磁盘当中,才是持久化存储。我们知道,数据库的数据存放在文件中,而文件存放在硬盘中,意味着我们如果要和数据库的数据进行交互,需要进行 IO 操作,IO 操作很耗时间。

如果是同一条 sql 语句执行多次,每一次都去连接数据库进行查询,那么效率实在太慢了。MyBatis 存在缓存机制,当我们执行 DQL 语句的时候,将查询结果放入缓存当中,如果下一次还是执行完全相同的 DQL 语句,如果结果相同的话,直接从缓存中拿去数据,减少和磁盘的 IO 操作,加快查询效率。

MyBatis 的缓存包括:

  1. 一级缓存:将查询的数据存放到 SqlSession 中(代表了一个 sql 会话)。
  2. 二级缓存:将查询的数据存放到 SqlSessionFactory 中(代表了整个数据库)。
  3. 集成第三方缓存:EhCache、Memcache。

缓存只针对 DQL 语句,也就是说缓存机制只对应 select 语句。

一级缓存

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

只要是使用同一个 SqlSession 对象执行同一条 Sql 语句,就会走缓存。

//以下代码只会执行一次sql语句
@Test
public void testSelectById() {
    SqlSession sqlSession = SqlSessionUtil.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
    //mapper第一次调用
    Car car1 = carMapper.selectById(1);
    System.out.println(car1);
    //mapper第二次调用
    Car car2 = carMapper.selectById(1);
    System.out.println(car2);
    //SqlSession调用完毕
    sqlSession.close();
}

不走缓存的两种情况:

  1. SqlSession 对象不是同一个,不走缓存。
  2. 两次查询的对象不一样,不走缓存。

第一次 DQL 和第二次 DQL 之间我们进行了以下操作都会导致缓存失效:

  1. 调用了 SqlSession 的clearCache()方法,手动清空缓存。
  2. 执行了 insert、delete、update 语句(和表没有关系,即操作 A 表也会清空 B 表的缓存)。

二级缓存

二级缓存的范围更大,属于整个数据库范围的,因为缓存是针对于 SqlSessionFactory 的。

使用二级缓存需要具备以下条件:

  1. <settingname="cacheEnabled"value="true">全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是 true,无需设置
  2. 需要使用二级缓存的 SqlMapper.xml 文件中添加配置:<cache/>
  3. 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口。
  4. SqlSession 对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
@Test
public void testSelectById2() throws IOException {

    //二级缓存对应的就是一个SqlSessionFactory对象
    SqlSessionFactory factory = new SqlSessionFactoryBuilder()
            .build(Resources.getResourceAsStream("mybatis-config.xml"));
    //创建两个sqlSession
    SqlSession sqlSession1 = factory.openSession();
    SqlSession sqlSession2 = factory.openSession();
    //获取两个mapper
    CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
    CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);

    //在这里我们不关闭SqlSession,此时二级缓存中还是没有数据的
    Car car1 = mapper1.selectById2(1);  //这行代码执行后,数据被放入一级缓存中
    System.out.println(car1);
    Car car2 = mapper2.selectById2(1);  //两个一级缓存是不一致的,因为是两个SqlSession
    System.out.println(car2);

    //关闭SqlSession,这个时候才将一级缓存放入二级缓存中
    sqlSession1.close();
    sqlSession2.close();

    //从二级缓存拿取数据
    Car car3 = factory.openSession()
            .getMapper(CarMapper.class)
            .selectById2(1);
    System.out.println(car3);

}

只要两次查询之间进行了增、删、改操作,二级缓存就会失效(一级缓存也会失效)。

二级缓存相关配置:

  1. eviction:从缓存中移除某个对象的淘汰算法。默认采用 LRU 策略。
    1. LRU:Least Recently Used。最近最少使用,优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
    2. FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
    3. SOFT:软引用,淘汰软引用指向的对象。具体算法和 JVM 的垃圾回收算法有关。
    4. WEAK:弱引用,淘汰弱引用指向的对象。具体算法和 JVM 的垃圾回收算法有关。
  2. flushInterval:二级缓存的刷新时间间隔,单位毫秒。如果没有设置,就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据,除非执行了增删改。
  3. readOnly:
    1. true:多条相同的 sql 语句执行之后返回的对象是共享的同一个。性能好,但是多线程并发可能会存在安全问题。
    2. false:多条相同的 sql 语句执行之后返回的对象是副本,调用了 clone 方法。性能一般,但安全。
  4. size:设置二级缓存中最多可存储的 Java 对象数量,默认值是 1024。

MyBatis 集成 EhCache

集成第三方二级缓存只能替代 MyBatis 的二级缓存,一级缓存试没办法替代的。因为 EhCache 是使用 Java 语言写的,故 MyBatis 集成 EhCache 比较常见。

首先引入 Ehcache 依赖:

<!-- 引入ehcache依赖 -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.3</version>
</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="d:/ehcache"/>

    <defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false"
    timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/>
    
</ehcache>

MyBatis 逆向工程

所谓的逆向工程是:根据数据库表逆向生成 Java 的 pojo 类,SqlMapper.xml 文件,以及 Mapper 接口类等。

要完成这个工作,我们可以借助别人写好的逆向工程插件。

使用这个插件的话,我们需要配置好以下信息:

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

逆向工程的配置与生成

pom.xml文件中配置:

<!-- 配置逆向工程插件 -->
<!-- 定制构建过程 -->
<build>
    <!-- 可配置多个插件 -->
    <plugins>
        <!-- 其中的一个插件,MyBatis的逆向工程插件 -->
        <plugin>
            <!-- 插件的GAV坐标 -->
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.4.2</version>
            <!-- 允许覆盖 -->
            <configuration>
                <overwrite>true</overwrite>
            </configuration>
            <!-- 插件的依赖 -->
            <dependencies>
                <!-- mysql驱动依赖 -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>8.0.33</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

在类的根路径下创建配置文件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/test"
                        userId="root"
                        password="MySQL:040809">
        </jdbcConnection>

        <!-- 生成pojo包名和位置 -->
        <javaModelGenerator targetPackage="cn.hnu.mybatis.pojo" targetProject="src/main/java">
            <!-- 是否开启子包 -->
            <property name="enableSubPackages" value="true"/>
            <!-- 是否去除字段名的前后空白 -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- 生成SQL映射文件的包名和位置 -->
        <sqlMapGenerator targetPackage="cn.hnu.mybatis.mapper" targetProject="src/main/resources">
            <!-- 是否开启子包 -->
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!-- 生成Mapper接口的包名和位置 -->
        <javaClientGenerator
                type="xmlMapper"
                targetPackage="cn.hnu.mybatis.mapper"
                targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 表名和对应的实体类 -->
        <table tableName="t_car" domainObjectName="Car"/>

    </context>
</generatorConfiguration>

QBC 查询风格

上述如果我们选择增强版,则会生成的 Example 类,这个类是用来封装查询条件的。

QBC 查询风格便是面向对象的一种查询风格,看不到 SQL 语句,而是使用对象的创建来代替 SQL 语句。

@Test
public void testSelect() {
    SqlSession sqlSession = SqlSessionUtil.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);

    //查询1个
    Car car = carMapper.selectByPrimaryKey(25);
    System.out.println(car);

    //根据条件查询,如果是null,则查所有
    List<Car> list = carMapper.selectByExample(null);
    list.forEach(System.out::println);

    //按照条件查询
    //先封装查询条件
    CarExample carExample = new CarExample();

    //调用createCriteria创建查询条件

    //添加and条件
    carExample.createCriteria()
            .andBrandLike("凯美瑞")    //brand like '凯美瑞'
            .andGuidePriceGreaterThan(new BigDecimal("20.00")); //guide_price > 20.00
    //添加or条件
    carExample.or()
            .andCarTypeEqualTo("燃油车");  //car_type = '燃油车'

    //执行查询
    List<Car> cars = carMapper.selectByExample(carExample);
    cars.forEach(System.out::println);

    sqlSession.close();
}

PageHelper

标准的通用 MySql 分页代码:

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

每次分页前端要给后端传递两个参数,一个是pageNum,另一个是pageSize

但是我们还要考虑到,什么时候允许用户查看上一页,什么时候允许用户查看下一页等问题,比较麻烦,所以我们利用 PageHelper 来帮助我们实现这些功能。

使用 PageHelper

引入依赖:

<!-- 引入pageHelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>

mybatis-config.xml添加拦截器:

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

在使用的时候,sql 语句直接进行查询即可:

<!-- 在使用插件的情况下,直接select * 即可,并且语句末尾不能加分号 -->
<select id="selectAll" resultType="Car">
    select
        *
    from
        t_car
</select>
@Test
public void testSelectAll() {
    SqlSession sqlSession = SqlSessionUtil.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
    //在执行DQL语句之前,开启分页功能
    int pageNum = 1;
    int pageSize = 2;
    PageHelper.startPage(pageNum, pageSize);
    //进行分页
    List<Car> list = carMapper.selectAll();
    list.forEach(System.out::println);
    sqlSession.close();
}

获取 PageInfo 对象

PageInfo 是封装分页相关的信息的对象。我们获取 PageInfo 对象的目的是为了给前端传递数据,PageInfo 对象将来会存储到 reques 域中,在页面上展示。

@Test
public void testSelectAll() {
    SqlSession sqlSession = SqlSessionUtil.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);

    //在执行DQL语句之前,开启分页功能
    int pageNum = 1;
    int pageSize = 2;
    PageHelper.startPage(pageNum, pageSize);

    //进行分页
    List<Car> list = carMapper.selectAll();

    //封装分页信息对象
    //5是分页导航卡片数
    PageInfo<Car> carPageInfo = new PageInfo<>(list, 5);
    System.out.println(carPageInfo);

    sqlSession.close();
}

注解式开发

在 MyBatis 中,简单的 sql 语句使用注解式开发,复杂的 sql 语句(包含各种标签)官方建议使用配置文件

在实际中,我们通常注解和配置混合开发。

@Insert

public interface CarMapper {
    @Insert("insert into " +
                "t_car " +
            "values " +
                "(null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType})")
    int insert(Car car);
}

@Delete

public interface CarMapper {
    @Delete("delete from t_car where id = #{id}")
    int deleteById(Integer id);
}

@Update

public interface CarMapper {
    @Update("update t_car set car_num = #{carNum} where id = #{id}")
    int update(Car car);
}

@Select

public interface CarMapper {
    @Select("select * from t_car where id = #{id}")
    Car selectById(Integer id);
}

@Results

该注解是用来进行命名映射的。

public interface CarMapper {
    @Select("select * from t_car where id = #{id}")
    @Results(id = "selectByIdMap", value = {
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "carNum", column = "car_num"),
            @Result(property = "brand", column = "brand"),
            @Result(property = "guidePrice", column = "guide_price"),
            @Result(property = "produceTime", column = "produce_time"),
            @Result(property = "carType", column = "car_type"),
    })
    Car selectById(Integer id);
   
    @Select("select * from t_car where id = #{id}")
    //使用ResultMap可以对之前的映射集进行复用
    @ResultMap("selectByIdMap")
    Car selectById2(Integer id);
}

dom4j 解析 XML 文件

引入依赖:

<dependencies>
    <!-- 引入dom4j组件,解析xml -->
    <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>2.1.3</version>
    </dependency>

    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.2.0</version>
    </dependency>

    <!-- 引入junit测试依赖 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

解析mybatis-config.xml

@Test
public void testParseMyBatisConfigXML() throws DocumentException {
    //创建SAXReader对象解析对象
    SAXReader reader = new SAXReader();

    //从类根路径下读XML文件
    InputStream is = ClassLoader
            .getSystemClassLoader()
            .getResourceAsStream("mybatis-config.xml");
    Document document = reader.read(is);

    //获取文档根标签
    //Element rootElement = document.getRootElement();

    //获取default默认的环境id
    String xpath = "/configuration/environments";  //xpath是做标签路径匹配的,帮助我们快速定位元素
    Element environments = (Element) document.selectSingleNode(xpath);
    //获取属性的值
    String defaultEnvironmentId = environments.attributeValue("default");

    //获取具体环境environment
    xpath = "/configuration/environments/environment[@id='" + defaultEnvironmentId + "']";
    Element environment = (Element) document.selectSingleNode(xpath);

    //获取environment内部的子标签,使用element获取孩子节点
    Element transactionManager = environment.element("transactionManager");
    String transactionType = transactionManager.attributeValue("type");
    System.out.println("事务管理的类型: " + transactionType);
    Element dataSource = environment.element("dataSource");
    String dataSourceType = dataSource.attributeValue("type");
    System.out.println("数据源的类型: " + dataSourceType);

    //获取dataSource下的所有子节点
    List<Element> propertyElements = dataSource.elements();
    //获取键和值
    propertyElements.forEach(ele -> {
        String name = ele.attributeValue("name");
        String value = ele.attributeValue("value");
        System.out.println(name + "=" + value);
    });

    //获取所有mapper标签
    //不想从根下获取,我们从任意位置获取,使用两个//
    xpath = "//mapper";
    List<Node> mappers = document.selectNodes(xpath);
    mappers.forEach(ele -> {
        Element mapperElement = (Element) ele;
        String resource = mapperElement.attributeValue("resource");
        System.out.println("resource: " + resource);
    });
}

解析 Mapper 文件:

@Test
public void testParseSqlMapperXML() throws DocumentException {
    SAXReader reader = new SAXReader();

    InputStream is = ClassLoader
            .getSystemClassLoader()
            .getResourceAsStream("CarMapper.xml");
    Document document = reader.read(is);

    //获取namespace
    String xpath = "/mapper";
    Element mapper = (Element) document.selectSingleNode(xpath);
    String namespace = mapper.attributeValue("namespace");
    System.out.println("namespace: " + namespace);

    //获取所有子标签
    List<Element> elements = mapper.elements();
    elements.forEach(ele -> {
        //获取sql语句id
        String id = ele.attributeValue("id");
        System.out.println("id: " + id);
        //获取resultType
        String resultType = ele.attributeValue("resultType");
        System.out.println("resultType: " + resultType);
        //获取sql语句,获取标签中的文本内容并且去除前后空白
        String sql = ele.getTextTrim();
        System.out.println("sql: " + sql);
        //将sql语句改成jdbc形式
        String jdbcSql = sql.replaceAll("#\\{[^}]*}", "?");
        System.out.println("jdbcSql: " + jdbcSql);
    });
}

文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录