《手写Spring渐进式源码实践》实践笔记(第二十章 实现简单ORM框架)
文章目录
- 第二十章 简单ORM框架实现
- 背景
- 技术背景
- 基本概念
- 工作原理
- 优点
- 缺点
- 常见的ORM框架
- 业务背景
- 目标
- 设计
- 实现
- 代码结构
- 类图
- 实现步骤
- 测试
- 事先准备
- 属性配置文件
- 测试用例(selectOne)
- 测试结果
- 测试用例(selectList)
- 测试结果
- 总结
第二十章 简单ORM框架实现
背景
技术背景
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框架,并与前面章节实现的small-spring框架做对接。
目标
实现一个简易版的类似mybatis的orm框架,屏蔽了对 JDBC 操作的复杂性, 让外部的调用方可以更加简单的方式使用数据库。
设计
一个ORM框架要实现的核心内容, 包括参数映射、SQL解析、SQL执行、结果映射
,这块的内容会被封装,对内通过jdbc与数据库进行交互,对外提供SqlSession工厂类进行接口调用。
整体设计结构如下图:
实现
代码结构

源码实现:https://github.com/swg209/spring-study/tree/main/step20-simple-orm
类图
- 在整个类图中,SqlSession是与数据库交互的核心接口,定义了多种查询方法,包括
selectOne
和selectList
,分别用于查询单个对象和对象列表,同时提供了close
方法用于关闭会话。依赖于SqlSessionFactory
接口来创建其实例。 - SqlSessionFactory负责创建
SqlSession
实例,通过其openSession
方法实现。 - DefaultSqlSession类是
SqlSession
接口的具体实现类,提供了selectOne
、selectList
和close
等方法的实现。包含了数据库连接connection
和映射元素mapperElement
等属性,用于管理数据库连接和SQL映射。提供了resultSet2Obj
和buildParameter
等辅助方法,用于结果集到对象的转换和参数构建。 - DefaultSqlSessionFactory类是
SqlSessionFactory
接口的具体实现类,负责创建和管理SqlSession
对象。包含了配置configuration
属性,用于存储数据库连接和SQL映射的配置信息。通过其openSession
方法返回SqlSession
实例。 - SqlSessionFactoryBuilder类用于构建
SqlSessionFactory
实例,通过其build
方法读取配置文件并创建DefaultSqlSessionFactory
对象。在构建过程中,会解析配置文件并生成相应的配置信息。
实现步骤
-
定义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();}
-
定义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;}@Overridepublic <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;}@Overridepublic <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);}}}@Overridepublic <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;}@Overridepublic <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;}@Overridepublic void close() {} }
-
定义sqlSessionFactory接口
public interface SqlSessionFactory {SqlSession openSession(); }
-
定义sqlSessionFactory具体实现类 DefaultSqlSessionFactory
public class DefaultSqlSessionFactory implements SqlSessionFactory {private final Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}@Overridepublic SqlSession openSession() {// 创建会话return new DefaultSqlSession(configuration.connection, configuration.mapperElement);} }
-
定义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");//SELECTList<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; // 用户IDprivate 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="123456"/></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.springframework.mybatis.test.dao.IUserDao"><select id="queryUserInfoById" parameterType="java.lang.Long"resultType="cn.suwg.springframework.mybatis.test.po.User">SELECT id, user_id, user_name, user_head, user_password, create_timeFROM my_userwhere id = #{id}</select><select id="queryUserList" parameterType="cn.suwg.springframework.mybatis.test.po.User"resultType="cn.suwg.springframework.mybatis.test.po.User">SELECT id, user_id, user_name, user_head, user_password, create_time, update_timeFROM my_userwhere user_name = #{userName}</select></mapper>
测试用例(selectOne)
public class ApiTest {@Testpublic 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.springframework.mybatis.test.dao.IUserDao.queryUserInfoById", 1L);System.out.println(JSON.toJSONString(user));} finally {session.close();reader.close();}} catch (Exception e) {e.printStackTrace();}}
}
测试结果
测试用例(selectList)
public class ApiTest {@Testpublic 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.springframework.mybatis.test.dao.IUserDao.queryUserList",new User("小苏"));System.out.println(JSON.toJSONString(userList));} finally {session.close();reader.close();}} catch (Exception e) {e.printStackTrace();}}
}
测试结果
- 从测试结果中可以看到,可以正常与数据库进行交互,可以正常查询到用户表的数据。
总结
-
在本章节中,我们简单实现一个类似mybatis的ORM框架,当前只是实现了基本功能,后续可以基于这个框架再进行完善。
-
通过编写测试用例,验证了该 ORM 框架的基本功能,包括查询单个对象和对象列表。测试结果表明,该框架能够正常与数据库进行交互,并正确映射查询结果到 Java 对象。
参考书籍:《手写Spring渐进式源码实践》
书籍源代码:https://github.com/fuzhengwei/small-spring