关于ORM的思考

ORM,全称是Object-relational mapping,即对象关系映射,借助ORM可以把类和数据库表(schema)对应起来。

我想大家手头上都不会少了ORM这个工具,借助ORM,我们可以以面向对象的方式来写SQL,例如:

session.query(User).where(cls.id == 1).order_by(id.desc()).first()

就相当于

SELECT * FROM user WHERE id == 1 ORDER BY id DESC LIMIT 1

对于SQL生手来说,ORM的方式更容易让人理解。

使用ORM的原因莫过于这么几个:

  • 跨数据库,写一遍ORM查询便可以在多个数据库间切换
  • 易于维护,毕竟ORM的查询语句看起来更像是自然语言
  • 一般ORM都会自动帮我们做好防注入处理,只要按照ORM规定的方式写,我们就不用自己操心注入的事情
  • 可以动态的更新查询条件,即根据不同业务场景 if...elif...else... 追加查询条件,如果用SQL写,就比较麻烦
  • 可以以类的方式写数据库,那么也就意味着,可以用继承或者mixin的方式,把所有表中共有的属性,放在基类里

但是ORM也有很大的弊端,那就是:

  • 跨数据库对于大多数系统来说都是伪需求,选定一个数据库之后,你真的会换一个数据库吗?
  • 跨数据库也没有能够完美的跨数据库,例如SQLAlchemy中,对于 UPDATE/DELETE/INSERT 语句,只有 MS SQL Server 支持 with_hint 操作,而 MySQL/PG/SQLite 都只能使用 prefix_with,但是如果你想要对 UPDATE/DELETE 使用数据库索引提示,则是无法实现的,只能 裸写SQL。也就是说,ORM虽然号称跨数据库,其实效果并不好,很多数据库的细节并不能覆盖。
  • 生成的SQL不够明确,而且通常比较长。例如上面的查询,ORM会生成 SELECT user.id, user.name, user.age ... 等,把 user 的所有属性 都查出来,当然,我们手写的 SELECT * 也差不多,这都属于浪费行为,对于一个小型数据库系统,这当然是没有问题,但是数据量一旦达到 亿行的级别,浪费就比较大了。
  • 高级一些的SQL或者对性能要求较高的SQL无法准确表述
  • 写久了ORM,SQL忘的又快又光,这可以算得上是一个使用ORM的弊端了

所以其实我们需要的是一个能轻松防注入,能自行映射类和数据库表,能轻松写SQL的工具,Python中,SQLAlchemy仍然是首选,Golang中 我倒是认为sqlx能胜任。

一个ORM的错误用法

我经常在ORM中这样写:

class BaseMixin:
    id = Column(Integer, primary=True)
    created_at = Column(Datetime, index=True)
    updated_at = Column(Datetime, index=True)
    deleted_at = Column(Datetime, index=True)

Go中也是类似的。对于数据量小的系统来说,上面的写法无所谓,但是对于数据量大的系统来说,每一个地方都可能是性能瓶颈。上面的写法 有这么几个缺点:

  • created_at 不应该加索引,对于没有水平分表的系统来说,id是连续自增的(即便分了,也是自增的,不过这个时候就要根据具体场景 来判断是否可以这么做了),因此不需要多加 created_at 这个索引
  • deleted_at 的类型不应该是时间,且不应该单独加索引,deleted_at 一般是用来做软删除,记录删除时间是没有必要的,因为有 updated_at,而将其改为 Booleanl 类型之后,索引更是没有必要,因为区分度特别低,不是True就是False。正确的方式应该是根据 查询条件,结合前置条件做联合索引。
  • 不应该使用 Mixin 或者继承来写基类,不过前两点改造完成之后,倒也的确能够适用于99%的系统。

参考资料:


更多文章
  • 《计算机网络-系统方法》读书笔记
  • Y Combinator《如何创业》笔记
  • Go类型嵌套
  • etcd源码阅读与分析(五):mvcc
  • etcd源码阅读与分析(四):lease
  • 干了这碗叔本华牌毒鸡汤 --- 《人生的智慧》
  • Memory leak in net/http
  • etcd源码阅读与分析(三):wal
  • etcd源码阅读与分析(二):raft
  • etcd源码阅读与分析(一):raftexample