Mybatis学习总结

一. 什么是Mybatis

  1. Mybatis是一款优秀的持久层框架,它支持定制SQL、存储过程以及高级映射。
  2. Mybatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。
  3. Mybatis可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJO映射成数据库中的记录。

二. 回顾JDBC连接数据库操作

package com.example.zhang;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import com.example.zhang.pojo.User;


public class JdbcTest {
    public static void main(String[] args) {

        PreparedStatement preparedStatement;
        ResultSet resultSet;

        try {
            // 1. 加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2. 获取数据库连接
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "root");
            // 3. 定义sql
            String sql = "select * from user where id = ?";
            // 4. 获取预处理statement
            preparedStatement = connection.prepareStatement(sql);
            // 5. 设置参数(序号从1开始)
            preparedStatement.setInt(1, 2);
            // 6. 执行sql
            resultSet = preparedStatement.executeQuery();

            // 7. 遍历结果集
            while (resultSet.next()) {
                User user = new User();
                user.setId(resultSet.getInt("id"));
                user.setName(resultSet.getString("name"));
                user.setPwd(resultSet.getString("pwd"));
                System.out.println(user);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

通过JDBC操作数据库的代码,可以看出有一些不好的地方:

  1. 在创建connection时,存在硬编码,也即是直接将连接信息写死,不方便后期维护
  2. sql语句存在硬编码问题,不利于维护,一旦SQL语句修改,需要对java重新编译
  3. 每进行一次数据库连接后都会关闭数据库连接,频繁开启/关闭数据库连接影响性能
  4. resultSet遍历结果集数据时,存在硬编码,将获取表的字段进行硬编码,不利于系统维护

而Mybatis就是来解决掉这些问题的。

Mybatis是将数据库连接信息写在配置文件中,不存在硬编码问题;

mybatis执行的sql语句也是通过配置文件进行配置,不需要写在Java代码中;

mybatis的连接池管理、缓存管理等让数据库和查询数据效率更高

三. 第一个MyBatis程序

  1. 创建测试表

    CREATE TABLE `user` (
        id INT(10) NOT NULL,
        NAME VARCHAR(20) DEFAULT NULL,
        pwd VARCHAR(20) DEFAULT NULL,
        PRIMARY KEY(`id`)
    )ENGINE=INNODB DEFAULT CHARSET=utf8;
    
    INSERT INTO USER (id, NAME, pwd) VALUES 
    (1, '张三', '123456'),
    (2, '李四', 'abcd'),
    (3, '王五', 'abc123');
    
  2. 导包

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.5</version>
    </dependency>
    
  3. 创建对应的javaBean

    package com.example.zhang.pojo;
    
    public class User {
        private int id;
        private String name;
        private String pwd;
    
    
        public int getId() {
            return this.id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPwd() {
            return this.pwd;
        }
    
        public void setPwd(String pwd) {
            this.pwd = pwd;
        }
    
        @Override
        public String toString() {
            return "{" +
                " id='" + getId() + "'" +
                ", name='" + getName() + "'" +
                ", pwd='" + getPwd() + "'" +
                "}";
        }
    
    }
    
    
  4. 创建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">
    <configuration>
        <environments default="development">
            <environment id="development">
                <!-- 使用jdbc事务管理 -->
                <transactionManager type="JDBC"/>
                <!-- 数据库连接池 -->
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
                    <property name="username" value="root"/>
                    <property name="password" value="root"/>
                </dataSource>
            </environment>
        </environments>
     
        <!-- 加载映射文件 -->
        <mappers>
            <mapper resource="com/example/zhang/dao/userMapper.xml"/>
        </mappers>
    </configuration>
    
  5. 编写Mybatis工具类

    package com.example.zhang.utils;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    // MyBatis工具类
    public class MyBatisUtils {
    
        private static SqlSessionFactory sqlSessionFactory;
    
        static {
            /*
             根据全局配置文件,利用SqlSessionFactoryBuilder创建SqlSessionFactory
            */
            try {
                String resource = "mybatis-config.xml";
                InputStream inputStream = Resources.getResourceAsStream(resource);
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
     
        /*
         使用SqlSessionFactory获取SqlSession对象。
         一个SqlSession对象代表和数据库的一次会话。
        */
        public static SqlSession getSqlSession() {
            return sqlSessionFactory.openSession();
        }
    }
    
  6. 编写DAO接口类(命名为Mapper)

    package com.example.zhang.dao;
    
    import java.util.List;
    
    import com.example.zhang.pojo.User;
    
    public interface UserMapper {
        List<User> selectUser();
    }
    
    
  7. 编写Mapper的xml配置文件(以前是写DAO接口实现类,现在不需要,只写对应的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">
    <!-- 命名空间:对sql进行分类管理,namespace必须唯一 -->
    <mapper namespace="com.example.zhang.dao.UserMapper">
        <select id="selectUser" resultType="com.example.zhang.pojo.User">
            select * from user
        </select>
    </mapper>
    
  8. 编写测试类

    package com.example.zhang;
    
    import java.util.List;
    
    import com.example.zhang.dao.UserMapper;
    import com.example.zhang.pojo.User;
    import com.example.zhang.utils.MyBatisUtils;
    
    import org.apache.ibatis.session.SqlSession;
    import org.junit.jupiter.api.Test;
    
    
    /**
     * Unit test for simple App.
     */
    public class AppTest {
        @Test
        public void selectUserTest() {
            SqlSession session = MyBatisUtils.getSqlSession();
    
            // 方法一 不推荐
            // List<User> users = session.selectList("com.example.zhang.dao.UserMapper.selectUser");
    
            // 方法二
            UserMapper mapper = session.getMapper(UserMapper.class);
            List<User> users = mapper.selectUser();
    
            for (User user : users) {
                System.out.println(user);
            }
    
            session.close();
        }
    }
    
  9. 说明

    • SQLSession的实例不是线程安全的,因此是不能被共享的
    • SQLSession每次使用完成后需要正确的关闭,这个关闭操作是必须的
    • 推荐使用SqlSession获取到DAO接口的代理类,执行代理对象的方法,可以更安全的进行类型检查操作(也即是上面测试类的第二种方式)

四. MyBatis全局核心配置文件

MyBatis的配置文件包含了影响MyBatis行为甚深的设置(settings)和属性(properties)信息。

能配置的内容如下:(元素节点必须按顺序,否则会报错

  • configuration配置
    • properties 属性
    • settings 设置
    • typeAliases 类型命名
    • typeHandlers 类型处理器
    • objectFactory 对象工厂
    • plugins 插件
    • environments 环境
      • envionment 环境变量
        • transactionManager 事务管理器
        • dataSource 数据源
    • databaseIdProvider 数据库厂商标识
    • mappers 映射器
4.1 properties属性
prop.driver=com.mysql.jdbc.Driver
prop.url=jdbc:mysql:///mybatis
prop.username=root
prop.password=root

数据库连接参数可以在外部进行配置,并可以进行动态替换。

我们既可以在典型的Java属性文件中配置这些属性(就是db.properties)

<!-- 引入外部属性文件 -->
<properties resource="db.properties"></properties>

也可以在外部属性文件的基础上,在properties元素的子元素中设置

<!-- properties 元素的子元素中设置 -->
<properties resource="db.properties">
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</properties>

最后这些设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值,比如

<dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
</dataSource>

还可以在SqlSessionFactoryBuilder.build()方法中传入属性值,例如:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);

// ... 或者 ...

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);

如果属性在不只一个地方进行了配置,那么Mybatis将按照下面的顺序来加载:

  1. 在properties元素体内指定的属性先被读取
  2. 然后根据properties元素中的resource属性读取类路径下属性文件,或根据url属性指定的路径读取属性文件,并覆盖已读取的同名属性
  3. 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性
4.2 settings属性

settings属性设置是Mybatis中极为重要的,它们会改变Mybatis的运行时行为

设置名 描述 有效值 默认值
cacheEnabled 该配置影响所有映射器中配置的缓存的全局开关 true|false true
lazyLoadingEnabled 延迟加载的全局开关,当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态 true|false false
mapUnderscoreToCamelCase 是否开启自动驼峰命名规则映射,即从经典数据库列名A_COLUMN到经典Java属性名aColumn的类似映射 true|false false
logImpl 指定Mybatis缩影日志的具体实现,未指定时将自动查找 SLF4J |LOG4J |LOG4J2 |JDK_LOGGING |COMMONS_LOGGING |STDOUT_LOGGING |NO_LOGGING 未设置
<settings>
    <setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
4.3 typeAliases别名处理器
  1. 类型别名是为Java类型设置一个短的名字,可以方便我们引用某个类

    它仅用于XML配置,存在的意义在于降低冗余的全限定类名书写。比如:

    <typeAliases>
     <typeAlias type="com.example.zhang.Employee" alias="employee"/>
        <typeAlias type="com.example.zhang.Department" alias="department"/>
    </typeAliases>
    

    当这样配置后,employeedepartment可以用在任何使用com.example.zhang.Employeecom.example.zhang.Department的地方

  2. 在类很多的情况下,可以批量设置别名,为这个包下的每一个类创建一个默认的别名,默认是简单类名小写,若有注解@Alias,则别名为注解值

    <typeAliases>
     <package name="com.example.zhang" />
    </typeAliases>
    
    @Alias("hello")
    public class Employee {
        ...
    }
    

    若是这样,Employee类的别名就是hello。

  3. 需要注意的是,Mybatis已经为许多常见的Java类型内建了相应的别名,它们都是大小写不敏感的,我们在起别名时不可占用已有的别名

    mybatis1.png
4.4 typeHandlers 类型处理器(了解即可)
**无论是Mybatis在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成Java类型**。

我们也可以重写类型处理器或创建自己的类型处理器来处理不支持的或非标准的类型。
4.5 对象工厂(objectFactory)(了解即可)
Mybatis每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。

默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法来实例化。

如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。
4.6 plugins 插件
插件是Mybatis提供的一个非常强大的机制,我们可以通过插件来修改Mybatis的一些核心行为。**插件通过动态代理机制**,可以介入四大对象的任何一个方法的执行。(以后再学)
  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)
4.7 environments 环境配置
  1. MyBatis可以配置多种环境,比如开发,测试和生产环境需要有不同的配置
  2. 每种环境使用一个environment标签进行配置并指定唯一标识符
  3. 可以通过environments标签中的default属性指定一个环境的标识符来快速切换环境
4.7.1 environment -指定具体环境
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </dataSource>
    </environment>
</environments>
  • id:指定当前环境的唯一标识
  • transactionManager:事务管理器
    • type:JDBC | MANAGED
      • JDBC:使用JDBC的提交和回滚设置,依赖于从数据源得到的连接来管理事务范围
      • MANAGED:不提交或回滚一个连接,让容器来管理事务的整个生命周期
  • dataSource:数据库连接源
    • type:UNPOOLED | POOLED | JNDI
      • UNPOOLED:不使用连接池
      • POLLED:使用连接池
      • JNDI:在EJB或应用服务器这类容器中查找指定的数据源

实际开发中,我们使用Spring管理数据源,并进行事务控制的配置来覆盖上述配置

4.8 mappers 映射器
在自动查找资源方面,Java并没有提供一个很好的解决方案,所以最好的方法是直接告诉Mybatis去哪里找映射文件,mappers映射器就是用来定义映射SQL语句文件的。

我们可以使用相对于类路径的资源引用,或完全限定资源定位符(包括`file:///`形式的URL),或类名和包名。

使用相对于类路径的资源引用:

<mappers>
    <mapper resource="com/example/zhang/dao/userMapper.xml"/>
</mappers>

使用完全限定资源定位符(URL):

<mappers>
    <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>

使用映射器接口实现类的完全限定类名(需要配置文件名称和接口名称一致,并且位于同一目录下):

<mappers>
    <mapper class="com.example.zhang.dao.UserMapper"/>
</mappers>

将包内的映射器接口实现全部注册为映射器(需要配置文件名称和接口一致,并且位于统一目录下):

<mappers>
    <package name="com.example.zhang.dao" />
</mappers>

五.Mapper映射文件

映射文件指导着Mybatis如何进行数据库增删改查,有着非常重要的意义。

SQL映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

  • cache:命名空间的二级缓存配置
  • cache-ref:其他命名空间缓存配置的引用
  • resultMap:自定义结果集映射
  • parameterMap:已废弃!老式风格的参数映射
  • sql:抽取可重用语句块
  • insert:映射插入语句
  • update:映射更新语句
  • delete:映射删除语句
  • 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="com.example.zhang.dao.UserMapper">
    <select id="selectUser" resultType="com.example.zhang.pojo.User">
        select * from user
    </select>
</mapper>

namespace:命名空间,必须跟某个接口同名,接口中的方法与映射文件中sql语句id应该一一对应。

5.1 select
  • select标签是mybatis中最常用的标签之一

  • select语句有很多属性可以详细配置每一条SQL语句

    mybatis-select.png

    重点关注几个:

    • id:唯一标识符,用来引用这条语句,需要和接口的方法名一致
    • parameterType:参数类型,可以不传,Mybatis会根据TypeHandler自动推断
    • resultType:返回值类型,别名或者全类名,如果返回的是集合,定义集合中元素的类型,不能和resultMap同时使用

需求:根据id查找用户

  1. 在接口类UserMapper中添加对应的接口方法

    public interface UserMapper {
        List<User> selectUser();
    
        // 根据ID查找用户
        User findUserById(int id);
    }
    
  2. 在映射文件UserMapper.xml中添加select语句

    <mapper namespace="com.example.zhang.dao.UserMapper">
        <select id="selectUser" resultType="com.example.zhang.pojo.User">
            select * from user
        </select>
    
        <!-- 根据id查找用户 -->
        <select id="findUserById" resultType="com.example.zhang.pojo.User">
            select * from user where id = #{id}
        </select>
    
    </mapper>
    
  3. 编写测试代码

    @Test
    public void findUserByIdTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.findUserById(2);
    
        System.out.println(user);
        session.close();
    }
    
5.2 insert,update和delete

数据变更语句insert,update和delete的实现非常接近,和select使用差不多。

mybatis-insert.png

需求一:增加一个用户

  1. 在UserMapper接口中新增对应的方法

    public interface UserMapper {
        List<User> selectUser();
    
        // 根据ID查找用户
        User findUserById(int id);
    
        // 新增一个用户
        int addUser(User user);
    
    }
    
  2. 在映射文件UserMapper.xml中添加insert语句

    <!-- 新增一个用户 -->
    <insert id="addUser" parameterType="com.example.zhang.pojo.User">
        insert into user values(#{id}, #{name}, #{pwd})
    </insert>
    
  3. 编写测试代码

    @Test
    public void addUserTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = new User(5, "老虎", "abcd123");
    
        userMapper.addUser(user);
    
        session.commit();
        session.close();
    }
    

    注意增,删,改操作需要提交事务。

需求二:修改用户信息

  1. 在UserMapper接口中新增对应的方法

    public interface UserMapper {
        List<User> selectUser();
    
        // 根据ID查找用户
        User findUserById(int id);
    
        // 新增一个用户
        int addUser(User user);
    
        // 修改用户信息
        int updateUser(User user);
    }
    
  2. 在映射文件UserMapper.xml中添加update语句

    <!-- 修改用户信息 -->
    <update id="updateUser" parameterType="com.example.zhang.pojo.User">
        update user set name = #{name}, pwd = #{pwd} where id = #{id}
    </update>
    
  3. 编写测试代码

    @Test
    public void updateUserTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = new User(5, "大熊猫", "123456");
    
        userMapper.updateUser(user);
        session.commit();
        session.close();
    }
    

    注意增,删,改操作需要提交事务。

需求三:删除指定ID的用户

  1. 在UserMapper接口中新增对应的方法

    public interface UserMapper {
        List<User> selectUser();
    
        // 根据ID查找用户
        User findUserById(int id);
    
        // 新增一个用户
        int addUser(User user);
    
        // 修改用户信息
        int updateUser(User user);
    
        // 删除指定用户
        int deleteUserById(int id);
    }
    
  2. 在映射文件UserMapper.xml中添加delete语句

    <!-- 删除指定Id的用户 -->
    <delete id="deleteUserById">
        delete from user where id = #{id}
    </delete>
    
  3. 编写测试代码

    @Test
    public void deleteUserByIdTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        userMapper.deleteUserById(5);
    
        session.commit();
        session.close();
    }
    

    注意增,删,改操作需要提交事务。

以上增删改操作都需要提交事务才生效,每次都是要在最后加上一句session.commit().

也可以直接在openSession时候默认自动提交事务,修改MyBatisUtils工具类:在openSession传递参数true

public static SqlSession getSqlSession() {
    return sqlSessionFactory.openSession(true);
}

六. 参数传递

  1. 单个参数

    可以接受基本类型,对象类型,集合类型的值,这种情况Mybatis可以直接使用这个参数,不需要经过任何处理

  2. 多个参数

    任意多个参数,都会被Mybatis重新包装成一个Map传入,Map的key就是param1,param2...,值就是参数的值

  3. 命名参数

    可以为参数使用@Param起一个名字,Mybatis就会将这些参数封装金Map中,key就是我们自己指定的名字

  4. Map(万能的map)

    我们也可以封装多个参数为map,直接传递一个map

七. 结果映射(初级)

  • resultMap元素是Mybatis中最重要最强大的元素,它可以让你从90%的JDBCResultSets数据提取代码中解放出来。
  • ResultMap的设计事项时,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
  • resultMap就是用来解决结果集属性名与数据库列名不一致导致获取不到数据的问题。

下面来看一个例子:

  1. 数据库中三个字段属性名分别为:id,name,pwd

  2. Java中实体类设计如下:

    public class Customer {
        private int id;
        private String name;
        private String password;
         
        // 构造函数,get/set ,toString()省略 ...
    }
    
  3. 接口方法

    public interface CustomerMapper {
        Customer selectCustomerById(int id);
    }
    
  4. mapper映射文件

    <mapper namespace="com.example.zhang.dao.CustomerMapper">
        <select id="selectCustomerById" resultType="com.example.zhang.pojo.Customer">
            select * from user where id = #{id}
        </select>
    </mapper>
    
  5. 测试结果

    mybatis-resultmap.png

    查询结果发现password为空,说明出现问题

    分析:

    sql语句:select * from user where id = #{id} 实质上可以看做select id,name,pwd from user where id = #{id},mybatis会根据这些查询的列名(列名会转化为小写,数据库不区分大小写)去对应的实体类中查找相应列名的set方法设置值,由于找不到setPwd(),所以password返回null。

    这种就是数据库列名与java类属性名不一致的表现,有这两种解决方案:

    • 使用别名,为列名指定别名,别名和java类属性名一致

      <select id="selectCustomerById" resultType="com.example.zhang.pojo.Customer">
          select id,name,pwd as password from user where id = #{id}
      </select>
      
    • 使用结果集映射ResultMap

      <resultMap id="CustomerMap" type="com.example.zhang.pojo.Customer">
          <!-- id为主键 -->
          <id column="id" property="id"/>
          <!-- column是数据库表的列名,property是对应实体类的属性名 -->
          <result column="name" property="name"/>
          <result column="pwd" property="password"/>
      </resultMap>
      
      <select id="selectCustomerById" resultMap="CustomerMap">
          <!-- select id,name,pwd as password from user where id = #{id} -->
          select * from user where id = #{id}
      </select>
      

八. 日志

我们在开发过程中,如果出现了异常需要排错,如果有日志,能帮助我们快速定位问题。

Mybatis通过使用内置的日志工程提供日志功能,内置的日志工程有以下几种实现:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

Mybatis具体选择哪个日志实现工具是由mybatis的内置日志工程确定的,它会使用最先找到的(按以上的顺序查找),如果一个都没找到,就会禁用日志功能

不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging。注意,在这种配置环境下,MyBatis 会把 Commons Logging 作为日志工具。这就意味着在诸如 WebSphere 的环境中,由于提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。这个时候你就会感觉很郁闷:看起来 MyBatis 将你的 Log4J 配置忽略掉了(其实是因为在这种配置环境下,MyBatis 使用了 Commons Logging 作为日志实现)。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志实现,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择其它日志实现。

<configuration>
    <settings>
        ...
        <setting name="logImpl" value="LOG4J"/>
        ...
    </settings>
</configuration>

logImpl可选的值有:

  • SLF4J
  • LOG4J(掌握)
  • LOG4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING
  • NO_LOGGING

在Mybatis中具体使用哪一个日志实现,在设置settings中设置

  1. 标准日志实现

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

    运行测试程序,终端会输出日志:

    mybatis-log.png
  2. Log4j

    简介

    • Log4j是Apache的一个开源项目
    • 通过使用Log4j,我们可以控制日志信息输送的目的地,可以是控制台,文本,GUI组件等
    • 我们也可以控制每一条日志的输出格式
    • 通过定义每一条日志信息的级别,能够更加细致地控制日志的生成过程。
    • 通过配置文件灵活的进行配置,不需要修改应用的代码

    使用步骤:

    • 导入Log4j的包

      <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
      </dependency>
      
    • 编写配置文件

      #将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
      log4j.rootLogger=DEBUG,console,file
      
      #控制台输出的相关设置
      log4j.appender.console = org.apache.log4j.ConsoleAppender
      log4j.appender.console.Target = System.out
      log4j.appender.console.Threshold=DEBUG
      log4j.appender.console.layout = org.apache.log4j.PatternLayout
      log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
      
      #文件输出的相关设置
      log4j.appender.file = org.apache.log4j.RollingFileAppender
      log4j.appender.file.File=./log/zhang.log
      log4j.appender.file.MaxFileSize=10mb
      log4j.appender.file.Threshold=DEBUG
      log4j.appender.file.layout=org.apache.log4j.PatternLayout
      log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
      
      #日志输出级别
      log4j.logger.org.mybatis=DEBUG
      log4j.logger.java.sql=DEBUG
      log4j.logger.java.sql.Statement=DEBUG
      log4j.logger.java.sql.ResultSet=DEBUG
      log4j.logger.java.sql.PreparedStatement=DEBUG
      
    • setting设置日志实现为LOG4J

      <settings>
          <!-- <setting name="logImpl" value="STDOUT_LOGGING" /> -->
          <setting name="logImpl" value="LOG4J" />
      </settings>
      
    • 测试程序中使用Log4j

      public class AppTest {  
          // 参数为当前类
         static Logger logger = Logger.getLogger(AppTest.class);
      
          @Test
          public void findUserByIdTest() {
              logger.info("info: 进入findUserByIdTest方法");
              logger.debug("debug: 进入findUserByIdTest方法");
              logger.error("error: 进入findUserByIdTest方法");
              SqlSession session = MyBatisUtils.getSqlSession();
      
              UserMapper userMapper = session.getMapper(UserMapper.class);
              User user = userMapper.findUserById(2);
      
              System.out.println(user);
              session.close();
          }
      }
      

      输出结果如下:

      mybatis-log4j.png

      不仅终端生成了日志信息,还生成了一个日志文件,在指定路径里./log/zhang.log

九. Mybatis分页实现

思考:为什么需要分页?

在使用数据库时,会经常对数据库进行增删改查操作,其中使用最多的是查询操作,查询大量数据的时候,我们往往使用分页查询,也就是每次处理小部分数据,这样对数据库压力就在可控范围内。

1. 使用Limit实现分页

语法:

select * from table limit startIndex, pageSize

select * from table limit 5, 10;    # 检索记录行 6-15

# 为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为-1
select * from table limit 95, -1;   # 检索记录行 96 - 末尾

# 如果只给定一个参数,它表示返回最大的记录行数目
select * from table limit 5; # 检索前5个记录行

# 相当于 limit n 等价于 limit 0, n

操作步骤:

  1. 添加接口方法,参数为map

    public interface UserMapper {
        // 查找用户
        List<User> selectUser(Map<String,String> map);
    }
    
  2. 修改Mapper文件

    <select id="selectUser" parameterType="map" resultType="com.example.zhang.pojo.User">
        select * from user limit #{startIndex}, #{pageSize}
    </select>
    
  3. 编写测试方法

    推断:起始位置 = (当前页面 -1) 页面大小*

    @Test
    public void testSelectUser() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
    
        int currentPage = 2;    // 第几页
        int pageSize = 2;       // 每页显示几个
        Map<String,Integer> map = new HashMap<String,Integer>();
        map.put("startIndex", (currentPage -1) * pageSize);
        map.put("pageSize", pageSize);
    
        List<User> users = userMapper.selectUser(map);
    
        for (User u:users) {
            System.out.println(u);
        }
        session.close();
    }
    

2. RowBounds分页(了解即可)

使用RowBounds在Java代码层面实现分页

  1. 接口方法

    List<User> getUserByRowBounds();
    
  2. Mapper文件

    <select id="getUserByRowBounds" resultType="com.example.zhang.pojo.User">
        select * from user
    </select>
    
  3. 测试方法

    @Test
    public void testUserByRowBounds() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        int currentPage = 2;    // 第几页
        int pageSize = 2;       // 每页显示几个
    
        RowBounds rowBounds = new RowBounds((currentPage -1) * pageSize, pageSize);
    
        // 通过session.* 方法传递rowBounds, 这种方法已经不推荐使用了
        List<User> users = session.selectList("com.example.zhang.dao.UserMapper.getUserByRowBounds", null, rowBounds);
    
        for (User u:users) {
            System.out.println(u);
        }
        session.close();
    }
    

3. PageHelper

这是一款Mybatis的分页插件,官网文档为:https://pagehelper.github.io/
mybatis-pagehelper.png

十. 注解开发

Mybatis最初配置信息是基于XML的,映射语句SQL也是定义在XML中,但到了Mybatis 3提供了新的基于注解的配置,但Java注解的表达力和灵活性十分有限,并不能完全用注解来构建。

注解方式主要用于SQL,主要有几个注解:
  • @Select()
  • @Update()
  • @Insert()
  • @Delete()

