说说通用ORM框架的设计

关于这个项目

项目地址:https://github.com/phospher/SchoolEndORM

造一个轮子

ORM在软件工程里面算是一个非常成熟的领域了,几乎每个主流的编程语言,都能找到对应的ORM框架。从大的流派讲,有独立的ORM框架,比如Java的hibernate、MyBatis,.NET的Entity Framework,go的gorm;也有作为一个大解决方案框架里的一个部份,比如PHP里的Larvavel,Python里的django,go里的beego等等。从实用性角度讲,针对一个成熟的编程语言新建一个通用ORM框架,其实已经是一件性价比非常低的事情了,因为开源社区里很多大牛,在这门开发语言初期就会开始着手设计这门语言的ORM框架,并随着这门语言的发展不断完善。不过我造这个轮子的目的,只是为了大学毕业的论文有题目,并且希望能把大学学到的各种知识来个大总结,并非希望做一个生产级的ORM框架。当然,其实它也远达不到生产级ORM框架的水平。

为何选择C#

我一直觉得C#是我用过的最好的编程语言之一,不会像python、JS一样过分的灵活,导致如果同一个项目里成员水平参差很大的话,代码质量也会参差很大;也不会像Java一样,不提供该有的灵活度,导致需要各种设计模式为语言特性打补丁,虽然Java近几年在各种迅猛发展,语言特性也好了很多了,但比起C#,差距还是有的,比如我很喜欢的运行时泛型,和委托类型。

但其实哪怕是2011年初的那个时候,为C#造一个ORM轮子,依然是一个性价比很低的事情了。那个时候,NHibernate已经挺流行的了。是的,在那个年代,Java里的大火框架,很多都会有对应的.NET版本。但真正的大杀器,其实是那个时候刚发布不久的Linq和Entity Framework。微软亲自下场开发出来的ORM框架,加上Linq这种有点像SQL的语言特性,一下子就让各种项目拥抱这套ORM方案了。到了如今.net framework已经升级成了.net core了,Entity Framework在当前.NET体系下,依然非常流行。

回到为什么选择C#语言来开发这个问题上来,其实是个偶然,纯粹是因为当时的我刚好结束了一个为期半年的实习,而这个实习工作用的技术栈是.NET的,当时的我对.NET体系比较熟悉,仅此而已。当时我用的.NET版本还非常的老旧,所以很多后来C#的新语法特性,也没有用上。最近几年也仅仅为这个框架升级到了.net core,但并未对代码做太多的重写。

以接口构建模块间的依赖

像Java、C#、Go这些有明确接口类型定义的语言,开发者都会很喜欢使用接口,模块与模块间的依赖,大家也很喜欢基于接口来依赖,而不是直接依赖具体的实现。这种习惯,在Java领域,由于Spring的统治地位,甚至成为了事实标准。大量可能整个软件生命周期里,也仅有一个实现类的接口被定义出来。这种几乎跟多态不搭边的接口,其实真的很难让开发者确信基于接口来构建依赖有多少多少的好处。

测试友好的代码结构

所以在这里,我也不想谈那些虚无的灵活性。在我看来,模块间依赖接口而不是实现类,最大的好处,就是对单元测试友好了。单元测试的重要性,我相信是毋庸置疑的。尽管因为各种各样的原因,我自己也真的很少写单元测试,但在我看来,有没有完整的单元测试,依然是评价一个开源项目,和一个基础组件是否优秀的重要标准之一。 那为什么说依赖接口,是一个对单元测试非常友好的设计呢?

要回答这个问题,需要从优秀的单元测试需要具备哪些条件出发。优秀的单元测试需要具备的条件很多,比如必须简洁,能快速运行结束,一个测试用例只有一个Assert等等。但跟接口息息相关的特性,就是每个测试用例都需要有独立性。单元测试的独立性主要包含两个方面:

  • 测试用例仅测试当前被测试逻辑的正确性,不负责其他模块的正确性。
  • 测试用例是可以随时重复执行,且在代码不改动的情况下,应该每次运行的结果是一致的。这就要求测试用例不能依赖有状态的组件,比如文件系统、数据库、缓存等等。

