自己动手实现一个ORM框架
共 10252字,需浏览 21分钟
·
2022-05-27 04:29
点击关注公众号,Java干货及时送达
作者 | 汪伟俊
出品 | Java技术迷(ID:JavaFans1024)
引言
本篇文章我们来自己动手实现一个ORM框架,我们先来看一下传统的JDBC代码:
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String JDBC_URL = "jdbc:mysql:///user";
static final String USER_NAME = "root";
static final String PASS_WORD = "123456";
public static void main(String[] args) {
Class.forName(JDBC_DRIVER);
Connection conn = DriverManager.getConnection(JDBC_URL, USER_NAME, PASS_WORD);
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM user";
ResultSet rs = stmt.executeQuery(sql);
while(rs.next()){
int id = rs.getInt("id");
int age = rs.getInt("age");
System.out.println("ID: " + id);
System.out.println("Age: " + age);
}
rs.close();
}
以上代码通过JDBC实现了对数据表的查询操作,不过这里有一些明显的问题,对于数据库的配置信息是硬编码在代码中的,想要修改配置信息还得来修改代码,我们可以将其抽取成一个配置文件;对于sql的编写也是硬编码在代码中,也可以考虑将其抽取出去;然后是对结果集的封装,每次都需要通过循环解析结果集也非常麻烦。综上所述,我们借鉴MyBatis来实现一个自己的ORM框架。
ORM框架整体架构
我们先来梳理一下框架的整体架构,首先我们需要解析一下配置文件,正如MyBatis框架那样,我们需要使用到两种配置文件,一个是框架的全局配置文件,一个是Mapper配置文件,定义格式如下:
<configuration>
configuration>
<mapper>
mapper>
那么首先框架的第一步就是读取配置文件,全局配置文件中应该包含数据源配置信息和Mapper配置文件所在位置,如下所示:
<configuration>
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///user"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
dataSource>
<mapper resource="UserMapper.xml"/>
configuration>
对该配置文件进行解析后,我们可以将这些数据封装成一个Java实体,该实体包含了所有的配置信息,由于全局配置文件中可能含有多个Mapper文件的配置,所以将其封装成一个Map集合:
Map
集合的key为String类型,value为MapperStatement类型,MapperStatement是对Mapper配置文件的一个封装:
<mapper namespace="user">
<select id="selectList" resultType="com.wwj.pojo.User">
select * from e_user
select>
mapper>
这里需要注意一点,框架会将整个项目中的Mapper配置文件都封装成一个MapperStatement并保存到Map中,这就需要对每个MapperStatement进行区分,区分的关键就是Mapper配置文件中的namespace
和id
,我们将其拼接起来作为statementId
。到这里,配置文件的解析就完成了,然后我们提供对应的查询方法,该查询方法的作用是对sql语句进行解析并调用JDBC查询数据库,通过内省封装结果集。以上是框架的一个整体思路,大家可能现在还没有理解到,没关系,接下来是对实现过程的一个详细概述。
解析配置文件
新建一个类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 {
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
* value:封装好的MapperStatement对象
*/
private Map mappedStatementMap = new HashMap<>();
}
那么现在的关键就是对全局配置文件的解析了,我们提供一个类XmlConfigBuilder,该类的parseConfig方法可以将输入流转换为Configuration对象,实现如下:
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
//
Element rootElement = document.getRootElement();
// 全局查找标签
List propertyList = rootElement.selectNodes("//property");
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文件
List mapperList = rootElement.selectNodes("//mapper");
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;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
// 得到根标签
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
// 得到所有
StringBuilder sb = new StringBuilder();
List selectList = rootElement.selectNodes("//select");
selectList.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);
// 将MapperStatement对象保存到Configuration中
sb.append(namespace).append(".").append(id);
configuration.getMappedStatementMap().put(sb.toString(), mapperStatement);
sb.setLength(0);
});
}
}
同样地读取每个配置的属性名和属性值,对于MappedStatementMap的封装,其Map的key为namespace + id
。
执行查询
读取完配置文件之后,我们就得到了一个DefaultSqlSessionFactory对象,该对象需要提供一个openSession方法来获得SqlSession对象:
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
我们返回SqlSession接口的默认实现DefaultSqlSession:
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public List selectList(String statementId, Object... params) throws Exception {
Executor executor = new SimpleExecutor();
List
在该对象中,我们需要实现查询操作,同样地,我们借助一个SimpleExecutor类来实现具体的查询,这里只需调用一下即可,想象一下,查询操作需要哪些参数。首先configuration一定需要,里面封装的是数据源和MapperStatement信息,其次,需要具体的MapperStatement对象,当然了,MapperStatement也可以在方法内部取,最后是查询的一些参数信息,这样就能够实现查询了。
实现查询
public class SimpleExecutor implements Executor {
@Override
public List 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中的 #{} 替换为 ?
ReplaceSql replaceSql = getReplaceSql(sql);
PreparedStatement preparedStatement = connection.prepareStatement(replaceSql.getSql());
// 获取到参数的全限定类名
String parameterType = mapperStatement.getParameterType();
Class> parameterClass = getClassType(parameterType);
// 设置参数
List parameterMappingList = replaceSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String content = parameterMapping.getContent();
// 反射设置值
Field field = parameterClass.getDeclaredField(content);
field.setAccessible(true);
Object o = field.get(params[0]);
preparedStatement.setObject(i + 1, o);
}
// 执行sql
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mapperStatement.getResultType();
Class> resultClass = getClassType(resultType);
List
整个框架的核心部分就是这个SimpleExecutor类了,我们知道,JDBC中的preparedStatement类执行的sql是以?
作为占位符的,所以我们把#{}
替换成?
,并将#{id}
里面的属性名取出来,这就是查询的一些参数信息。将参数类型和返回类型均通过反射内省技术进行值的封装,即可得到最终结果。
测试一下
通过以上步骤便实现了一个简单的ORM框架,项目结构如下:
com.wwj.config
-ReplaceSql
-XmlConfigBuilder
-XmlMapperBuilder
com.wwj.io
-Resources
com.wwj.pojo
-Configuration
-MapperStatement
com.wwj.sqlSession
-Executor
-SqlSession
-SqlSessionFactory
-SqlSessionFactoryBuilder
com.wwj.sqlSession.impl
-DefaultSqlSession
-DefaultSqlSessionFactory
-SimpleExecutor
com.wwj.utils
-GenericTokenParser
-ParameterMapping
-ParameterMappingTokenHandler
-TokenHandler
接下来我们测试一下,首先创建一个项目,引入自定义框架:
<dependency>
<groupId>com.wwjgroupId>
<artifactId>My-MyBatisartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
编写全局配置文件:
<configuration>
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
dataSource>
<mapper resource="UserMapper.xml"/>
configuration>
编写UserMapper配置文件:
<mapper namespace="user">
<select id="selectList" resultType="com.wwj.pojo.User">
select * from e_user
select>
mapper>
编写测试代码:
@Test
public void test() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = new User();
user.setId(1);
user.setName("lisi");
List userList = sqlSession.selectList("user.selectList");
System.out.println(userList);
}
执行结果:
[User{id=1, name='lisi', password='admin'}]