Java之手写实现ORM框架

article/2025/10/19 7:20:33

借鉴Mybatis框架手写一个ORM框架。mybatis整体架构中的整体思路是,首先解析一下配置文件,一个是框架的全局配置文件,一个是mapper配置文件,定义格式如下

<configuration>
</configuration><mapper>
</mapper>

其中,全局配置文件中包含数据源配置信息和mapper配置文件所在位置,如下

<configuration><!-- 数据源配置 --><dataSource><property name="driverClass" value="com.mysql.jdbc.Driver"/><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf8"/><property name="username" value="root"/><property name="password" value="root"/></dataSource><!-- 记录mapper.xml的路径 --><!--<mapper resource="UserMapper"/>--><mapper resource="com/***/UserMapper.xml"/>
</configuration>

对该配置文件解析后,可以将这些数据封装成一个java实体,该实体包含了所有的配置信息,全局配置文件中可能包含多个mapper文件的配置,可以将其封装成一个map集合,如下

Map<String, MapperStatement>

其中,集合的key为String类型,value为MapperStatement类型,MapperStatement是对mapper配置文件的一个封装,如下

<mapper namespace="com.***.UserMapper"><select id="selectList" resultType="com.***.User">select * from user</select>
</mapper>

框架会将整个工程中的每个mapper配置文件都封装成一个个MapperStatement并保存到map中,这就需要对每个MapperStatement进行区分,区分的关键就是mapper配置文件中的namespace和id,将其拼接起来作为statementId。
然后提供对应的curd方法,方法的作用是对sql语句进行解析并调用jdbc查询数据库,最后封装结果集。

具体实现如下:

  1. 解析配置文件
    新建一个类Resources,该类负责将一个文件转换成输入流,如下
public class Resources {/*** 根据配置文件的路径将配置文件加载成字节输入流** @param path* @return*/public static InputStream getResourceAsStream(String path) {return Resources.class.getClassLoader().getResourceAsStream(path);}
}

接下来新建一个SqlSessionFactoryBuilder类,该类提供一个build方法来生成SqlSessionFactory,如下

public class SqlSessionFactoryBuilder {/*** 构建** @param inputStream 文件配置流* @return* @throws DocumentException* @throws PropertyVetoException*/public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException {// 使用dom4j解析配置文件,将解析出来的内容封装到Configuration中XmlConfigBuilder builder = new XmlConfigBuilder();Configuration configuration = builder.parseConfig(inputStream);// 创建SqlSessionFactory对象SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);return sqlSessionFactory;}
}

其中SqlSessionFactory是一个接口,需要创建它的默认实现类DefaultSqlSessionFactory,该类需要传入一个Configuration类型对象,这个Configuration就是对全局配置的一个封装,如下

public class Configuration {private DataSource dataSource;/***  key:statementId,namespace + id*  value:封装好的MapperStatement对象*/private Map<String, MapperStatement> mapperStatementMap = new HashMap<>();}

那么现在关键就是对全局配置文件进行解析了,需提供一个类XmlConfigBuilder,该类提供parseConfig方法来将输入流转换为Configuration对象,如下

public class XmlConfigBuilder {private Configuration configuration;public XmlConfigBuilder() {this.configuration = new Configuration();}public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {Document document = new SAXReader().read(inputStream);// 全局查找<configuration>标签Element rootElement = document.getRootElement();// 全局查找<property>标签List<Node> list = rootElement.selectNodes("//property");List<Element> propertyList = list.stream().map(node -> (Element) node).collect(Collectors.toList());Properties properties = new Properties();propertyList.forEach(element -> {// 获取到标签中的name和value属性String name = element.attributeValue("name");String value = element.attributeValue("value");properties.setProperty(name, value);});// 创建数据源ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));comboPooledDataSource.setUser(properties.getProperty("username"));comboPooledDataSource.setPassword(properties.getProperty("password"));configuration.setDataSource(comboPooledDataSource);// 解析mapper.xml文件XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);List<Node> list2 = rootElement.selectNodes("//mapper");List<Element> mapperList = list2.stream().map(node -> (Element) node).collect(Collectors.toList());for (Element element : mapperList) {String mapperPath = element.attributeValue("resource");InputStream mapperAsStream = Resources.getResourceAsStream(mapperPath);//XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);xmlMapperBuilder.parse(mapperAsStream);}return configuration;}
}

借助dom4j可以很容易的实现解析,将每个标签中的属性和属性值读取处理进行对应的封装即可,对应mapper配置文件的解析也是如此,通过resource属性可以得到mapper文件位置,然后将其转为输入流并解析,如下

public class XmlMapperBuilder {private Configuration configuration;public XmlMapperBuilder(Configuration configuration) {this.configuration = configuration;}/*** mapper配置文件解析成MappedStatement对象** @param inputStream* @throws DocumentException*/public void parse(InputStream inputStream) throws DocumentException {Document document = new SAXReader().read(inputStream);// 得到<mapper>根标签Element rootElement = document.getRootElement();String namespace = rootElement.attributeValue("namespace");// 得到<select>标签List<Node> list = rootElement.selectNodes("//select");List<Element> selectList = list.stream().map(node -> (Element) node).collect(Collectors.toList());handlerMapperStatement(namespace, selectList);// 得到<insert>标签List<Node> list2 = rootElement.selectNodes("//insert");List<Element> insertList = list2.stream().map(node -> (Element) node).collect(Collectors.toList());handlerMapperStatement(namespace,insertList);}/*** 封装MapperStatement对象* @param namespace   命名空间* @param selectNodes 操作节点*/private void handlerMapperStatement(String namespace, List<Element> selectNodes) {if (selectNodes == null || selectNodes.size() == 0) {return;}selectNodes.forEach(element -> {String id = element.attributeValue("id");String resultType = element.attributeValue("resultType");String parameterType = element.attributeValue("parameterType");String sql = element.getTextTrim();// 封装MapperStatement对象MapperStatement mapperStatement = new MapperStatement();mapperStatement.setId(id);mapperStatement.setResultType(resultType);mapperStatement.setParameterType(parameterType);mapperStatement.setSql(sql);//key:namespace+id,用于标识哪个命名空间下的哪个方法,不然不同文件可能有重名方法String key = namespace + "." + id;// 将MapperStatement对象保存到Configuration中configuration.getMapperStatementMap().put(key, mapperStatement);});}
}

同样读取每个配置的属性名和属性值,对应MappedStatementMap的封装,其map的key为namespace+id。

