《手写Mybatis渐进式源码实践》实践笔记 (第一章 实现一个简单的ORM框架)

第1章 实现一个简单的ORM框架

mybatis


背景

技术背景

ORM(Object-Relational Mapping)是一种用于简化开发的技术,它通过将面向对象编程语言中的对象与关系型数据库中的表进行映射,使得开发者可以用面向对象的方式操作数据库,而无需直接编写SQL语句。以下是对ORM的详细解释:

基本概念
  • ORM全称为对象关系映射(Object-Relational Mapping),它是一种编程技术,用于在面向对象的编程语言和关系型数据库管理系统之间建立桥梁。
  • ORM通过将编程语言中的类映射为数据库中的表,将类的实例(对象)映射为表中的记录,实现了对象和关系型数据库之间的数据交互。
工作原理
  • ORM技术允许开发者以面向对象的方式操作数据,例如创建、读取、更新和删除对象,而不需要直接写SQL语句。
  • 在ORM中,数据库表被视为对象类,表中的每一条记录被视为该类的一个实例,表的列则映射为对象的属性。
  • ORM框架通常提供了一系列的API和工具,使得开发人员可以通过面向对象的方式来进行数据库操作,如查询、插入、更新和删除等。
优点
  • ORM技术极大地简化了应用程序中的数据访问层开发,提高了开发效率和代码的可维护性。
  • ORM通过面向对象的方式来操作数据库,使得代码更加易于维护和扩展,同时也提高了代码的可读性和可重用性。
  • ORM支持多种数据库,使得开发人员可以更加灵活地选择数据库,提高了代码的可移植性和可扩展性。
缺点
  • ORM框架的性能可能不如手写SQL,对于大量数据的查询和操作可能会有一定的性能损失。
  • ORM框架需要掌握一定的知识和技能,学习成本较高。
  • 不同的ORM框架对于相同的数据类型和操作可能会有不同的支持程度,可能存在可移植性的问题。
  • ORM框架通常需要进行配置和映射,复杂性较高,需要一定的技术水平和经验。
常见的ORM框架
  • Java:Hibernate、MyBatis等。
  • Python:SQLAlchemy、Django ORM、Peewee等。
  • .NET:Entity Framework (EF) Core等。
  • PHP:Laravel ORM、Yii ORM、ThinkPHP ORM等。

业务背景

ORM 对象关系映射,是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换,也让我们可以更方便的使用数据库。那么,怎样实现类似于 MyBatis 这样的 ORM 框架呢?本章节我们就来以实现一个 ORM 框架为目标,看看该怎么设计和实现。另外关于 ORM 框架的实现,下一章节开始,我们会继续完善这个ORM框架,实现mybatis 的核心功能。

目标

实现一个简易版的类似mybatis的orm框架,屏蔽了对 JDBC 操作的复杂性, 让外部的调用方可以更加简单的方式使用数据库。

设计

一个ORM框架要实现的核心内容, 包括参数映射、SQL解析、SQL执行、结果映射,这块的内容会被封装,对内通过jdbc与数据库进行交互,对外提供SqlSession工厂类进行接口调用。

整体设计结构如下图:

image-20241111183431280

实现

代码结构

image-20241206103001536

源码实现:https://github.com/swg209/small-mybatis-study/tree/step1-simple-orm

类图

image-20241111183852923

  1. 在整个类图中,SqlSession是与数据库交互的核心接口,定义了多种查询方法,包括selectOneselectList,分别用于查询单个对象和对象列表,同时提供了close方法用于关闭会话。依赖于SqlSessionFactory接口来创建其实例。
  2. SqlSessionFactory负责创建SqlSession实例,通过其openSession方法实现。
  3. DefaultSqlSession类是SqlSession接口的具体实现类,提供了selectOneselectListclose等方法的实现。包含了数据库连接connection和映射元素mapperElement等属性,用于管理数据库连接和SQL映射。提供了resultSet2ObjbuildParameter等辅助方法,用于结果集到对象的转换和参数构建。
  4. DefaultSqlSessionFactory类是SqlSessionFactory接口的具体实现类,负责创建和管理SqlSession对象。包含了配置configuration属性,用于存储数据库连接和SQL映射的配置信息。通过其openSession方法返回SqlSession实例。
  5. SqlSessionFactoryBuilder类用于构建SqlSessionFactory实例,通过其build方法读取配置文件并创建DefaultSqlSessionFactory对象。在构建过程中,会解析配置文件并生成相应的配置信息。

