ORM框架设计及实现

article/2025/10/19 7:21:25

文章目录

    • 一 对象记录映射
    • 二 效率和性能
    • 三 数据访问描述信息
    • 四 字段类型转换
    • 五 数据访问基类
    • 六 统一异常处理
    • 七 数据访问框架
    • 八 数据源创建工厂
    • 九 SQL指令解析
    • 十 补充DAO接口实现
    • 十一 测试
    • 十二 其他问题

一 对象记录映射

  本质上来说,ORM框架需要处理的就是如何将JAVA对象与数据表记录进行关联,便于JAVA对象的持久化,以及将表记录自动转换为JAVA对象。

  这就需要存在一个中间态的描述信息,描述信息必须能够指明JAVA对象和表记录的映射关系,包括但不限于JAVA对象类型和表的关系,对象成员和字段的关系。

  市面上成熟的ORM框架很多,但总体设计思路一定离不开中间描述信息,典型的如Mybaties,它通过配置文件来描述,又如Spring通过配置或者注解来描述。

  我们称这种与平台、语言无关的描述信息为IDL,依赖于这种设计,实际上对配置的要求变高了,而且当数据表量激增时很难维护。

二 效率和性能

  无论是通过文件配置还是通过注解的方式来描述对象记录的映射关系,都没办法直接将SQL字段类型直接转换为JAVA对象的成员类型,而且为了能够准确的定位到对象成员,必须使用大量的反射才能实现。

  只要涉及反射,那么效率和性能就很难得到保障。那么有没有一种办法能够避开这种低效率高消耗的映射实现呢?是可以做到的,我们可以赋予DAO对象一种具备自我描述的能力,无需再通过读取配置文件或者通过注解反射来实现对象和记录的映射关系。

  这也是本文设计的出发点,尽可能的通过对象自身的信息来完成映射转换,避免使用反射等低效高消耗的操作。

三 数据访问描述信息

  要赋予DAO对象自我描述的能力,需要设计一组可供采集信息的接口定义:

public interface DataAccessDescription {public String getTableName();public String[] getPrimaryKeyArray();public String[] getIndexArray();public Map<String, SqlData> getFieldMap();
}

  本文仅提供设计思路,并不完全实现所有功能,上述接口定义已经可以满足示意的需求。一个DAO对象可提供自身对应的表名,主键集合、索引集合以及表字段和DAO对象成员的映射关系,这些信息足以应对简单的CRUD操作。

  注意getFieldMap方法返回的Map对象中,其Value类型是SqlData,关于SqlData的说明见下文。

四 字段类型转换

  映射关系解决之后,还有一项非常重要的事情,如何将SQL字段值赋予对象成员。

  首先我们无法保证SQL返回的类型一定和成员类型匹配,比如说表字段为varchar类型,那么SQL返回的应该是String类型,而成员类型很可能是int。

  第二点,ORM在将记录转化为JAVA对象的时候,必然要对成员赋值,这就涉及到如何访问对象成员的问题,我相信大家的第一反应一定是反射,然而不通过反射我们一样可以做到。

  我们需要赋予DAO对象成员能够对SQL返回值自动转化并赋予成员的能力,定义接口SqlData来表示所有的DAO成员:

public interface SqlData {Object getData();void setSqlData(Object value);
}

  接口定义完毕后,需要对其进行实现,以满足DAO成员类型的需求,这里以String作为样例供大家参考:

public class SqlString implements SqlData {private String value = null;public SqlString() {this.value = null;}public SqlString(String value) {this.value = value;}public String getValue() {return value;}public void setValue(String value) {this.value = value;}public Object getData() {return this.value;}public void setSqlData(Object value) {if (value instanceof String) {setValue((String) value);} else {setValue(String.valueOf(value));}}
}

五 数据访问基类

  目前我们已经赋予了DAO自我描述,以及成员的自动转换赋值的能力,为了便于应用的使用,我们还需要在DAO基类中加入常规的CRUD操作,如此的话其定义如下:

