MyBatis基础

3/30/2022 MyBatis

# ORM(Object Relational Mapping,对象-关系映射)

img

  1. JDBC是Java与数据库交互的统一API,实际上它分为两组API,一组是面向Java应用程序开发人员的API,另一组是面向数据库驱动程序开发人员的API。
  2. 在实际开发Java系统时,我们可以通过JDBC完成多种数据库操作。这里以传统JDBC编程中的查询操作为例进行说明,其主要步骤如下:

(1)注册数据库驱动类,明确指定数据库URL地址、数据库用户名、密码等连接信息。

(2)通过DriverManager打开数据库连接。

(3)通过数据库连接创建Statement对象

(4)通过Statement对象执行SQL语句,得到ResultSet对象。

(5)通过ResultSet读取数据,并将数据转换成JavaBean对象。

(6)关闭ResultSet、Statement对象以及数据库连接,释放相关资源。

上述步骤1~步骤4以及步骤6在每次查询操作中都会出现,在保存、更新、删除等其他数据库操作中也有类似的重复性代码。在实践中,为了提高代码的可维护性,可以将上述重复性代码封装到一个类似DBUtils的工具类中。步骤5中完成了关系模型到对象模型的转换,要使用比较通用的方式封装这种复杂的转换是比较困难的。

为了解决该问题,ORM(Object Relational Mapping,对象-关系映射)框架应运而生。如图1-1所示,ORM框架的主要功能就是根据映射配置文件,完成数据在对象模型与关系模型之间的映射,同时也屏蔽了上述重复的代码,只暴露简单的API供开发人员使用。

另外,实际生产环境中对系统的性能是有一定要求的,数据库作为系统中比较珍贵的资源,极易成为整个系统的性能瓶颈,**所以我们不能像上述JDBC操作那样简单粗暴地直接访问数据库、直接关闭数据库连接。**应用程序一般需要通过集成缓存、数据源、数据库连接池等组件进行优化,如果没有ORM框架的存在,就要求开发人员熟悉相关组件的API并手动编写集成相关的代码,这就提高了开发难度并延长了开发周期。

# 持久层框架

# Hibernate

Hibernate是一款Java世界中最著名的ORM框架之一。

Hibernate通过hbm.xml映射文件维护Java类与数据库表的映射关系。通过Hibernate的映射,Java开发人员可以用看待Java对象的角度去看待数据库表中的数据行。数据库中所有的表通过hbm.xml配置文件映射之后,都对应一个Java类,表中的每一行数据在运行过程中会被映射成相应的Java对象。在Java对象之间存在一对多、一对一、多对多等复杂的层次关系,Hibernate 的hbm.xml映射文件也可以维护这种层次关系,并将这种关系与数据库中的外键、关联表等进行映射,这也就是所谓的“关联映射”。

Hibernate除了能够实现对象模型与关系模型的映射,还可以帮助开发人员屏蔽不同数据库产品中SQL语句的细微差异。

另外,Hibernate的API没有侵入性,业务逻辑不需要继承Hibernate的任何接口。Hibernate默认提供了一级缓存和二级缓存,这有利于提高系统的性能,降低数据库压力。

Hibernate还有其他的特性和优点,例如,支持透明的持久化、延迟加载、由对象模型自动生成数据库表等。

但是,Hibernate并不是一颗万能药。数据库本身有自己的组织方式,并不是数据库中所有的概念都能在面向对象的世界中找到合适的映射,例如,索引、存储过程、函数等,尤其是索引,它对数据库查询的性能帮助很大,适当优化SQL语句,选择使用合适的索引会提高整个查询的速度。**但是,我们很难修改Hibernate生成的SQL语句,当数据量比较大、数据库结构比较复杂时,Hibernate生成SQL语句会非常复杂,而且要让生成的SQL语句使用正确的索引也比较困难,这就会导致出现大量慢查询的情况。**在有些大数据量、高并发、低延迟的场景下,Hibernate并不是特别适合。最后,Hibernate对批处理的支持并不是很友好,这也会影响部分性能。后来出现了iBatis(Mybatis的前身)这种半自动化的映射方式来解决性能问题。