实现步骤

  1. 定义sqlSession接口

    public interface SqlSession {
    
        <T> T selectOne(String statement, Object parameter);
    
        <T> T selectOne(String statement);
    
        <T> List<T> selectList(String statement, Object parameter);
    
        <T> List<T> selectList(String statement);
    
        void close();
    
    }
    
  2. 定义sqlSession具体实现类 DefaultSqlSession

    public class DefaultSqlSession implements SqlSession {
    
        //连接信息.
        private Connection connection;
    
        //配置信息.
        private Map<String, XNode> mapperElement;
    
        public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
            this.connection = connection;
            this.mapperElement = mapperElement;
        }
    
    
        @Override
        public <T> T selectOne(String statement) {
            try {
                XNode xNode = mapperElement.get(statement);
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects.get(0);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    
        private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
            List<T> list = new ArrayList<>();
            try {
                ResultSetMetaData metaData = resultSet.getMetaData();
                int columnCount = metaData.getColumnCount();
                //每次遍历一行值
                while (resultSet.next()) {
                    //创建对象
                    T obj = (T) clazz.newInstance();
                    //遍历每一列
                    for (int i = 1; i <= columnCount; i++) {
                        //获取列名
                        String columnName = metaData.getColumnName(i);
                        //驼峰命名
                        columnName = WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", "");
    
                        //获取列值
                        Object value = resultSet.getObject(i);
                        //获取set方法
                        String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                        Method method;
                        if (value instanceof Timestamp) {
                            method = clazz.getMethod(setMethod, Date.class);//调用set方法
                        } else {
                            method = clazz.getMethod(setMethod, value.getClass());//调用set方法
                        }
                        method.invoke(obj, value);
                    }
                    list.add(obj);
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return list;
        }
    
        @Override
        public <T> T selectOne(String statement, Object parameter) {
            XNode xNode = mapperElement.get(statement);
            Map<Integer, String> parameterMap = xNode.getParameter();
            try {
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                buildParameter(preparedStatement, parameter, parameterMap);
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects.get(0);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private void buildParameter(PreparedStatement preparedStatement,
                                    Object parameter,
                                    Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {
            int size = parameterMap.size();
            // 单个参数
            if (parameter instanceof Long) {
                for (int i = 1; i <= size; i++) {
                    preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
                }
                return;
            }
    
            if (parameter instanceof Integer) {
                for (int i = 1; i <= size; i++) {
                    preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
                }
                return;
            }
    
            if (parameter instanceof String) {
                for (int i = 1; i <= size; i++) {
                    preparedStatement.setString(i, parameter.toString());
                }
                return;
            }
    
            Map<String, Object> fieldMap = new HashMap<>();
            // 对象参数
            Field[] declaredFields = parameter.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                String name = field.getName();
                field.setAccessible(true);
                Object obj = field.get(parameter);
                field.setAccessible(false);
                fieldMap.put(name, obj);
            }
    
            for (int i = 1; i <= size; i++) {
                String parameterDefine = parameterMap.get(i);
                Object obj = fieldMap.get(parameterDefine);
    
                if (obj instanceof Short) {
                    preparedStatement.setShort(i, Short.parseShort(obj.toString()));
                    continue;
                }
    
                if (obj instanceof Integer) {
                    preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
                    continue;
                }
    
                if (obj instanceof Long) {
                    preparedStatement.setLong(i, Long.parseLong(obj.toString()));
                    continue;
                }
    
                if (obj instanceof String) {
                    preparedStatement.setString(i, obj.toString());
                    continue;
                }
    
                if (obj instanceof Date) {
                    preparedStatement.setDate(i, (java.sql.Date) obj);
                }
    
            }
        }
    
    
        @Override
        public <T> List<T> selectList(String statement, Object parameter) {
            try {
                XNode xNode = mapperElement.get(statement);
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                buildParameter(preparedStatement, parameter, xNode.getParameter());
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects;
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public <T> List<T> selectList(String statement) {
            try {
                XNode xNode = mapperElement.get(statement);
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects;
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public void close() {
    
        }
    }
    
  3. 定义sqlSessionFactory接口

    public interface SqlSessionFactory {
        SqlSession openSession();
    }
    
    
  4. 定义sqlSessionFactory具体实现类 DefaultSqlSessionFactory

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private final Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public SqlSession openSession() {
            // 创建会话
            return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
        }
    }
    
    
  5. 定义SqlSessionFactoryBuilder,实现读取配置中的xml文件,解析获取配置源信息dataSource、

​ SQL语句信息mapperElement、数据库连接信息Connection, 写入Configuration

public class SqlSessionFactoryBuilder {


    /**
     * 构建会话工厂.
     *
     * @param reader
     * @return
     */
    public SqlSessionFactory build(Reader reader) {
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new InputSource(reader));
            Configuration configuration = parseConfiguration(document.getRootElement());
            return new DefaultSqlSessionFactory(configuration);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Configuration parseConfiguration(Element root) {
        Configuration configuration = new Configuration();
        configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
        configuration.setConnection(connection(configuration.dataSource));
        configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
        return configuration;
    }


    /**
     * 获取数据源配置信息.
     */
    private Map<String, String> dataSource(List<Element> list) {
        Map<String, String> dataSource = new HashMap<>(4);
        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String name = e.attributeValue("name");
            String value = e.attributeValue("value");
            dataSource.put(name, value);
        }
        return dataSource;
    }

    /**
     * 获取connection信息.
     */
    private Connection connection(Map<String, String> dataSource) {
        try {
            Class.forName(dataSource.get("driver"));
            return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return null;

    }

    /**
     * 获取SQL语句信息.
     */
    private Map<String, XNode> mapperElement(List<Element> list) {
        Map<String, XNode> map = new HashMap<>();

        Element element = list.get(0);
        List content = element.content();
        for (Object o : content) {
            Element e = (Element) o;
            String resource = e.attributeValue("resource");

            try {
                Reader reader = Resources.getResourceAsReader(resource);
                SAXReader saxReader = new SAXReader();
                Document document = saxReader.read(new InputSource(reader));
                Element root = document.getRootElement();
                //命名空间
                String namespace = root.attributeValue("namespace");

                //SELECT
                List<Element> selectNodes = root.selectNodes("select");
                for (Element node : selectNodes) {
                    String id = node.attributeValue("id");
                    String parameterType = node.attributeValue("parameterType");
                    String resultType = node.attributeValue("resultType");
                    String sql = node.getText();

                    // ? 匹配
                    Map<Integer, String> parameter = new HashMap<>();
                    Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                    Matcher matcher = pattern.matcher(sql);
                    for (int i = 1; matcher.find(); i++) {
                        String g1 = matcher.group(1);
                        String g2 = matcher.group(2);
                        parameter.put(i, g2);
                        sql = sql.replace(g1, "?");
                    }

                    XNode xNode = new XNode();
                    xNode.setNamespace(namespace);
                    xNode.setId(id);
                    xNode.setParameterType(parameterType);
                    xNode.setResultType(resultType);
                    xNode.setSql(sql);
                    xNode.setParameter(parameter);

                    map.put(namespace + "." + id, xNode);

                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return map;
    }
}

测试

事先准备

mysql数据库,配置好连接信息, 建表语句。

#创建数据库()
CREATE DATABASE IF NOT EXISTS mybatis;


#创建用户表
USE mybatis;

CREATE TABLE `my_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` varchar(9) DEFAULT NULL COMMENT '用户ID',
  `user_head` varchar(16) DEFAULT NULL COMMENT '用户头像',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `user_name` varchar(64) DEFAULT NULL COMMENT '用户名',
  `user_password` varchar(64) DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

# 造数
INSERT INTO my_user (user_id, user_head, create_time, update_time, user_name, user_password) VALUES
('1', '头像1', '2024-11-12 18:00:12', '2024-11-12 18:00:12', '小苏1', 's123asd'),
('2', '头像2', '2024-11-12 18:00:12', '2024-11-12 18:00:12', '小苏2', 's123asd');


User类

  • 创建 User类,承载数据库表的对象数据。IUserDao定义用户表Dao查询方法。
public class User {
    private Long id;
    private String userId;          // 用户ID
    private String userName;    // 昵称
    private String userHead;        // 头像
    private String userPassword;    // 密码
    private Date createTime;        // 创建时间
    private Date updateTime;        // 更新时间

    public User() {
    }

    public User(String userName) {
        this.userName = userName;
    }


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userNickName) {
        this.userName = userNickName;
    }

    public String getUserHead() {
        return userHead;
    }

    public void setUserHead(String userHead) {
        this.userHead = userHead;
    }

    public String getUserPassword() {
        return userPassword;
    }

    public void setUserPassword(String userPassword) {
        this.userPassword = userPassword;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }

}

IUserDao

public interface IUserDao {
    User queryUserInfoById(Long id);
  
    List<User> queryUserList(User user);
}

属性配置文件

mybatis-config-datasource.xml ORM配置文件

配置数据库源连接信息,配置mappers声明.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    </mappers>

</configuration>

User_Mapper.xml mapper配置文件

配置用户表查询方法.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.suwg.mybatis.test.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long"
            resultType="cn.suwg.mybatis.test.po.User">
        SELECT id, user_id, user_name, user_head, user_password, create_time
        FROM my_user
        where id = #{id}
    </select>

    <select id="queryUserList" parameterType="cn.suwg.mybatis.test.po.User"
            resultType="cn.suwg.mybatis.test.po.User">
        SELECT id, user_id, user_name, user_head, user_password, create_time, update_time
        FROM my_user
        where user_name = #{userName}
    </select>

</mapper>

测试用例(selectOne)

public class ApiTest {

    @Test
    public void test_queryUserInfoById() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

            SqlSession session = sqlMapper.openSession();
            try {
                User user = session.selectOne("cn.suwg.mybatis.test.dao.IUserDao.queryUserInfoById", 1L);
                System.out.println(JSON.toJSONString(user));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

测试结果

image-20241206103116808

测试用例(selectList)

public class ApiTest {

    @Test
    public void test_queryUserInfoList() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

            SqlSession session = sqlMapper.openSession();
            try {
                List<User> userList = session.selectList("cn.suwg.mybatis.test.dao.IUserDao.queryUserList",
                        new User("小苏"));
                System.out.println(JSON.toJSONString(userList));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

测试结果

image-20241206103159792

  • 从测试结果中可以看到,可以正常与数据库进行交互,可以正常查询到用户表的数据。

总结

  • 在本章节中,我们简单实现一个类似mybatis的ORM框架,当前只是实现了基本功能,后续可以基于这个框架再进行完善。

  • 通过编写测试用例,验证了该 ORM 框架的基本功能,包括查询单个对象和对象列表。测试结果表明,该框架能够正常与数据库进行交互,并正确映射查询结果到 Java 对象。

参考书籍:《手写Mybatis渐进式源码实践》

书籍源代码:https://github.com/fuzhengwei/book-small-mybatis

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值