在Java Web编程系列的Blog中,我们学习了使用JDBC去通过代码操作数据库,我们知道JDBC是一组规范,一组API,也在使用的过程中发现一些代码控制上的不友好感,例如我们需要自己去写代码控制连接的生命周期;获取到结果集需要自己去转换数据类型再进行模型的设置;sql语句强嵌入到代码中不好维护等等诸多问题。其实这些也是可以理解的,JDBC只是提供了一组规范,细粒度的解决代码访问数据库这个问题,最佳实践还仰赖ORM框架来实现,所以其实后续有很多封装的ORM框架作为DAO层和数据库Model的管理者。例如我早期在5年前使用过的Hibernate。当然今时不同往日,现在技术上比较留下的是MyBatis这种半ORM框架,再加上5年前的记忆和理解程度都不算太深了,这次通过MyBatis来再次对这些内容进行一个梳理吧。
业务对象和数据对象
什么是业务对象,什么又是数据对象,这两个为什么要区分开呢?其实在【Java Web编程 十四】深入理解MVC架构模式中我就将Model有意的区分为两个Model:
这两个Model也就是业务对象和数据对象:
- 业务对象,也就是Service Model是面向对象的,它本身是一个对象,包含一些属性和基本的方法,在上层更符合程序员的认知,因为我们是面向对象开发的。比如Person这个Model就是包含了人的一些属性,姓名、年龄、手机号、银行账号、兴趣爱好等,我们从业务使用上去理解和构造这个Model。
- 数据对象,也就是DAO Model是面向关系的(关系型数据库),它虽然也是一个对象,但其实是对数据表的映射,表间并没有对象的认知,它们只是一组组关系,例如业务的Person对应过来,可能会对应好几个DAO Model,例如PersonInfo【人员基本信息表】、PersonAccount【人员账户表】、PersonLike【人员兴趣表】,它们可能都是通过人员主键关联的,但如果不是熟悉的开发人员,从这些定义想象不出一个完备的业务对象是什么样的
所以其实我们下文中提到的对象都是数据对象,一般而言,一个数据类对应一张表,一个数据对象对应于表中的一条数据。
持久层的概念
数据为什么要持久化?持久化是将程序数据从瞬时状态向持久状态和间转换的机制
- 方式:把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)
- 过程:将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML数据文件中等
- 实现:JDBC就是一种持久化机制。文件IO也是一种持久化机制。
为什么需要持久化服务呢?那是由于内存本身的缺陷引起的,内存断电后数据会丢失,但有一些数据是无论如何都不能丢失的,比如用户信息、银行账号等,但是我们无法保证内存永不掉电,所以我们需要持久化,那么什么是持久层呢?
- 完成持久化工作的代码块,也就是上图的 DAO层 ,DAO (Data Access Object) 数据访问对象
我们的系统架构中,应该有一个相对独立的逻辑层面,专注于数据持久化逻辑的实现,与系统其他部分相对而言,这个层面应该具有一个较为清晰和严格的逻辑边界,说白了就是用来操作数据库存在的。
为什么使用MyBatis
除了JDBC可以做DAO层外,我们知道还有很多基于JDBC规范的ORM框架
ORM框架的优势
什么是ORM框架呢?ORM(Object Relational Mapping)框架采用元数据来描述对象与关系映射的细节,元数据一般采用XML格式,并且存放在专门的对象一映射文件中,简单理解为一种框架的格式。使用ORM框架后,应用程序不再直接访问底层数据库,而是以面向对象的方式来操作持久化对象,而ORM框架则会通过映射关系将这些面向对象的操作转换成底层的SQL操作。
那么比起JDBC,ORM的优势是什么呢?
- 传统的JDBC操作 , 有很多重复代码。比如 : 数据取出时的封装 , 数据库的建立连接等。通过框架可以减少重复代码,提高开发效率
- 传统的JDBC操作 , sql语句与代码紧耦合,不利于维护,如果数据表结构变了,那么得全局搜索相关表调整sql语句
- 传统的JDBC操作,不能让程序员像面向对象一样的方式操作数据。
而ORM框架就可以轻松的解决以上的问题,更多区别参照为什么使用MyBatis
Hibernate和MyBatis比较
MyBatis比起Hibernate来说其实是半自动的ORM框架,sql语句还是需要我们来维护的,它的优势如下:
- 简单易学:本身就很小且简单。没有任何第三方依赖,安装只要两个jar文件+配置几个sql映射文件即可,易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现
- 代码解耦:mybatis不会对应用程序或者数据库的现有设计强加任何影响。sql写在xml里,便于统一管理和优化。通过sql语句可以满足操作数据库的所有需求,sql和代码的分离,提高了可维护性。
- 映射标签:提供映射标签,支持对象与数据库的orm字段关系映射;提供对象关系映射标签,支持对象关系组建维护
- 动态sql:提供xml标签,支持编写动态sql
Hibernate是一个全自动的ORM框架,它的优点如下:
- 配置全面:消除了代码的映射规则,它全部被分离到XML或者注解里面去配置,无需再管理数据库连接,它也配置到XML里面。
- 操作简单:一个会话中,不需要操作多个对象,只要操作Sesison即可,关闭资源只需要关闭一个Session即可。
我们知道框架没有最好,只有最合适的,技术往往对应于某种业务场景,那么该如何依照需求选用框架呢?
- Hibernate作为Java ORM框架,编程简易,消除了代码的映射规则,完全通过配置实现。同时无需编写SQL确实开发效率优于MyBatis。而且它也提供了缓存、日志、级联、等强大的功能,但是Hibernate的缺陷也是十分的明显的。就是在多表关联复杂的SQL时、数据系统权限限制时、需要动态SQL时、存储过程使用用时Hibernate十分不便,而性能又难以通过SQL来优化。所以Hibernate一般只适用于场景不太复杂的、性能要求不太苛刻的系统。
- MyBatis 是一个灵活的、可以动态生成映射关系的框架,它几乎可以替代JDBC。拥有动态列、动态表名,存储过程都支持。同时提供了简易的缓存(如(默认)一级缓存,还有二级缓存)、日志、级联。但是它的缺陷是需要自己提供映射规则和SQL,所以它的开发工作量一般要比Hibernate略大一些,适用于场景复杂,使用灵活的大型系统
总之需要根据项目的实际情况去选择框架。MyBatis具有高度灵活、可优化、易维护等特点。生产环境下系统的复杂度一般较高,所以我们更多的是在使用MyBatis
MyBatis框架使用步骤
最终实现的项目结构如下,包含核心配置文件,mapper配置文件,核心配置文件读取类,mapper对应的Dao接口,Model和测试类以及数据库表。
1 创建一个Java Web项目
首先第一步还是创建一个Java Web项目,创建步骤和之前一样,我们创建一个名为MyBatis的项目:
2 搭建数据库表
我们继续使用在之前的Java Web部分使用的库表来进行学习:
并且我们提前插入两条数据。
3 配置Maven依赖
然后再pom.xml中配置Maven依赖,来获取MyBatis的包导入到项目中来,我使用的数据库是MySQL8的版本,而mybatis当前的最新版本是3.5.7【2021年8月8日】:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency>
4 编写核心配置文件
在资源目录下创建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核心配置文件--> <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?userSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> </configuration>
5 编写核心配置读取类
有了核心配置文件后我们还需要配置文件的读取类,二者结合起来类似于JDBC之前使用的JdbcUtils类,只不过我们这里做了解耦操作:
//sqlSessionFactory --> sqlSession public class MybatisUtils { static SqlSessionFactory sqlSessionFactory = null; static { try { //使用Mybatis第一步 :获取sqlSessionFactory对象 String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } //既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例. // SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。 public static SqlSession getSqlSession(){ return sqlSessionFactory.openSession(); } }
这里涉及两个重要对象:
- SqlSessionFactory:是单个数据库映射关系经过编译后的内存镜像,其主要作用是创建SqlSession对象。SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象来构建,而SqlSessionFactoryBuilder则可以通过XML配置文件或一个预先定义好的Configuration实例构建出SqlSessionFactory的实例。SqlSessionFactory对象是线程安全的,它一旦被创建,在整个应用执行期间都会存在。如果我们多次地创建同一个数据库的SqlSessionFactory,那么此数据库的资源将很容易被耗尽。为了解决此问题,通常每一个数据库都会对应一个SqlSessionFactory,所以在构建SqlSessionFactory实例时,建议使用单例模式。
- SqlSession:是应用程序与持久层之间执行交互操作的一个单线程对象,其主要作用是执行持久化操作。SqlSession对象包含了数据库中所有执行SQL操作的方法,由于其底层封装了JDBC连接,所以可以直接使用其实例来执行已映射的SQL语句。每一个线程都应该有一个自己的SqlSession实例,并且该实例是不能被共享的。同时,SqlSession实例也是线程不安全的,因此其使用范围最好在一次请求或一个方法中,决不能将其放在一个类的静态字段、实例字段或任何类型的管理范围(如Servlet的HttpSession)中使用。使用完SqlSession对象之后,要及时关闭它,通常可以将其放在finally块中关闭。
可以理解为会话对象,在一次会话操作中需要使用该对象开启连接和操作。
6 编写数据对象实体类
我们需要创建一个数据对象实体类来和数据表Person进行映射匹配。
package com.example.MyBatis.dao.model; import lombok.Data; /* * person表对应对象 * */ @Data public class Person { private int id; private String username; private String password; private int age; private int phone; private String email; }
7 编写Mapper接口
接下来就是编写实体类对应的操作接口:
package com.example.MyBatis.dao.mapper; import com.example.MyBatis.dao.model.Person; import java.util.List; public interface PersonDao { List<Person> getPersonList(); }
8 编写Mapper接口实现类
由原来的UserDaoImpl转变为一个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"> <!--namespace=绑定一个指定的Dao/Mapper接口--> <mapper namespace="com.example.MyBatis.dao.mapper.PersonDao"> <select id="getPersonList" resultType="com.example.MyBatis.dao.model.Person"> select * from person </select> </mapper>
我们编写好mapper配置文件后需要注册到核心配置文件中:
<?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核心配置文件--> <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?userSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/personMapper.xml"/> </mappers> </configuration>
9 编写测试类
最后我们编写一个测试类,测试一下效果:
import com.example.MyBatis.dao.mapper.PersonDao; import com.example.MyBatis.dao.model.Person; import com.example.MyBatis.utils.MybatisUtils; import org.apache.ibatis.session.SqlSession; import org.junit.jupiter.api.Test; import java.util.List; public class PersonTest { @Test public void test(){ //1.获取SqlSession对象 SqlSession sqlSession = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao = sqlSession.getMapper(PersonDao.class); List<Person> personList = personDao.getPersonList(); for (Person person : personList) { System.out.println(person); } //关闭sqlSession sqlSession.close(); } }
打印结果如下,可以看到数据库内的两条数据被打印出来了:
Mybatis框架操作数据库过程总结
依据以上过程我们可以梳理Mybatis框架操作数据库步骤
- 读取Mybatis配置文件
mybatis-config.xml
。mybatis-config.xml
作为Mybatis的全局配置文件,配置Mybatis的运行环境等信息,其中主要内容是获取数据库连接。 - 加载映射文件
Mapper.xml
。Mapper.xml
文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在mybatis-config.xml
中注册才能执行。mybatis-config.xml
可以加载多个配置文件,每个配置文件对应数据库中的一张表。 - 构建会话工厂。通过Mybatis的环境等配置信息构建会话工厂SqlSessionFactory。
- 创建SqlSession对象。由会话工厂创建SqlSession对象,该对象中包含执行SQL的所有方法。
到这里一个连接数据库的会话就创建完成了,因为拥有数据库的账号密码所以会话可以连接数据库成功,模拟我们登录数据库成功,又因为它内部注册了PersonMapper,所以可以通过getMapper获取到PersonDao对象实例,然后调用getPersonList方法,该方法的实现由我们编写的sql语句提供。
- Mybatis底层定义了一个Executor接口来操作数据库,它会根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
- 在Executor接口的执行方法中,包含一个MappedStatement类型的参数,该参数对映射信息的封装,用于存储要映射的SQL语句的id、参数等。Mapper.xml文件中一个SQL对应一个MappedStatement对象,SQL的id即是MappedStatement的id。
- 输入参数映射。在执行方法时,MappedStatement对象会对用户执行SQL语句的输入参数进行定义(可以定义为Map、List类型、基本类型和POJO类型),Executor执行器会通过MappedStatement对象在执行SQL前,将输入的Java对象映射到SQL语句中。这里对输入参数的映射过程就类似于JDBC编程中对preparedStatement对象设置参数的过程。
- 输入结果映射。在数据库中执行完SQL语句后,MappedStatement对象会对SQL执行输出的结果进行定义(可以定义为Map和List类型、基本类型、POJO类型),Executor执行器会通过MappedStatement对象在执行SQL语句后,将输出结果映射至Java对象中。这种将输出结果映射到Java对象的过程就类似于JDBC编程中对结果的解析处理过程。
这里我们没有用到参数映射,通过id的定义我们能找到方法的映射,然后执行完对应sql语句后,将返回对象映射到结果Model上即可。
总结一下
MyBatis使用过之后才发现确实灵活,因为具体的某个方法的实现都是通过我们编写sql脚本实现的,它做到了sql与代码的解耦,也做到了我们自定义sql的能力,确实非常灵活可维护。对比Hibernate我们可以看到二者是有很强的相似性,从核心配置到Model类到Model类的配置文件。最重要的区别是:Hibernate操作都是通过Hibernate自己封装的HQL语言或者Session实现的,相当于Hibernate提供了一组封装了对sql的api给DAO层,而MyBatis则还是通过sql语句实现的。