# JPA

JPA仅仅是一个持久化的规范,它并没有提供具体的实现。其他持久化厂商会提供JPA规范的具体实现,例如,Hibernate、EclipseLink等都提供了JPA规范的具体实现。

# Spring JDBC

# Mybatis

MyBatis前身是Apache基金会的开源项目iBatis,在2010年该项目脱离Apache基金会并正式更名为MyBatis。在2013年11月,MyBatis迁移到了GitHub。

MyBatis与前面介绍的持久化框架一样,可以帮助开发人员屏蔽底层重复性的原生JDBC 代码。MyBatis 通过映射配置文件或相应注解将ResultSet映射为Java对象,其映射规则可以嵌套其他映射规则以及子查询,从而实现复杂的映射逻辑,也可以实现一对一、一对多、多对多映射以及双向映射。

相较于Hibernate , MyBatis 更加轻量级, 可控性也更高, 在使用MyBatis时我们直接在映射配置文件中编写待执行的原生SQL语句,这就给了我们直接优化SQL语句的机会,让SQL语句选择合适的索引,能更好地提高系统的性能,比较适合大数据量、高并发等场景。在编写SQL语句时,我们也可以比较方便地指定查询返回的列,而不是查询所有列并映射对象后返回,这在列比较多的时候也能起到一定的优化效果。

MyBatis提供了强大的动态SQL功能来帮助开发人员摆脱这种窘境,开发人员只需要在映射配置文件中编写好动态SQL语句,MyBatis就可以根据执行时传入的实际参数值拼凑出完整的、可执行的SQL语句。

MyBatis最大的成功之处在于:

  1. 不屏蔽 SQL,意味着可以更为精确地定位 SQL 语句,可以对其进行优化和改造,这有利于互联网系统性能的提高,符合互联网需要性能优化的特点。

  2. 提供强大、灵活的映射机制,方便Java开发者使用。提供动态SQL的功能,允许我们根据不同条件组装SQL,这个功能远比其他工具或者Java编码的可读性和可维护性高得多,满足各种应用系统的同时也满足了需求经常变化的互联网应用的要求。

  3. 在MyBatis中,提供了使用Mapper的接口编程,只要一个接口和一个XML就能创建映射器,进一步简化我们的工作,使得很多框架API在MyBatis中消失,开发者能更集中于业务逻辑。

# 框架选择

  1. 从性能角度来看,Hibernate生成的SQL语句难以优化,Spring JDBC和MyBatis直接使用原生SQL语句,优化空间比较大,MyBatis和Hibernate有设计良好的缓存机制,三者都可以与第三方数据源配合使用

  2. 从可移植性角度来看,Hibernate帮助开发人员屏蔽了底层数据库方言,而Spring JDBC和MyBatis在该方面没有做很好的支持,但实践中很少有项目会来回切换底层使用的数据库产品,所以这点并不是特别重要;

  3. 从开发效率的角度来看,Hibernate和MyBatis都提供了XML映射配置文件和注解两种方式实现映射,Spring JDBC则是通过ORM化的Callback的方式进行映射。

# Mybatis执行流程

  1. 应用程序首先会加载mybatis-config.xml配置文件,并根据配置文件的内容创建SqlSessionFactory 对象;

  2. 然后, 通过SqlSessionFactory对象创建SqlSession对象,SqlSession接口中定义了执行SQL语句所需要的各种方法

  3. 之后,通过SqlSession对象执行映射配置文件中定义的SQL语句,完成相应的数据操作;

  4. 最后,通过SqlSession对象提交事务,关闭SqlSession对象。

img

# Mybatis架构

img

img

# 主要构件及其相互关系

img