  1. 执行查询
    读取完配置文件之后,就得到了一个SqlSessionFactory接口,其实现类为DefaultSqlSessionFactory对象,该对象提供一个openSession方法来获得SqlSession对象,如下
public class DefaultSqlSessionFactory implements SqlSessionFactory {private Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}@Overridepublic SqlSession openSession() {return new DefaultSqlSession(configuration);}
}

其中,返回的SqlSession接口的默认实现DefaultSqlSession,如下

public class DefaultSqlSession implements SqlSession {private Configuration configuration;public DefaultSqlSession(Configuration configuration) {this.configuration = configuration;}@Overridepublic <E> List<E> selectList(String statementId, Object... params) throws Exception {Executor executor = new SimpleExecutor();MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);List<Object> objects = executor.query(configuration, mapperStatement, params);return (List<E>) objects;}@Overridepublic <T> T selectOne(String statementId, Object... params) throws Exception {List<Object> objects = this.selectList(statementId, params);if (objects != null && objects.size() == 1) {return (T) objects.get(0);} else {throw new RuntimeException("返回结果为空或返回结果过多");}}@Overridepublic Integer insertOne(String statementId, Object... params) throws Exception {Executor executor = new SimpleExecutor();MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);return executor.save(configuration, mapperStatement, params);}@Overridepublic <T> T getMapper(Class<?> mapperClass) {//通过jdk动态代理获取对象Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//方法名(这也是为什么xml中id要和接口方法名一致)String methodName = method.getName();//类全路径名(这也是为什么xml中namespace要和接口全路径一致)String className = method.getDeclaringClass().getName();String statementId = className + "." + methodName;Type genericReturnType = method.getGenericReturnType();if (genericReturnType instanceof ParameterizedType) {return selectList(statementId, args);}if (genericReturnType.getTypeName().contains("Integer")) {return insertOne(statementId, args);}return selectOne(statementId, args);}});return (T) proxyInstance;}}

在该对象中,实现查询操作时需要借助一个SimpleExecutor类来实现具体的查询,那执行查询操作需要那些参数,包括configuration、MapperStatement、查询参数,其中configuration里面是数据源和MapperStatement对象,这样就可以实现查询了。

