MyBatis之手写简易MyBatis
前言
现在市面上有很多成熟的ORM框架,每一种框架都有其优势和不足。在众多的优秀框架中,MyBatis无论是从地位还是市场占有率都占有很大的比重。我们也有很多小伙伴想着通过阅读源码的方式来深入了解其底层原理,进而提升自己的架构能力和抽象能力。也可以将其改造优化和改造。
在我看来,想要深入了解一个框架,就要从其设计思想入手。先摸清楚它的设计思路,再根据思路去看实现细节,这样才能达到事半功倍的效果。
本博文会记录博主自己编写的一个简易的MyBatis的实现过程,希望可以对有这方面需求的小伙伴提供帮助。也是对自己学习之路的一个小小的总结。
设计思路
我们要知道,市面上的ORM框架,说白了就是对于JDBC的封装。我们用JDBC开发过软件的小伙伴,肯定会知道使用JDBC编程的一些弊端。比如操作繁琐,SQL写在代码中存在硬编码问题,冗余重复的代码很多等等。
所以,我们在开发之初,要想清楚开发的目的,也就是解决JDBC的哪些问题,以及自己的实现思路,并根据自己的实现思路先画出自己的设计图。在我们的实际编码中,切忌不要有硬编码问题。而且要面向接口编程,时刻谨记单一职责和开闭原则的设计思想。
我们先来看一段JDBC的示例:
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
// 定义sql语句?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1, "tom");
// 向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历查询结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装User
user.setId(id);
user.setUsername(username);
}
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
由以上代码我们可以总结以下几个问题:
- 数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能
- SQL语句在代码中硬编码,造成代码不易维护,实际应用中SQL变化的可能较大,SQL变动需要改变java代码
- 使用preparedStatement向占有位符号传参数存在硬编码,因为SQL语句的WHERE条件不一定,可能多也可能少,修改SQL还要修改代码,系统不易维护
- 对结果集解析存在硬编码(查询列名),SQL变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便
我们的对应解决方案如下: - 使用连接池技术避免重复创建销毁连接
- 将SQL语句封装在配置文件中,程序通过读取配置文件来执行操作,解决SQL硬编码问题
- 将参数也配置在配置文件中,程序通过反射技术动态匹配参数,解决参数硬编码问题
- 将解析结果配置到配置文件中,通过反射动态拼装需要的结果类型,解决查询结果转换硬编码问题
基于以上解决方案,我们的架构设计图如下图所示:
基本架构分为以上几个大的模块:
- 使用构建者模式通过读取配置文件来构建SqlSessionFactory,并封装配置对象Configration
- 通过SqlSessionFactory对象产生SqlSession,SqlSession负责具体的增删改查的业务处理
- SqlSession将与数据库的交互委托给Excutor(封装jdbc)执行
项目准备
新建一个maven工程,导入以下坐标:
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
核心配置文件格式如下:
<configuration>
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///mybatis-demo"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</dataSource>
<mapperScan>
<property name="base-package" value="mappers"></property>
</mapperScan>
</configuration>
初始化数据库sql如下:
create schema `mybatis-demo` collate utf8_general_ci;
use `mybatis-demo`;
create table dept_info
(
id int auto_increment comment '主键'
primary key,
dept_name varchar(255) default '' not null comment '部门名称',
parent_id int default 0 not null comment '上级部门id'
)
comment '部门表';
create table user_info
(
id int auto_increment comment '主键'
primary key,
username varchar(255) default '' not null comment '用户姓名'
)
comment '用户表';
sql配置的mapper.xml如下:
<mapper namespace="com.rubin.mybatis.dao.IUserDao">
<select id="selectAll" resultType="com.rubin.mybatis.pojo.UserInfo">
SELECT id AS id, username AS username FROM user_info
</select>
<select id="selectByCondition" resultType="com.rubin.mybatis.pojo.UserInfo" parameterType="com.rubin.mybatis.pojo.UserInfo">
SELECT id AS id, username AS username FROM user_info WHERE username = #{username}
</select>
<insert id="insert" parameterType="com.rubin.mybatis.pojo.UserInfo">
INSERT INTO user_info(username) VALUES (#{username})
</insert>
<update id="update" parameterType="com.rubin.mybatis.pojo.UserInfo">
UPDATE user_info SET username = #{username} WHERE id = #{id}
</update>
<delete id="delete" parameterType="com.rubin.mybatis.pojo.UserInfo">
DELETE FROM user_info WHERE id = #{id}
</delete>
</mapper>
<mapper namespace="com.rubin.mybatis.dao.IDeptDao">
<select id="selectAll" resultType="com.rubin.mybatis.pojo.DeptInfo">
SELECT id AS id, parent_id AS parentId, dept_name AS deptName FROM dept_info
</select>
<select id="selectByCondition" resultType="com.rubin.mybatis.pojo.DeptInfo" parameterType="com.rubin.mybatis.pojo.DeptInfo">
SELECT id AS id, parent_id AS parentId, dept_name AS deptName FROM dept_info WHERE id = #{id} AND parent_id = #{parentId} AND dept_name = #{deptName}
</select>
</mapper>
表映射的实体如下:
/**
* 用户信息实体
* Created by rubin on 2021/3/6.
*/
@Data