计算机系统应用教程网站

网站首页 > 技术文章 正文

自定义一个简单的 ORM 框架

btikc 2024-09-22 01:10:28 技术文章 24 ℃ 0 评论

自定义一个简单的 ORM 框架

前面我们通过跟踪源码的方式剖析了Hibernate 和MyBatis 两个框架是如何应用 ORM 思想的,接下来我们自己定义一个简单的 ORM 框架(名为 MiniORM),希望能通过这种方式让大家亲自零距离的去应用一下 ORM。

4.1MiniORM 框架的结构设计

1.第一层为配置层:

?miniORM.cfg.xml 是框架的核心配置文件,主要用来设置数据库连接信息和映射配置文件路径信息

?Xxx.mapper.xml 是框架的映射配置文件,主要用来设置类和表之间以及属性和字段之间的映射关系

?Xxx.java 是带有映射注解的实体类,主要用来设置类和表之间以及属性和字段之间的映射关系,和 Xxx.mapper.xml 的作用一样,只不过采用的是注解方式,两者二选一

2.第二层为解析层:

?Dom4jUtil 类用来解析 miniORM.cfg.xml 和Xxx.mapper.xml 两个配置文件的数据

?AnnotationUtil 类用来解析实体类中的映射注解

3.第三层为封装层:

?ORMConfig 类用来封装和存储从 miniORM.cfg.xml 文件中解析得到的数据

?Mapper 类用来封装和存储从 Xxx.mapper.xml 或实体类中解析得到的映射数据

4.第四层为功能层:

?ORMSession 类主要用来从 ORMConfig 和 Mapper 中获取相关数据,然后生成 sql 语句, 最后通过对 JDBC 的封装最终实现增删改查功能

4.2MiniORM 框架的代码实现

1.pom.xml

<groupId>cn.itcast.framework.miniorm</groupId>

<artifactId>MiniORM</artifactId>

<version>1.0-SNAPSHOT</version>

<packaging>jar</packaging>

<dependencies>

<dependency>

<groupId>dom4j</groupId>

<artifactId>dom4j</artifactId>

<version>1.6.1</version>

</dependency>

</dependencies>

<build>

<finalName>MiniORM</finalName>

<pluginManagement>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.2</version>

<configuration>

<source>1.8</source>

<target>1.8</target>

<encoding>UTF-8</encoding>

<compilerArguments>

<bootclasspath>${JAVA_HOME}/jre/lib/rt.jar</bootclasspath>

</compilerArguments>

</configuration>

</plugin>

</plugins>

</pluginManagement>

</build>

MiniORM 框架依赖 dom4j 和 jdk1.8, 编译时会打成 jar 包并 install 到本地仓库中,如下图所示:

2.miniORM.cfg.xml 是框架的核心配置文件,主要用来设置数据库连接信息和映射配置文件路径信息,源码如下所示:

<?xml version='1.0' encoding='utf-8'?>
<orm-factory>
<!--数据库连接数据-->
<property name="connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="connection.driverClass">com.mysql.jdbc.Driver</property>
<property name="connection.username">root</property>
<property name="connection.password">123</property>
<!--采用 xml 配置映射数据-->
<mapping resource="cn/itcast/orm/test/entity/Book.mapper.xml"/>
<!--采用实体类注解配置映射数据-->
<entity package="cn.itcast.orm.test.entity"/>
</orm-factory>

3.Xxx.mapper.xml 是框架的映射配置文件,主要用来设置类和表之间以及属性和字段之间的映射关系,以Book.mapper.xml 为例,源码如下所示:

<?xml version='1.0' encoding='UTF-8'?>
<!--实体类和表之间的映射关系配置-->
<orm-mapping>
<class name="cn.itcast.orm.test.entity.Book" table="t_book">
<id name="id" column="bid"/>
<property name="name" column="bname"/>
<property name="author" column="author"/>
<property name="price" column="price"/>
</class>
</orm-mapping>