public abstract class Dao implements DataAccessDescription {public boolean select() throws DataAccessException {return false;}public boolean update() throws DataAccessException {return false;}public boolean insert() throws DataAccessException {return false;}public boolean delete() throws DataAccessException  {return false;}
}

  注意所有的CRUD方法都可能抛出DataAccessException异常,它的介绍见下文。

六 统一异常处理

  为了便于异常处理,我们需要定义一个受检查的异常类型,用以描述所有归属与ORM框架的异常:

public class DataAccessException extends Exception {private static final long serialVersionUID = 1L;public DataAccessException() {super();}public DataAccessException(String message) {super(message);}public DataAccessException(Throwable t) {super(t);}public DataAccessException(String message, Throwable t) {super(message, t);}}

七 数据访问框架

  DAO相关设计基本上结束了,接下来需要处理和数据库交互的问题了。我们把所有和数据库交互的功能都封装在数据访问框架中,它仅关注向数据库提交的SQL指令:

public class DataAccessFramework implements ConnectionManager {private static final String DATA_SOURCE_TYPE = "type";private static class DataAccessFrameworkHolder {static DataAccessFramework daf = new DataAccessFramework();}public static DataAccessFramework getInstance() {return DataAccessFrameworkHolder.daf;}private DataAccessFramework() {}private DataSource dataSource = null;public Connection getConnection() throws DataAccessException {try {if (dataSource == null) {Properties properties = PropertiesUtils.loadProperties("ds.properties");dataSource = DataSourceFactory.getDataSource(properties.getProperty(DATA_SOURCE_TYPE), properties);}return dataSource.getConnection();} catch (Exception e) {throw new DataAccessException(e);}}public boolean execute(String command) throws DataAccessException {return execute(command, null);}/*** 用于提交非查询类SQL指令** @param command SQL指令* @param params  预编译指令参数* @return 执行结果* @throws DataAccessException*/public boolean execute(String command, Object[] params) throws DataAccessException {return execute(command, params, null) > 0 ? true : false;}/*** 用于提交查询类SQL指令** @param command SQL指令* @param params  预编译指令参数* @param rets    查询结果集* @return 当SQL指令为更新删除等操作时,返回值为数据库受影响的行数;当SLQ指令为查询语句时返回值为结果条目数* @throws DataAccessException*/public int execute(String command, Object[] params, Object[] rets) throws DataAccessException {Connection conn = getConnection();if (conn == null) {throw new DataAccessException("无法获取数据库连接");}PreparedStatement stmt = null;ResultSet rs = null;try {stmt = conn.prepareStatement(command);if (params.length > 0) {for (int i = 0; i < params.length; i++) {stmt.setObject(i + 1, params[i]);}}System.out.println(stmt);if (rets != null) {rs = stmt.executeQuery();int count = 0;while (rs.next()) {for (int i = 0; i < rets.length; i++) {rets[i] = rs.getObject(i + 1);}++count;}return count;} else {return stmt.executeUpdate();}} catch (SQLException e) {throw new DataAccessException(e);} finally {DataAccessUtils.close(conn, stmt, rs);}}
}

  实际上和数据库进行交互时,无非就三种场景需要考虑:

  1. 直接提交SQL指令
  2. 提交带有预编译参数的SQL指令
  3. 提交带有预编译参数的SQL查询指令

  所以数据访问框架的核心接口就三个execute方法。

  注意数据访问框架不仅需要解决与数据库交互的问题,还需要维护数据库连接,那么如果采用连接池组件,各类数据源的维护就变得复杂起来,所以在数据访问框架看来,它只关注DataSource,而DataSource实例的创建则被封装在DataSourceFactory内部,见下文。

八 数据源创建工厂

  为了支持不同的数据库连接池组件,以及各类数据源组件,我们将DataSource对象的创建过程封装起来,通过配置来应对不同的实现,这里以常见的连接池组件为例,示意实现过程如下:

public final class DataSourceFactory {private static final String DEFAULT_DATA_SOURCE = "default";private static final String C3P0_DATA_SOURCE = "c3p0";private static final String DRUID_DATA_SOURCE = "druid";private static final String HIKARI_DATA_SOURCE = "hikari";private DataSourceFactory() {}public static DataSource getDataSource(String dataSourceType, Map<?, ?> properties) throws DataAccessException {try {if (dataSourceType.equals(DEFAULT_DATA_SOURCE)) {return getDefaultDataSource(properties);} else if (dataSourceType.equals(C3P0_DATA_SOURCE)) {return getC3P0DataSource(properties);} else if (dataSourceType.equals(DRUID_DATA_SOURCE)) {return getDruidDataSource(properties);} else if (dataSourceType.equals(HIKARI_DATA_SOURCE)) {return getHikareDataSource(properties);} else {throw new DataAccessException("错误的数据源类型");}} catch (Exception e) {throw new DataAccessException("无法获取数据源");}}private static DataSource getDefaultDataSource(Map<?, ?> properties) {// TODO Auto-generated method stubreturn null;}private static DataSource getC3P0DataSource(Map<?, ?> properties) {// TODO Auto-generated method stubreturn null;}private static DataSource getDruidDataSource(Map<?, ?> properties) throws Exception {return DruidDataSourceFactory.createDataSource(properties);}private static DataSource getHikareDataSource(Map<?, ?> properties) {// TODO Auto-generated method stubreturn null;}
}

九 SQL指令解析

  现在DAO的映射描述问题,以及数据库交互问题都解决了,那么怎么将DAO对象的信息转变为SQL指令呢?我们根据常见的数据访问场景来封装一个SQL指令的解析器,由它来将一个DAO对象转变为不同访问操作的SQL指令:

public class SqlCommandParser {public static String getSelectCommand(String tableName, String[] queryFields, String[] paramFields) {StringBuilder builder = new StringBuilder("SELECT ");for (int i = 0; i < queryFields.length; i++) {builder.append(queryFields[i]);if (i + 1 < queryFields.length) {builder.append(",");}}builder.append(" FROM ").append(tableName).append(" WHERE ");for (int i = 0; i < paramFields.length; i++) {builder.append(paramFields[i]);builder.append("=?");if (i + 1 < paramFields.length) {builder.append(" AND ");}}return builder.toString();}public static String getInsertCommand(String tableName, String[] fieldNames) {StringBuilder builder = new StringBuilder("INSERT INTO " + tableName + " (");StringBuilder preparedParams = new StringBuilder();for (int i = 0; i < fieldNames.length; i++) {builder.append(fieldNames[i]);preparedParams.append("?");if (i + 1 < fieldNames.length) {builder.append(",");preparedParams.append(",");} else {builder.append(") VALUES (");builder.append(preparedParams.toString());builder.append(")");}}return builder.toString();}public static void main(String[] args) {String sql = getInsertCommand("TEST", new String[]{"field1", "field2", "field3"});System.out.println(sql);}
}

十 补充DAO接口实现

  现在所有的准备工作都完毕了,但是DAO基类中还有CRUD方法没有实现,这里就利用上述基础设施来实现它,以select和insert为例:

public abstract class Dao implements DataAccessDescription {public boolean select() throws DataAccessException {String[] primaryKeyArray = getPrimaryKeyArray();if (primaryKeyArray == null || primaryKeyArray.length == 0) {throw new DataAccessException("未设置主键");}Map<String, SqlData> fieldMap = getFieldMap();if (fieldMap == null || fieldMap.size() == 0) {throw new DataAccessException("未设置字段成员映射");}String command = SqlCommandParser.getSelectCommand(getTableName(),getFieldMap().keySet().toArray(new String[getFieldMap().size()]), primaryKeyArray);Object[] params = new Object[primaryKeyArray.length];for (int i = 0; i < primaryKeyArray.length; i++) {params[i] = fieldMap.get(primaryKeyArray[i]).getData();}return DataAccessFramework.getInstance().execute(command, params, fieldMap.values().toArray()) > 0;}public boolean update() {return false;}public boolean insert() throws DataAccessException {String[] primaryKeyArray = getPrimaryKeyArray();if (primaryKeyArray == null || primaryKeyArray.length == 0) {throw new DataAccessException("未设置主键");}String[] fieldNameArray = getFieldMap().keySet().toArray(new String[getFieldMap().size()]);Object[] fieldValueArray = new Object[getFieldMap().size()];for (int i = 0; i < fieldNameArray.length; i++) {fieldValueArray[i] = getFieldMap().get(fieldNameArray[i]).getData();}String command = SqlCommandParser.getInsertCommand(getTableName(), fieldNameArray);return DataAccessFramework.getInstance().execute(command, fieldValueArray);}public boolean delete() {return false;}
}

十一 测试

  所有开发工作完毕,进入最后的测试阶段,首先创建一个名为user的表,结构如下:

user表结构

  接下来创建一个与之关联的DAO:

public class User extends Dao {private SqlLong id;private SqlString uuid;private SqlString password;private SqlString nickname;private SqlString phone;private SqlDate creatime;private SqlDate updatime;public User() {this.id = new SqlLong();this.uuid = new SqlString();this.password = new SqlString();this.nickname = new SqlString();this.phone = new SqlString();this.creatime = new SqlDate();this.updatime = new SqlDate();}public long getId() {return id.getValue();}...//各种getter、setter,省略public String getTableName() {return "user";}public String[] getPrimaryKeyArray() {return new String[] { "id" };}public String[] getIndexArray() {return null;}public Map<String, SqlData> getFieldMap() {Map<String, SqlData> map = new HashMap<String, SqlData>();map.put("id", id);map.put("uuid", uuid);map.put("password", password);map.put("nickname", nickname);map.put("phone", phone);map.put("creatime", creatime);map.put("updatime", updatime);return map;}
}

  注意咯,所有的DAO成员类型都是SqlData,而且在构造时进行初始化,并且每个DAO类型都需要实现DataAccessDescription接口,以提供映射描述信息。

  再配置一下数据源相关的文件信息,在根目录下加入db.properties:

type=druid
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/***?useUnicode=true&amp;characterEncoding=utf-8&useSSL=false
username=root
password=***

  通过type指定了数据库连接池采用Druid,其他的则为数据库访问的必要信息。

  最后编写测试案例,我们创建一个user对象,将它插入到数据库中:

public class TestAccessUser {public static void main(String[] args) {User user = new User();user.setUuid(UUID.randomUUID().toString().replaceAll("-", ""));user.setNickname("test");user.setPassword("123");user.setPhone("13275186860");try {if (user.insert()) {System.out.println("插入成功");} else {System.out.println("插入失败");}} catch (DataAccessException e) {e.printStackTrace();}}
}

  编译执行结果如下:

user插入结果

  到数据库核对一下记录是否正确:

数据库记录

  在管理工具上的确看到了这条记录,还可以编写查询案例来测试一下是否能从DB查到数据并转换为User对象,这里不再详细说明了。

十二 其他问题

  一个非常简易的ORM框架就设计完毕了,这里没有用到任何表的配置文件,也没有用到任何反射技术,可以说执行效率绝对是可以保证的。

  但是这样一个简陋的ORM框架显然不能支撑一个大规模项目的需求,它还存在许多需要改进的地方:

  1. 支持批量的CRUD
  2. 完善的事务管理机制
  3. 支持部分字段的查询
  4. 支持自定义过滤条件的查询
  5. ……

  整个设计并没有花费我太多的时间,但是如何解决目前普遍存在的配置文件和注解问题困扰了我很久,其实任何设计都不需要多么复杂的技术实现,只要思路是正确的,用最简单易懂的代码实现就是完美的。

  希望这篇介绍能给朋友们一些设计思路吧。


http://chatgpt.dhexx.cn/article/1WlyQW7C.shtml

相关文章

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;…

io多路复用的原理和实现_IO多路复用机制详解

select&#xff0c;poll&#xff0c;epoll机制区别总结: 服务器端编程经常需要构造高性能的IO模型&#xff0c;常见的IO模型有四种&#xff1a; (1)同步阻塞IO(Blocking IO)&#xff1a;即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO)&#xff1a;默认创建的socket都是…