注意:使用注解开发就不需要mapper.xml映射文件了

使用方法:

  1. 在接口方法中添加注解

    @Select("select * from user")
    List<User> selectUser();
    
  2. mybatis核心配置文件,使用class绑定接口(不需要mapper.xml文件了)

    <mappers>
        <!-- <mapper resource="com/example/zhang/dao/userMapper.xml"/> -->
        <mapper class="com.example.zhang.dao.UserMapper"/>
    </mappers>
    
  3. 测试代码,与之前一样

    @Test
    public void selectUserTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = session.getMapper(UserMapper.class);
        List<User> users = mapper.selectUser();
    
        for (User user : users) {
            System.out.println(user);
        }
    
    }
    

注解方式实现增删改查:

注意增删改需要提交事务

  1. 编写接口方法

    // 根据ID查找用户
    @Select("select * from user where id = #{id}")
    @Results(id = "userMap", value = {
        @Result(id=true,column = "id",property = "id"),
        @Result(column = "name",property = "name"),
        @Result(column = "pwd", property = "password")
    })
    User findUserById(@Param("id")int id);
    
    // 新增一个用户
    @Insert("insert into user (id,name,pwd) values(#{id},#{name},#{password})")
    @ResultMap(value = {"userMap"})
    int addUser(User user);
    
    // 修改用户信息
    @Update("update user set name=#{name},pwd=#{password} where id = #{id} ")
    int updateUser(User user);
    
    // 删除指定用户
    @Delete("delete from user where id = #{id}")
    int deleteUserById(int id);
    

    这里特意将数据库列名pwd与实体类属性名password设置为不一致,在XML配置中我们可以使用resultMap结果集映射的方式解决,但在注解方式中,就得使用注解@Results,@ResultMap,@Result实现了

    @Results()注解

    有两个常用的参数,一个是id,一个是value。

    id的作用在于唯一标记这个Results注解,在其他接口抽象方法中需要通过@Results注解来解决 属性名与字段名不一致问题时,能被引用。

    value的作用在于使用@Result注解建立实体类与数据库表映射关系,可以写多个@Result,如果是主键字段,@Result注解中需要设置id=true,比如上面例子的id是主键,column代表数据库字段名,property代表实体类属性名

    @ResultMap()注解:

    用于引用@Results注解,value值为数据,是@Results注解的id值

  2. 测试方法

    @Test
    public void findUserByIdTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.findUserById(2);
    
        System.out.println(user);
        session.close();
    }
    
    @Test
    public void addUserTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = new User(9, "华南虎", "abcd123");
    
        userMapper.addUser(user);
    
        session.close();
    }
    
    @Test
    public void updateUserTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = new User(5, "大熊猫", "123456789");
    
        userMapper.updateUser(user);
        session.commit();
        session.close();
    }
    
    @Test
    public void deleteUserByIdTest() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        userMapper.deleteUserById(5);
    
        session.commit();
        session.close();
    }
    

关于注解@Param

@Param注解用于给方法参数起一个名字,以下是使用原则总结:

  • 在方法只接受一个参数的情况下,可以不适用@Param
  • 在方法接受多个参数的情况下,建议一定要用@Param注解给参数命名
  • 如果参数是JavaBean,则不能使用@Param
  • 不使用@Param注解时,参数只能有一个,并且是JavaBean

# 与 $ 的区别

  • #{}的作用主要是替换预编译语句(PrepareStatement)中的占位符? (推荐使用)
  • ${}的作用是直接进行字符串替换

总结

**使用注解和配置文件协同开发,才是Mybatis的最佳实践!**

十一. Mybatis执行流程

Mybatis使用Mapper调用接口方法,本质是反射机制实现,底层使用动态代理

Mybatis详细的执行流程:

mybatis-liucheng.png

十二. 结果映射(高级)

1. 多对一处理

多对一的理解:

  • 多个学生对应一个老师
  • 对于学生这边而言,是关联,多个学生关联一个老师(多对一)
  • 对于老师而言,是集合,一个老师有很多学生(一对多)

(1)设计数据库:

# 老师表
CREATE TABLE teacher (
id INT(10) NOT NULL,
NAME VARCHAR(30) DEFAULT NULL,
PRIMARY KEY(id)
)ENGINE=INNODB DEFAULT CHARSET=utf8

