了解我的人都知道, 本人一直非常排斥 ORM 框架, 由于对象关系阻抗不匹配, 一直觉得它没有什么用, 操作数据库最好的手段是 sql+动态语言. 但这两年想法有了重大改变.

2013 年用 js 实践过一个 GUI 的开发, 结论是对于软件工程来说, 静态类型是必须的.

但在数据库方面我却一直回避了这个问题, 实际上这个问题在数据库的交互中同样存在. 数据库的 scheme 可以认为是静态的, 不惟表结构, 随表结构产生的视图等, 其实都是静态的. 所以说数据库是动态语言或者动态VM, 这个说法是错误的. 但是静态的数据库却提供了一种动态手段, 这就是即兴化的查询. 关系运算可以打破静态结构提供一次性的 scheme, 这是一个特别的现象. 数据库在静态类型的基础上又提供了动态类型————当然, 关系运算的设计者并没有考虑这些, 关系和关系运算, 是关系型数据库的全部出发点.

静态类型适合工程化, 动态类型也有不可或缺的价值, 我们看到, 如果从动态类型静态类型的角度来考察, 关系型数据库协调的很好, 我们甚至意识不到问题的存在. 我们可以像画静态图一样画 ER 图, 又能在图的基础上即兴的给出查询, 想要几个字段就要几个字段, 甚至支持运算列. 而这些即兴的查询随时可以建一个视图把它们固化下来, 变成静态类型.

数据库字段一旦有变动, 相关的视图会失效, 这时数据库会提醒受到影响的数据库对象. 如果 IDE 得当, 可以很容易实现重构数据库字段同时重构相应视图, 即使不支持复杂的重构行为, 如分拆表, 至少可以列出引用者, 确定受影响的范围再行重构.

那么, 数据库很完美我们不要用编程语言如何?

也不尽然, 数据库的 scheme 对于编程语言来说并不完美, 如果我们把表看做类的话, 数据库里类的级别很高, 不支持一些即兴的如匿名内部类这样的东西. 不过这也是一个方向, 有人甚至基于数据库做操作系统.

现在我们回到 ORM 问题.

可能你已经意识到我在想什么了.

面向对象编程语言和关系型数据库虽然位于两个领域, 但在静态类型上, 它们是可以对应的, 这也就是 ORM 框架可以适配的地方. 但是, 我们经常在程序里放即兴的 SQL 代码, 这些代码都会面临阻抗失配的问题:

  1. 首先这种即兴的关系运算的结果没有对应的类. 在 Java 实践中通常用 Map 来表达, 这种蹩脚的表达远不如动态语言方便
  2. 一旦发生数据库重构, 这些 SQL 仅仅是字符串, 重构时这些 SQL 无法被 IDE 覆盖, 甚至在编译时都无法抛出错误.

在我以往的认知里 ORM 只是增删改方便, 对 SQL 查询很不适用. 故 MyBatis 大大优于 hibernate, 而 d2js 大大优于 MyBatis. groovy sql 则在 d2js 和 mybatis 之间, 而 ORM 带来的好处可以忽略不计. 最近两年我发现这个认知有很大问题, 关键就在上面的第 2 点. 静态类型的好处不光是增删改方便, 重要的是只有静态类型适合工程化, 所以 ORM 不是削足适履, 而是必须的.

遗憾的是 ORM 在应对即兴的 SQL 方面仍然有欠缺, 并且流行的解决方案都没有处理好这个问题.

  1. MyBatis 通过 XML 写 SQL, 导致 SQL 和业务场景分离, 代码跳来跳去, 更难工程化
  2. 也有通过注解写 SQL 的, 解决了场景割裂的问题, 如 JPA 等, MyBatis 好像也支持了, 新版的 Java 有了文档字符串支持多行了, 似乎好用起来了, 但同样也无法避免上面谈到的问题
  3. JPA 提出了一种自己的 SQL, Hibernate 也搞了一个 HQL, 这可以说毫无意义, 首先, 这些 SQL 还是字符串, 同样面临重构问题, 其次, 数据库的 SQL 功能远比这些低级 SQL 强, 反过来如果数据库比它弱它也映射不过去. 比如现在 pg 有一个 pgvector, Hibernate 要把它包进去难度可想而知
  4. 还有一些 ORM 搞起了 reactive, 号称 DSL, 像这样 .select().from() ... 这种甚至不是 SQL, 表达关系运算捉襟见肘, 更无法复制到 SQL Console 执行, 大大增加了迭代难度
  5. ORM 自己搞的 SQL 或 reactive dsl 的学习成本进一步降低了开发效率, 除了显示开发者会搞 SQL Parser 没有别的价值
  6. DLINQ 风格, DLINQ 解决了重构问题, 但 DLINQ 支持的 SQL 毕竟是有限的, 同样无法发挥数据库 SQL 的全部功能

本文提出一点新思路:

方案 1. 在 ORM 基础上使用动态语言搞即兴查询, 例如 groovy, 我试过这个方案, 开发效率很高, 和 d2js 接近, 当然这个方案同样面临重构的困境, 毕竟 SQL 仍然是字符串, 但它和数据库的开发方式最像, ORM 对应静态类型, groovy 可以以动态类型写即兴查询, 有必要固化下来 groovy 又可以把动态类型转为静态.

方案 2. 加强静态语言, 为每个即兴查询创造相应的静态类型. 这涉及到一个工序的调整, 不能按现在的 MyBatis 等方式开发. 开发过程应当是这样, 编写完 SQL 后, 粘贴到 Java 代码前, 使用某些数据库手段, 如 EXPLAIN 等, 得出所有关联的字段, 再通过 ORM 逆映射, 得到一个即兴查询类. 上述"使用某些数据库手段, 如 EXPLAIN 等, 得出所有关联的字段, 再通过 ORM 逆映射, 得到一个即兴查询类"可以作为一个 IDE 小工具提供.

这种类可以是这样

class OrderByUserIdQuery extends Query{
class Params{
Object[] placeHolders = [User.PlaceHolder, Order.PlaceHolder];
int userId = placeHodlers[0].id;
getUserId, setUserId...
String orderNum = placeHolders[1].orderNum;
getOrderNum, setOrderNum...
}
class Result{
Object[] placeHolders = [Order.PlaceHolder];
Date created = placeHolders[0].created;
getter; setter;
Date expired = placeHolders[1].expired;
getter; setter;
} String sql = "select o.created, o.expired from order o, user u where o.user = u.id and u.id = :userId and order_num = :orderNum"; @Override
Class getResultClass = Result.class;
@Override
Class getParamsClass = Params.class;
}

这个方案较为完美, 只需要调整一下工作流做一个小工具就可以————我发现这和 DataSet 设计器做的事情很像. 这个方案唯一的缺陷是这些代码毕竟看起来就很累.

如进一步吸收 d2js 的特色, 这个方案可进一步升级

sql{.
select o.created, o.expired from order o, user u where o.user = u.id and u.id = :userId and order_num = :orderNum
.}

这里 IDE 应通过分析 SQL 识别出 SQL 中可以关联到 OO 的相关 class 及 field. 这个策略对 groovy 的方案同样有效.

刚才分析的是即兴查询问题, ORM 方案还有两个具体问题:

  1. 数据库字段和 OO 语言数据类型的适配
  2. 数据库外键和 OO 语言的适配

第一个问题现在往往搞一堆注解来解决, 但以注解对应数据库的字段很不 OO, 例如在字段里, NUMBER(64) 构成一个类型, 而在Java里都用 int 去对应. 我的想法是应当全盘类型化.

什么意思? 看代码:

interface Column{
boolean nullable();
boolean primary();
void validate() throws ValidationError;
}
class Number extends java.lang.Number implements Column{
int pricision;
}
class Number64 extends Number{
pricision = 64;
}

可能有人要问了, Number(N) 中 N 是可变的, 难道要搞无穷无尽的 Number? 这就多虑了, N 固然是无限的, 但一个数据库中有哪些是有限的, 框架提供几个常用的, 没有提供的项目自己补充就好了.

当然即使如此上面的 nullable 和 primary 也是行不通的, 声明字段提供的是类型而非实例, Column 的实例才有 nullable 和 primary. 如改为静态方法, 静态方法又不支持覆盖. 所以只能

PrimaryKey<Nullable<Number64>> userId

这里一个可能的解决方案是即兴类, 但无疑 Java 并不支持————Java 的匿名类只能定义在实例部分, 除了动态语言比如 ruby, scala kotlin 等等都不支持这种科幻特性.

即兴类退一步的版本是为每个字段定义为一个类, 这个方案的好处是很容易引用字段名, 对工程化非常有用. 例如上面的 sql 语句, 假如每个字段都是一个类型名, 识别起来就极为简单了, 用起来也不费劲. 如上面的

sql{.
select o.created, o.expired from order o, user u where o.user = u.id and u.id = :userId and order_num = :orderNum
.}

假如 User.Created 是一个类型, 则从 sql 中的 o.created 很容易就定位到该字段了, 对工程化非常有利.

设想一下它的形态大致是这样:

class User extends Entity{
class Id extends Integer implements PrimaryKey, Serial;
class Name extends VarChar64 implements Nullable{
@Override void valiate() throws ValidationError { }
}
public Id id;
public Name name;
}

这也很有意思. 当然, 这种代码在 Java 里就只能生成了.

第二个问题似乎框架都解决的不错了, JPA 也给了一些注解方案, 凡是支持 JPA 必然支持外键关联, 我就不多废话了.