  1. 实现查询
    执行查询需要Executor来完成,其实现类如下:
public class SimpleExecutor implements Executor {@Overridepublic <T> List<T> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception {Connection connection = configuration.getDataSource().getConnection();// select * from e_user where id = #{id} and name = #{name}String sql = mapperStatement.getSql();// 将sql中的 #{} 替换为 ?SqlMapping sqlMapping = getSqlMapping(sql);PreparedStatement preparedStatement = connection.prepareStatement(sqlMapping.getParseSql());// 获取到参数的全限定类名String parameterType = mapperStatement.getParameterType();Class<?> parameterClass = getClassType(parameterType);// 设置参数List<ParameterMapping> parameterMappings = sqlMapping.getParameterMappings();for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);String fieldName = parameterMapping.getParam();// 反射设置值Field field = parameterClass.getDeclaredField(fieldName);field.setAccessible(true);Object param = field.get(params[0]);preparedStatement.setObject(i + 1, param);}// 执行sqlResultSet resultSet = preparedStatement.executeQuery();String resultType = mapperStatement.getResultType();Class<?> resultTypeClass = getClassType(resultType);List<Object> list = new ArrayList<>();// 封装返回结果集while (resultSet.next()) {// 获取实体实例Object instance = resultTypeClass.newInstance();// 获取元数据ResultSetMetaData metaData = resultSet.getMetaData();for (int i = 1; i <= metaData.getColumnCount(); ++i) {// 获取字段名String columnName = metaData.getColumnName(i);// 获取字段值Object columnValue = resultSet.getObject(columnName);// 内省设置值,映射表和实体的关系PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);Method writeMethod = propertyDescriptor.getWriteMethod();writeMethod.invoke(instance, columnValue);}list.add(instance);}return (List<T>) list;}@Overridepublic Integer save(Configuration configuration, MapperStatement mappedStatement, Object... params) throws Exception {Connection connection = configuration.getDataSource().getConnection();SqlMapping sqlMapping = getSqlMapping(mappedStatement.getSql());PreparedStatement preparedStatement = connection.prepareStatement(sqlMapping.getParseSql());List<ParameterMapping> parameterMappings = sqlMapping.getParameterMappings();String parameterType = mappedStatement.getParameterType();Class<?> parameterTypeClass = getClassType(parameterType);for (int i = 0; i < parameterMappings.size(); i++) {String fieldName = parameterMappings.get(i).getParam();Field field = parameterTypeClass.getDeclaredField(fieldName);field.setAccessible(true);Object param = field.get(params[0]);preparedStatement.setObject(i + 1, param);}return preparedStatement.executeUpdate();}private Class<?> getClassType(String parameterType) throws ClassNotFoundException {if (parameterType != null) {return Class.forName(parameterType);}return null;}/*** 解析sql** @param sql* @return*/private SqlMapping getSqlMapping(String sql) {// 配合标记解析器完成占位符的解析ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", tokenHandler);// sql解析String parseSql = genericTokenParser.parse(sql);// 解析#{}中的参数名称List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();return new SqlMapping(parseSql, parameterMappings);}
}

整个框架的核心部分就是这个SimpleExecutor类,我们知道,JDBC中preparedStatement类执行的sql是以?作为占位符的,所以我们把#{}替换成?,并将#{id}里面的属性名取出来,这就是查询的一些参数信息,将参数类型和返回类型均通过反射内省技术进行值的封装后,即可得到查询结果。
其整体的工程结构如下:
在这里插入图片描述