但是众所周知,我们写的代码,不可能完全不依赖其他模块的代码,也很少能完全不存储数据。那假如说,我们对其他模块的依赖写成了如下代码那样(对有状态组件的依赖同理):

public class MyClass {
	public void doSomething() {
		// 一些逻辑
		
		OtherClass oc = new OtherClass();
		String result = oc.doOtherthing();
		
		// 另一些基于result结果的逻辑
	}
}

OtherClass是另一个模块的类,我们针对MyClass的doSomething()方法写的单元测试,仅为了测试这个方法自己的逻辑是否正确,默认是认为OtherClass的doOtherthing()方法逻辑是正确的。但万一doOtherting()方法有bug,返回的结果不符合我们预期呢?或者甚至doOtherting()方法还没有开发完呢?那我们写的单元测试的正确性,就完成依赖doOtherthing()方法的正确性了,严格上来说,这个就不是单元测试,这是集成测试了。有状态组件依赖也同理,如果doOtherthing()里里使用了数据库,数据库的数据又对逻辑有影响,那很有可能这个单元测试就没法重复执行了。

为了解决上述问题,我们可能更希望在单元测试阶段,由我们自己来实现一个简化版的OtherClass类,保证它的执行结果是符合我们预期,从而可以让我们更专注于验证MyClass类里的逻辑是否正确。这就是所谓的Mock类和Mock对象了,使用了Mock类以后,我们就可以把上面的代码稍作改动,以方便我们传入Mock对象:

public class MyClass {
	
	private OtherClass oc;

	public MyClass(OtherClass oc) {
		this.oc = oc;
	}

	public void doSomething() {
		// 一些逻辑
		
		String result = this.oc.doOtherthing();

		// 另一些基于result结果的逻辑
	}
}

感谢面向对象的继承机制,这个时候,只要我们的Mock类继承OtherClass类,把doOtherthing()方法重写掉,在新建MyClass对象的时候,往构造函数传入Mock对象,就可以把doOtherting()的逻辑,替换成我们想要的逻辑了。

不过,上述的做法并不是一个最佳实践,还有很大的优化空间。在面向对象设计里,有个流传已久的传说,叫“组合优于继承”,就是说,公共的逻辑,封装到另一个类里提供公共方法,比放在基类里提供公共方法更好。在面向对象设计的眼里,继承其实是一件很重的事,因为我们不知道,整个继承链路下来,有哪些像构造函数这种会被默认执行的逻辑,会影响到我们自己写的逻辑。但接口不一样,接口仅仅只是一个声明,它没有逻辑,哪怕在Java和C#都引入了接口默认方法,也仅允许编写无状态的方法,不会对实现接口的类有副作用影响。所以,上述的代码就又可以进一步优化:

public interface OtherInterface {
	String doOtherting();
}

public class MyClass {
	private OtheInterface oc;

	public MyClass(OtherInterface oc) {
		this.oc = oc;
	}

	public void doSomething() {
		// 一些逻辑

		String result = this.oc.doOtherting();

		// 另一些基于result结果的逻辑
	}
}

这个时候,我们的Mock类,就可以通过实现OhterInterface接口来达到模拟OtherClass逻辑的目的了,也不同担心OtherClass那些有状态的操作,可能会影响到我们的单元测试。来到这里,我们还可以更进一步,假如从业务逻辑上,OtherInterface要提供什么样的接口,是由我们来提需求的话,那我们还可以直接把OtherInterface接口纳入到我们的模块后中来,让实现OtherClass的团队实现我们的接口,为我们提供服务。这个就是所谓的依赖倒置了,这种情况在实际场景下是非常常见的。使用框架把OtherInterface的实际实现传入到MyClass的技术,就是依赖注入。

从上面的代码演进,我们可以看到,基于接口来构建模块间的依赖,是非常有利于单元测试的Mock实现的。单元测试是软件开发非常重要的一环,是保证我们代码质量的基石,也是我们重构代码的底气。

打造一个有生命力的框架

