30、综合实战:从零构建一个迷你ORM框架

终于到了最后一章。说实话,前面讲了那么多封装、继承、多态、设计模式、元类,如果你觉得有点散,那这一章就是把这些珠子串起来的那根线。

我们要干一件什么事呢?从零写一个迷你ORM框架。ORM,全称Object-Relational Mapping,说白了就是把数据库表映射成Python对象。你操作对象,就等于操作数据库。Django的ORM、SQLAlchemy,底层核心思想都差不多。

我个人习惯,学一个新东西,一定要自己动手造一个轮子。哪怕很简陋,但造完之后,再看那些工业级框架,你就能看懂它们为什么要那么设计了。

30.1 整体架构设计

先想清楚我们要做什么。一个迷你ORM,至少要有这几个能力:

  • 定义模型类,自动映射到数据库表
  • 支持增删改查(CRUD)操作
  • 字段类型自动校验
  • 查询结果自动封装成对象

嗯,这里要注意,我们不搞复杂的关联查询、懒加载那些。核心是让你看清框架的骨架。

我画了一张架构图,你看一眼就明白了:

迷你ORM框架架构图 用户代码(定义模型、调用CRUD) Model 基类(元类驱动) 自动生成表名、字段映射、CRUD方法 字段描述符(Field / IntegerField / CharField) 类型校验、默认值、约束检查 数据库适配器(SQLite / MySQL) 执行SQL、返回结果集

你看,从上到下四层。用户只管定义模型,底层自动帮你拼SQL、做映射。这就是封装的力量。

30.2 元类:自动注册模型

先解决第一个问题:怎么让一个类自动知道它对应哪张表?

用元类。我在项目中遇到过很多次,元类最适合干这种「类创建时自动做点手脚」的事。

class ModelMeta(type):
    def __new__(cls, name, bases, attrs):
        # 跳过 Model 基类本身
        if name == 'Model':
            return super().__new__(cls, name, bases, attrs)
        
        # 自动生成表名:类名小写 + 's'
        table_name = name.lower() + 's'
        attrs['__table__'] = table_name
        
        # 收集字段信息
        fields = {}
        for key, value in attrs.items():
            if isinstance(value, Field):
                fields[key] = value
        attrs['__fields__'] = fields
        
        return super().__new__(cls, name, bases, attrs)

这段代码干了三件事:

  • 跳过基类,只处理用户定义的模型类
  • 自动生成表名(比如 User → users)
  • 收集所有 Field 类型的属性,存到 __fields__ 里

你想想看,如果没有元类,你得在每个模型类里手动写 table_name 和 fields 字典。有了元类,这些脏活累活全自动了。

30.3 字段描述符:类型校验

接下来是字段。每个字段都要能校验类型、处理默认值。这里用描述符(Descriptor)最合适。

class Field:
    def __init__(self, default=None, nullable=True):
        self.default = default
        self.nullable = nullable
        self.name = None  # 会在元类中设置
    
    def __set_name__(self, owner, name):
        self.name = name
    
    def validate(self, value):
        raise NotImplementedError

class IntegerField(Field):
    def validate(self, value):
        if value is None and self.nullable:
            return
        if not isinstance(value, int):
            raise TypeError(f'{self.name} 必须是整数,收到 {type(value).__name__}')
        return value

class CharField(Field):
    def __init__(self, max_length=255, **kwargs):
        super().__init__(**kwargs)
        self.max_length = max_length
    
    def validate(self, value):
        if value is None and self.nullable:
            return
        if not isinstance(value, str):
            raise TypeError(f'{self.name} 必须是字符串')
        if len(value) > self.max_length:
            raise ValueError(f'{self.name} 长度不能超过 {self.max_length}')
        return value

这里有个小细节:__set_name__ 是 Python 3.6 引入的,描述符可以自动知道自己在类里的属性名。我以前用旧写法还得手动传 name 参数,现在省事多了。

30.4 Model 基类:封装 CRUD

有了元类和字段,Model 基类就水到渠成了。它负责提供 save、delete、find 等方法。