4.当然 MiniORM 框架也支持在实体类上以注解方式去配置映射关系,以 Book.java 为例,源码如下所示:

import cn.itcast.orm.annotation.ORMColumn; import cn.itcast.orm.annotation.ORMId; import cn.itcast.orm.annotation.ORMTable;
//实体类:图书@ORMTable(name = "t_book") public class Book {
@ORMId @ORMColumn(name = "bid") private int id; //主键
@ORMColumn(name="bname") private String name; //图书名字
@ORMColumn(name="author") private String author; //图书作者
@ORMColumn(name="price") private double price; //图书价格
... ...
}

实体类中的@ORMTable、@ORMId、@ORMColumn 是我们自定义的三个注解,@ORMTable 用来设置当前类和哪个表对应,@ORMColumn 用来设置当前属性和表中哪个字段对应, @ORMId 用来设置哪个属性对应的字段是主键。

5.Dom4jUtil 类是一个基于 Dom4j 的工具类, 主要用来解析 miniORM.cfg.xml 和Xxx.mapper.xml,源码如下所示:

public class Dom4jUtil {

/**

*通过文件的路径获取 xml 的 document 对象

*

*@param path 文件的路径

*@return 返回文档对象

*/

public static Document getXMLByFilePath(String path) { if (null == path) {

return null;

}

Document document = null; try {

SAXReader reader = new SAXReader(); document = reader.read(new File(path));

} catch (Exception e) { e.printStackTrace();

}

return document;

}

/**

*获得某文档中某元素内某属性的值和元素的文本信息

*

*@param documentxml 文档对象

*@param elementName 元素名

*@param attrName属性名

*@return 返回一个 Map 集合

*/

public static Map<String, String> Elements2Map(Document document, String elementName,

String attrName) {

List<Element> propList = document.getRootElement().elements(elementName); Map<String, String> propConfig = new HashMap<>();

for (Element element : propList) {

String key = element.attribute(attrName).getValue(); String value = element.getTextTrim(); propConfig.put(key, value);

}

return propConfig;

}

/**

*针对 mapper.xml 文件,获得映射信息并存到 Map 集合中

*@param document xml 文档对象

*@return 返回一个 Map 集合

*/

public static Map<String, String> Elements2Map(Document document) { Element classElement = document.getRootElement().element("class"); Map<String, String> mapping = new HashMap<>();

Element idElement = classElement.element("id"); String idKey = idElement.attribute("name").getValue();

String idValue = idElement.attribute("column").getValue();

mapping.put(idKey, idValue);

List<Element> propElements = classElement.elements("property"); for (Element element : propElements) {

String propKey = element.attribute("name").getValue(); String propValue = element.attribute("column").getValue(); mapping.put(propKey, propValue);

}

return mapping;

}

/**

*针对 mapper.xml 文件,获得主键的映射信息并存到 Map 集合中

*

*@param document xml 文档对象

*@return 返回一个 Map 集合

*/

public static Map<String, String> ElementsID2Map(Document document) { Element classElement = document.getRootElement().element("class"); Map<String, String> mapping = new HashMap<>();

Element idElement = classElement.element("id"); String idKey = idElement.attribute("name").getValue();

String idValue = idElement.attribute("column").getValue(); mapping.put(idKey, idValue);

return mapping;

}

/**

*获得某文档中某元素内某属性的值

*

*@param documentxml 文档对象

*@param elementName 元素名

*@param attrName属性名

*@return 返回一个 Set 集合

*/

public static Set<String> Elements2Set(Document document, String elementName, String attrName) { List<Element> mappingList = document.getRootElement().elements(elementName);

Set<String> mappingSet = new HashSet<>(); for (Element element : mappingList) {

String value = element.attribute(attrName).getValue(); mappingSet.add(value);

}

return mappingSet;

}

/**

*获得某文档中某元素内某属性的值

*

*@param documentxml 文档对象

*@param elementName 元素名

*@param attrName属性名

*@return 返回一个 Set 集合

*/

public static String getPropValue(Document document, String elementName, String attrName) { Element element = (Element) document.getRootElement().elements(elementName).get(0); return element.attribute(attrName).getValue();

}

}