INSERT INTO teacher(id,NAME) VALUE(1,'张老师');

# 学生表
CREATE TABLE student (
id INT(10) NOT NULL,
NAME VARCHAR(30) DEFAULT NULL,
tid INT(10) DEFAULT NULL,
PRIMARY KEY(id),
KEY fktid(tid) ,
CONSTRAINT fktid FOREIGN KEY(tid) REFERENCES teacher(id)
)ENGINE=INNODB DEFAULT CHARSET=utf8

INSERT INTO student(id, NAME, tid) VALUES(1, '小明', 1);
INSERT INTO student(id, NAME, tid) VALUES(2, '小红', 1);
INSERT INTO student(id, NAME, tid) VALUES(3, '小米', 1);
INSERT INTO student(id, NAME, tid) VALUES(4, '小李', 1);
INSERT INTO student(id, NAME, tid) VALUES(5, '小王', 1);

两个表的对应关系:

mybatis-多对一.png

(2)添加实体类:

public class Teacher {
    private int id;
    private String name;

    // 构造器,get/set方法,toString 省略...
}
public class Student {
    private int id;
    private String name;

    // 多个学生可以是同一个老师,多对一
    private Teacher teacher;
    // 构造器,get/set方法,toString 省略...
}

(3)编写实体类对应的Mapper接口类

无论有没有需求,都应该写上,以备后来之需

public interface TeacherMapper {
    
}
public interface StudentMapper {
    
}

(4) 编写对应的Mapper.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="com.example.zhang.dao.StudentMapper">
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.zhang.dao.TeacherMapper">
</mapper>

(5) 按查询嵌套处理(子查询)

  • 给StudentMapper接口添加方法

    public interface StudentMapper {
        // 获取所有学生及对应老师信息
        List<Student> getStudents();
    }
    
  • 编写对应的Mapper文件(记得要在mybatis-config.xml中注册Mapper)

    <mapper namespace="com.example.zhang.dao.StudentMapper">
        <!--
        获取所有学生及对应老师的信息
        思路:
            1.获取所有学生的信息
            2.根据获取的学生信息的老师ID -> 再去获取老师的信息
            3.思考问题:这样学生的结果集中应该包含老师,该如何处理?
                        在数据库中一般使用关联查询?
                1.做一个结果集映射StudentTeacher
                2.StudentTeacher结果集的类型是Student
                3.学生中老师的属性为teacher,对应数据库中为tid
                4.Mybatis官网使用association 处理关联查询
        -->
        <select id="getStudents" resultMap="StudentTeacher">
            select * from student
        </select>
    
        <resultMap id="StudentTeacher" type="com.example.zhang.pojo.Student">
            <result property="id" column="id"/>
            <result property="name" column="name"/>
            <!-- 复杂的属性,需要单独处理,对象使用association select子查询-->
            <association property="teacher" column="tid" javaType="com.example.zhang.pojo.Teacher" select="getTeacher"></association>
        </resultMap>
    
        <!-- 关联通过getTeacher查询的结果 -->
        <select id="getTeacher" resultType="com.example.zhang.pojo.Teacher">
            select * from teacher where id = #{id}
        </select>
    </mapper>
    
  • 测试

    @Test
    public void test3() {
        SqlSession session = MyBatisUtils.getSqlSession();
        StudentMapper studentMapper = session.getMapper(StudentMapper.class);
    
        List<Student> students = studentMapper.getStudents();
    
        for (Student s: students) {
            System.out.println(s);
        }
        session.close();
    }
    