一个框架,要怎么才能在日新月异的技术浪潮里不被淘汰呢?Java领域的Spring可以给我们一个很好的参考。从早期的Spring MVC,到最近比较火的Spring Cloud,还有赶上时髦的Spring AI,Spring团队核心做的事情其实都是定义规范和制定使用组件的流程,具体干活的组件,其实都是可以被灵活替换的。Spring MVC可以替换模版引擎和授权组件;Spring Cloud能替换的组件就更多了,从服务注册发现、RPC调用组件到服务治理的各个模块,都可以替换。

这里给我最大的启示是,框架设计可以主要以定义规范和流程为主,各个规范只要在框架内提供一个默认实现即可。规范的具体落地形式就是形形色色的接口,流程就是把这些接口串联起来的程序逻辑。如果未来技术有了新的迭代更新,只要新增某个接口的具体实现即可,对框架主体是可以不影响的。

使用控制反转

要想做到框架的核心是规范和控制流程,保证各个模块的可扩展性,控制反转(IoC)是必不可少的。一般来说,我们的程序引入一个第三方库,是我们主动调用第三方库的接口的,由我们的程序逻辑去控制第三方库在什么时间执行。但控制反转不一样,它是由第三方库来控制什么时候调用我们实现的程序逻辑,比较典型的例子就是MVC框架,MVC框架是不需要我们主动是接收HTTP请求的,几乎都是由框架来接收HTTP请求,经过一些列转换后,由框架来决定调用哪个由我们自己实现的Controller。

所以,在这个ORM框架里,我也为ORM过程中需要使用到的实体,定义了一些规范(接口):

  • SessionFactory:创建Session的工厂
  • Session:主要用于做上下文管理、状态管理和事务管理等
  • Cache:管理缓存,由需要的话也可以作为保存缓存数据的容器
  • SQL:负责生成SQL给数据库执行

尽管框架代码中为上述规范都编写了默认实现,但只要新的实现类实现的逻辑符合这些接口的定义规范,其实这些默认实现都是可以被替换的。唯一不能替换的就是这些在框架内已经定义好的接口,还有使用这些接口的控制逻辑。这其实对规范的设计者是提出了很高要求的,需要对这个领域的知识有很深入的理解,设计出来的规范是能够适用未来很多年的技术发展,要不然过几年有新的技术诞生,发现设计的接口和流程没法支持新技术,那框架也就有可能被业界淘汰了。

事件模型的应用

框架的扩展性,除了体现在替换组件底层实现可以不影响框架主体结构以外,还有往框架里添加逻辑更加便捷。以OMR框架为例,CRUD的每个操作,都可能涉及非常多的不同操作,除非必须把请求推送给数据库以外,还可能有记录日志、生成主键、数据校验等,甚至随着功能的日益完善和需求的不断变更,更多意想不到的功能可能会被加进来。尽管上述的这些功能,都可以使用AOP实现,但在我看来,AOP通常是一个无可奈何的选择。因为AOP的逻辑,通常都不会体现的主流程的代码里,如果涉及到核心业务逻辑的代码放在了AOP上,那么对于代码结构不熟悉的开发者,想定位问题就会变得非常困难。

那么,想要解决这个问题,事件模型就是一个非常好的解决方案了。CRUD的每一个操作,我们都可以认为是一个事件,框架里定义出来每个事件的Event类和EventListener类,当有相应的事件被触发的时候(比如Select操作被触发了),就可以通过调用一个或多个EventListener来做具体的逻辑处理了。我的这个ORM框架里,除了基本的CRUD操作都有对应的事件以外,对事务提交操作和直接执行SQL的操作,都定义了事件。具体定义的事件类型如下:

  • SelectEvent和ISelectEventListener:处理查询数据的事件
  • UpdateEvent和IDeleteEventListener:处理删除数据的事件
  • UpdateEvent和IUpldateEventListener:处理更新数据的事件
  • UpdateEvent和IInsertEventListener:处理插入数据的事件
  • ActionEvent和ISubmitEventListener:处理提交事务的事件
  • CommandEvent和ICommentEventListner:处理直接执行SQL操作的事件