主要的核心部件解释如下:

  1. SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
  2. Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  3. StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  4. ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
  5. ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
  6. TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
  7. MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装,
  8. SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
  9. BoundSql 表示动态生成的SQL语句以及相应的参数信息
  10. Configuration MyBatis所有的配置信息都维持在Configuration对象之中。

# 参考

MyBatis详解 - 总体框架设计 | Java 全栈知识体系 (opens new window)

# Mybatis映射器

# # 和 $ 的区别?

  1. #符号主要用来传递参数,相当于jdbc中的?占位符。
  2. #{}会将传入的数据都当成一个字符串,会对自动传入的数据增加双引号。很大程度上阻止了sql注入。
  3. $主要用来防止进行参数转义,直接显示,可以用来表示表名和字段名等。$符号不安全,容易造成sql注入的风险

# SQL注入及解决

# SQL注入

没有(运行时)编译,就没有注入

SQL注入产生的原因,和栈溢出、XSS等很多其他的攻击方法类似,就是未经检查或者未经充分检查的用户输入数据意外变成了代码被执行。针对于SQL注入,则是用户提交的数据,被数据库系统编译而产生了开发者预期之外的动作。也就是,SQL注入是用户输入的数据,在拼接SQL语句的过程中,超越了数据本身,成为了SQL语句查询逻辑的一部分,然后这样被拼接出来的SQL语句被数据库执行,产生了开发者预期之外的动作。

SQL注入即是指网络应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在网络应用程序中预先定义好的查询语句的末尾添加额外的SQL语句,在管理员不知情的情况下实现非法操作,从而来实现欺骗数据库服务器执行非授权的任意查询,从而进一步获得相应的数据信息。

# SQL注入解决

所以从根本上防止上述类型攻击的手段,还是避免数据变成代码被执行,时刻分清代码和数据的界限。而具体到SQL注入来说,被执行的恶意代码是通过数据库的SQL解释引擎编译得到的,所以只要避免用户输入的数据被数据库系统编译就可以了。

现在的数据库系统都提供SQL语句的预编译(prepare)和查询参数绑定功能,在SQL语句中放置占位符'?',然后将带有占位符的SQL语句传给数据库编译,执行的时候才将用户输入的数据作为执行的参数传给用户。这样的操作不仅使得SQL语句在书写的时候不再需要拼接,看起来也更直接,而且用户输入的数据也没有机会被送到数据库的SQL解释器被编译执行,也不会越权变成代码。

至于为什么这种参数化的查询方式没有作为默认的使用方式,我想除了兼容老系统以外,直接使用SQL确实方便并且也有确定的使用场合。

多说一点,从代码的角度来看,拼接SQL语句的做法也是不恰当的。

# SQL注入案例

模拟一个用户登录的SQL注入案例,用户在控制台上输入用户名和密码,然后使用语句串联的方式实现用户的登录。

数据库中先创建用户表及数据**

