MyBatis:MyBatis的学习与总结

MyBatis

1. 简介

1.1 什么是 MyBatis ?

  • MyBatis 本是 apache 的一个开源项目 iBatis,2010年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis。2013年11月迁移到 Github。

  • MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。

  • MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。

  • MyBatis 可以通过简单的 XML注解 来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

1.2 MyBatis 的特点

  • 简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个 jar 文件+配置几个 sql 映射文件。易于学习,易于使用。通过文档和源代码,可以比较完全的掌握它的设计思路和实现。
  • 灵活:mybatis 不会对应用程序或者数据库的现有设计强加任何影响。 sql 写在 xml 里,便于统一管理和优化。通过 sql 语句可以满足操作数据库的所有需求。
  • 解除 sql 与程序代码的耦合:通过提供 DAO 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。
  • 提供映射标签,支持对象与数据库的 orm 字段关系映射。
  • 提供对象关系映射标签,支持对象关系组建维护。
  • 提供 xml 标签,支持编写动态 sql。

1.2 如何获取 MyBatis ?

1.3 持久化

持久层框架是一组软件服务,将应用程序与其使用和操纵的数据源分离,隐藏对数据源的访问机制。

什么叫持久化?

数据持久化就是 将内存中的数据模型转化为数据库的存储模型,以及将存储模型转化为数据模型的统称,简单来说就是对数据库的操作。

为什么要持久化?

  • 减少数据库访问次数,加快程序执行速度。
  • 提高代码的可重用性。
  • 松散耦合,使持久层不依赖于底层数据库和业务逻辑,更改数据库时只需要修改配置文件即可,不需要修改代码。

1.4 持久层

什么是持久层?

实现数据持久化的一个相对独立的部分。

2. MyBatis 配置文件

MyBatis 提供两个配置文件,一个是核心配置文件,另一个是 SQL 映射文件。

2.1 核心配置文件

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

<?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="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

配置文件中的核心标签的含义

configuration:核心配置。

environments:开发环境的选择,default 属性用来匹配标签内 environment 的 id 值。

environment :可以定义多个开发环境,环境的 id 可以自定义。

transactionManager:事务管理器,type 标签定义采用什么方式管理事务。

dataSource:配置数据源,POOLED 为数据库连接池,负责数据库连接的创建与回收,一旦数据库操作完成,mybaties会将此连接返回给连接池。

mappers:扫描 SQL 映射文件,每一个 Mapper.xml 都需要在核心配置文件中注册。

在核心配置文件中,每一个核心标签都有其定义的顺序,例如:properties 属性被规定放在 configuration 标签内的第一个位置,就不能放在最后一位。以下为每一个标签从前到后定义的顺序:

properties -> settings -> typeAliases -> typeHandlers -> objectFactory -> objectWrapperFactory -> reflectorFactory -> plugins -> environments -> databaseIdProvider -> mappers

2.2 SQL 映射文件

<?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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

其中,mapper 标签对应一个映射关系,namespace 为命名空间,唯一且不可重复,是 SQL 映射文件的唯一标识,Mapper 标签内定义数据库的增删改查语句。

命名解析:为了减少输入量,MyBatis 对所有的命名配置元素(包括语句,结果映射,缓存等)使用如下的命名解析规则。

  • 完全限定名(比如“com.mypackage.MyMapper.selectAllThings”)将被直接查找并且找到即用。
  • 短名称(比如“selectAllThings”)如果全局唯一也可以作为一个单独的引用。如果不唯一,有两个或两个以上的相同名称(比如“com.foo.selectAllThings ”和“com.bar.selectAllThings”),那么使用时就会产生”短名称不唯一的“的错误,这种情况下就必须使用完全限定名。
<select id="queryAllUser" resultType="com.fei.pojo.User">
    select * from user
</select>

select:数据库查询语句。

id :当前 SQL 映射文件中此 select 查询的唯一标识符。

resultType:返回结果类型,若返回结果为 List 集合,则返回 List 集合中的数据的类型。

<select id="queryUserById" resultType="com.fei.pojo.User" parameterType="int">
    select * from user where id = #{id}
</select>

parameterType:传入参数的数据类型。