其中,ActionEvent是所有事件的基类,维护一些事件的通用属性。可以看到,增删改操作用了同一个事件类,其实这不是一个很好的设计,哪怕现阶段这几个操作的事件属性是一样的,但区分不同的类也是有必要的,不然后面如果某个操作的事件类需要增加属性,就是对框架定义的破坏性更新了。

框架里对每个事件监听器都提供了一个默认实现,也是最基本的实现,就是对数据库的增删改查。不过这里有点反直觉的是,这些事件监听器的默认实现,除了ICommandEventLIstener的默认实现以外,其他的监听器实现都是对缓存的操作,而实际对数据库的操作,都是在缓存里实现的。对于缓存相关的内容,后面章节会有更详细的描述。

到这里,差不多就是事件模型的全部了。事件模型为框架带来高可扩展性主要体现在,一个事件可以注册多个监听器,这样增加逻辑,既可以不破坏程序的基本框架,也不需要修改原有的代码,为后续扩展功能提供了便利,特别是针对多人开发的项目,不会轻易修改到他人的代码。不过不可否认的是,这样组织的代码结构,是变复杂了,可读性变差了不少。但我觉得,对于基础框架来说,为了程序的灵活性,适当牺牲可读性是可以接受的。框架程序的一个非常重要的特性就是稳定性,在演进的过程中,尽量不能对框架造成严重的破坏,加上现在稍微有点规模的开源项目,都是来自五湖四海的多个人协同了,保证程序的稳定真的非常重要。反观一般的业务逻辑代码,能方便地通过代码表达业务逻辑,提高代码的可读性,比使用各种花里胡哨的设计模式要实用。

用类图替代ER图

使用类的视角做CRUD

从上古时代的Hibernate开始,到现在非常流行的Spring Data JPA和Entity Framework,ORM框架都一直做着一个努力,就是让开发者可以不再关心数据库表结构,而是用类和对象的视角去做数据库的CRUD。我觉得这个努力算是完成了一半吧,单表的简单CRUD,基本上都能通过基于类的接口完成了,而这类操作也几乎占了日常数据CRUD需求的80%以上。但是对于复杂SQL的执行,无论什么ORM框架,依然比不上写原生SQL那样高效和容易维护,所以MyBatis这种“简陋”的ORM框架依然大行其道,各个ORM框架也依然允许开发者使用原生SQL。说过题外话,前文称呼Hibernate为上古时代的ORM框架,但也不意味着Hibernate就没人使用了,实际上现在Hibernate还在被广泛使用着,只是新生代的Java开发者,可能更多是通过Spring Data JPA间接地使用着Hibernate。

Hibernate曾经发明过一款类SQL语言,叫HQL,它可以基于类和类的属性查询数据库。但我觉得,随着编程语言的发展,这种方案其实已经不是那么适应时代了。就我个人来说,其实我会更喜欢C#的Linq和Mybatis-plus的labmda表达式方案,因为HQL的类型检查必须等到程序运行到那个地方才能做,如果属性名称或者类名称写错了,得把程序运行起来才能发现,而Linq和lambda表达式可以在编译期就做完类型检查,甚至能借助IDE的代码补全功能提升使用体验,这个无论对框架的使用者还是框架的开发者,都是一个极大的减负。不过这个项目毕竟不是需要投入生产的项目,为了炫技,我还是选择了使用类似HQL的方案,而下面将会从语法分析的角度,详细说下框架是怎么解析这个查询字符串的。

从编译原理里借鉴灵感

实现的这个ORM框架,并没有像HQL一样实现了那么完备的一个类SQL查询语言,它只是实现了一个简化版。它不支持指定返回的属性列表,不支持参数化查询等等。但至少,它允许开发者使用类的属性来编写查询,然后自动转换成表字段。比如一个类定义了StudentId属性映射到表里的student_id字段,则使用StudentId==1的查询语句,最终可以转换为student_id = 1的SQL语句。