(6)按结果嵌套处理(联表查询)

  • 接口方法

    List<Student> getStudents1();
    
  • Mapper文件

    <!-- 
            按查询结果嵌套处理
            思路:直接查出结果,进行结果集的映射
        -->
    <select id="getStudents1" resultMap="StudentTeacher1">
        select s.id sid, s.name sname, t.name tname from student s, teacher t where s.tid = t.id
    </select>
    <resultMap id="StudentTeacher1" type="com.example.zhang.pojo.Student">
        <id property="id" column="sid"/>
        <result property="name" column="sname"/>
        <association property="teacher" javaType="com.example.zhang.pojo.Teacher">
            <result property="name" column="tname"/>
        </association>
    </resultMap>
    

(7)小结

按照查询进行嵌套处理就像SQL中的子查询

按照结果进行嵌套处理就像SQL中的联表查询

2. 一对多处理

一对多的理解:

  • 一个老师拥有多个学生
  • 对于老师这边,就是一个一对多的现象,即从一个老师下面有一群学生(集合)

按查询嵌套处理(子查询)

(1) 实体类编写

public class Teacher {
    private int id;
    private String name;

    private List<Student> students;
    // 构造器,get/set方法,toString 省略...
}
public class Student {
    private int id;
    private String name;
    private int tid;
    // 构造器,get/set方法,toString 省略...
}

(2) TeacherMapper接口编写方法

public interface TeacherMapper {
    // 获取指定老师,以及老师下的所有学生
    public Teacher getTeacher(int id);
}

(3) 编写接口对应的Mapper文件

<!-- 按照查询嵌套处理 -->
<select id="getTeacher" resultMap="TeacherStudent">
    select * from teacher where id = #{id}
</select>
<!-- 因为查询的结果是返回一个Teacher,所以type是Teacher 通过老师id查找student 
    collection里的colum是将父查询的数据传递给子查询语句的参数-->
<resultMap id="TeacherStudent" type="com.example.zhang.pojo.Teacher">
    <collection property="students" javaType="ArrayList" ofType="com.example.zhang.pojo.Student"
                column="id" select="getStudentByTeacherId"></collection>
</resultMap>

<!-- 一个子查询,返回是Student -->
<select id="getStudentByTeacherId" resultType="com.example.zhang.pojo.Student">
    select * from student where tid = #{id}
</select>

(4)测试

@Test
public void test4() {
    SqlSession session = MyBatisUtils.getSqlSession();
    TeacherMapper teacherMapper = session.getMapper(TeacherMapper.class);

    Teacher teacher = teacherMapper.getTeacher(1);

    System.out.println(teacher);

    session.close();
}

按结果嵌套处理(联表查询)

(1)编写接口对应的Mapper文件

<!-- 
        思路:
            1.从学生表和老师表查出学生id,学生姓名,老师姓名
            2.对查询出来的操作做结果集映射
                集合的话使用collection
                JavaType和ofType都是用来指定对象类型的
                JavaType用来指定pojo中属性的类型
                ofType用来指定映射到list集合属性中pojo的类型
    -->
<select id="getTeacher" resultMap="TeacherStudent">
    select s.id sid,s.name sname, t.name tname,t.id tid from student s,teacher t where s.tid = t.id and t.id = #{id}
</select>
<resultMap id="TeacherStudent" type="com.example.zhang.pojo.Teacher">
    <result property="id" column="tid"/>
    <result property="name" column="tname"/>
    <collection property="students" ofType="com.example.zhang.pojo.Student">
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <result property="tid" column="tid"/>
    </collection>
</resultMap>

