mybatis的本质和原理

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用版 2核4GB 50GB
简介: mybatis的本质和原理

背景


   项目需要,我们需要自己做一套mybatis,或者使用大部分mybatis的原始内容。对其改造,以适应需要。这就要求我再次学习一下mybatis,对它有更深入的了解。

 

是什么


   MyBatis是一个持久层框架,用来处理对象关系映射。说白了就是以相对面向对象的方式来提交sql语句给jdbc。如果想找个简单、快速上手的例子,最好是和spring想结合的。直接用官网的吧,简单清晰也没谁了:http://mybatis.org/spring/getting-started.html

https://mybatis.org/mybatis-3/getting-started.html

 

为什么


   Java开发都是面向对象的思维,如果用传统下面自己去调用连接拼装sql的方式,维护成本高,代码可读性差。


public static void main(String[] args) {
    //数据库连接对象
    Connection conn = null;
    //数据库操作对象
    PreparedStatement stmt = null;
    //1、加载驱动程序
    try {
        Class.forName(DBDRIVER);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    //2、连接数据库
    //通过连接管理器连接数据库
    try {
        //在连接的时候直接输入用户名和密码才可以连接
        conn = DriverManager.getConnection(DBURL, USERNAME, PASSWORD);
    } catch (SQLException e) {
        e.printStackTrace();
    }
    //3、向数据库中插入一条数据
    String sql = "INSERT INTO person(name,age) VALUES (?,?)";
    try {
        stmt = conn.prepareStatement(sql);
        stmt.setString(1,"陈昆仑");
        stmt.setInt(2,21);
        stmt.executeQuery();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    //4、执行语句
    try {
        ResultSet resultSet = stmt.executeQuery();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    //5、关闭操作,步骤相反哈~
    try {
        stmt.close();
        conn.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}


怎么做


   我们来看一下底层是怎么处理和交互的。基本流程如下:


1112728-20210424194426733-1933003966.png


1112728-20210424194426733-1933003966.png看着头大?没事,我们先从最简化的版本开始添枝加叶。MyBatis可以用配置文件或者注解形式注入sql。因为配置文件方式可以方便的处理动态SQL(动态SQL就是sql语句里有if else for这些的,可以根据参数的变化最终sql也跟着变化)等优点,用的更为普遍。


   假设现在是2000年,Clinton Begin还没有发起ibatis(mybatis的前身)项目。而apache基金会内部发起了讨论要设计这样一个产品,指派你作为项目负责人。现在思考,你的思路是什么?


   一般思路是先把架构搭建起来,做成一个MVP最小可行性版本,然后再做功能增强。


   从功能最简化方面来看,需要两步:第一步要将sql及所需要的元素以对象的形式输入,第二步是获取到这些信息转换成jdbc信息处理。


   这样拆解后的思路是将sql及所需要的元素拆解成类方法的参数形式,方法本身要做的事情就是将这些参数以jdbc编程需要的形式传给jdbc执行。这里方法内部做的事情是一样的,那就自然而然的想到不用每个类都有一个实现。只要定义好接口,把实现用代理或者上层切面的方式统一处理就可以了。


根据这个思路,首先要用代理来获取参数。我设计使用方式是Insert、Select等注解里写sql元语句。通过方法参数注入参数。最终返回结果。如下


public interface UserMapper {
    @Insert("INSERT INTO person(name,age) VALUES (#{name},#{age})")
    Integer insertUser(User user);
}


 要实现接口的解析。先建立一个类,里面构造一个代理类,实现类似于SqlSession,所以起名叫YunaSession(yuna是我给经典java学习场景工程https://github.com/xiexiaojing/yuna 起的名字)


public class YunaSession {
   public static Object dealSql(Class clazz) {
       Class c[] = new Class[]{clazz};
   return Proxy.newProxyInstance(YunaSession.class.getClassLoader(), c,
new YunaInvocationHandler());
   }
}    


下面要实现的是代理中YunaInvocationHandler真正要实现的逻辑:将这些参数以jdbc编程需要的形式传给jdbc执行。也就是说把上面【为什么】部分一开始的那段执行jdbc的代码贴进去,将sql和参数的部分做替换。


我们把关键再贴一遍便于说明问题


//3、向数据库中插入一条数据
String sql = "INSERT INTO person(name,age) VALUES (?,?)";
try {
    stmt = conn.prepareStatement(sql);
    stmt.setString(1,"陈昆仑");
    stmt.setInt(2,21);
    stmt.executeQuery();
} catch (SQLException e) {
    e.printStackTrace();
}


这里有两个?,而jdbc的预处理语句传入参数的时候要明确的知道第一个参数的类型是什么,如果传过来是对象的话,要知道对应对象的哪个值。这就是为什么接口里的预处理语句传入是


INSERT INTO person(name,age) VALUES (#{name},#{age})


  因为可以通过匹配#{XX}这样的确定都是哪些参数,因为User对象里有定义参数的类型。所以类型和值都确定了。这个就是MappedStatement对象做的事情。以下是用正则表达式匹配+反射来达到解析sql并和对象值做匹配的实现:


public static void main(String[] args) throws Exception{
    Matcher m= pattern.matcher("INSERT INTO person(name,age) VALUES (#{name},#{age})");
    User user1 = new User();
    user1.setId(1);
    user1.setName("贾元春");
    user1.setAge(27);
    int i=1;
    while(m.find()) {
        System.out.println(m.group());
        String group = m.group();
        String fieldName = group.replace("#{","").replace("}","");
        Field field = User.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        if("java.lang.Integer".equals(field.getType().getName())) {
            System.out.println("stmt.setInt("+i+","+field.get(user1)+")");
        } else if("java.lang.String".equals(field.getType().getName())) {
            System.out.println(" stmt.setString("+i+","+field.get(user1)+")");
        }
        i++;
    }
}


运行结果是


1112728-20210424194454842-1959556838.png


可以看到实现了效果。下面就是和jdbc连接结合起来。


public class YunaInvocationHandler implements InvocationHandler {
    public static final String DBDRIVER = "org.xx.mm.mysql.Driver";
    public static final String DBURL = "jdbc:mysql://localhost:3306/mydb";
    //现在使用的是mysql数据库,是直接连接的,所以此处必须有用户名和密码
    public static final String USERNAME = "root";
    public static final String PASSWORD = "mysqladmin";
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Exception{
        Object result = null;
        Insert insert = method.getAnnotation(Insert.class);
        if (insert != null) {
            String sql = insert.value()[0];
            System.out.println("插入语句为"+s);
            YunaSqlDeal yunaSqlDeal = new YunaSqlDeal();
            yunaSqlDeal.insert(s, Arrays.toString(args));
            //1、加载驱动程序
            try {
                Class.forName(DBDRIVER);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            //2、连接数据库
            //通过连接管理器连接数据库
            //数据库连接对象
            Connection conn = null;
            try {
                //在连接的时候直接输入用户名和密码才可以连接
                conn = DriverManager.getConnection(DBURL, USERNAME, PASSWORD);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            composeStatement(sql, args[0], conn);
        }
        return 1;
    }
    private static final String PATTERN = "#\\{[A-Za-z0-9]+\\}";
    private static Pattern pattern = Pattern.compile("("+PATTERN+")");
    public static void composeStatement(String sql, Object obj, Connection conn) throws Exception{
        PreparedStatement stmt = conn.prepareStatement(sql.replaceAll(PATTERN, ""));
        Matcher m= pattern.matcher(sql);
        int i=1;
        while(m.find()) {
            System.out.println(m.group());
            String group = m.group();
            String fieldName = group.replace("#{","").replace("}","");
            Field field = User.class.getDeclaredField(fieldName);
            field.setAccessible(true);
            if("java.lang.Integer".equals(field.getType().getName())) {
                System.out.println("stmt.setInt("+i+","+field.get(obj)+")");
                stmt.setInt(i, Integer.parseInt(field.get(obj).toString()));
            } else if("java.lang.String".equals(field.getType().getName())) {
                stmt.setString(i, field.get(obj).toString());
            }
            i++;
        }
        stmt.execute();
        stmt.close();
        conn.close();
    }
}


  这个实现的是insert的,返回值类型固定,如果是select查询语句,涉及到返回的结果封装成对象。思路也是通过反射,和参数转换步骤差不多,就不贴代码了。


   到此,我们实现了一个简化版的mybatis框架。比贴的架构图简化在少用了很多设计模式的东西,和出于性能考虑重用的东西。mybatis的核心就实现完了。

 

总结


本文从mybatis的设计者角度出发,构造了一个简化的mybatis框架。具体可运行的完整代码放到了我的github上,地址:

https://github.com/xiexiaojing/yuna


   很多原理性的东西看过之后会忘,但是如果真正站在设计者角度实现过一个简化的版本,相信会增强记忆。同时也能和真正的实现做对比,更深层学习技术大牛们的设计精华

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
SQL XML Java
|
3月前
|
SQL XML Java
一文搞懂Mybatis执行原理
一文搞懂Mybatis执行原理
115 1
|
3月前
|
SQL Java 数据库连接
mybatis常见分页技术和自定义分页原理实战
mybatis常见分页技术和自定义分页原理实战
111 0
|
23天前
|
SQL Java 数据库连接
springboot~mybatis-pagehelper原理与使用
【7月更文挑战第15天】MyBatis-PageHelper是用于MyBatis的分页插件,基于MyBatis的拦截器机制实现。它通过在SQL执行前动态修改SQL语句添加LIMIT子句以支持分页。使用时需在`pom.xml`添加依赖并配置方言等参数。示例代码: PageHelper.startPage(2, 10); List<User> users = userMapper.getAllUsers(); PageInfo<User> pageInfo = new PageInfo<>(users); 这使得分页查询变得简单且能获取总记录数等信息。
|
2月前
|
SQL Java 数据库连接
深入探索MyBatis Dynamic SQL:发展、原理与应用
深入探索MyBatis Dynamic SQL:发展、原理与应用
|
1月前
|
SQL Java 数据库连接
Java面试题:简述ORM框架(如Hibernate、MyBatis)的工作原理及其优缺点。
Java面试题:简述ORM框架(如Hibernate、MyBatis)的工作原理及其优缺点。
31 0
|
2月前
|
Java 数据库连接 数据库
MyBatis TypeHandler详解:原理与自定义实践
MyBatis TypeHandler详解:原理与自定义实践
|
2月前
|
SQL Java 数据库连接
MyBatis插件深度解析:功能、原理、使用、应用场景与最佳实践
MyBatis插件深度解析:功能、原理、使用、应用场景与最佳实践
|
3月前
|
存储 缓存 Java
探秘MyBatis缓存原理:Cache接口与实现类源码分析
探秘MyBatis缓存原理:Cache接口与实现类源码分析
64 2
探秘MyBatis缓存原理:Cache接口与实现类源码分析
|
3月前
|
SQL 缓存 Java