自己動手實(shí)現(xiàn)一個(gè)ORM框架
點(diǎn)擊關(guān)注公眾號,Java干貨及時(shí)送達(dá)

作者 |?汪偉俊?
出品 | Java技術(shù)迷(ID:JavaFans1024)
引言
本篇文章我們來自己動手實(shí)現(xiàn)一個(gè)ORM框架,我們先來看一下傳統(tǒng)的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實(shí)現(xiàn)了對數(shù)據(jù)表的查詢操作,不過這里有一些明顯的問題,對于數(shù)據(jù)庫的配置信息是硬編碼在代碼中的,想要修改配置信息還得來修改代碼,我們可以將其抽取成一個(gè)配置文件;對于sql的編寫也是硬編碼在代碼中,也可以考慮將其抽取出去;然后是對結(jié)果集的封裝,每次都需要通過循環(huán)解析結(jié)果集也非常麻煩。綜上所述,我們借鑒MyBatis來實(shí)現(xiàn)一個(gè)自己的ORM框架。
ORM框架整體架構(gòu)
我們先來梳理一下框架的整體架構(gòu),首先我們需要解析一下配置文件,正如MyBatis框架那樣,我們需要使用到兩種配置文件,一個(gè)是框架的全局配置文件,一個(gè)是Mapper配置文件,定義格式如下:
<configuration>
configuration>
<mapper>
mapper>
那么首先框架的第一步就是讀取配置文件,全局配置文件中應(yīng)該包含數(shù)據(jù)源配置信息和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>
對該配置文件進(jìn)行解析后,我們可以將這些數(shù)據(jù)封裝成一個(gè)Java實(shí)體,該實(shí)體包含了所有的配置信息,由于全局配置文件中可能含有多個(gè)Mapper文件的配置,所以將其封裝成一個(gè)Map集合:
Map
集合的key為String類型,value為MapperStatement類型,MapperStatement是對Mapper配置文件的一個(gè)封裝:
<mapper?namespace="user">
????<select?id="selectList"?resultType="com.wwj.pojo.User">
????????select?*?from?e_user
????select>
mapper>
這里需要注意一點(diǎn),框架會將整個(gè)項(xiàng)目中的Mapper配置文件都封裝成一個(gè)MapperStatement并保存到Map中,這就需要對每個(gè)MapperStatement進(jìn)行區(qū)分,區(qū)分的關(guān)鍵就是Mapper配置文件中的namespace和id,我們將其拼接起來作為statementId。到這里,配置文件的解析就完成了,然后我們提供對應(yīng)的查詢方法,該查詢方法的作用是對sql語句進(jìn)行解析并調(diào)用JDBC查詢數(shù)據(jù)庫,通過內(nèi)省封裝結(jié)果集。以上是框架的一個(gè)整體思路,大家可能現(xiàn)在還沒有理解到,沒關(guān)系,接下來是對實(shí)現(xiàn)過程的一個(gè)詳細(xì)概述。
解析配置文件
新建一個(gè)類Resources,該類負(fù)責(zé)將一個(gè)文件轉(zhuǎn)換成輸入流:
public?class?Resources?{
????/**
?????*?根據(jù)配置文件的路徑將配置文件加載成字節(jié)輸入流
?????*
?????*?@param?path
?????*?@return
?????*/
????public?static?InputStream?getResourceAsStream(String?path)?{
????????return?Resources.class.getClassLoader().getResourceAsStream(path);
????}
}
接下來我們需要一個(gè)SqlSessionFactoryBuilder對象,該對象會提供一個(gè)build方法來生成SqlSessionFactory:
public?class?SqlSessionFactoryBuilder?{
????public?SqlSessionFactory?build(InputStream?inputStream)?throws?DocumentException,?PropertyVetoException?{
????????//?使用dom4j解析配置文件,將解析出來的內(nèi)容封裝到Configuration中
????????XmlConfigBuilder?builder?=?new?XmlConfigBuilder();
????????Configuration?configuration?=?builder.parseConfig(inputStream);
????????//?創(chuàng)建SqlSessionFactory對象
????????SqlSessionFactory?sqlSessionFactory?=?new?DefaultSqlSessionFactory(configuration);
????????return?sqlSessionFactory;
????}
}
SqlSessionFactory是一個(gè)接口,我們創(chuàng)建它的默認(rèn)實(shí)現(xiàn)類DefaultSqlSessionFactory,該類需要傳入一個(gè)Configuration類型對象,這個(gè)Configuration就是對全局配置文件的一個(gè)封裝:
public?class?Configuration?{
????private?DataSource?dataSource;
????/**
?????*? key:statementId
?????*? value:封裝好的MapperStatement對象
?????*/
????private?Map?mappedStatementMap?=?new?HashMap<>();
}
那么現(xiàn)在的關(guān)鍵就是對全局配置文件的解析了,我們提供一個(gè)類XmlConfigBuilder,該類的parseConfig方法可以將輸入流轉(zhuǎn)換為Configuration對象,實(shí)現(xiàn)如下:
public?Configuration?parseConfig(InputStream?inputStream)?throws?DocumentException,?PropertyVetoException?{
????????Document?document?=?new?SAXReader().read(inputStream);
????????//?
????????Element?rootElement?=?document.getRootElement();
????????//?全局查找標(biāo)簽
????????List?propertyList?=?rootElement.selectNodes("http://property");
????????Properties?properties?=?new?Properties();
????????propertyList.forEach(element?->?{
????????????//?獲取到標(biāo)簽中的name和value屬性
????????????String?name?=?element.attributeValue("name");
????????????String?value?=?element.attributeValue("value");
????????????properties.setProperty(name,?value);
????????});
????????//?創(chuàng)建數(shù)據(jù)源
????????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("http://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可以很容易地實(shí)現(xiàn)解析,將每個(gè)標(biāo)簽中的屬性和屬性值讀取出來,進(jìn)行對應(yīng)的封裝即可,對于Mapper配置文件的解析也是如此,通過resource屬性可以得到Mapper文件位置,然后將其轉(zhuǎn)為輸入流并解析:
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);
????????//?得到根標(biāo)簽
????????Element?rootElement?=?document.getRootElement();
????????String?namespace?=?rootElement.attributeValue("namespace");
????????//?得到所有
????????StringBuilder?sb?=?new?StringBuilder();
????????List?selectList?=?rootElement.selectNodes("http://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);
????????});
????}
}
同樣地讀取每個(gè)配置的屬性名和屬性值,對于MappedStatementMap的封裝,其Map的key為namespace + id。
執(zhí)行查詢
讀取完配置文件之后,我們就得到了一個(gè)DefaultSqlSessionFactory對象,該對象需要提供一個(gè)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接口的默認(rèn)實(shí)現(xiàn)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 在該對象中,我們需要實(shí)現(xiàn)查詢操作,同樣地,我們借助一個(gè)SimpleExecutor類來實(shí)現(xiàn)具體的查詢,這里只需調(diào)用一下即可,想象一下,查詢操作需要哪些參數(shù)。首先configuration一定需要,里面封裝的是數(shù)據(jù)源和MapperStatement信息,其次,需要具體的MapperStatement對象,當(dāng)然了,MapperStatement也可以在方法內(nèi)部取,最后是查詢的一些參數(shù)信息,這樣就能夠?qū)崿F(xiàn)查詢了。
實(shí)現(xiàn)查詢
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());
????????//?獲取到參數(shù)的全限定類名
????????String?parameterType?=?mapperStatement.getParameterType();
????????Class>?parameterClass?=?getClassType(parameterType);
????????//?設(shè)置參數(shù)
????????List?parameterMappingList?=?replaceSql.getParameterMappingList();
????????for?(int?i?=?0;?i?????????????ParameterMapping?parameterMapping?=?parameterMappingList.get(i);
????????????String?content?=?parameterMapping.getContent();
????????????//?反射設(shè)置值
????????????Field?field?=?parameterClass.getDeclaredField(content);
????????????field.setAccessible(true);
????????????Object?o?=?field.get(params[0]);
????????????preparedStatement.setObject(i?+?1,?o);
????????}
????????//?執(zhí)行sql
????????ResultSet?resultSet?=?preparedStatement.executeQuery();
????????String?resultType?=?mapperStatement.getResultType();
????????Class>?resultClass?=?getClassType(resultType);
????????List 整個(gè)框架的核心部分就是這個(gè)SimpleExecutor類了,我們知道,JDBC中的preparedStatement類執(zhí)行的sql是以?作為占位符的,所以我們把#{}替換成?,并將#{id}里面的屬性名取出來,這就是查詢的一些參數(shù)信息。將參數(shù)類型和返回類型均通過反射內(nèi)省技術(shù)進(jìn)行值的封裝,即可得到最終結(jié)果。
測試一下
通過以上步驟便實(shí)現(xiàn)了一個(gè)簡單的ORM框架,項(xiàng)目結(jié)構(gòu)如下:
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?????
接下來我們測試一下,首先創(chuàng)建一個(gè)項(xiàng)目,引入自定義框架:
<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);
}
執(zhí)行結(jié)果:
[User{id=1,?name='lisi',?password='admin'}]
? ? ?
往 期 推 薦
1、我在產(chǎn)品上線前不小心刪除了7 TB的視頻 2、程序員最硬大佬,你絕對想不到!!! 3、IntelliJ IDEA快捷鍵大全 + 動圖演示 4、打不過就加入?微軟強(qiáng)推“親兒子”上位,還是中國特供版 5、活久見!NVIDIA正式開源其Linux GPU內(nèi)核模塊 點(diǎn)分享
點(diǎn)收藏
點(diǎn)點(diǎn)贊
點(diǎn)在看