要想达到上述的转换效果,简单的字符串替换肯定是不行的,别看举的例子简单,但实际场景非常复杂,简单的字符串替换很难顾及所有的情况。因此,ORM框架引入了编译原理里常用的词法分析和语法分析来提取查询语句里的元素,并转换成SQL语句。

用词法分析提取token

首先分享一下词法分析。词法分析的作用是把语句里的语法单元从语句里分离出来,这些语法单元在编译原理里一般成为token,每个token都是后续做语法分析的最小单元。提取token的过程没有什么特别的技巧,就是逐个循环语句里的字符,遇到不同类型的字符就进入相应的状态机,并提取下一个字符,直到满足状态机退出的条件,从而完成一个token的提取。这部分原理是经典的编译原理词法分析的内容,就不多做介绍了,编译原理相关的书籍有更详细和全面的讲解。在提取token的过程中,框架也会对token做个简单的分类,以便后续的语法分析。框架会对token做以下分类:

  • 属性:对应类的属性名称
  • 常量:比如true、false,数字1、2、3,字符串’abcd’等,主要用于判断条件的值
  • 运算符:比如==、>、<、&&、||等等

从运算符里可以看到,框架设计的查询语句语法,主要是沿用的C#的语法,同时由于不支持参数化查询,所以也并不支持提取变量。

说到这里,可能也有读者想到,是否可以用正则表达式来实现词法分析呢?这是一个很自然而然的想法,毕竟不是每个开发者都学过编译原理,但几乎每个开发者或多或少都会接触过正则表达式。我的想法是,不是说完全不可以,但这个正则表达式可能会很复杂,最终维护起来未必比传统词法分析的代码容易,在极端调优下也性能未必比传统词法分析快。正则表达式的好处是,它是声明式的,不像自己写词法分析那样是命令式的,这样开发者可以免去实现很多字符判断和状态机的细节,但词法分析的真正复杂度来自于语法的复杂度,如果查询语句支持的功能和特性很多,无论使用正则表达式,还是自己实现词法分析,都会同样复杂。至于两个方案哪个更优,可能不同人会有不同的选择,当时我选择自己实现词法分析,其实也是出于炫技,毕竟这是一个毕业论文,能用上大学里学过的理论知识,是一件很愉快的事情,毕竟工作了那么多年,编译原理的知识真没在实际生产项目中使用过。

语法分析在框架里的应用

自定义查询语句的最终归宿毕竟是生成对应的SQL语句,所以完成了词法分析以后,下一步便是基于提取出来的token生成SQL语句了。由于SQL里的where语句语法规则和C#里判断语句的语法规则都是用中序表达式表达的,所以在上述词法分析的过程中,会同步生成查询语句的中序表达式,生成SQL的过程中,只要遍历这个中序表达式,对不同类型的token做以下转换处理,就可以了:

token类型转换逻辑
属性转换成对应的表列明
常量无需转换
运算符转换为SQL对应的运算符,比如“==”转换成“=”、“&&”转换成“and”等等

语法分析模块针对一个查询语句,除了输出一个中序表达式,还输出了一个后序表达式。为什么需要一个后序表达式呢?因为框架里也支持缓存,默认实现的是一个本地内存缓存,查询请求会优先从缓存里获取对象,如果没获取到,才把查询请求发送给数据库查询。为了能在内存的缓存对象池里找到目标对象,就需要实际计算查询语句的结果了。后序表达式是一种天然适配程序逻辑计算公式的数据结构,只需要两个堆栈就可以计算出来一个带有各种括号嵌套的表达式。所以,在语法分析成功把查询语句构造出来中序表达式以后,框架就会同步基于中序表达式生成一个后序表达式,当在缓存中检索对象时,会先基于后序表达式,把对象属性的值带入到表达式中计算,返回最终计算结果为true的对象。到了这一步,已经使用到了一些语义分析的皮毛了,但也仅限到这里为止了。

可能我不会再复刻的方案

选择这个还需要应用到编译原理知识的查询数据方案,哪怕在当时的环境下,也是一个不再推荐的方案了。在那个时候的.NET体系下,Linq和Entity Framework也已经发布了,正如我前文所言,Linq能在编译期就做了很大一部份的正确性检查,是一个更高效率的解决方案,基于C#设计的ORM框架,是完全可以使用Linq设计数据查询方案的。当初使用这种有点像自定义DSL的方案,针对就是纯粹的炫技,在生成项目中,这种炫技是需要警惕的。

缓存是一把双刃剑

使用全局缓存提升查找性能

尽管这个只是一个实验性质的ORM框架,但我还是为这个框架设计了一个全局缓存,用来暂存那些被查询出来的对象。框架里提供的默认全局缓存是在内存里维护一个对象池。理论上说,对于频繁读的场景,全局缓存还是很能提升性能的,比如如果查询请求都落到了数据库中的话,可能会有大量的网络IO和磁盘IO,在做OR-Mapping的过程可能会使用到大量的反射,这些都可能成为性能瓶颈,而如果查询命中了全局缓存,这些消耗都能节省掉。

为了保证数据的一致性,对于写操作,框架都是优先把写操作的请求发送给数据库,但暂时不提交事务,待全局缓存也被成功更新后,再提交数据库的事务,如果中间的过程出现异常,则回滚数据库的事务,从而保证全局缓存里的数据跟数据库里的是一致的。对于内存缓存而言,故事也差不多到此为止了,尽管这样的方案其实也有非常非常极端的情况是,全局缓存被更新了,但数据库事务提交失败,也会导致数据不一致。不过对于内存数据来说,把程序重启一下,也算是解决了。但如果全局缓存使用了redis之类的缓存中间件,那其实还需要至少实现一套TCC机制,保证分布式事务的正确性的。

但是,在当前主流的程序架构上,特别是微服务环境下,本地内存缓存是完全不可接受的方案,因为很容易导致不同节点间的数据不一致。就是这个原因,mencache、redis这种专门的缓存中间件才被大量使用。Hibernate的缓存就很容易可以对接到mencache或redis上。不过当前这个ORM框架的全局缓存管理是写在一个静态累里的,这是一个很糟糕的设计,应该为全局缓存管理器设计一个接口,接口的对象使用单例模式管理,这样日后替换全局缓存的实现,比如接入redis,也会更加容易。

其实,缓存的管理是应该集成在ORM框架里,还是由开发者自己维护,其实是一个很争议的事情。当时写这个ORM框架的时候,我是觉得应该由框架处理的,这样可以减轻开发者的负担。但经过多年的实战经验,现在的我会更倾向由开发者自己维护缓存。最主要的一个原因是,缓存的管理策略,其实是跟业务场景息息相关的。如何设计缓存的key,从而可以快速定位到缓存的值?缓存的淘汰机制是什么?内存总是有限的,什么数据该缓存,什么数据不该缓存?这些问题的答案,都是依赖业务场景的。ORM框架如果要把上述的灵活性都兼顾了的话,接口或者配置的复杂度都会大大增加,那对于开发者来说,跟直接使用缓存组件来管理缓存有什么区别呢?毕竟对于各个语言,无论是内存缓存,还是缓存中间件,都有很成熟的组件支持的。

Session缓存实现不依赖数据库的事务

与Hibernate类似,这个ORM框架也支持了二级缓存。当开发者创建了一个新的Session对象以后,Session对象内部就会创建一个对象池,这个对象池每个Session都是独立的,也会随着Session的消亡而消亡。这是一种比较简单地模拟数据库事务的方案。如果开发者需要修改数据,数据会先从全局缓存加载的Session缓存,当然如果全局缓存也没有,数据先从数据库加载到全局缓存,再进一步加载的Session缓存,然后才修改的Session的数据。每个Session调用修改数据的接口,直接修改的也仅仅是各自Session缓存了的数据,在最终提交事务之前,不会修改全局缓存,更不会修改数据库,待开发者提交事务时,才会执行修改数据库和全局缓存的流程。

可以看到,上述的过程实现了数据库事务的两个特性:

  1. 同一事务内的修改即时可见:因为写数据的请求都会先作用在Session缓存上,而下一次读这个数据,也会读取的Session缓存里的数据,毕竟数据已经被加载到Session缓存了;
  2. 不同事务间的修改不可见:因为在最终提交事务前,修改的都仅仅是Session缓存的数据,等于说每次修改都只是修改的各个Session自己的数据副本,各个Session也就当然感受不到其他Session的数据修改了。

这是这个挺典型的可重复读的事务隔离机制,但也仅停留于此了,这种方案也没法像正常数据库一样可以让用户自由选择事务隔离级别。生产级的ORM框架,默认的事务管理机制就是把ORM框架里的事务,与数据库事务绑定,这是一种既能实现完备的事务管理,又能最小化开发复杂度的方案。除了传统的ACID的事务管理模型,随着微服务的流行和各种中间件的广泛使用,分布式事务也日益被广泛关注,但分布式事务管理的模型就是完全的另一套模型了,当前主流的ORM框架对事务管理业主要集中在ACID模型,分布式事务的管理业界已经有更完备的框架了,这也是术业有专攻吧。

有状态还是无状态

最后想聊一下的关于ORM框架的问题,就是关于Session了。在设计之初,其实我很犹豫是否要把Session的概念拋给框架的使用者,毕竟其实对于大多数使用数据库的场景,其实也就执行一条SQL,对于开发者最方便的方式,就是调用一个接口,开启一个数据库连接,执行完SQL以后,数据库连接自动关闭。不过调研了不少ORM框架,包括Java的Hibernate、MyBatis,.NET的Entity Framework等等,他们都并未把Session的概念完全向开发者屏蔽,所以我开始思考Session的必要性。

通俗来说,ORM框架的Session,可以理解为执行SQL语句的上下文,主要管理的内容是数据库的Connection对象和事务,当然在这个ORM框架上,还有Session缓存。让开发者自己管理事务的需求是不言而喻的,毕竟不是所有的业务逻辑都只是执行一条SQL语句,如果涉及多条SQL语句,为了保持数据一致性,提供接口给开发者启动事务、提交事务和回滚事务是必须的,而为了让开发者可以自主控制事务,一个有状态的上下文对象,也是必不可少的。其实,数据库的使用,本身就是一个有状态的过程,用一系列无状态的接口封装一个有状态的过程,必定会丢失很多功能。

但正如上文所说的,很多场景其实也就执行一条SQL,如果每次使用都必须先创建Session,使用完以后也必须记得关闭Session,那么使用起来也挺不方便的。所以Spring Data JPA、Mybatis-Spring就通过Proxy对象的方式,把Session的创建和关闭逻辑封装起来,让开发者可以像调用无状态接口一样使用Dao,这也不失为一种不错的解决方案,但必须把ORM框架置于一个更大的框架里面才能实现,比如python里的django orm。

它不完善,但足够炫技

总的来说,这个ORM框架仅仅是一个面临毕业的大学生的毕业设计题目,当时的我,无论是时间还是经验,都没法开发出一个生产级的ORM框架,当然,哪怕是十年后的我,也没有这种能力和经历,或许在ORM这个已经如此成熟的领域,为一门成熟语言开发一个好用的ORM框架,本身就不是一件可以单打独斗完成的事情了。但至少这个ORM框架足够炫技,我觉得作为一个毕业设计作品,它能体现出来不少在大学里学到的知识,比如设计模式、依赖注入、编译原理等等。写这个框架的时候,对于每一行代码,我也是深思熟虑的,力求让它看起来尽可能的没,惭愧的是,工作了十年间,我基本上没有再这样写过代码了。可以说,这个项目是我这十多年程序员经历里,运用到了最多最复杂计算机基础知识的,毕竟工作中写的代码,往往会追求使用最高效的方式实现复杂的业务逻辑,不太可能如此大规模的造轮子了。其实我个人是很喜欢造轮子的,这也应该是每个程序员的爱好,只是很多时候必须向生活低头,希望我不再需要为生活烦恼的时候,也能痛快地写些自己喜欢的代码吧!

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值