【MyBatis】手写简易ORM框架(仿MyBatis)
在前面的文章我们分析了 MyBatis 的底层执行原理,现在我们也来自己实现一个简易的 ORM 框架,在架构上去模仿 MyBatis。跟平时开发软件一样,我们首先要做的就是明确需求。
现在我有一个 User 表,有如下四个字段:
所以我希望能像使用 mybatis 那样通过一个 UserMapper 加一个配置文件去实现数据库操作。
1.用户模块
1)数据库表对应的实体类
public class User {
private long uid;
private String name;
private String phone;
private String email;
//...getter、setter、toString
}
2)Mapper 接口
public interface UserMapper {
// 根据主键uid查询user
User selectUserById(long uid);
}
3)写 SQL 的 Mapper 配置文件
PS:由于 xml 文件解析过于复杂,所以这里采用 properties 文件
#这里通过 key 来表示相应的 StatementID
#StatementID = namespace + sqlId = Mapper接口全类名 + 方法名
#占位符%d我们表示上层传入的参数,我们的框架会对它解析后生成正规SQL语句
com.xupt.yzh.mapper.UserMapper.selectUserById=SELECT * FROM `user` WHERE uid=%d
4)测试类
public class Main{
public static void main(String[] args) {
// 1.获取 SqlSessionFactory
MYSqlSessionFactory sqlSessionFactory = new MYSqlSessionFactory();
// 2.从 SqlSessionFactory 中获取 SqlSession
MYSqlSession sqlSession = sqlSessionFactory.getSqlSession();
// 注意:这里其实可以直接调用 SqlSession#selectOne(statementID,param) 去执行,只不过是硬编码方式(不推荐)
// User user = sqlSession.selectOne("com.xupt.yzh.mapper.UserMapper.selectUserById", 1);
// 3.从 SqlSession 中获取 MapperProxy
UserMapper UserMapper = sqlSession.getMapper(UserMapper.class);
// 4.MapperProxy 调用 Executor 去执行sql
// 调用链路:selectUserById() --> SqlSession#selectOne(statemetId,param) --> Executor#query(sql,param)
User user = UserMapper.selectUserById(1);
System.out.println(user);
}
}
好了,用户部分的代码已经分析完了,我们来看一下整体的项目结构
下面我们就去实现我们的简易 orm 框架 – mebatis!
2.自定义框架
MYSqlSessionFactory
这个类里面只定义了一个方法 getSqlSession,因为他本来就是个工厂。
PS:在 MyBatis 中,获取 SqlSessionFactory 是一个接口,默认实现是 DefaultSqlSessionFactory ,这里为了简单就直接写成实现类了。另外,SqlSessionFactory 不是直接 new 的,而是采用基于 SqlSessionFactoryBuilder 的建造者模式
public class MYSqlSessionFactory {
public MYSqlSession getSqlSession() {
// 在创建 SqlSession 时传入配置类 MYConfiguration
return new MYSqlSession(new MYConfiguration());
}
}
MYConfiguration
Configuration 类主要就是封装了那些配置文件中的所有配置项。在 MyBatis 中配置文件分为两类,一类是配置全局属性的 mybatis-conf.xml,它里面有很多能配置的选项,比如<settings>、<typeAlias>、<typeHandlers>、<plugins>
等等。另一类是写 sql 语句的 mapper.xml,它的核心就在于通过 statementId(namespace+sqlId)找到正确的 sql。
在我们的简易框架中并没有搞 mybatis-conf.xml 那些全局配置属性,只是保留了写 sql 的 mesql.properties。
由于我们要采用基于 Mapper 接口的编码方式,所以肯定还需要一个具体的对象去实现接口,这里采用的是 JDK 动态代理
PS:和我们平常使用的动态代理有所不同,Mapper 接口并没有一个单独实现类(也没必要),而是直接采用 SqlSession 中的执行器 Executor 去执行方法。
public class MYConfiguration {
// 拿到具体的Mapper文件
public static final ResourceBundle sqlMappings;
// 配置文件加载一次就够了,所以这了是通过静态代码块去加载配置文件 mesql.properties
static {
sqlMappings = ResourceBundle.getBundle("mesql");
}
// 通过 JDK 动态代理生成代理对象
// clazz(Mapper接口)并没有单独的实现类,
// 代理类 MapperProxy 传入 SqlSession 是因为要借助 SqlSession 中的 Executor 去执行方法
public <T> T getMapper(Class clazz, MYSqlSession sqlSession) {
return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{
clazz},
new MYMapperProxy(sqlSession));
}
}
MYSqlSession
最核心的一个对象,里面还组合了配置类 Configuration 和执行器 Executor,其中 Configuration 在这里的主要作用是拿到代理对象,而 Executor 是用来具体执行 sql 的。
正因为 Executor 是用来执行具体 sql 的,而 sql 还在配置文件中没有被解析,所以用户不可以直接调用到 Executor,而是使用 SqlSession 提供的 selectOne()、selectList() 、insert()、delete() 、update() 等入参为 statementID 和 param 的方法,这些方法通过 statementID 拿到配置文件中的 sql 语句后,再在方法内调用 Executor。
PS:在 MyBatis 中 SqlSession 是一个接口,默认实现是 DefaultSqlSession,这里为了简单就直接写成实现类了。另外,SqlSession 的生命周期是请求/方法,所以每次有请求过来都会创建一个,所以 DefaultSqlSession 是线程不安全的,所以后来 Spring 重新封装了一个线程安全的 SqlSessionTemplate。
public class MYSqlSession {
private MYConfiguration configuration;
private MYExecutor excutor;
// 在 SqlSessionFactory 构造时传入了配置对象 Configuration
public MYSqlSession(MYConfiguration configuration) {
this.configuration = configuration;
this.excutor = new MYExecutor();
}
// 先根据statementId解析sql,然后调用Excutor提供的方法执行查询
public <T> T selectOne(String statementId, Object parameter) {
// 1.根据statementId拿到SQL
String sql = MYConfiguration.sqlMappings.getString(statementId);
// 2.调用 Executor 去执行SQL
return excutor.query(sql, parameter);
}
// 通过Configuration获取提过给客户端的代理对象
public <T> T getMapper(Class clazz) {
return configuration.getMapper(clazz, this);
}
}
MYMapperProxy
代理类,目的是解决用户直接调用 SqlSession#selectOne() 传入 statementID 的硬编码问题,所以 MapperProxy 的核心工作就是自行解析 statementID,然后再调用 SqlSession#selectOne() 方法。
由于 statementID = namespace + sqlId,所以当采用基于 Mapper 接口的编码方式时 namespace = mapper接口全类名,sqlId = 方法名,即 statementID = mapper接口全类名 + 方法名。
由于是采用的 JDK 动态代理,所以 MapperProxy 实现了 InvocationHandler 接口,即对接口方法的调用都会走到 invoke()。所以在 invoke 方法中我们就可以通过当前调用的 method 拿到 mapper 接口全类名和方法名(即statementID),然后再调用 SqlSession 中封装的相应方法。
PS:在 MyBatis 中会做操作类型的判断然后再调用 SqlSession 中的 insert()、delete()、update()、selectOne()、selectList() 等等,但这里为了简单就直接写死成查询操作的 selectOne()。
public class MYMapperProxy implements InvocationHandler {
private MYSqlSession sqlSession;
// 在 Configuration#getMapper() 创建动态代理时就传入了 SqlSession
public MYMapperProxy(MYSqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1.拼装statementID
String mapperInterface = method.getDeclaringClass().getName();
String methodName = method.getName();
String statementId = mapperInterface + "." + methodName;
// 2.调用 SqlSession#selectOne()
return this.sqlSession.selectOne(statementId, args[0]);
}
}
MYExecutor
执行器,具体执行数据库操作,里面是具体的 JDBC 代码。
PS:在 MyBatis 中 Excutor 是一个接口,它有一个抽象子类 BaseExecutor,在这下面还有四个具体的实现类,默认的实现类是 SimpleExecutor。在这里我们将 Executor 写成实现类。
另外,在 MyBatis 中还有一个 ResultSetHandler 去做结果集映射,但这里只是为了模仿,所以把对数据库的连接和结果的封装直接写死了。
public class MYExecutor {
public <T> T query(String sql, Object paramater) {
Connection conn = null;
Statement stmt = null;
User user = new User();
try {
// 1.注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.创建与数据库的连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "123456");
// 3.获取执行平台 Statement
stmt = conn.createStatement();
// 4.执行SQL语句
// 注意:这里的 format() 是为了将mesql.properties中 SQL 的占位符 %d 替换成参数(相当于变相的sql拼接)
ResultSet rs = stmt.executeQuery(String.format(sql, paramater));
// 5.获取并封装结果集
while (rs.next()) {
long uid = rs.getLong("uid");
String name = rs.getString("name");
String phone = rs.getString("phone");
String email = rs.getString("email");
user.setUid(uid);
user.setName(name);
user.setPhone(phone);
user.setEmail(email);
}
// 6.释放资源
rs.close();
stmt.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (stmt != null) stmt.close();
} catch (SQLException se2) {
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
return (T)user;
}
}
最后查询结果如下:
完整代码我放 GitHub 上了,有需要的同学点击这里跳转…
还没有评论,来说两句吧...