十三. 动态SQL

  1. 介绍

    什么是动态SQL?

    动态SQL指的是根据不同的查询条件,生成不同的SQL语句

    引用官方描述:

       MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
       虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。
       动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。
       MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。
       MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。
    
      -------------------------------
      - if
      - choose (when, otherwise)
      - trim (where, set)
      - foreach
      -------------------------------
    
  2. 搭建环境

    • 创建数据库

      CREATE TABLE blog (
          id VARCHAR(50) NOT NULL COMMENT '博客id',
          title VARCHAR(100) NOT NULL COMMENT '博客标题',
          author VARCHAR(30) NOT NULL COMMENT '博客作者',
          create_time DATETIME NOT NULL COMMENT '创建时间',
          views INT(30) NOT NULL COMMENT '浏览量'
      )ENGINE=INNODB DEFAULT CHARSET=utf8
      
    • 创建IDUtils工具类(用来生产博客id)

      import java.util.UUID;
      
      public class IDUtils {
      
          public static String genId() {
              return UUID.randomUUID().toString().replaceAll("-", "");
          }
      }
      
    • 编写实体类

      public class Blog {
          private String id;
          private String title;
          private String author;
          private Date createTime;
          private int views;
          // 省略get/set toString()犯法
          // ...
      }
      

      注意这里的属性createTime与数据库的列create_time不是一致!后面做处理

    • 编写Mapper接口类,添加一个方法,新增博客,用于初始化

      public interface BlogMapper {
      
          // 新增一个博客
          public int addBlog(Blog blog);
      }
      
    • 编写对应的Mapper.xml

      <mapper namespace="com.example.zhang.dao.BlogMapper">
          <insert id="addBlog">
              insert into blog(id, title, author, create_time, views)
              values(#{id}, #{title}, #{author}, #{createTime}, #{views});
          </insert>
      </mapper>
      
    • mybatis-config核心配置文件,增加设置:下划线驼峰自动转换,用来解决上面数据库列名create_time和实体类属性名createTime不一致问题,会自动转换。

      注册mapper文件

      <setting name="mapUnderscoreToCamelCase" value="true" />
      ....
      <mapper class="com.example.zhang.dao.BlogMapper"/> 
      
    • 因为ID使用了随机的UUID生产,这里先初始化一些数据库数据

      @Test
      public void addBlogTest() {
          SqlSession session = MyBatisUtils.getSqlSession();
      
          BlogMapper blogMapper = session.getMapper(BlogMapper.class);
      
          Blog blog = new Blog();
          blog.setId(IDUtils.genId());
          blog.setAuthor("张三");
          blog.setTitle("Mybatis如此简单");
          blog.setCreateTime(new Date());
          blog.setViews(9999);
      
          blogMapper.addBlog(blog);
      
          blog.setId(IDUtils.genId());
          blog.setTitle("Java如此简单");
          blogMapper.addBlog(blog);
      
          blog.setId(IDUtils.genId());
          blog.setTitle("Spring如此简单");
          blogMapper.addBlog(blog);
      
          blog.setId(IDUtils.genId());
          blog.setTitle("Python如此简单");
          blogMapper.addBlog(blog);
      
      
          session.close();
      }
      
  3. if 标签

    需求:根据作者名字和博客名字来查询博客,如果作者名字为空,那么只根据博客名字查询,反之,则根据作者名来查询

    • 编写接口类

      // 使用万能Map作为传参
      List<Blog> queryBlogIf(Map map);
      
    • 编写SQL语句

      <select id="queryBlogIf" parameterType="map" resultType="com.example.zhang.pojo.Blog">
          select * from blog where
          <if test="title != null">
              title = #{title}
          </if>
          <if test="author != null">
              and author = #{author}
          </if>
      </select>
      
    • 测试代码

      @Test
      public void testQueryBlogIf() {
          SqlSession session = MyBatisUtils.getSqlSession();
      
          BlogMapper blogMapper = session.getMapper(BlogMapper.class);
          HashMap<String, String> map = new HashMap<>();
      
          map.put("title", "Mybatis如此简单");
          map.put("author", "张三");
      
          List<Blog> blogs = blogMapper.queryBlogIf(map);
          System.out.println(blogs);
          session.close();
      }
      

    分析:

    上面查询结果是两个条件都提供了,title和author,也能正确查询出结果。

    但是只用了if语句,再看这个SQL语句,如果author为null,title不为null,那查询语句就会变成:select * from blog where title = #{title},刚好可以执行。

    如果title为null的情况下呢?那查询语句就变成了select * from blog where and author = #{author},这就是错误的sql语句了,显然这样子只依赖if语句是不行的,需要配合另一个where语句,下面讲解。

  4. where 标签

    • 修改上面的SQL语句,使用where标签

      <select id="queryBlogIf" parameterType="map" resultType="com.example.zhang.pojo.Blog">
          select * from blog 
          <where>
              <if test="title != null">
                  title = #{title}
              </if>
              <if test="author != null">
                  and author = #{author}
              </if>
          </where>
      </select>
      

      where标签会知道如果它包含的标签中有返回值的话,他就插入一个where。如果标签返回的内容是以and或者or开头的,它就会将and或or剔除,保证sql的正确

    • 测试

      title 为null,author不为null

      @Test
      public void testQueryBlogIf() {
          SqlSession session = MyBatisUtils.getSqlSession();
      
          BlogMapper blogMapper = session.getMapper(BlogMapper.class);
          HashMap<String, String> map = new HashMap<>();
      
          //map.put("title", "Mybatis如此简单");
          map.put("author", "张三");
      
          List<Blog> blogs = blogMapper.queryBlogIf(map);
          System.out.println(blogs);
          session.close();
      }
      

      测试结果:

      mybatis-where.png

      可以看到确实将where给加上了,sql语句变为:select * from blog where title = ?而且结果也是正常的返回。

  5. set标签

    同样的,在select语句中会有where关键字,那如果是更新操作,含有的set关键字,就要使用set标签了。

    • 编写接口方法

      int updateBlog(Map map);
      
    • 编写sql配置文件

      <update id="updateBlog" parameterType="map">
          update blog
          <set>
              <if test="title != null">
                  title = #{title},
              </if>
              <if test="author != null">
                  author = #{author}
              </if>
          </set>
          where id = #{id};
      </update>
      

      注意:set是用逗号隔开,set元素会动态的在行首插入set关键字,并会删除额外的逗号

    • 测试

      @Test
      public void testUpdateBlog() {
          SqlSession session = MyBatisUtils.getSqlSession();
      
          BlogMapper mapper = session.getMapper(BlogMapper.class);
      
          HashMap<String,String> map = new HashMap<String,String>();
      
          map.put("id", "2156717bb4894bb5bfc5dc82f6030c70");
          map.put("title", "动态SQL");
          map.put("author", "李四");
      
          mapper.updateBlog(map);
          session.close();
      }
      
  6. choose标签

    有时候我们不想用到所有的查询条件,只想选择其中的一个,查询条件有一个满足即可,使用choose标签可以解决此类问题,类似于java的switch语句

    • 编写接口

      List<Blog> queryBlogChoose(Map map);
      
    • 编写SQL配置文件

      <select id="queryBlogChoose" parameterType="map" resultType="com.example.zhang.pojo.Blog">
          select * from blog
          <where>
              <choose>
                  <when test="title != null">
                      title = #{title}
                  </when>
                  <when test="author != null">
                      and author = #{author}
                  </when>
                  <!-- otherwise是一定会应用,就好似default,但前面已经有条件语句,所以得加上and,where语句会智能帮助是否去掉and -->
                  <otherwise>
                      and views = #{views}
                  </otherwise>
              </choose>
          </where>
      </select>
      
    • 测试

      @Test
      public void testQueryBlogChoose() {
          SqlSession session = MyBatisUtils.getSqlSession();
      
          BlogMapper mapper = session.getMapper(BlogMapper.class);
      
          HashMap<String, Object> map = new HashMap<String, Object>();
          map.put("title", "Java如此简单");
          // map.put("author", "张三");
          // map.put("views", 9999);
          List<Blog> blogs = mapper.queryBlogChoose(map);
      
          System.out.println(blogs);
          session.close();
      }
      
  7. foreach标签

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

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

    示例:

    先将数据库的前三个数据的id修改为固定的1,2,3

    需求:我们需要查询blog表中id分别为1,2,3的博客信息

    • 编写接口

      List<Blog> queryBlogForeach(Map map);
      
    • 编写对应mapper文件

      <select id="queryBlogForeach" parameterType="map" resultType="com.example.zhang.pojo.Blog">
          select * from blog
          where id in 
          <!-- 
                  collection:指定输入对象中的集合属性
                  item:每次遍历生成的对象
                  open:开始遍历时的拼接字符串
                  close:结束遍历时的拼接字符串
                  separator:遍历对象之间需要拼接的字符串
                  select * from blog where id in(1,2,3)
                  -->
          <foreach collection="ids" item="id" open="(" close=")" separator=",">
              #{id}
          </foreach>
      
      </select>
      

      foreach一个常见使用场景是对集合进行遍历,尤其是在构建IN条件语句的时候

    • 测试

      @Test
      public void testQueryBlogForeach() {
          SqlSession session = MyBatisUtils.getSqlSession();
      
          BlogMapper blogMapper = session.getMapper(BlogMapper.class);
      
          HashMap<String, Object> map = new HashMap<>();
          List<Integer> ids = new ArrayList<>();
          ids.add(1);
          ids.add(2);
          ids.add(3);
      
          map.put("ids", ids);
      
          List<Blog> blogs = blogMapper.queryBlogForeach(map);
      
          System.out.println(blogs);
      
          session.close();
      }
      
  8. SQL片段

    有时候可能某个sql语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。

    抽取SQL片段:

    <sql id="if-title-author">
     <if test="title!=null">
         title = #{title}
        </if>
        <if test="author!=null">
         and author = #{author}
        </if>
    </sql>
    

    引用SQL片段:

    <select id="queryBlogIf" parameterType="map" resultType="com.example.zhang.pojo.Blog">
     select * from blog
        <where>
         <!-- 引用sql片段,如果refid指定的不在本文件中,那么需要在前面加上namespace -->
            <include refid="if-title-author"></include>
            <!--在这里还可以引用其他的sql片段 -->
        </where>
    </select>
    

    注意:

    1. 最好基于单表来定义sql片段,提高片段的可重用性
    2. 在sql片段中不要包括where

十四.缓存

Mybatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。

Mybatis系统中默认定义了两级缓存:一级缓存和二级缓存

  1. 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存)
  2. 二级缓存需要手动开启和配置,它是基于namespace级别的缓存
  3. 为了提高扩展性,Mybatis定义了缓存接口Cache,我们可以通过实现Cache接口来自定义二级缓存。