6.AnnotationUtil 类主要用来通过反射技术解析实体类中的注解,从而获得映射数据,源码如下所示:

public class AnnotationUtil {

/*

得到的类名

*/

public static String getClassName(Class clz) { return clz.getName();

}

/*

得到ORMTable 注解中的表名

*/

public static String getTableName(Class clz) {

if (clz.isAnnotationPresent(ORMTable.class)) {

ORMTable ormTable = (ORMTable) clz.getAnnotation(ORMTable.class); return ormTable.name();

} else {

System.out.println("缺少 ORMTable 注解"); return null;

}

}

/*

得到主键属性和对应的字段

*/

public static Map<String, String> getIdMapper(Class clz) { boolean flag = true;

Map<String, String> map = new HashMap<>(); Field[] fields = clz.getDeclaredFields();

for (Field field : fields) {

if (field.isAnnotationPresent(ORMId.class)) { flag = false;

String fieldName = field.getName();

if (field.isAnnotationPresent(ORMColumn.class)) {

ORMColumn ormColumn = field.getAnnotation(ORMColumn.class); String columnName = ormColumn.name();

map.put(fieldName, columnName); break;

} else {

System.out.println("缺少 ORMColumn 注解");

}

}

}

if (flag) {

System.out.println("缺少 ORMId 注解");

}

return map;

}

/*

得到类中所有属性和对应的字段

*/

public static Map<String, String> getPropMapping(Class clz) { Map<String, String> map = new HashMap<>(); map.putAll(getIdMapper(clz));

Field[] fields = clz.getDeclaredFields(); for (Field field : fields) {

if (field.isAnnotationPresent(ORMColumn.class)) {

ORMColumn ormColumn = field.getAnnotation(ORMColumn.class); String fieldName = field.getName();

String columnName = ormColumn.name(); map.put(fieldName, columnName);

}

}

return map;

}

/*

获得某包下面的所有类名

*/

public static Set<String> getClassNameByPackage(String packagePath) { Set<String> names = new HashSet<>();

String packageFile = packagePath.replace(".", "/");

String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); if (classpath == null) {

classpath = Thread.currentThread().getContextClassLoader().getResource("/").getPath();

}

try {

classpath = java.net.URLDecoder.decode(classpath, "utf-8");

} catch (UnsupportedEncodingException e) { e.printStackTrace();

}

File dir = new File(classpath + packageFile); if (dir.exists()) {

File[] files = dir.listFiles(); for (File f : files) {

String name = f.getName();

if (f.isFile() && name.endsWith(".class")) {

name = packagePath + "." + name.substring(0, name.lastIndexOf(".")); names.add(name);

}

}

} else {

System.out.println("包路径不存在");

}

return names;

}

}

7.Mapper 类用来封装并存储从 Xxx.mapper.xml 中或从实体类中解析得到的映射信息,哪个表和哪个类映射,哪个字段和哪个属性映射等等,源码如下所示:

public class Mapper {
private String className; //类名private String tableName; //表名
private Map<String,String> idMapper=new HashMap(); //主键字段和属性private Map<String,String> propMapping=new HashMap(); //非主键字段和属性
... ...
}

8.ORMConfig 类主要用来存储 miniORM.cfg.xml 配置文件中的信息和 Mapper 映射信息,该类内部会使用 Dom4jUtil、AnnotationUtil 工具类去解析数据,源码如下所示:

public class ORMConfig {
public static String classpath; //类路径public static File cfgFile; //核心配置文件
public static Map<String, String> propConfig; //核心配置文件数据public static Set<String> mappingSet; //映射配置文件
public static Set<String> entitySet; //实体类
public static List<Mapper> mapperList; //解析出来的 Mapper
// 从 classpath 中加载框架的核心配置文件 miniORM.cfg.xml static {
classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); if (classpath == null) {
classpath = Thread.currentThread().getContextClassLoader().getResource("/").getPath();
}
try {
classpath = java.net.URLDecoder.decode(classpath, "utf-8");
} catch (UnsupportedEncodingException e) { e.printStackTrace();
}
cfgFile = new File(classpath + "miniORM.cfg.xml"); if (cfgFile.exists()) {
Document document = Dom4jUtil.getXMLByFilePath(cfgFile.getPath()); propConfig = Dom4jUtil.Elements2Map(document, "property", "name"); mappingSet = Dom4jUtil.Elements2Set(document, "mapping", "resource"); entitySet = Dom4jUtil.Elements2Set(document, "entity", "package");
} else {
cfgFile = null;
System.out.println("未找到核心配置文件 miniORM.cfg.xml");
}
}
//从 propConfig 获得信息,连接数据库
private Connection getConnection() throws ClassNotFoundException, SQLException { String url = propConfig.get("connection.url");
String driverClass = propConfig.get("connection.driverClass"); String username = propConfig.get("connection.username"); String password = propConfig.get("connection.password"); Class.forName(driverClass);
Connection connection = DriverManager.getConnection(url, username, password); connection.setAutoCommit(true);
return connection;
}
//从 mappingSet 中挨个解析 mapper.xml 配置文件,获得实体类和表之间的映射信息
//从 entitySet 中挨个解析实体类中的注解,获得实体类和表之间的映射信息
private void getMapping() throws ClassNotFoundException { mapperList = new ArrayList<>();
for (String xmlPath : mappingSet) {
Document document = Dom4jUtil.getXMLByFilePath(classpath + xmlPath);
Map<String, String> mapping = Dom4jUtil.Elements2Map(document);
String className = Dom4jUtil.getPropValue(document, "class", "name"); String tableName = Dom4jUtil.getPropValue(document, "class", "table"); Map<String, String> id_id = Dom4jUtil.ElementsID2Map(document);
Mapper mapper = new Mapper(); mapper.setClassName(className); mapper.setTableName(tableName); mapper.setIdMapper(id_id); mapper.sePropMapping(mapping);
mapperList.add(mapper);
}
for (String packagePath : entitySet) {
Set<String> nameSet = AnnotationUtil.getClassNameByPackage(packagePath); for (String name : nameSet) {
Class clz = Class.forName(name);
String className = AnnotationUtil.getClassName(clz); String tableName = AnnotationUtil.getTableName(clz);
Map<String, String> id_id = AnnotationUtil.getIdMapper(clz); Map<String, String> mapping = AnnotationUtil.getPropMapping(clz); Mapper mapper = new Mapper(); mapper.setClassName(className); mapper.setTableName(tableName);
mapper.setIdMapper(id_id); mapper.sePropMapping(mapping);
mapperList.add(mapper);
}
}
}
public ORMSession buildORMSession() throws Exception {
//从 propConfig 获得信息,连接数据库
Connection connection = getConnection();
//从 mappingSet 中挨个解析 mapper.xml 配置文件,获得实体类和表之间的映射信息
getMapping();
//创建 ORMSession 对象
return new ORMSession(connection);
}
}

9.ORMSession 类主要用来从 ORMConfig 和 Mapper 中获取相关数据,然后生成 sql 语句,最后通过对 JDBC 的封装最终实现增删改查功能,源码如下所示:

public class ORMSession {
private Connection connection;
public ORMSession(Connection conn) { this.connection = conn;
}
//保存数据
public void save(Object entity) throws Exception { String insertSQL = "";
//1. 从 ORMConfig 中获得保存有映射信息的集合
List<Mapper> mapperList = ORMConfig.mapperList;
//2. 遍历集合,从集合中找到和 entity 参数相对应的 mapper 对象
for (Mapper mapper : mapperList) {
if (mapper.getClassName().equals(entity.getClass().getName())) { String tableName = mapper.getTableName();
String insertSQL1 = "insert into " + tableName + "( "; String insertSQL2 = " ) values ( ";
//3. 得到当前对象所属类中的所有属性
Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) {
field.setAccessible(true);
//4. 遍历过程中根据属性得到字段名
String columnName = mapper.getPropMapper().get(field.getName());
//5. 遍历过程中根据属性得到它的值
String columnValue = field.get(entity).toString();
//6. 拼接 sql 语句
insertSQL1 += columnName + ","; insertSQL2 += "'" + columnValue + "',";
}
insertSQL = insertSQL1.substring(0, insertSQL1.length() - 1) + insertSQL2.substring(0,
insertSQL2.length() - 1) + " )";
break;
}
}
// 把 sql 语句打印到控制台
System.out.println("MiniORM-save: " + insertSQL);
//7. 通过 JDBC 发送并执行 sql
PreparedStatement statement = connection.prepareStatement(insertSQL); statement.executeUpdate();
statement.close();
}
//根据主键进行数据删除 delete from 表名 where 主键 = 值
public void delete(Object entity) throws Exception { String delSQL = "delete from ";
//1. 从 ORMConfig 中获得保存有映射信息的集合
List<Mapper> mapperList = ORMConfig.mapperList;
//2. 遍历集合,从集合中找到和 entity 参数相对应的 mapper 对象
for (Mapper mapper : mapperList) {
if (mapper.getClassName().equals(entity.getClass().getName())) {
// 3. 得到我们想要的 mapper 对象,并得到表名String tableName = mapper.getTableName(); delSQL += tableName + " where ";
// 4. 得到主键的字段名和属性名
Object[] idProp = mapper.getIdMapper().keySet().toArray(); //idProp[0] Object[] idColumn = mapper.getIdMapper().values().toArray(); //idColumn[0]
// 5. 得到主键的值
Field field = entity.getClass().getDeclaredField(idProp[0].toString()); field.setAccessible(true);
String idVal = field.get(entity).toString();
// 6. 拼接 sql
delSQL += idColumn[0].toString() + " = " + idVal;
// 把 sql 语句打印到控制台
System.out.println("MiniORM-delete: " + delSQL);
break;
}
}
//7. 通过 JDBC 发送并执行 sql
PreparedStatement statement = connection.prepareStatement(delSQL); statement.executeUpdate();
statement.close();
}
// 根据主键进行查询 select * from 表名 where 主键字段 = 值
public Object findOne(Class clz, Object id) throws Exception{
String querySQL = "select * from ";
//1. 从 ORMConfig 中得到存有映射信息的集合
List<Mapper> mapperList=ORMConfig.mapperList;
//2. 遍历集合拿到我们想要的 mapper 对象
for (Mapper mapper : mapperList) {
if (mapper.getClassName().equals(clz.getName())) {
// 3. 获得表名
String tableName = mapper.getTableName();
//4. 获得主键字段名
Object[] idColumn = mapper.getIdMapper().values().toArray(); //idColumn[0]
//5. 拼接 sql
querySQL += tableName + " where " + idColumn[0].toString() + " = " + id;
break;
}
}
System.out.println("MiniORM-findOne:" +querySQL);
//6. 通过 jdbc 发送并执行 sql, 得到结果集
PreparedStatement statement=connection.prepareStatement(querySQL); ResultSet rs=statement.executeQuery();
//7. 封装结果集,返回对象
if(rs.next()){
// 查询到一行数据
// 8.创建一个对象,目前属性的值都是初始值Object obj=clz.newInstance();
// 9. 遍历 mapperList 集合找到我们想要的 mapper 对象
for(Mapper mapper:mapperList){
if (mapper.getClassName().equals(clz.getName())) {
//10. 得到存有属性-字段的映射信息
Map<String,String> propMap = mapper.getPropMapper();
//11. 遍历集合分别拿到属性名和字段名Set<String> keySet = propMap.keySet(); for(String prop:keySet){ //prop 就是属性名
String column = propMap.get(prop);	//column 就是和属性对应的字段名
Field field = clz.getDeclaredField(prop); field.setAccessible(true); field.set(obj,rs.getObject(column));
}
break;
}
}
//12. 释放资源statement.close(); rs.close();
//13. 返回查询出来的对象
return obj;
}else {
// 没有查到数据
return null;
}
}
//关闭连接,释放资源
public void close() throws Exception{ if(connection!=null){
connection.close(); connection = null;
}
}
}