3. MyBatis 实现增删改查

  1. 搭建 Mysql 数据库

    <!-- 创建数据库 -->
    CREATE DATABASE blog
    <!-- 使用数据库 -->
    USE `blog`
    <!-- 创建数据表 -->
    CREATE TABLE `user`(
    `id` INT(11) NOT NULL PRIMARY KEY,
    `userName` VARCHAR(10),
    `userPwd` VARCHAR(10)
    )
    <!-- 添加数据 -->
    INSERT INTO `user`(`id`,`userName`,`userPwd`) VALUES
    (1,'张三','123456'),
    (2,'李四','654321')
    
  2. 创建一个 Maven 项目,在 pom.xml 中导入依赖坐标

    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.48</version>
    </dependency>
    <!-- junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    
  3. 在 resource 目录下新建一个 MyBatis 的 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.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/blog?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8"/>
                    <property name="username" value="root"/>
                    <property name="password" value=""/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <mapper resource="com/fei/dao/UserMapper.xml"/>
        </mappers>
    </configuration>
    

    其中,dataSource 标签内的 JDBC 配置为自己的数据库,这里是我创建的数据库,如果不相同请勿直接复制。

  4. 创建实体类(这里定义的成员变量名需要和数据库字段保持一致,当 sql 语句查询出结果时,如果对应到实体类中成员变量的名字一样,且定义了 set 方法,则返回结果会一一映射到实体类属性值(自动映射))

    public class User {
        private int id;
        private String userName;
        private String userPwd;
    
        public User() {}
    
        public User(int id, String userName, String userPwd) {
            this.id = id;
            this.userName = userName;
            this.userPwd = userPwd;
        }
    
        public int getId() { return id; }
    
        public void setId(int id) { this.id = id; }
    
        public String getUserName() { return userName; }
    
        public void setUserName(String userName) { this.userName = userName; }
    
        public String getUserPwd() { return userPwd; }
    
        public void setUserPwd(String userPwd) { this.userPwd = userPwd; }
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", userName='" + userName + '\'' +
                    ", userPwd='" + userPwd + '\'' +
                    '}';
        }
    }
    
  5. 在 dao 层定义数据访问接口(如果有多个类型的参数,则可以添加 @Param 注解标识,在 SQL 映射文件中就可以无需添加 parameterType 属性,#{} 值同 @Param 值一一对应即可)

    public interface UserMapper {
        // 查询所有用户
        List<User> queryAllUser();
    
        // 根据id查询用户
        User queryUserById(int id);
    
        // 修改用户密码
        int updateUserPwd(@Param("id") int id, @Param("userNewPwd") String userNewPwd);
    
        // 添加用户
        int addUser(User user);
    
        // 删除用户
        int deleteUser(int id);
    }
    
  6. 在 dao 层添加映射文件 UserMapper.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.fei.dao.UserMapper">
        <select id="queryAllUser" resultType="com.fei.pojo.User">
            select *
            from user
        </select>
        <select id="queryUserById" resultType="com.fei.pojo.User" parameterType="int">
            select *
            from user
            where id = #{id}
        </select>
        <update id="updateUserPwd">
            update user
            set userPwd = #{userNewPwd}
            where id = #{id}
        </update>
        <insert id="addUser" parameterType="com.fei.pojo.User">
            insert into user (id, userName, userPwd)
            values (#{id}, #{userName}, #{userPwd})
        </insert>
        <delete id="deleteUser" parameterType="int">
            delete
            from user
            where id = #{id}
        </delete>
    </mapper>
    
  7. 编写一个工具类,封装 XML 中构建 SqlSessionFactory 以及从 SqlSessionFactory 中获取 SqlSession 的代码

    public class GetSqlSessionUtil {
        private static SqlSessionFactory sqlSessionFactory;
        static {
            try {
                // 获取 MyBatis 配置文件
                String resource = "mybatis-config.xml";
                // 使用类加载器去加载配置文件
                InputStream inputStream = Resources.getResourceAsStream(resource);
                // 构建 SqlSession 工厂类
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        public static SqlSession getSqlSession() {
            // 返回 SqlSession 对象,如果括号内传入一个 true值,即可在操作数据时自动提交事务
            return sqlSessionFactory.openSession();
        }
    }
    
  8. 编写测试方法,运行程序

    public class MyTest {
        @Test
        // 查询所有用户
        public void queryAllUser() {
            // 从SqlSession工厂中获取SqlSession
            SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
            // 获取代理对象
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            // 执行查询,获取返回的数据
            List<User> userList = userMapper.queryAllUser();
            // 遍历输出 List<User> 集合
            for (User user:userList) {
                System.out.println(user);
            }
            // 关闭会话
            sqlSession.close();
        }
    
        @Test
        // 根据id查询用户
        public void queryUserById() {
            SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = userMapper.queryUserById(1);
            System.out.println(user);
            sqlSession.close();
        }
        @Test
        // 修改用户密码
        public void updateUserPwd() {
            SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            int num = userMapper.updateUserPwd(1,"111111");
            if(num==1){
                System.out.println("用户密码修改成功");
            }
            // 增删改操作需要提交事务,否则无法执行
            sqlSession.commit();
            sqlSession.close();
        }
        @Test
        // 添加用户信息
        public void addUser() {
            SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            int num = userMapper.addUser(new User(3,"王五","222222"));
            if(num ==1) {
                System.out.println("用户信息添加成功");
            }
            sqlSession.commit();
            sqlSession.close();
        }
        @Test
        // 删除用户
        public void deleteUser() {
            SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            int num = userMapper.deleteUser(3);
            if(num ==1) {
                System.out.println("用户删除成功");
            }
            sqlSession.commit();
            sqlSession.close();
        }
    }
    
  9. 如果程序报错,可能为系统在编译的过程中过滤掉了静态资源 Mapper.xml,需要在 pom.xml 中依赖坐标标签的下面添加以下配置信息。

    <!--静态资源导出-->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
    

4. 作用域(Scope)和生命周期

作用域和生命周期至关重要,错误的使用会导致严重的并发问题。

4.1 SqlSessionFactoryBuilder

这个类可以被实例化、使用以及丢弃,一旦创建好 SqlSessionFactory 以后就不再需要这个类了。其最佳作用域为 方法作用域(即局部方法变量)。

4.2 SqlSessionFactory

这个类一旦被创建,在运行期间就会一直存在,不建议丢弃以及创建新的实例。因此,这个类的的最佳作用域是 应用作用域,使用单例模式或静态单例模式。

4.3 SqlSession

每一个线程都应该有一个其自己的 SqlSession 实例。SqlSession 实例不是线程安全的,因此不能被共享,其最佳作用域为 请求或方法作用域。每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它,否则会造成资源被占用的情况。其中,关闭操作应放到 finally 块中。

try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}

4.4 映射器实例

映射器是一些绑定映射语句的接口,映射器接口的实例是从 SqlSession 中获得的。其最佳作用域为 方法作用域,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的应用逻辑代码
}

5. XML 核心配置文件

MyBatis 核心配置文件中的配置有不少,除了 2.1 描述的那些以外,在这里了解一些其他的相关配置,之后接触时再回过头继续学习。有需要的可以进入MyBatis中文文档:mybatis – MyBatis 3 | 配置 中继续学习。

5.1 属性(properties)

在核心文件中配置数据源,可以通过直接定义的方式实现。

<dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/blog?				 					useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value=""/>
</dataSource>

这些属性也可以在外部进行配置,并可以进行动态替换。在 resource 目录下新建一个 db.properties 文件,添加 value 值。

driver = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost:3306/blog?useSSL=false&useUnicode=true&characterEncoding=utf8
username = root
password =

在核心配置文件中的 configuration 标签内的顶部添加 properties 标签(MyBatis 定义 的 XML 标签顺序),用来指向外部配置文件,同时 properties 标签内也可以定义 这些配置属性。MyBatis 启动后优先会去加载 db.properties 外部配置文件,如果没有引用 db.properties 外部配置文件,则会去寻找 properties 标签里面的属性值;如果同时添加了外部配置文件的引用以及标签内的属性值,则只会去加载外部配置文件。

<properties resource="db.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>

5.2 类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。

5.2.1 给类起别名

在核心配置文件规定的位置使用 typeAlias 标签给一个类起别名,可以同时定义多个类。

<typeAliases>
    <typeAlias type="com.fei.pojo.User" alias="user"/>
</typeAliases>

定义好以后,user 就可以替换任何定义 com.fei.pojo.User 的地方。

5.2.2 指定包名

通过 typeAliases 标签里的可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean。

<typeAliases>
    <package name="com.fei.pojo"/>
</typeAliases>

定义好以后,被扫描到的包下的所有类,都可以通过首字母小写的非限定类名作为其别名。例如:user 就可以替换任何定义 com.fei.pojo.User 的地方,但是,如果使用到了 @Alias("users") ,则别名为 users 注解值。其中,@Alias("users") 用在需要起别名的类上。

5.2.3 Java 类型的类型别名

下面是一些为常见的 Java 类型内建的类型别名,它们都是不区分大小写的。另外,为了应对原始类型的命名重复,采取了特殊的命名风格。

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

5.3 设置(settings)

MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为,这里只大致了解一些设置并列举出来,有更多需求请前往官方文档进行学习。

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true | false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 true | false fals
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 NONE, PARTIAL, FULL PARTIAL
autoMappingUnknownColumnBehavior 指定发现自动映射目标未知列(或未知属性类型)的行为。NONE: 不做任何反应WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARNFAILING: 映射失败 (抛出 SqlSessionException) NONE, WARNING, FAILING NONE
defaultStatementTimeout 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 任意正整数 未设置 (null)
defaultResultSetType 指定语句默认的滚动策略。(新增于 3.5.2) FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置) 未设置 (null)
safeRowBoundsEnabled 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 true | false False
safeResultHandlerEnabled 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 true | false True
mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 true | false False
jdbcTypeForNull 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 OTHER
lazyLoadTriggerMethods 指定对象的哪些方法触发一次延迟加载。 用逗号分隔的方法列表。 equals,clone,hashCode,toString
logPrefix 指定 MyBatis 增加到日志名称的前缀。 任何字符串 未设置
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J(deprecated since 3.5.9) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置
proxyFactory 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 CGLIB | JAVASSIST JAVASSIST (MyBatis 3.3 以上)

在配置文件中,通过 setting 标签的 name 属性和 value 属性去设置。

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="xxx" value="xxx"/>
</settings>

5.4 映射器(mappers)

映射器用来扫描 SQL 映射文件,有以下几种映射方式。

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="com/fei/dao/UserMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///D:/Users/IdeaProjects/Blog/src/main/java/com/fei/dao/UserMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="com.fei.dao.UserMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="com.fei.dao"/>
</mappers>

注:如果使用 mapper 标签的 class 属性以及使用 package 标签的 name 属性获取资源,需要满足以下几点:

  • 接口及其Mapper配置文件必须同名,即接口类 UserMapper 对应其配置文件 UserMapper.xml。
  • 接口及其配置文件必须在同一个包下。

6. XML 映射文件

在映射文件中,MyBatis 规定的顶级标签也应该按照定义的规则去排列,从前往后依次为

  • cache:该命名空间的缓存配置。
  • cache-ref:引用其它命名空间的缓存配置。
  • resultMap:描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • parameterMap:老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。
  • sql:可被其它语句引用的可重用语句块。
  • insert、update、delete、select:映射增删改查语句。

6.1 自动映射

在前面第 3 章节,已经接触到了自动映射的概念。所谓自动映射,就是当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性,如果根据 ID列 查询不到 id(或其大小写)属性,则返回 null 值。

数据库列名若为多个单词的合成词时,通常会使用下划线 “_” 区分,比如:user_sex ,而在 Java 中,合成词通常会遵循驼峰命名规则,比如:userSex,两者若要启动自动映射,需要在核心配置文件的 settings 标签内将 mapUnderscoreToCamelCase 属性设置为 true( MyBatis 默认该属性为 false ),在上一章节有提到过。

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

如果不希望列名和属性名保持一致,可以手动设置映射,有两种方法

其中一种办法为通过修改 sql语句来映射

<select id="queryAllUser" resultType="com.fei.pojo.User">
  select
    id             as "id",
    userName       as "name",
    userPwd        as "pwd"
  from user
  where id = #{id}
</select>

在定义实体类属性的时候,就可以根据 as 定义的值来起别名。

private int id;
private String name;
private String pwd;

另一种方法就是 结果映射,在下一小节中介绍。

使用 resultMap 标签设置字段名和属性名的映射(按照规定的排序规则去定义)

6.2 结果映射

对于上一小节的结尾,可以通过结果映射,使用 resultMap 标签去设置字段名和属性名的映射(按照 MyBatis 规定的排序规则去定义 resultMap 标签的位置)

<!-- resultMap 的 id 属性为 select 标签 resultMap属性的值,type 属性相当于 resultType 的值 -->
<resultMap id="userResultMap" type="com.fei.pojo.User">
  <!-- property 为属性名,column 为字段名,两个名字一一映射,其他字段和属性名相同的就无需再定义了 -->
  <result property="pwd" column="userPwd"/>
</resultMap>
<!-- 这里就无需再定义 resultType 属性的值 -->
<select id="queryAllUser" resultMap="userResultMap">
  select
    id,userName,userPwd
  from user
  where id = #{id}
</select>

对于一些复杂的 sql 语句,官方也提出了 高级结果映射 的实例,这里暂时先不去考虑了。

7. 日志

配置日志的目的在于可以在控制台清晰地看到程序的执行情况以及输出的内容。在平常的 Java 代码中我们可以通过 System.out.println(); 去打印输出一些信息,但是在 MyBatis 中,我们无法在配置文件中直接打印输出一些内容。因此,MyBatis 提供了一个 logImpl 设置,在 5.3 节中有提到,接下来尝试打开这个设置去输出一些日志信息。

7.1 标准日志

STDOUT_LOGGING 是 MyBatis 自带的标准的输出日志,可用于在控制台输出程序执行的一些步骤信息。

在核心配置文件规定的位置上添加以下配置

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

执行查询语句,可以看到控制台输出了一些执行信息,部分信息如下所示。

7.2 log4j 日志

Log4j 是 Apache 的一个开源项目,通过使用 Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI 组件,甚至是套接口服务器、NT 的事件记录器、UNIX Syslog 守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

  1. 导入 log4j 的依赖坐标

    <!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  2. 在 MyBatis 核心配置文件中开启 log4j 日志

    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
    
  3. 在 resources 目录下新建一个 log4j.properties 文件,复制以下配置信息

    #将等级为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/fei.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
    
  4. 运行程序,可以观察到控制台会输出一些日志信息,同时会根据 log4j.properties 的配置内容,在根目录下添加一个 log 目录,保存 log 日志。

  1. 在一个 Java 类中也可以定义日志信息来进行输出和保存(注意包名为org.apache.log4j.Logger)。

    package com.fei.test;
    
    import org.apache.log4j.Logger;
    
    public class LogTest {
        public static Logger logger = Logger.getLogger(LogTest.class);
    
        public static void main(String[] args) {
            logger.info("这是一条info信息");
            logger.debug("这是一条debug信息");
            logger.error("这是一条error信息");
        }
    }
    

8. 使用注解实现增删查改

MyBatis 映射语句除了可以用 Mapper.xml 实现语句映射,还可以通过直接在接口上定义注解的方式实现。通过使用注解可以简化代码,但对于复杂的语句,则力不从心。对于一些复杂的操作,最好还是通过 XML 来实现。下面只介绍 Select 注解,其他操作的注解类似。

  1. 在接口上添加注解

    // 查询所有用户
    @Select("select * from user")
    List<User> queryAllUser();
    
    // 根据id查询用户
    @Select("select * from user where id = #{id}")
    User queryUserById(int id);
    
    // 修改用户密码
    @Update("update user set userPwd = #{userNewPwd} where id = #{id}")
    int updateUserPwd(@Param("id") int id, @Param("userNewPwd") String userNewPwd);
    
    // 添加用户
    @Insert("insert into user (id, userName, userPwd) values (#{id}, #{userName}, #{userPwd})")
    int addUser(User user);
    
    // 删除用户
    @Delete("delete from user where id = #{id}")
    int deleteUser(int id);
    
  2. 在核心配置文件中绑定接口,然后运行对应的程序即可。当前查询操作无需映射文件即可查询,需保证字段名和属性名保持一致。

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

    本质:反射机制实现

    底层:动态代理

9. 缓存

9.1 缓存的概念

  1. 什么是缓存?

    有些数据是需要经常访问的,将读取比较频繁的一些数据存储在缓存中,再次读取时就可以直接从缓存中获取,这样以来提高了查询的效率,有效解决高并发系统的性能问题。

  2. 什么样的数据需要用到缓存?

    经常访问,且不易改变。

  3. 为什么要使用缓存?

    减少和数据库的交互次数,降低项目成本和开销,提高系统的效率。

9.2 Mybatis 缓存

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。

MyBatis 提供了两极缓存:一级缓存二级缓存

9.2.1 一级缓存

一级缓存是一个 sqlsession 级别的,默认情况下,MyBatis 只自动启用本地的会话缓存(SqlSession),它仅仅对一个会话中的数据进行缓存,即一级缓存。

@Test
// 查询所有用户
public void queryAllUser() {
    // 从SqlSession工厂中获取SqlSession
    SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
    // 获取代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    // 执行查询,获取返回的数据
    List<User> userList = userMapper.queryAllUser();
    // 遍历输出 List<User> 集合
    for (User user:userList) {
        System.out.println(user);
    }
    // 关闭会话
    sqlSession.close();
}

一级缓存存在于会话的创建和关闭期间,在一次会话期间查询出来的数据,会放到本地会话缓存中,将来如果需要获取相同数据,直接从缓存中获取,无需再查询数据库;如果获取不同的数据则会更新缓存,举例说明一下。

  1. 修改查询方法,运行查看日志
@Test
// 根据id查询用户
public void queryUserById() {
    SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.queryUserById(1);
    System.out.println(user);
    System.out.println("---------------------------");
    User user2 = userMapper.queryUserById(1);
    System.out.println(user2);
    System.out.println(user == user2);
    sqlSession.close();
}

根据日志信息可以看到,执行第一次查询时读取到的是数据库的返回结果,第二次查询时是直接获取缓存里面的数据。此时,若修改第二个查询用户的 id,则运行的结果为

说明:由于两次查询的条件不同,缓存就会被刷新。

一级缓存失效的情况:

  • 查询的条件不同

  • 增删改操作,每次操作可能会改变原来的数据,因此必定会刷新缓存,哪怕操作的数据一样也会刷新。

  • 查询不同的 Mapper.xml 映射文件

  • 手动清理缓存

    sqlSession.clearCache();
    

9.2.2 二级缓存

二级缓存也叫全局缓存,是一个 mapper 级别的,默认不开启,一个 namespace 命名空间对应一个二级缓存。

开启二级缓存:

  1. 在核心配置文件中添加设置

    <settings>
      <setting name="cacheEnabled" value="true"/>
    </settings>
    
  2. 在需要使用二级缓存的映射文件中添加一行

    <cache/>
    

    开启此标签后的效果为:

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

    这些效果可以通过修改 cache 标签的属性来修改

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

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

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

    默认的清除策略是 LRU。

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

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

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

  3. Mapper.xml 所使用到的 pojo 类需要实现序列化接口

    public class User implements Serializable
    
  4. 修改测试方法,运行程序

    @Test
    // 根据id查询用户
    public void queryUserById() {
        SqlSession sqlSession = GetSqlSessionUtil.getSqlSession();
        SqlSession sqlSession2 = GetSqlSessionUtil.getSqlSession();
    
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.queryUserById(1);
        System.out.println(user);
        sqlSession.close();
        System.out.println("---------------------------");
    
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = userMapper2.queryUserById(1);
        System.out.println(user2);
    
        System.out.println(user == user2);
        sqlSession2.close();
    }
    

可以看到,开启两个不同的会话,第一次的数据是从数据库里面读取的,读取之后数据将被保存在二级缓存当中,即使关闭第一个会话,缓存的数据也不会丢失。在接下来的相同操作中,会直接从二级缓存中获取数据,而不会从数据库当中读取。

注:这里比较的 user 和 user2 不相等是由于cache 的 readOnly 属性默认为 false,刚才有提到 cache 标签的 readOnly 属性,只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。如果要返回一个相同的实例,需要在映射文件中将 cache 标签的 readOnly 属性值设置为 true。

总结:

  • 只有添加了 cache 标签的映射文件才能开启二级缓存。
  • 开启了二级缓存的Mapper.xml 所使用到的 pojo 类需要实现序列化接口。
  • 所有的数据会先放在一级缓存中。
  • 当会话提交或关闭的时候,才会将数据提交到二级缓存中。

9.2.3 工作原理

MyBatis 缓存的执行顺序:

二级缓存 ——> 一级缓存 ——> 数据库

  • 先判断二级缓存是否开启,如未开启,则判断一级缓存是否开启,如未开启,访问数据库。
  • MyBatis 默认一级缓存打开,二级缓存关闭。
  • 如果一、二级缓存都打开,则会首先判断二级缓存当中是否有数据,如有数据直接返回数据,如没有,则判断一级缓存是否有数据,如果有,则直接返回数据,如果没有,则查询数据库。
  • 如果一级缓存关闭,即使二级缓存打开也无法写入数据,因为会话过程提交或关闭时才会将数据写入二级缓存。
posted @ 2022-01-27 14:14  亦浮云  阅读(74)  评论(0)    收藏  举报