  1. 工程应用
    新建一个java工程,引入手写的ORM框架,如下
	<dependencies><dependency><groupId>com.***</groupId><artifactId>mybatis_***</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency></dependencies><build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes><filtering>true</filtering></resource></resources></build>

注意其前提是将手写的ORM工程打包到maven的本地仓库,这样就可以通过pom进行引入了。
全局配置文件,如下

<configuration><!-- 数据源配置 --><dataSource><property name="driverClass" value="com.mysql.jdbc.Driver"/><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf8"/><property name="username" value="root"/><property name="password" value="root"/></dataSource><!-- 记录mapper.xml的路径 --><!--<mapper resource="UserMapper"/>--><mapper resource="com/***/UserMapper.xml"/>
</configuration>

mapper配置文件,如下

<mapper namespace="com.ldc.mapper.UserMapper"><select id="selectList" resultType="com.ldc.model.User">select * from user</select><select id="selectOne" parameterType="com.ldc.model.User" resultType="com.ldc.model.User">select * from user where id = #{id} and name = #{name}</select><insert id="insertOne" parameterType="com.ldc.model.User" resultType="Integer">insert into user(name) values(#{name})</insert></mapper>

测试,如下

@Testpublic void test() throws Exception {InputStream inputStream = Resources.getResourceAsStream("Mybatis-Config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();User user = new User();user.setId(2);user.setName("caocao");List<User> userList = sqlSession.selectList("com.ldc.mapper.UserMapper.selectList");System.out.println(userList);User user2 = sqlSession.selectOne("com.ldc.mapper.UserMapper.selectOne", user);System.out.println(user2);
//        Integer integer = sqlSession.insertOne("com.ldc.mapper.UserMapper.insertOne", user);
//        System.out.println(integer);UserMapper userMapper = sqlSession.getMapper(UserMapper.class);List<User> userList2 = userMapper.selectList();System.out.println(userList2);User user3 = userMapper.selectOne(user);System.out.println(user3);
//        Integer integer2 = userMapper.insertOne(user);
//        System.out.println(integer2);}

测试结果,如下
在这里插入图片描述
继续深入理解ORM框架底层,做到知其然,知其所以然。


http://chatgpt.dhexx.cn/article/6vvsh0Fd.shtml

相关文章

ORM框架设计及实现

文章目录 一 对象记录映射二 效率和性能三 数据访问描述信息四 字段类型转换五 数据访问基类六 统一异常处理七 数据访问框架八 数据源创建工厂九 SQL指令解析十 补充DAO接口实现十一 测试十二 其他问题 一 对象记录映射 本质上来说&#xff0c;ORM框架需要处理的就是如何将JAV…

SpringBoot 构建ORM框架

目前常用的ORM框架有 Mybatis&#xff08;batis&#xff09;、MybatisPlus&#xff0c;Hibernate、Jpa等几个框架&#xff0c;今天就简单介绍一下搭建Mybatisplus框架的流程。 1.增加依赖 <dependencies><!-- 第一步&#xff1a;选择ORM框架&#xff0c;使用…

DjangoORM框架

DjangoORM框架 文章目录 DjangoORM框架一、ORM框架1.ORM简介2.使用django进行数据库开发的步骤如下&#xff1a;3.定义模型类4.迁移5.数据操作5.对象的关联操作6.ORM框架的功能 二、后台管理1.简介2.使用步骤3.管理界面本地化4.创建管理员5.注册模型类6.自定义管理页面 模型设计…

mybatis —— ORM框架

一、什么是框架 框架是一种经过校验、具有一定功能的半成品软件品&#xff0c;已经对基础的代码进行了封装并提供相应的API&#xff0c;开发者在使用框架是直接调用封装好的api可以省去很多代码编写&#xff0c;从而提高工作效率和开发速度。 二、什么是ORM ORM&#…

ORM框架——SqlSugar

目录 一、ORM框架 二、SqlSugar现状 三、SqlSugar的优点 四、SqlSugar的使用 五、主要函数 六、传参 五、读写分离 一、ORM框架 常用的ORM框架有Dapper、EF/EF Core、FreeSql、Dos.ORM、SqlSugar等&#xff0c;SqlSugar是国产的 二、SqlSugar现状 SqlSugar ORM 5.X 官…

很多小伙伴不太了解ORM框架的底层原理,这不,冰河带你10分钟手撸一个极简版ORM框架(赶快收藏吧)

大家好&#xff0c;我是冰河~~ 最近很多小伙伴对ORM框架的实现很感兴趣&#xff0c;不少读者在冰河的微信上问&#xff1a;冰河&#xff0c;你知道ORM框架是如何实现的吗&#xff1f;比如像MyBatis和Hibernate这种ORM框架&#xff0c;它们是如何实现的呢&#xff1f; 为了能够…

.Net Core ORM 框架

我的.net core orm 框架 一个简单的orm框架支持的数据库版本新的版本项目地址使用方式实现方式高级特性扩展函数性能bug 一个简单的orm框架 作者在使用很多orm框架的时候觉得查询语句写法似乎不是很好用&#xff0c;例如sqlSugar,EF,sqlSugar呢链接的时候必须注意表的别称&…

SQLAlchemy ORM框架

ORM简介 ORM 全称 Object Relational Mapping, 叫对象关系映射。简单的说&#xff0c;ORM 将数据库中的表与面向对象语言中的类建立了一种对应关系。这样&#xff0c;我们要操作数据库&#xff0c;数据库中的表或者表中的一条记录就可以直接通过操作类或者类实例来完成。 对于…

ORM框架

ORM框架 一、什么是ORM框架 对象关系映射&#xff08;Object Relational Mapping&#xff0c;简称ORM&#xff09;模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术 ORM框架是连接数据库的桥梁&#xff0c;只要提供了持久化类与表的映射关系&#xff0c;OR…

ORM框架使用优缺点

1. 什么是ORM? 对象-关系映射&#xff08;Object-Relational Mapping&#xff0c;简称ORM&#xff09;&#xff0c;面向对象的开发方法是当今企业级应用开发环境中的主流开发方法&#xff0c;关系数据库是企业级应用环境中永久存放数据的主流数据存储系统。对象和关系数据是业…

Redis I/O 多路复用

引出IO多路复用 为什么 Redis 中要使用 I/O 多路复用这种技术呢&#xff1f; 首先&#xff0c;Redis 是跑在单线程中的&#xff0c;所有的操作都是按照顺序线性执行的&#xff0c;但是由于读写操作等待用户输入或输出都是阻塞的&#xff0c;所以 I/O 操作在一般情况下往往不能…

IO多路复用—由Redis的IO多路复用yinch

linux IO多路复用有epoll&#xff0c; poll, select&#xff0c;epoll性能比其他几者要好。 名词比较绕口&#xff0c;理解涵义就好。一个epoll场景&#xff1a;一个酒吧服务员&#xff08;一个线程&#xff09;&#xff0c;前面趴了一群醉汉&#xff0c;突然一个吼一声“倒酒”…

什么是IO多路复用?用来解决什么问题?如何实现?

白话IO多路复用 这里引述知乎大佬对于IO多路复用的机场空管的比喻和理解&#xff1a; 假设你是一个机场的空管&#xff0c; 你需要管理到你机场的所有的航线&#xff0c; 包括进港&#xff0c;出港&#xff0c; 有些航班需要放到停机坪等待&#xff0c;有些航班需要去登机口接…

I/O多路复用

https://blog.csdn.net/baixiaoshi/article/details/48708347 https://blog.csdn.net/z69183787/article/details/52943917 select&#xff0c;poll&#xff0c;epoll都是IO多路复用的机制。所谓I/O多路复用机制&#xff0c;就是说通过一种机制&#xff0c;可以监视多个描述符…

概念 多路复用 到底是个啥?通俗易懂的理解

前言&#xff1a;教育的公平才是最大的公平&#xff0c;本科和专科还是有点差别的。时间长点学方面会充实些。 先给个结论&#xff1a;I/O多路复用技术&#xff08;就是大家经常说的事件循环&#xff09;实际上&#xff0c;事件驱动模型还有另外一个名字&#xff0c;而且更加出…

Redis的IO多路复用原理

什么是阻塞&#xff0c;非阻塞&#xff0c;异步同步&#xff0c;select&#xff0c;poll&#xff0c;epoll&#xff1f;今天我们用一遍文章解开这多年的迷惑。 首先我们想要通过网络接收消息&#xff0c;是这样的一个步骤。 用户空间向内核空间请求网络数据内核空间把网卡数据…

什么是IO多路复用,理解IO多路复用

什么是IO多路复用&#xff1f; IO 多路复用是一种同步IO模型&#xff0c;实现一个线程可以监视多个文件句柄&#xff1b;一旦某个文件句柄就绪&#xff0c;就能够通知应用程序进行相应的读写操作&#xff1b;没有文件句柄就绪就会阻塞应用程序&#xff0c;交出CPU。 多路是指网…

多路复用与多路分用

从现在开始&#xff0c;我们开始传输层的学习&#xff0c;自顶向下第六版中改成了运输层&#xff0c;感觉怪怪的 书中打了邮政服务和代收发信件的兄弟姐妹之间的比方&#xff0c;非常贴切&#xff0c;这是传输层和网络层的作用区别&#xff0c;也就是说&#xff0c;传输层管的是…

多路复用(

apue 多路复用 需求来自用户&#xff0c;用户的需求来自实际的使用场景。在实际运用中&#xff0c;一个系统或者程序需要处理的事件并不是只有一个或一类&#xff0c;而是存在各种各样的事件在一小段事件内一起发生&#xff0c;此时按照没学多线程的逻辑的处理方式就是这样&…

多路复用

讲多路复用先我觉得有必要讲一下什么是阻塞IO、非阻塞IO、同步IO、异步IO这几个东西&#xff1b;linux的五种IO模型&#xff1a; 1)阻塞I/O&#xff08;blocking I/O&#xff09; 2)非阻塞I/O&#xff08;nonblocking I/O&#xff09; 3) I/O复用(select和poll)&#xff08;…