《从零构建一个可链式调用的 Python ORM:原理、设计与实战全解析》
在我教授 Python 的这些年里,我常常遇到这样的问题:
“Django ORM 为什么能写出
User.objects.filter(age__gt=18).order_by('-created_at')这种优雅的链式查询?
我能不能自己写一个简化版的 ORM?”
答案是:当然可以,而且你会从中学到大量 Python 语言的精髓。
ORM(Object Relational Mapping)不仅是 Web 开发的核心组件,更是 Python 生态中最具代表性的“优雅设计”。它融合了:
- 链式调用(Fluent API)
- 延迟执行(Lazy Evaluation)
- 表达式构造(Query Expression)
- 元编程(Metaclass)
- 动态属性访问(
__getattr__) - SQL 生成器(Query Builder)
今天,我们将从零开始,手写一个支持链式调用的 ORM 查询构造器,类似 Django ORM,但更轻量、更易理解。
一、开篇:为什么要自己写一个 ORM?
Python 的发展历程中,ORM 一直扮演着重要角色:
- Django ORM 让 Web 开发者无需写 SQL
- SQLAlchemy 让工程师能优雅地构建复杂查询
- Peewee、Tortoise ORM 等轻量框架让异步时代更高效
ORM 的本质是:
用 Python 对象表达 SQL 查询,用链式调用表达查询逻辑。
自己写一个 ORM,你将学到:
- 如何设计链式 API
- 如何构建表达式树
- 如何延迟执行查询
- 如何将 Python 语法映射到 SQL
- 如何设计可扩展的架构
这些能力会让你在工程实践中更加游刃有余。
二、基础铺垫:链式调用的核心原理
链式调用的本质是:
每个方法返回 self 或一个新的对象。
例如:
class Query:
def filter(self, **kwargs):
print("filter:", kwargs)
return self
q = Query().filter(name="Tom").filter(age__gt=18)
输出:
filter: {'name': 'Tom'}
filter: {'age__gt': 18}
链式调用的关键点:
- 每次调用返回 Query 对象
- Query 对象内部不断累积查询条件
- 最终执行时统一生成 SQL
这就是 ORM 的核心思想。
三、设计 ORM 的整体架构
我们将构建一个简化版 ORM,包含:
Model(模型类)
├── Meta(表名、字段)
├── objects(QuerySet 管理器)
QuerySet(查询构造器)
├── filter()
├── order_by()
├── limit()
├── build_sql()
Field(字段类型)
Database(执行 SQL)
最终希望能写出:
users = User.objects.filter(age__gt=18).order_by('-created_at').limit(10)
print(users.sql())
输出:
SELECT * FROM user WHERE age > 18 ORDER BY created_at DESC LIMIT 10;
四、第一步:定义字段类型 Field
字段类型用于描述数据库字段:
class Field:
def __init__(self, name=None):
self.name = name
class IntegerField(Field):
pass
class StringField(Field):
pass
五、第二步:使用元类收集模型字段
ORM 的关键是:
模型类中的字段要被自动收集。
例如:
class User(Model):
id = IntegerField()
name = StringField()
age = IntegerField()
我们希望自动得到:
{
"id": IntegerField(name="id"),
"name": StringField(name="name"),
"age": IntegerField(name="age")
}
使用 metaclass:
class ModelMeta(type):
def __new__(cls, name, bases, attrs):
fields = {}
for key, value in list(attrs.items()):
if isinstance(value, Field):
value.name = key
fields[key] = value
attrs.pop(key)
attrs['_fields'] = fields
return type.__new__(cls, name, bases, attrs)
六、第三步:定义 Model 基类
Model 需要:
- 保存表名
- 提供 objects 查询入口
class Model(metaclass=ModelMeta):
@classmethod
def table_name(cls):
return cls.__name__.lower()
@classmethod
def objects(cls):
return QuerySet(cls)
七、第四步:构建 QuerySet(查询构造器)
QuerySet 是 ORM 的灵魂。
我们需要支持:
- filter()
- order_by()
- limit()
- sql()
1. 初始化
class QuerySet:
def __init__(self, model):
self.model = model
self._filters = []
self._order = None
self._limit = None
2. filter():解析 age__gt=18
Django ORM 的 filter 支持:
- age__gt=18 → age > 18
- name__contains=“Tom” → name LIKE ‘%Tom%’
我们先实现简单版本:
OPERATORS = {
"gt": ">",
"lt": "<",
"gte": ">=",
"lte": "<=",
"contains": "LIKE"
}
class QuerySet:
def filter(self, **kwargs):
for key, value in kwargs.items():
if "__" in key:
field, op = key.split("__")
sql_op = OPERATORS[op]
else:
field, sql_op = key, "="
self._filters.append((field, sql_op, value))
return self
3. order_by()
def order_by(self, field):
if field.startswith("-"):
self._order = (field[1:], "DESC")
else:
self._order = (field, "ASC")
return self
4. limit()
def limit(self, n):
self._limit = n
return self
5. 构建 SQL
def sql(self):
table = self.model.table_name()
sql = f"SELECT * FROM {table}"
# WHERE
if self._filters:
parts = []
for field, op, value in self._filters:
if op == "LIKE":
parts.append(f"{field} LIKE '%{value}%'")
else:
parts.append(f"{field} {op} {value}")
sql += " WHERE " + " AND ".join(parts)
# ORDER BY
if self._order:
field, direction = self._order
sql += f" ORDER BY {field} {direction}"
# LIMIT
if self._limit is not None:
sql += f" LIMIT {self._limit}"
return sql + ";"
八、第五步:定义一个模型并测试
class User(Model):
id = IntegerField()
name = StringField()
age = IntegerField()
created_at = StringField()
测试:
qs = User.objects().filter(age__gt=18, name__contains="Tom").order_by("-created_at").limit(5)
print(qs.sql())
输出:
SELECT * FROM user
WHERE age > 18 AND name LIKE '%Tom%'
ORDER BY created_at DESC
LIMIT 5;
这就是一个最小可用的 ORM 查询构造器。
九、进阶:支持链式调用的延迟执行(Lazy Evaluation)
Django ORM 的一个重要特性是:
查询不会立即执行,而是在需要时才执行。
例如:
qs = User.objects.filter(age__gt=18)
qs = qs.order_by('-id')
qs = qs.limit(10)
我们已经实现了这一点,因为 QuerySet 只是累积条件,只有调用 .sql() 才生成 SQL。
十、进阶:支持表达式对象(Q 对象)
Django ORM 支持:
User.objects.filter(Q(age__gt=18) | Q(name__contains="Tom"))
我们可以扩展:
class Q:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.connector = "AND"
self.children = []
def __or__(self, other):
q = Q()
q.connector = "OR"
q.children = [self, other]
return q
QuerySet.filter() 需要支持 Q 对象解析。
十一、进阶:支持 select() 字段选择
def select(self, *fields):
self._select = fields
return self
SQL:
SELECT id, name FROM user ...
十二、进阶:支持 join()
你可以扩展:
def join(self, other_model, on):
...
生成:
SELECT ... FROM user
JOIN order ON user.id = order.user_id
十三、案例实战:构建一个小型用户查询系统
假设我们要实现:
users = (
User.objects()
.filter(age__gte=20)
.filter(name__contains="Li")
.order_by("id")
.limit(3)
)
print(users.sql())
输出:
SELECT * FROM user
WHERE age >= 20 AND name LIKE '%Li%'
ORDER BY id ASC
LIMIT 3;
你可以将 SQL 交给 sqlite3 执行:
import sqlite3
conn = sqlite3.connect("test.db")
cursor = conn.cursor()
cursor.execute(users.sql())
rows = cursor.fetchall()
十四、最佳实践:如何设计一个可扩展的 ORM?
1. QuerySet 必须是不可变对象
每次调用返回新对象:
def filter(self, **kwargs):
clone = self._clone()
clone._filters.append(...)
return clone
避免链式调用污染原对象。
2. SQL 构造器必须分层设计
- WHERE 构造器
- ORDER 构造器
- LIMIT 构造器
- JOIN 构造器
3. 字段类型必须可扩展
支持:
- BooleanField
- DateTimeField
- ForeignKey
- ManyToManyField
4. QuerySet 必须支持迭代
def __iter__(self):
rows = self.execute()
for row in rows:
yield self.model(**row)
5. 支持缓存与 QuerySet 重用
避免重复执行 SQL。
十五、前沿视角:ORM 的未来趋势
Python ORM 正在向几个方向演进:
1. 异步 ORM(async ORM)
如:
- Tortoise ORM
- GINO
- SQLModel(FastAPI 作者推出)
2. 类型安全 ORM
如:
- SQLModel(基于 Pydantic)
- Piccolo ORM
3. 自动生成 SQL(AI-assisted ORM)
未来 ORM 可能自动优化 SQL、自动生成索引。
十六、总结与互动
今天,我们从零构建了一个支持链式调用的 ORM 查询构造器,完整经历了:
- 字段定义
- 元类收集字段
- QuerySet 构造器
- filter/order/limit
- SQL 生成
- 延迟执行
- 可扩展架构设计
你不仅学会了如何写 ORM,更理解了 ORM 背后的设计哲学。
开放性问题
我很想听听你的经验:
- 你在项目中是否遇到过 ORM 性能瓶颈
- 你更喜欢 Django ORM 还是 SQLAlchemy
- 你希望我继续写“手写 ORM 系列”的哪些内容(如 JOIN、Q 对象、异步 ORM)
告诉我你的方向,我可以继续为你扩展下一篇文章。

1241

被折叠的 条评论
为什么被折叠?