一级缓存

一级缓存也叫本地缓存:

  • 与数据库同一次会话期间查询到的数据会放在本地缓存中
  • 以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库

测试:(加入日志,方便查看结果)

  1. 编写接口方法

    User findUserById(@Param("id")int id);
    
  2. 接口对应的mapper文件

    <select id="findUserById" resultType="com.example.zhang.pojo.User">
        select * from user where id = #{id}
    </select>
    
  3. 测试

    @Test
    public void test() {
        SqlSession session = MyBatisUtils.getSqlSession();
        UserMapper userMapper = session.getMapper(UserMapper.class);
    
        User user = userMapper.findUserById(1);
        System.out.println(user);
    
        User user2 = userMapper.findUserById(1);
        System.out.println(user);
    
        System.out.println(user == user2);
        session.close();
    }
    
  4. 结果分析

mybatis-缓存1.png

一级缓存失效的四种情况

一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它。

  1. sqlSession不同

    @Test
    public void test5() {
        SqlSession session = MyBatisUtils.getSqlSession();
        SqlSession session1 = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        UserMapper userMapper1 = session1.getMapper(UserMapper.class);
    
        User user = userMapper.findUserById(1);
        System.out.println(user);
    
        User user1 = userMapper1.findUserById(1);
        System.out.println(user1);
    
        System.out.println(user == user1);
    
        session.close();
        session1.close();
    }
    

    运行结果:发送了两条SQL语句

    结论:每个sqlsession中的缓存相对独立

    mybatis-缓存2.png
  1. sqlSession相同,查询条件不同

    @Test
    public void test5() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        // UserMapper userMapper1 = session.getMapper(UserMapper.class);
    
        User user = userMapper.findUserById(1);
        System.out.println(user);
    
        User user1 = userMapper.findUserById(2);
        System.out.println(user1);
    
        System.out.println(user == user1);
        session.close();
    }
    

    运行结果:发送了两条SQL语句

    结论:当前缓存中,不存在这个数据,就没法用缓存数据直接读取

    mybatis-缓存3.png
  2. sqlSession相同,两次查询之间执行了增删改操作

    增加接口方法:

    int updateUser(Map map);
    

    编写mapper文件:

    <update id="updateUser" parameterType="map">
        update user set name = #{name} where id = #{id}
    </update>
    

    测试:

    @Test
    public void test6() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.findUserById(1);
        System.out.println(user);
    
        HashMap<String,Object> map = new HashMap<>();
        map.put("id", 2);
        map.put("name", "哪吒");
        userMapper.updateUser(map);
    
        User user1 = userMapper.findUserById(1);
        System.out.println(user1);
    
        System.out.println(user == user1);
    
        session.close();
    }
    

    运行结果:同一个查询在中间执行了增删改的操作后,重新执行了

    结论:因为增删改操作可能会对当前数据产生影响,所以不会直接读取缓存的了

    mybatis-缓存4.png
  3. sqlSession相同,手动清除一级缓存

    @Test
    public void test7() {
        SqlSession session = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.findUserById(1);
        System.out.println(user);
    
        // 手动清除缓存
        session.clearCache();
    
        // 再重新查询
        User user1 = mapper.findUserById(1);
        System.out.println(user1);
    
        System.out.println(user == user1);
        session.close();
    }
    

    运行结果:

    mybatis-缓存5.png

二级缓存

  • 二级缓存也叫全局缓存,一级缓存的作用域太低了,所以诞生了二级缓存
  • 基于namespace级别的缓存,一个名称空间对应一个二级缓存
  • 工作机制
    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
    • 如果当前会话关闭了,这个会话对应的一级缓存就没了,但是我们想要的是,会话关闭了,一级缓存的数据被保存到二级缓存中;
    • 新的会话查询信息,就可以从二级缓存中获取内容;
    • 不同的mapper查出的数据会放在自己对应的缓存(map)中

使用步骤:

  1. 开启全局缓存(在核心配置文件中mybatis-config.xml)

    <!-- 开启全局缓存 -->
    <setting name="cacheEnabled" value="true" />
    
  2. 在每个要使用二级缓存的mapper.xml中配置

    <cache></cache>
    

    也可以自定义参数(官方例子)

    <!-- 官方示例=====>查看官方文档 -->
    <cache
     eviction="FIFO"
     flushInterval="60000"
     size="512"
     readOnly="true"/>
    <!-- 这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。-->
    
  3. 代码测试

    注意:必须将所有实体类实现序列化接口,否则会报错

    org.apache.ibatis.cache.CacheException:Error serializing object. Cause: java.io.NotSerializableException: com.example.zhang.pojo.User

    User类加上序列化接口:

    public class User implements Serializable

    测试代码:(查询完关闭一级缓存,再进行一次查询)

    @Test
    public void testCache() {
        SqlSession session = MyBatisUtils.getSqlSession();
        SqlSession session2 = MyBatisUtils.getSqlSession();
    
        UserMapper userMapper = session.getMapper(UserMapper.class);
        UserMapper userMapper2 = session2.getMapper(UserMapper.class);
    
        User user = userMapper.findUserById(1);
        System.out.println(user);
        session.close();
    
        User user2 = userMapper2.findUserById(1);
        System.out.println(user2);
        System.out.println(user == user2);
    
        session2.close();
    }
    

    测试结果:

    mybatis-缓存6.png
  4. 总结

    • 只要开启了二级缓存,我们在同一个Mapper中查询,可以在二级缓存中拿到数据
    • 查出的数据都会被默认放在一级缓存中
    • 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中

缓存原理

mybatis-缓存7.png

自定义缓存

可以使用第三方缓存实现--EhCache

Ehcache是一种广泛使用的java分布式缓存,用于通用缓存,以后缓存会用Redis。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容