理解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执行、结果映射 的基本原理,在上面的案例中,没有实现参数处理、缓存机制、插件机制,感兴趣的可以继续完善。

相关推荐
莳光.9 小时前
122、java的LambdaQueryWapper的条件拼接实现数据sql中and (column1 =1 or column1 is null)
java·mybatis
qq_4419960513 小时前
Mybatis官方生成器使用示例
java·mybatis
꧁惜若༒奔已꧂1 天前
spring使用xml文件整合事务+druid+mybatis
xml·spring·mybatis
小桥流水人家jjh1 天前
Mybatis执行自定义SQL并使用PageHelper进行分页
java·数据库·spring boot·sql·mybatis
黑马师兄1 天前
Mybatis
java·mybatis
Neoest2 天前
场景解决之mybatis当中resultType= map时,因某个字段为null导致返回的map的key不存在怎么处理
mybatis
ZWZhangYu2 天前
【MyBatis源码】深入分析TypeHandler原理和源码
数据库·oracle·mybatis
小鸡脚来咯3 天前
springboot 整合mybatis
java·spring boot·mybatis
种树人202408193 天前
MyBatis xml 文件中 SQL 语句的小于号未转义导致报错
mybatis
码农派大星。3 天前
MyBatis操作--进阶
mybatis