class Model(metaclass=ModelMeta):
    def __init__(self, **kwargs):
        for name, field in self.__fields__.items():
            value = kwargs.get(name, field.default)
            setattr(self, name, value)
    
    def save(self):
        # 拼 INSERT 或 UPDATE SQL
        fields = list(self.__fields__.keys())
        values = [getattr(self, name) for name in fields]
        
        if self.pk is None:
            # INSERT
            placeholders = ', '.join(['?' for _ in fields])
            columns = ', '.join(fields)
            sql = f"INSERT INTO {self.__table__} ({columns}) VALUES ({placeholders})"
            cursor = db.execute(sql, values)
            self.pk = cursor.lastrowid
        else:
            # UPDATE
            set_clause = ', '.join([f"{name}=?" for name in fields])
            sql = f"UPDATE {self.__table__} SET {set_clause} WHERE id=?"
            db.execute(sql, values + [self.pk])
    
    @classmethod
    def find(cls, **kwargs):
        # 拼 SELECT SQL
        conditions = ' AND '.join([f"{k}=?" for k in kwargs.keys()])
        sql = f"SELECT * FROM {cls.__table__} WHERE {conditions}"
        rows = db.fetchall(sql, list(kwargs.values()))
        return [cls(**row) for row in rows]

注意看 save 方法里的 pk 判断。如果主键是 None,说明是新记录,执行 INSERT;否则执行 UPDATE。这就是多态的一种体现——同一个方法,根据对象状态走不同逻辑。

核心思想:用户只需要操作 Python 对象,完全不用写 SQL。这就是 ORM 的终极目标——让数据库操作变得像操作普通对象一样自然。

30.5 使用示例

来看看最终效果:

class User(Model):
    id = IntegerField(default=None, nullable=False)
    name = CharField(max_length=50)
    age = IntegerField(default=0)

# 创建用户
user = User(name='张三', age=25)
user.save()  # 自动 INSERT

# 查询用户
users = User.find(name='张三')
for u in users:
    print(u.name, u.age)
    u.age = 26
    u.save()  # 自动 UPDATE

是不是很清爽?你完全感觉不到 SQL 的存在。这就是封装的力量——把复杂性藏在内部,对外只暴露简洁的接口。

30.6 避坑指南

我曾经在项目里用自己写的 ORM 踩过一个坑:没有处理连接池。每次 save 都新建数据库连接,高并发下直接打崩了数据库。后来加了个简单的连接池,问题才解决。

还有一点要注意:字段校验一定要在 setattr 时做,而不是在 save 时。否则你存了一个非法数据到对象里,等到 save 才发现,调试起来很痛苦。

小技巧:可以在 Field 描述符的 __set__ 方法里直接调用 validate,这样赋值时就校验,早发现早解决。

30.7 设计模式回顾

这个迷你 ORM 里用到了哪些设计模式?我帮你梳理一下:

设计模式 应用位置 作用
模板方法 Model.save() 定义 save 的骨架,子类只需提供字段
工厂方法 Model.find() 根据查询结果创建对象实例
策略模式 字段校验 不同字段类型有不同的校验策略
元类模式 ModelMeta 在类创建时自动注入表名和字段信息

你看,设计模式不是纸上谈兵。在实际框架里,它们无处不在。

30.8 总结

这一章,我们从零构建了一个迷你 ORM 框架。虽然简陋,但五脏俱全。你看到了:

  • 元类如何自动注册模型信息
  • 描述符如何做字段校验
  • 封装如何隐藏 SQL 细节
  • 多态如何让 save 方法自适应 INSERT/UPDATE
  • 设计模式如何让代码更灵活

说实话,能坚持看到这里,你已经超越了绝大多数人。面向对象编程不是背概念,而是把这些思想内化成你的肌肉记忆。以后你写任何代码,都会不自觉地思考:这里能不能封装?那里能不能用多态?这个类要不要用元类?

嗯,这就是我想教给你的东西。

最后提醒:这个迷你 ORM 仅供学习,不要直接用于生产环境。生产环境请用 Django ORM、SQLAlchemy 等成熟框架。它们处理了太多你想象不到的边界情况。


公众号:蓝海资料掘金营,微信deep3321