\-- 创建一张用户表
CREATE TABLE \`users\` (
  \`id\` INT(11) NOT NULL AUTO\_INCREMENT,
  \`username\` VARCHAR(20),
  \`password\` VARCHAR(50),
  PRIMARY KEY (\`id\`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

-- 插入数据
INSERT INTO  users(username,\`password\`) VALUES('张飞','123321'),('赵云','qazxsw'),('诸葛亮','123Qwe');
INSERT INTO  users(username,\`password\`) VALUES('曹操','741258'),('刘备','plmokn'),('孙权','!@#$%^');

-- 查看数据
SELECT  \* FROM users;

复制代码

编写一个登录程序**

import java.sql.\*;
import java.util.Scanner;

public class TestSQLIn {
    public static void main(String\[\] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        String url = "jdbc:mysql://127.0.0.1:3306/testdb?characterEncoding=UTF-8";
        Connection conn = DriverManager.getConnection(url,"root","123456");
        //System.out.println(conn);
        // 获取语句执行平台对象 Statement
        Statement smt = conn.createStatement();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String userName = sc.nextLine();
        System.out.println("请输入密码:");
        String password = sc.nextLine();

        String sql = "select  \* from users where username = '" + userName + "'  and  password = '" + password +"'";
        //打印出SQL
        System.out.println(sql);
        ResultSet resultSet = smt.executeQuery(sql);
        if(resultSet.next()){
            System.out.println("登录成功!!!");
        }else{
            System.out.println("用户名或密码错误,请重新输入!!!");
        }

        resultSet.close();
        smt.close();
        conn.close();

    }

}

模拟SQL注入

拼接的字符串中有或'1'='1'为恒成立条件,因此及时前面的用户以及密码不存在也会收回所有记录,因此提示“登录成功”

img

SQL语法报错

使用拼接的方式,将会出现SQL语法错误等报错,例如

img

# 2.解决方案

使用Statement方式,用户可以通过串联拼接,更改原始SQL真正的符号,导致存在SQL注入的风险。

编写一个新程序

import java.sql.\*;
import java.util.Scanner;

public class TestSQLIn {
    public static void main(String\[\] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        String url = "jdbc:mysql://127.0.0.1:3306/testdb?characterEncoding=UTF-8";
        Connection conn = DriverManager.getConnection(url,"root","123456");
        //System.out.println(conn);
        // 获取语句执行平台对象 Statement
        // Statement smt = conn.createStatement();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String userName = sc.nextLine();
        System.out.println("请输入密码:");
        String password = sc.nextLine();

        String sql = "select  \* from users where username = ? and  password = ? ";
        // System.out.println(sql);
        // ResultSet resultSet = smt.executeQuery(sql);
        PreparedStatement preparedStatement = conn.prepareStatement(sql);
        preparedStatement.setString(1,userName);
        preparedStatement.setString(2,password);

        ResultSet  resultSet = preparedStatement.executeQuery();
        if(resultSet.next()){
            System.out.println("登录成功!!!");
        }else{
            System.out.println("用户名或密码错误,请重新输入!!!");
        }


        preparedStatement.close();
        resultSet.close();
        // smt.close();
        conn.close();

    }

}

# 延迟加载

MyBatis支持延迟加载,我们希望一次性把常用的级联数据通过SQL直接查询出来,而对于那些不常用的级联数据不要取出,而是等待要用时才取出,这些不常用的级联数据可以采用了延迟加载的功能

lazyLoadingEnabled 是一个开关,决定开不开启延迟加载,默认值为false,则不开启延迟加载。所以正如上面的例子,我们什么都没有配置时它就会把全部信息加载进来,所以当获取员工信息时,所有的信息都被加载进来。

aggressiveLazyLoading配置项是一个层级开关,当设置为true时,它是一个开启了层级开关的延迟加载,所以在实践中看到了层级的加载。

选项lazyLoadingEnabled决定是否开启延迟加载,而选项aggressiveLazyLoading则控制是否采用层级加载,但是它们都是全局性的配置,并不能解决我们的需求。加载雇员信息时,只加载雇员任务信息,因为层级加载会把工牌信息也加载进来。为了处理这个问题,在 MyBatis 中使用 fetchType 属性,它可以处理全局定义无法处理的问题,进行自定义。fetchType出现在级联元素(association、collection,注意,discriminator没有这个属性可配置)中,它存在着两个值:

  • eager,获得当前POJO后立即加载对应的数据。
  • lazy,获得当前POJO后延迟加载对应的数据。

# 参考

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/hanchanmingqi-zjjw3/kb/pgfr3x

# 缓存

MyBatis分为一级缓存和二级缓存,同时也可以配置关于缓存的设置。

**一级缓存是在SqlSession上的缓存,二级缓存是在SqlSessionFactory上的缓存。**默认情况下,也就是没有任何配置的情况下,MyBatis 系统会开启一级缓存,也就是对于SqlSession层面的缓存,这个缓存不需要POJO对象可序列化(实现java.io.Serializable接口)。

# 一级缓存

  1. 当一个SqlSession第一次通过SQL和参数获取对象后,它就会将其缓存起来,如果下次的SQL和参数都没有发生变化,并且缓存没有超时或者声明需要刷新时,那么它就会从缓存中获取数据,而不是通过SQL获取了。
  2. 注意:一级缓存只是在SqlSession层面的,如果使用两个不同的SqlSession执行相同的查询操作,不同的SqlSession对象不能共享。为了使SqlSession对象可以共享,需要开启二级缓存。

# 二级缓存

  1. 开启二级缓存的方法:在mapper文件中添加代码:
  2. 二级缓存需要POJO是一个可序列化对象,因为Mybatis会序列化和反序列化对应的POJO。
  3. 不同的SqlSession在获取同一条记录,都只是发送过一次SQL获取数据。因为这个时候 MyBatis 将其保存在SqlSessionFactory 层面,可以提供给各个SqlSession 使用,只是它需要一个序列化和反序列化的过程而已,因此它需要实现Serializable接口。

# 动态SQL

如果使用JDBC或者类似于Hibernate的其他框架,很多时候要根据需要去拼装SQL,这是一个麻烦的事情。因为某些查询需要许多条件,比如查询角色,可以根据角色名称或者备注等信息查询,当不输入名称时使用名称作条件就不合适了。通常使用其他框架需要大量的Java代码进行判断,可读性比较差,而MyBatis提供对SQL语句动态的组装能力,使用XML的几个简单的元素,便能完成动态SQL的功能。大量的判断都可以在MyBatis的映射 XML 里面配置,以达到许多需要大量代码才能实现的功能,大大减少了代码量,这体现了MyBatis的灵活、高度可配置性和可维护性。

MyBatis也可以在注解中配置SQL,但是注解配置功能受限,对于复杂的SQL而言可读性很差。

动态SQL主要包含几个核心元素:if,choose(when,otherwise),trim(where,set),foreach。

# SqlSession的运行过程

# Mapper的动态代理

映射器(Mapper)使用了动态代理的设计模式。

Mapper的XML文件的命名空间对应的是这个接口的全限定名,而方法就是那条SQL的id,这样MyBatis就可以根据全路径和方法名,将其和代理对象绑定起来。通过动态代理技术,让这个接口运行起来起来,而后采用命令模式。最后使用SqlSession接口的方法使得它能够执行对应的 SQL。只是有了这层封装,就可以采用接口编程,这样的编程更为简单明了。

# 运行流程

实际上SqlSession 的执行过程是通过 Executor、StatementHandler、ParameterHandler和ResultSetHandler来完成数据库操作和结果返回的,可以把它们简称为四大对象。

img

  1. Executor执行器:

    1. Executor是一个执行器。SqlSession其实是一个门面,真正干活的是执行器,它是一个真正执行Java和数据库交互的对象,所以它十分的重要。
    2. Executor代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL。其中StatementHandler是最重要的。
  2. StatementHandler(数据库会话器):

    1. 作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
    2. 数据库会话器就是专门处理数据库会话的。
    3. 一条查询SQL的执行过程:Executor先调用StatementHandler的prepare()方法预编译SQL,同时设置一些基本运行的参数。然后用parameterize()方法启用 ParameterHandler 设置参数,完成预编译,执行查询,update()也是这样的。如果是查询,MyBatis 会使用ResultSetHandler封装结果返回给调用者。
  3. ParameterHandler(参数处理器):

    1. MyBatis是通过ParameterHandler对预编译语句进行参数设置的,它的作用是完成对预编译参数的设置。
  4. ResultSetHandler(结果处理器):

    1. ResultSetHandler 是组装结果集返回的。
Last Updated: 4/2/2022, 8:54:13 AM