关于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的原因莫过于这么几个:

if...elif...else...

但是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%的系统。

参考资料: