理解MyBatis原理、思想

JDBC编码的缺点

  • 结果集解析复杂,列名硬编码,sql变化导致解析代码变化,系统不易维护。
  • Sql语句硬编码,难以维护。 数据库配置硬编码。
  • 频繁连接、释放数据库资源,降低系统性能。
  • preparedStatement向占位符号传参数存在硬编码,不易维护。

MyBatis的核心原理、思想

MyBatis的出现,使得我们不用在代码中进行SQL语句硬编码,代码更加容易维护,更好的管理数据库资源,提升性能。

手写一个简单的ORM框架,加深理解

用例分析

框架写出来是给开发人员用的,使用过程无非 导包、配置、按规则编写代码、运行。

功能分析

设计一个最小可用的仿MyBatis的ORM框架,那首先我们需要拿到数据源的配置信息、Mapper配置的信息,然后在调用Mapper方法的时候,解析方法参数,填充到Mapper方法对应的SQL中去,然后执行QL,拿到结果之后,按照配置解析并映射到对应的Java对象。

功能大概:

代码分析、设计

1.格式约定

首先,我们要约定数据源、Mapper配置信息的格式。

  • 存储数据源信息、Mapper 配置信息的格式
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
  <!-- 数据源信息 -->
    <dataSource>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/ruoyi?characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </dataSource>

    <!-- Mapper XML文件配置信息 -->
    <mappers>
        <mapper resource="SysRoleMapper.xml"/>
    </mappers>
</configuration>
  • 具体的Mapper XML文件的格式
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.hero.dao.SysRoleMapper">
  <!-- id:方法名称; resultType: 查询结果返回值-->
    <select id="findAll" resultType="com.hero.dao.SysRole">
        select * from sys_role;
    </select>
</mapper>

2.数据对象

然后,我们需要一个存储数据源信息、Mapper信息、SQL信息的载体,简单来说 XML -> Java对象。

  • Configuration类存储了数据源信息、Mapper XML解析之后的具体方法信息。其中,sqlSourceMap对象的key存储Mapper XML中方法的全路径,比如:com.hero.dao.SysRoleMapper.findAll,而value存储SqlSource对象,此对象有 具体sql 以及 查询的结果类型。
  • SqlSource类存储了Mapper XML 中方法对应对应的sql,以及方法结果类型。

从json格式的角度来看Configuration对象的数据的格式,这样更好理解一些:

json 复制代码
{
  "driver": "xxx",
  "url": "xxx",
  "sqlSourceMap": {
    "com.hero.dao.SysRoleMapper.findAll": {
        "sql": "select * from sys_role",
        "reultType": ""
    }
  },
  "password": "xxx",
  "username": "xxxx"
}

3.数据源加载、装配Mapper、解析SQL

有了载体之后,我们要实现 加载数据源配置 、 **装配Mapper XML功能、解析方法对应的SQL **,在这里,由SqlSessionFactoryBuilder类负载从xml文件中加载数据源信息 并 装配Mapper信息,统一写入Configuration对象。

4.通用SQL方法接口与执行器

有了上述信息,我们就可以创建执行SQL语句的规范接口SqlSession,此接口提供了通用的方法去处理业务的CRUD语句,默认实现类是DefaultSqlSessionImpl,接口方法传入 Mapper XML方法 全路径,实现类将全路径传递给Executor类,Executor会获取到具体的SQL,进行参数填充,并发送SQL到数据库,然后拿到执行结果,进行结果映射,最后返回给实现类。

在这里,我们不直接创建SqlSession实现类,而是通过SqlSessionFactory工厂类创建。

  • SqlSession接口。除了通用CURD方法(select\update\delete\insert),getMapper 也是一个重要的方法,可以获取Mapper接口的代理实例,通过Mapper接口即可执行SQL,不用直接使用SqlSession接口,例如 List<SysRole> list = sysRoleMapper.findAll()
  • Executor类负责发送SQL到数据库,处理SQL的参数填充(绑定)、结果集的映射。
  • SqlSessionFactory通过openeSession()方法创建SqlSession对象

小结

通过SqlSessionFactoryBuilder类,我们将XML信息写入到Configuration对象,并构建一个SqlSessionFactory会话工厂,通过工厂类可以获取SqlSession接口具体实现类实例DefaultSqlSessionImplSqlSession接口提供了通用的CURD方法,而实现类依赖Executor执行器,实现参数填充、SQL发送、结果映射功能。 同时,我们提到了SqlSession的getMapper方法,在后面代码中会看到。

代码实现

Configuration

java 复制代码
public class Configuration {
    private String driver;
    private String url;
    private String username;
    private String password;
    private Map<String, SqlSource> sqlSourceMap = new HashMap<>();
	// getting/setting
}

SqlSource

java 复制代码
public class SqlSource {
    //存储了Sql语句
    private String sql;
    //存储封装JavaBean对象类的全限定名称
    private String resultType;
    // getting/setting
}

SqlSessionFactoryBuilder

java 复制代码
public class SqlSessionFactoryBuilder {
    /**
     * 构建工厂对象
     * 参数:SqlMapConfig.xml配置文件的输入流对象
     */
    public SqlSessionFactory build(InputStream inputStream) throws
            DocumentException {
        Configuration configuration = new Configuration();
        //解析配置文件
        loadXmlConfig(configuration, inputStream);
        return new SqlSessionFactory(configuration);
    }

    /**
     * 解析框架使用者传入的配置文件
     */
    private void loadXmlConfig(Configuration configuration, InputStream
            inputStream) throws DocumentException {
        //创建解析XML文件对象SAXReader
        SAXReader saxReader = new SAXReader();
        //读取SqlMapConfig.xml配置文件流资源,获取文档对象
        Document document = saxReader.read(inputStream);
        //获取SqlMapConfig.xml 配置文件内所有property标签元素
        List<Element> selectNodes = document.selectNodes("//property");
        //循环解析property标签内容,抽取配置信息
        for (Element element : selectNodes) {
            String name = element.attributeValue("name");
            if ("driver".equals(name)) {//数据库驱动
                configuration.setDriver(element.attributeValue("value"));
            } else if ("url".equals(name)) {//数据库地址
                configuration.setUrl(element.attributeValue("value"));
            } else if ("username".equals(name)) {//用户名
                configuration.setUsername(element.attributeValue("value"));
            } else if ("password".equals(name)) {//密码
                configuration.setPassword(element.attributeValue("value"));
            }
        }
        //解析SqlMapConfig.xml 映射器配置信息
        List<Element> list = document.selectNodes("//mapper");
        for (Element element : list) {
            //SQL映射配置文件路径
            String resource = element.attributeValue("resource");
            //解析SQL映射配置文件
            loadSqlConfig(resource, configuration);
        }
    }


    /**
     * 解析SQL配置文件
     */
    private void loadSqlConfig(String resource, Configuration configuration)
            throws DocumentException {
        //根据SQL映射配置文件路径,读取流资源。classpath路径下
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resource);
        //创建解析XML文件的对象SAXReader
        SAXReader saxReader = new SAXReader();
        //读取UserMapper.xml配置文件文档对象
        Document document = saxReader.read(inputStream);
        //获取文档对象根节点:<mapper namespace="test">
        Element rootElement = document.getRootElement();
        //取出根节点的命名空间
        String namespace = rootElement.attributeValue("namespace");
        //获取当前SQL映射文件所有查询语句标签
        List<Element> selectNodes = document.selectNodes("//select");
        //循环解析查询标签select,抽取SQL语句
        for (Element element : selectNodes) {
            //查询语句唯一标识
            String id = element.attributeValue("id");
            //当前查询语句返回结果集对象类型
            String resultType = element.attributeValue("resultType");
            //查询语句
            String sql = element.getText();
            //创建Mapper对象
            SqlSource mapper = new SqlSource();
            mapper.setSql(sql);
            mapper.setResultType(resultType);
            //在configuration中设置mapper类,key:(命名空间+.+SQL语句唯一标识符)
            configuration.getSqlSourceMap().put(namespace + "." + id, mapper);
        }
    }
}

SqlSessionFactory

java 复制代码
public class SqlSessionFactory {

    private Configuration configuration;
    public SqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }
    /**
     * 创建SqlSession会话
     */
    public SqlSession openSession(){
        return new DefaultSqlSessionImpl(configuration);
    }

    public Configuration getConfiguration() {
        return configuration;
    }
}

SqlSession

java 复制代码
public interface SqlSession {

    <T> List<T> selectList(String statement) throws Exception;

    <T> List<T> selectList(String statement, Object parameter) throws Exception;

    int update(String statement) throws Exception;

    int update(String statement, Object parameter) throws Exception;

    int delete(String statement) throws Exception;

    int delete(String statement, Object parameter) throws Exception;

    int insert(String statement) throws Exception;

    int insert(String statement, Object parameter) throws Exception;

    <T> T getMapper(Class<T> type);

}

DefaultSqlSessionImpl

java 复制代码
public class DefaultSqlSessionImpl implements SqlSession {
    
    private Configuration configuration;

    public DefaultSqlSessionImpl(Configuration configuration) {
        this.configuration = configuration;
    }

    public <T> List<T> selectList(String statement) throws Exception {
        Executor executor = new Executor(configuration);
        return (List<T>) executor.executeQuery(statement);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        ClassLoader classLoader = type.getClassLoader();

        // 创建一个代理对象,用户给定一个Mappper接口,我们负责生成一个Mapper代理实例返回去
        // 这样在调用Mapper接口方法的时候,会进入代理类里面。
        InvocationHandler handler = new MapperProxy(configuration);

        return (T) Proxy.newProxyInstance(
                classLoader,
                new Class[]{type},
                handler
        );
    }

    // 其它略

}

Executor

java 复制代码
public class Executor {

    private Configuration configuration;

    public Executor(Configuration configuration) {
        this.configuration = configuration;
    }

    public List executeQuery(String statement) throws Exception {
        /**
         * 数据库连接信息硬编码
         */
        String driver = configuration.getDriver();
        String url = configuration.getUrl();
        String username = configuration.getUsername();
        String password = configuration.getPassword();
        Map<String, SqlSource> map = configuration.getSqlSourceMap();

        //SQL映射对象
        SqlSource mapper = map.get(statement);
        String sqlStr = mapper.getSql();
        //获取查询SQL
        String resultType = mapper.getResultType();     //获取返回值类的全限定名(包名        +类名)
        //1.注册MySQL驱动
        Class.forName(driver);
        //2.获取连接Connection对象
        /**
         * 数据库连接的创建和释放频繁,造成系统资源的浪费,严重影响了系统性能
         */
        Connection conn = DriverManager.getConnection(url, username, password);
        //3.创建SQL语句对象Statement,填写SQL语句
        PreparedStatement ps = conn.prepareStatement(sqlStr);
        //4.执行查询SQL,返回结果集ResultSet
        ResultSet rs = ps.executeQuery();
        //5.解析结果集,获取查询用户list集合
        //获取结果集元数据
        ResultSetMetaData metaData = rs.getMetaData();
        //获取总列数
        int columnCount = metaData.getColumnCount();
        //获取所有列的list集合
        List<String> columnNames = new ArrayList<String>();
        for (int i = 1; i <= columnCount; i++) {
            columnNames.add(metaData.getColumnName(i));
        }
        List list = new ArrayList();
        //循环解析结果集
        while (rs.next()) {
            //通过反射获取类的字节码对象,传入的参数是类的全限定名称
            Class<?> clazz = Class.forName(resultType);
            //反射创建对象
            Object user = clazz.newInstance();
            //反射获取当前类的所有方法
            Method[] methods = clazz.getMethods();
            //循环 遍历所有列名
            for (String columnName : columnNames) {
                for (Method method : methods) {
                    String methodName = method.getName();
                    //判断方法的名称,与set+列名相等,那么就把列名对应的值设置到当前对象的set方法中
                    if (("set" + columnName).equalsIgnoreCase(methodName)) {
                        //把列名column对应的值,设置到对象的set方法中,给属性赋值
                        method.invoke(user, rs.getObject(columnName));
                    }
                }
            }
            //将用户存入集合中
            list.add(user);
        }
        //关闭连接,释放资源
        rs.close();
        ps.close();
        conn.close();
        return list;
    }
}

MapperProxy

java 复制代码
public class MapperProxy implements InvocationHandler {

    private final Configuration configuration;

    public MapperProxy(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 获取方法所在类的包路径
        String packagePath = method.getDeclaringClass().getName();
        // 获取方法名
        String methodName = method.getName();

        // 将包路径和方法名通过"."拼接在一起
        String fullPath = packagePath + "." + methodName;


        SqlSource sqlSource = configuration.getSqlSourceMap().get(fullPath);

        SqlSessionFactory factory = new SqlSessionFactory(this.configuration);

        SqlSession sqlSession = factory.openSession();

        switch (getSQLStatementType(sqlSource.getSql())) {
            case SELECT:
                // 简单来
                return sqlSession.selectList(fullPath);
            case DELETE: break;
            case UPDATE: break;
            case INSERT: break;


        }
        throw new RuntimeException();
    }

    public enum SQLStatementType {
        SELECT,
        DELETE,
        UPDATE,
        INSERT,
        UNKNOWN
    }


    public static SQLStatementType getSQLStatementType(String sql) {
        String normalizedSQL = sql.trim().toLowerCase();

        if (normalizedSQL.startsWith("select")) {
            return SELECT;
        } else if (normalizedSQL.startsWith("delete")) {
            return SQLStatementType.DELETE;
        } else if (normalizedSQL.startsWith("update")) {
            return SQLStatementType.UPDATE;
        } else if (normalizedSQL.startsWith("insert")) {
            return SQLStatementType.INSERT;
        } else {
            return SQLStatementType.UNKNOWN;
        }
    }


}

使用Demo

按照之前的用例图,我们先导入工程的依赖包、配置数据源等信息、编写Mapper、编写Mapper XML,就可以运行了。

  • 导包
xml 复制代码
<!--自定义框架坐标jar包-->
<dependency>
    <groupId>cn.lsj</groupId>
    <artifactId>lsjORM</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

<!--mysql数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.6</version>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>compile</scope>
</dependency>
  • SqlMapConfig.xml
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <dataSource>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/ruoyi?characterEncoding=utf8"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </dataSource>
    <mappers>
        <mapper resource="SysRoleMapper.xml"/>
    </mappers>
</configuration>
  • SysRoleMapper.xml
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.hero.dao.SysRoleMapper">
    <select id="findAll" resultType="com.hero.dao.SysRole">
        select * from sys_role;
    </select>
</mapper>
  • 接口和实体
java 复制代码
public interface SysRoleMapper {

    List<SysRole> findAll();
}

public class SysRole {

    private Long role_id;

    private String role_name;

    // getting/setting
}
  • 测试类
java 复制代码
@Test
public void test() throws Exception {
    //1.创建SqlSessionFactoryBuilder对象
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    //2.builder对象构建工厂对象
    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("SqlMapConfig.xml");

    SqlSessionFactory sqlSessionFactory = builder.build(inputStream);

    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 调用方式一:直接指定方法全路径
    List<SysRole> objects = sqlSession.selectList("com.hero.dao.SysRoleMapper.findAll");
    for (SysRole u : objects) {
        System.out.println(u);
    }

    // 调用方式二:返回Mappper接口的代理实例
    SysRoleMapper sysRoleMapper = sqlSession.getMapper(SysRoleMapper.class);
    List<SysRole> users = sysRoleMapper.findAll();

    for (SysRole u : users) {
        System.out.println(u);
    }
}

总结

通过手写一个简单的ORM框架,了解到 SQL映射配置、SQL执行、结果映射 的基本原理,在上面的案例中,没有实现参数处理、缓存机制、插件机制,感兴趣的可以继续完善。

相关推荐
高山上有一只小老虎1 小时前
mybatisplus分页查询版本 3.5.8 以下和版本 3.5.9及以上的区别
java·spring boot·mybatis
人道领域1 小时前
javaWeb从入门到进阶(MyBatis拓展)
java·tomcat·mybatis
J2虾虾10 小时前
SpringBoot和mybatis Plus不兼容报错的问题
java·spring boot·mybatis
pp起床1 天前
【苍穹外卖】Day03 菜品管理
java·数据库·mybatis
九皇叔叔1 天前
【01】SpringBoot3 MybatisPlus 工程创建
java·mybatis·springboot3·mybatis plus
BD_Marathon1 天前
MyBatis逆向工程之清晰简洁版
mybatis
九皇叔叔1 天前
【02】SpringBoot3 MybatisPlus 加入日志功能
java·mysql·mybatis·日志·mybatisplus
齐 飞1 天前
MybatisPlus真正的批量新增
spring boot·mybatis
小北方城市网1 天前
Spring Cloud Gateway 生产问题排查与性能调优全攻略
redis·分布式·缓存·性能优化·mybatis
while(1){yan}1 天前
Spring事务
java·数据库·spring boot·后端·java-ee·mybatis