我们需要什么样的 ORM 框架的更多相关文章

  1. ASP.NET MVC 使用 Petapoco 微型ORM框架+NpgSql驱动连接 PostgreSQL数据库

    前段时间在园子里看到了小蝶惊鸿 发布的有关绿色版的Linux.NET——“Jws.Mono”.由于我对.Net程序跑在Linux上非常感兴趣,自己也看了一些有关mono的资料,但是一直没有时间抽出时间 ...

  2. 最好的5个Android ORM框架

    在开发Android应用时,保存数据有这么几个方式, 一个是本地保存,一个是放在后台(提供API接口),还有一个是放在开放云服务上(如 SyncAdapter 会是一个不错的选择). 对于第一种方式, ...

  3. [Android]Android端ORM框架——RapidORM(v2.1)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/6020412.html [Android]Android端ORM ...

  4. [Android]Android端ORM框架——RapidORM(v2.0)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5626716.html [Android]Android端ORM ...

  5. 轻量级ORM框架——第一篇:Dapper快速学习

    我们都知道ORM全称叫做Object Relationship Mapper,也就是可以用object来map我们的db,而且市面上的orm框架有很多,其中有一个框架 叫做dapper,而且被称为th ...

  6. ORM之殇,我们需要什么样的ORM框架?

    最近在研究ORM,究竟什么样的框架才是我们想要的 开发框架的意义在于 开发更标准,更统一,不会因为不同人写的代码不一样 开发效率更高,无需重新造轮子,重复无用的代码,同时简化开发流程 运行效率得到控制 ...

  7. 轻量级ORM框架初探-Dapper与PetaPoco的基本使用

    一.EntityFramework EF是传统的ORM框架,也是一个比较重量级的ORM框架.这里仍然使用EF的原因在于为了突出轻量级ORM框架的性能,所谓有对比才有更优的选择. 1.1 准备一张数据库 ...

  8. ORM框架示例及查询测试,上首页修改版(11种框架)

    继上次ORM之殇,我们需要什么样的ORM框架? 整理了11个ORM框架测试示例,通过示例代码和结果,能很容易了解各种框架的特性,优缺点,排名不分先后 EF PDF XCODE CRL NHiberna ...

  9. [Android]Android端ORM框架——RapidORM(v1.0)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4748077.html  Android上主流的ORM框架有很多 ...

  10. 吉特仓库管理系统-ORM框架的使用

    最近在园子里面连续看到几篇关于ORM的文章,其中有两个印象比较深刻<<SqliteSugar>>,另外一篇文章是<<我的开发框架之ORM框架>>, 第一 ...

随机推荐

  1. Android Qcom USB Driver学习(十四)

    UDC-Gadget UDC:(USB Device Controller)用于管理和控制USB设备与主机之间的通信. Gadget:Android在此层实现了adb,mtp(Media Transf ...

  2. USB 端点和管道的区别

    在USB体系架构中,经常会混用USB端点和USB管道的概念,包括本人也经常混用.但严格来说它们是两个不同的概念,具体表现在: 端点是USB设备端的概念,是真实的特理设备上的概念,其特性是通过端点描述符 ...

  3. BPF BTF 详解

    1. 介绍 BTF(BPF Type Format)是内嵌在BPF(Berkeley Packet Filter)程序中的数据结构描述信息.BPF原本是用于数据包过滤的编程语言,但随着eBPF(ext ...

  4. 4.1 数列的概念2 (递推公式、前n项和)

    \({\color{Red}{欢迎到学科网下载资料学习 }}\) [ [基础过关系列]高二数学同步精品讲义与分层练习(人教A版2019)] ( https://www.zxxk.com/docpack ...

  5. Java日期时间API系列24-----Jdk8中java.time包中的新的日期时间API类,MonthDay类源码和应用,对比相同月日时间。

    Java8中为月日新增了类MonthDay,可以用来处理生日,节日.纪念日和星座等周期性问题. 1.MonthDay 特别需要注意的:它的默认打印格式会带前缀"--" ,比如--1 ...

  6. 墨天轮沙龙 | 北京大学李文杰:面向知识图谱应用的图数据库系统gStore

    在6月8日举办的[墨天轮数据库沙龙第七期-开源生态专场]中,北京大学重庆大数据研究院图数据库与知识图谱实验室副主任.北京大学王选计算机研究所全职博士后 李文杰老师分享了<面向知识图谱应用的图数据 ...

  7. js递归遍历树形结构数据,获取所有数组id集合

    function getAllIds(tree, result) { //遍历树 获取id数组 for (const i in tree) { result.push(tree[i].id); // ...

  8. 怎么理解vue的单向数据流

    单向数据流是父组件传给子组件的数据,子组件没有权利修改,只能委托父组件修改,然后子组件更新

  9. .NET高级调试 - 3.7对象检查

    简介 在大多数调试会话中,首先需要检查的项目就是分析应用程序的状态.在确认程序的问题是某种无效状态造成的,我们便需要分析程序是如何变成无效状态的.那么在分析过程中,需要为我们深入了解对象的各种审查方法 ...

  10. 云原生周刊:Istio 1.19 发布 | 2023.9.11

    开源项目推荐 Timoni Timoni 是 Kubernetes 的软件包管理器,由 CUE 提供支持,灵感来自 Helm. Timoni 项目致力于改善编写 Kubernetes 配置的用户体验. ...