【MyBatis】手写简易ORM框架(仿MyBatis)

爱被打了一巴掌 2022-12-29 02:29 271阅读 0赞

在前面的文章我们分析了 MyBatis 的底层执行原理,现在我们也来自己实现一个简易的 ORM 框架,在架构上去模仿 MyBatis。跟平时开发软件一样,我们首先要做的就是明确需求。

现在我有一个 User 表,有如下四个字段:

在这里插入图片描述
所以我希望能像使用 mybatis 那样通过一个 UserMapper 加一个配置文件去实现数据库操作。

1.用户模块

1)数据库表对应的实体类

  1. public class User {
  2. private long uid;
  3. private String name;
  4. private String phone;
  5. private String email;
  6. //...getter、setter、toString
  7. }

2)Mapper 接口

  1. public interface UserMapper {
  2. // 根据主键uid查询user
  3. User selectUserById(long uid);
  4. }

3)写 SQL 的 Mapper 配置文件

PS:由于 xml 文件解析过于复杂,所以这里采用 properties 文件

  1. #这里通过 key 来表示相应的 StatementID
  2. #StatementID = namespace + sqlId = Mapper接口全类名 + 方法名
  3. #占位符%d我们表示上层传入的参数,我们的框架会对它解析后生成正规SQL语句
  4. com.xupt.yzh.mapper.UserMapper.selectUserById=SELECT * FROM `user` WHERE uid=%d

4)测试类

  1. public class Main{
  2. public static void main(String[] args) {
  3. // 1.获取 SqlSessionFactory
  4. MYSqlSessionFactory sqlSessionFactory = new MYSqlSessionFactory();
  5. // 2.从 SqlSessionFactory 中获取 SqlSession
  6. MYSqlSession sqlSession = sqlSessionFactory.getSqlSession();
  7. // 注意:这里其实可以直接调用 SqlSession#selectOne(statementID,param) 去执行,只不过是硬编码方式(不推荐)
  8. // User user = sqlSession.selectOne("com.xupt.yzh.mapper.UserMapper.selectUserById", 1);
  9. // 3.从 SqlSession 中获取 MapperProxy
  10. UserMapper UserMapper = sqlSession.getMapper(UserMapper.class);
  11. // 4.MapperProxy 调用 Executor 去执行sql
  12. // 调用链路:selectUserById() --> SqlSession#selectOne(statemetId,param) --> Executor#query(sql,param)
  13. User user = UserMapper.selectUserById(1);
  14. System.out.println(user);
  15. }
  16. }

好了,用户部分的代码已经分析完了,我们来看一下整体的项目结构

在这里插入图片描述
下面我们就去实现我们的简易 orm 框架 – mebatis!

2.自定义框架

MYSqlSessionFactory

这个类里面只定义了一个方法 getSqlSession,因为他本来就是个工厂。

PS:在 MyBatis 中,获取 SqlSessionFactory 是一个接口,默认实现是 DefaultSqlSessionFactory ,这里为了简单就直接写成实现类了。另外,SqlSessionFactory 不是直接 new 的,而是采用基于 SqlSessionFactoryBuilder 的建造者模式

  1. public class MYSqlSessionFactory {
  2. public MYSqlSession getSqlSession() {
  3. // 在创建 SqlSession 时传入配置类 MYConfiguration
  4. return new MYSqlSession(new MYConfiguration());
  5. }
  6. }

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 去执行方法。

  1. public class MYConfiguration {
  2. // 拿到具体的Mapper文件
  3. public static final ResourceBundle sqlMappings;
  4. // 配置文件加载一次就够了,所以这了是通过静态代码块去加载配置文件 mesql.properties
  5. static {
  6. sqlMappings = ResourceBundle.getBundle("mesql");
  7. }
  8. // 通过 JDK 动态代理生成代理对象
  9. // clazz(Mapper接口)并没有单独的实现类,
  10. // 代理类 MapperProxy 传入 SqlSession 是因为要借助 SqlSession 中的 Executor 去执行方法
  11. public <T> T getMapper(Class clazz, MYSqlSession sqlSession) {
  12. return (T)Proxy.newProxyInstance(this.getClass().getClassLoader(),
  13. new Class[]{
  14. clazz},
  15. new MYMapperProxy(sqlSession));
  16. }
  17. }

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。

  1. public class MYSqlSession {
  2. private MYConfiguration configuration;
  3. private MYExecutor excutor;
  4. // 在 SqlSessionFactory 构造时传入了配置对象 Configuration
  5. public MYSqlSession(MYConfiguration configuration) {
  6. this.configuration = configuration;
  7. this.excutor = new MYExecutor();
  8. }
  9. // 先根据statementId解析sql,然后调用Excutor提供的方法执行查询
  10. public <T> T selectOne(String statementId, Object parameter) {
  11. // 1.根据statementId拿到SQL
  12. String sql = MYConfiguration.sqlMappings.getString(statementId);
  13. // 2.调用 Executor 去执行SQL
  14. return excutor.query(sql, parameter);
  15. }
  16. // 通过Configuration获取提过给客户端的代理对象
  17. public <T> T getMapper(Class clazz) {
  18. return configuration.getMapper(clazz, this);
  19. }
  20. }

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()。

  1. public class MYMapperProxy implements InvocationHandler {
  2. private MYSqlSession sqlSession;
  3. // 在 Configuration#getMapper() 创建动态代理时就传入了 SqlSession
  4. public MYMapperProxy(MYSqlSession sqlSession) {
  5. this.sqlSession = sqlSession;
  6. }
  7. @Override
  8. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  9. // 1.拼装statementID
  10. String mapperInterface = method.getDeclaringClass().getName();
  11. String methodName = method.getName();
  12. String statementId = mapperInterface + "." + methodName;
  13. // 2.调用 SqlSession#selectOne()
  14. return this.sqlSession.selectOne(statementId, args[0]);
  15. }
  16. }

MYExecutor

执行器,具体执行数据库操作,里面是具体的 JDBC 代码。

PS:在 MyBatis 中 Excutor 是一个接口,它有一个抽象子类 BaseExecutor,在这下面还有四个具体的实现类,默认的实现类是 SimpleExecutor。在这里我们将 Executor 写成实现类。
另外,在 MyBatis 中还有一个 ResultSetHandler 去做结果集映射,但这里只是为了模仿,所以把对数据库的连接和结果的封装直接写死了。

  1. public class MYExecutor {
  2. public <T> T query(String sql, Object paramater) {
  3. Connection conn = null;
  4. Statement stmt = null;
  5. User user = new User();
  6. try {
  7. // 1.注册 JDBC 驱动
  8. Class.forName("com.mysql.jdbc.Driver");
  9. // 2.创建与数据库的连接
  10. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis", "root", "123456");
  11. // 3.获取执行平台 Statement
  12. stmt = conn.createStatement();
  13. // 4.执行SQL语句
  14. // 注意:这里的 format() 是为了将mesql.properties中 SQL 的占位符 %d 替换成参数(相当于变相的sql拼接)
  15. ResultSet rs = stmt.executeQuery(String.format(sql, paramater));
  16. // 5.获取并封装结果集
  17. while (rs.next()) {
  18. long uid = rs.getLong("uid");
  19. String name = rs.getString("name");
  20. String phone = rs.getString("phone");
  21. String email = rs.getString("email");
  22. user.setUid(uid);
  23. user.setName(name);
  24. user.setPhone(phone);
  25. user.setEmail(email);
  26. }
  27. // 6.释放资源
  28. rs.close();
  29. stmt.close();
  30. conn.close();
  31. } catch (SQLException se) {
  32. se.printStackTrace();
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. } finally {
  36. try {
  37. if (stmt != null) stmt.close();
  38. } catch (SQLException se2) {
  39. }
  40. try {
  41. if (conn != null) conn.close();
  42. } catch (SQLException se) {
  43. se.printStackTrace();
  44. }
  45. }
  46. return (T)user;
  47. }
  48. }

最后查询结果如下:

在这里插入图片描述

完整代码我放 GitHub 上了,有需要的同学点击这里跳转…

发表评论

表情:
评论列表 (有 0 条评论,271人围观)

还没有评论,来说两句吧...

相关阅读

    相关 简易Mybatis

    手写简易的Mybatis -------------------- 此篇文章用来记录今天花个五个小时写出来的简易版mybatis,主要实现了基于注解方式的增删查改,...

    相关 仿简易 ORM 框架

    在前面的文章我们分析了 MyBatis 的底层执行原理,现在我们也来自己实现一个简易的 ORM 框架,在架构上去模仿 MyBatis。跟平时开发软件一样,我们首先要做的就是明确