4.3MiniORM 框架的测试使用

我们自定义的 MiniORM 框架主要用来体现 ORM 思想,并不是为了开发一个成熟的持久层框架出来,因此很多逻辑并不完善,很多情况也未去考虑,请各位理解。接下来我们就测试一下该框架。

1.pom.xml

<groupId>cn.itcast.orm</groupId>
<artifactId>TestMiniORM</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>cn.itcast.framework.miniorm</groupId>
<artifactId>MiniORM</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.36</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes> <!--为了编译时能加载包中的 xml 文件-->
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource> <!--为了编译时能加载 resources 中的 xml 文件-->
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

我们前面把 MiniORM 框架打成 jar 包并 install 到了本地 Maven 仓库中,因此在使用该框架时需要从本地仓库进行加载。

2.miniORM.cfg.xml

<?xml version='1.0' encoding='utf-8'?>
<orm-factory>
<!--数据库连接-->
<property name="connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="connection.driverClass">com.mysql.jdbc.Driver</property>
<property name="connection.username">root</property>
<property name="connection.password">123</property>
<!--采用 xml 配置映射数据-->
<mapping resource="cn/itcast/orm/test/entity/Book.mapper.xml"/>
<!--采用注解配置映射数据-->
<entity package="cn.itcast.orm.test.entity"/>
</orm-factory>

这是框架的核心配置文件,我们既采用了 xml 方式配置映射数据,也采用了注解方式在实体

类中配置映射数据,最后可以二选一分别进行功能测试。

3.实体类和映射配置文件

@ORMTable(name = "t_book") public class Book {
@ORMId @ORMColumn(name = "bid") private int id; //主键
@ORMColumn(name="bname") private String name; //图书名字
@ORMColumn(name="author") private String author; //图书作者
@ORMColumn(name="price") private double price; //图书价格
... ...
}
<?xml version='1.0' encoding='UTF-8'?>
<!--实体类和表之间的映射关系配置-->
<orm-mapping>
<class name="cn.itcast.orm.test.entity.Book" table="t_book">
<id name="id" column="bid"/>
<property name="name" column="bname"/>
<property name="author" column="author"/>
<property name="price" column="price"/>
</class>
</orm-mapping>

注意:对于同一个表或实体类,不需要既进行 xml 配置,又进行注解配置,二选一即可,这

里同时进行配置只是为了测试方便。

测试类

public class BookDao {
private ORMConfig config; @Before
public void init() {
config = new ORMConfig();
}
@Test
public void testSave() throws Exception {
ORMSession session = config.buildORMSession(); Book book = new Book();
book.setId(1);
book.setName("降龙十八掌"); book.setAuthor("不知道"); book.setPrice(9.9); session.save(book); session.close();
}
@Test
public void testDelete() throws Exception { ORMSession session = config.buildORMSession(); Book book = new Book();
book.setId(1); session.delete(book); session.close();
}
@Test
public void testFindOne() throws Exception { ORMSession session = config.buildORMSession(); Book book = (Book) session.findOne(Book.class, 1); System.out.println(book);
session.close();
}

?我们调用 ORMSession 类的 save 方法完成了数据保存功能,由 MiniORM 框架生成 sql语句,运行效果如下图所示:

?我们调用 ORMSession 类的 findOne 方法完成了数据查询功能,由 MiniORM 框架生成sql语句,运行效果如下图所示:

?我们调用 ORMSession 类的 delete 方法完成了数据删除功能,由 MiniORM 框架生成 sql语句,运行效果如下图所示:

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表