Python 元类实现 ORM 框架详解:从原理到代码
本文深入解析 Python 中如何利用元类(Metaclass)特性从零构建一个轻量级 ORM 框架。文章首先阐述 ORM 对象关系映射的基本概念,随后通过具体代码示例,演示如何使用元类拦截类属性定义并自动生成 SQL 插入语句。内容涵盖字段类型检测、字符串引号处理、数据库连接获取及执行等核心环节,旨在帮助开发者理解 ORM 底层机制并掌握 Python 高级面向对象编程技巧。
ORM 是什么
ORM 全称为 Object-Relational Mapping,即对象-关系映射。其中,O 代表 Object(对象),对应编程中的类实例;R 代表 Relation(关系),指代关系型数据库中的数据表;M 代表 Mapping(映射)。ORM 框架的核心作用是在类的实例与数据库表之间建立映射关系,使得开发者可以通过操作类对象来间接操作数据库表中的数据。此外,优秀的 ORM 框架还能根据预先设计的类结构,自动生成对应的数据库表结构,从而免去手动编写建表语句的繁琐过程。
简而言之,ORM 的目标是:创建一个类的实例,以该类名作为数据表名,以类的属性对应数据表的字段。当对实例对象进行增删改查操作时,ORM 框架会自动将其转换为对应的 SQL 语句(如 MySQL 语句)并执行。
在实际开发中,许多主流框架都内置或提供了 ORM 支持。例如,Django 框架内嵌了强大的 ORM 系统,开发者无需直接编写 SQL,只需定义模型类(Model),即可通过模型类的实例完成对数据表的增删改查操作。此外,第三方库 SQLAlchemy 也是 Python 中广泛使用的 ORM 框架之一。

在深入实现之前,我们先明确预期实现的功能目标。
class User(父类省略):
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
...省略...
user = User(uid=123, name='hui', email='huidbk@163.com', password='123456')
user.save()
# 对应如下sql语句
# insert into User (uid,username,email,password) values (123,hui,huidbk@163.com,123456)
ORM 设计的初衷,是希望开发者在操作数据库时,能享受到如同操作普通对象属性般简单的体验,例如通过 `xxxx.attribute = yyyy` 的方式赋值,而底层自动处理复杂的 SQL 交互逻辑。
实现 ORM 中的 insert 功能
接下来,我们将通过 Python 的元类(Metaclass)机制,简要实现 ORM 框架中的 Insert(插入)功能。
# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 利用Python元类简单实现ORM框架的Insert插入功能 }
# @Date: 2021/05/17 17:02
class ModelMetaclass(type):
"""数据表模型元类"""
def __new__(mcs, cls_name, bases, attrs):
print(f'cls_name -> {cls_name}') # 类名
print(f'bases -> {bases}') # 继承类
print(f'attrs -> {attrs}') # 类中所有属性
print()
# 数据表对应关系字典
mappings = dict()
# 过滤出对应数据表的字段属性
for k, v in attrs.items():
# 判断是否是指定的StringField或者IntegerField的实例对象
# 这里就简单判断字段是元组
if isinstance(v, tuple):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
# 删除这些已经在字典中存储的字段属性
for k in mappings.keys():
attrs.pop(k)
# 将之前的uid/name/email/password以及对应的对象引用、类名字
# 用其他类属性名称保存
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = cls_name # 假设表名和类名一致
return type.__new__(mcs, cls_name, bases, attrs)
class User(metaclass=ModelMetaclass):
"""用户模型类"""
# 类属性名 表字段 表字段类型
uid = ('uid', 'int unsigned')
name = ('username', 'varchar(30)')
email = ('email', 'varchar(30)')
password = ('password', 'varchar(30)')
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)
def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))
# 表名
table_name = self.__table__
# 数据表中的字段
fields = ','.join(fields)
# 待插入的数据
args = ','.join([str(i) for i in args])
# 生成sql语句
sql = f"""insert into {table_name} ({fields}) values ({args})"""
print(f'SQL: {sql}')
def main():
user = User(uid=123, name='hui', email='huidbk@163.com', password='123456')
user.save()
if __name__ == '__main__':
main()
当为 User 类指定了元类后,uid、name、email、password 等类属性将不再直接存在于类的命名空间中,而是被提取并存储到由 `__mappings__` 属性指定的字典中。此时,User 类的这些属性表现如下所示:
__mappings__ = {
"uid": ('uid', "int unsigned")
"name": ('username', "varchar(30)")
"email": ('email', "varchar(30)")
"password": ('password', "varchar(30)")
}
__table__ = "User"
执行上述代码后的控制台输出效果如下:
cls_name -> User
bases -> ()
attrs -> {
'__module__': '__main__', '__qualname__': 'User', '__doc__': '用户模型类',
'uid': ('uid', 'int unsigned'),
'name': ('username', 'varchar(30)'),
'email': ('email', 'varchar(30)'),
'password': ('password', 'varchar(30)'),
'__init__': <function User.__init__ at 0x0000026D520C1048>,
'save': <function User.save at 0x0000026D520C10D8>
}
Found mapping: uid ==> ('uid', 'int unsigned')
Found mapping: name ==> ('username', 'varchar(30)')
Found mapping: email ==> ('email', 'varchar(30)')
Found mapping: password ==> ('password', 'varchar(30)')
SQL: insert into User (uid,username,email,password) values (123,hui,huidbk@163.com,123456)
完善对数据类型的检测
上述步骤生成的原始 SQL 语句如下:
insert into User (uid,username,email,password) values (12345,hui,huidbk@163.com,123456)
仔细观察可以发现,生成的 SQL 语句中,字符串类型的值(如用户名、邮箱、密码)缺少了必要的单引号包裹,这会导致 SQL 语法错误。
正确的 SQL 语句格式应当为字符串值加上单引号,例如:
insert into User (uid,username,email,password) values (123, 'hui', 'huidbk@163.com', '123456')
因此,我们需要修改 User 类的 save 方法,增加对数据类型的检测逻辑,确保字符串类型被正确添加引号。
class ModelMetaclass(type):
# 此处和上文一样, 故省略....
pass
class User(metaclass=ModelMetaclass):
"""用户模型类"""
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)
# 在这里完善数据类型检测
def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))
# 把参数数据类型对应数据表的字段类型
args_temp = list()
for temp in args:
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append(f"'{temp}'")
# 表名
table_name = self.__table__
# 数据表中的字段
fields = ','.join(fields)
# 待插入的数据
args = ','.join(args_temp)
# 生成sql语句
sql = f"""insert into {table_name} ({fields}) values ({args})"""
print(f'SQL: {sql}')
def main():
user = User(uid=123, name='hui', email='huidbk@163.com', password='123456')
user.save()
if __name__ == '__main__':
main()
修正后的运行效果如下:
cls_name -> User
bases -> ()
attrs -> {
'__module__': '__main__', '__qualname__': 'User', '__doc__': '用户模型类',
'uid': ('uid', 'int unsigned'),
'name': ('username', 'varchar(30)'),
'email': ('email', 'varchar(30)'),
'password': ('password', 'varchar(30)'),
'__init__': <function User.__init__ at 0x0000026D520C1048>,
'save': <function User.save at 0x0000026D520C10D8>
}
Found mapping: uid ==> ('uid', 'int unsigned')
Found mapping: name ==> ('username', 'varchar(30)')
Found mapping: email ==> ('email', 'varchar(30)')
Found mapping: password ==> ('password', 'varchar(30)')
SQL: insert into User (uid,username,email,password) values(123,'hui','huidbk@163.com','123456')
抽取到基类中
# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 利用Python元类实现ORM框架的Insert插入功能 }
# @Date: 2021/05/17 17:02
class ModelMetaclass(type):
"""数据表模型元类"""
def __new__(mcs, cls_name, bases, attrs):
print(f'cls_name -> {cls_name}') # 类名
print(f'bases -> {bases}') # 继承类
print(f'attrs -> {attrs}') # 类中所有属性
print()
# 数据表对应关系字典
mappings = dict()
# 过滤出对应数据表的字段属性
for k, v in attrs.items():
# 判断是否是对应数据表的字段属性, 因为attrs中包含所有的类属性
# 这里就简单判断字段是元组
if isinstance(v, tuple):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
# 删除这些已经在字典中存储的字段属性
for k in mappings.keys():
attrs.pop(k)
# 将之前的uid/name/email/password以及对应的对象引用、类名字
# 用其他类属性名称保存
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = cls_name # 假设表名和类名一致
return type.__new__(mcs, cls_name, bases, attrs)
class Model(object, metaclass=ModelMetaclass):
"""数据表模型基类"""
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)
def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))
# 把参数数据类型对应数据表的字段类型
args_temp = list()
for temp in args:
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append(f"'{temp}'")
# 表名
table_name = self.__table__
# 数据表中的字段
fields = ','.join(fields)
# 待插入的数据
args = ','.join(args_temp)
# 生成sql语句
sql = f"""insert into {table_name} ({fields}) values ({args})"""
print(f'SQL: {sql}')
# 执行sql语句
# ...
class User(Model):
"""用户表模型类"""
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
def main():
user = User(uid=123, name='hui', email='huidbk@163.com', password='123456')
user.save()
if __name__ == '__main__':
main()
添加数据库驱动执行sql语句
为了真正与数据库交互,我们需要引入数据库驱动。这里我们选择使用 pymysql 库来执行生成的 SQL 语句。
在 Model 基类中,我们需要新增一个名为 `get_connection` 的静态方法,用于获取与数据库的连接对象。
import pymysql
class Model(object, metaclass=ModelMetaclass):
"""数据表模型基类"""
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)
@staticmethod
def get_connection():
"""
获取数据库连接与数据游标
:return: conn, cursor
"""
conn = pymysql.connect(
database='testdb',
host='localhost',
port=3306,
user='root',
password='123456'
)
return conn, conn.cursor()
def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))
# 把参数数据类型对应数据表的字段类型
args_temp = list()
for temp in args:
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append(f"'{temp}'")
# 表名
table_name = self.__table__
# 数据表中的字段
fields = ','.join(fields)
# 待插入的数据
args = ','.join(args_temp)
# 生成sql语句
sql = f"""insert into {table_name} ({fields}) values ({args})"""
print(f'SQL: {sql}')
# 执行sql语句
conn, cursor = self.get_connection()
ret = cursor.execute(sql)
print(ret)
conn.commit()
cursor.close()
conn.close()
添加数据库驱动执行sql语句
同样地,这里我们使用 pymysql 数据库驱动,来执行 sql 语句
在 Model 类中新增一个 get_connection 的静态方法用于获取数据库连接
import pymysql
class Model(object, metaclass=ModelMetaclass):
"""数据表模型基类"""
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)
@staticmethod
def get_connection():
"""
获取数据库连接与数据游标
:return: conn, cursor
"""
conn = pymysql.connect(
database='testdb',
host='localhost',
port=3306,
user='root',
password='123456'
)
return conn, conn.cursor()
def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))
# 把参数数据类型对应数据表的字段类型
args_temp = list()
for temp in args:
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append(f"'{temp}'")
# 表名
table_name = self.__table__
# 数据表中的字段
fields = ','.join(fields)
# 待插入的数据
args = ','.join(args_temp)
# 生成sql语句
sql = f"""insert into {table_name} ({fields}) values ({args})"""
print(f'SQL: {sql}')
# 执行sql语句
conn, cursor = self.get_connection()
ret = cursor.execute(sql)
print(ret)
conn.commit()
cursor.close()
conn.close()
测试功能
准备数据库
首先,需要在数据库中准备好名为 testdb 的数据库以及对应的 user 数据表。
create database testdb charset=utf8;
use testdb;
create table user(
uid int unsigned auto_increment primary key,
username varchar(30) not null,
email varchar(30),
password varchar(30) not null
);
user 表的具体结构定义如下:
+----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+------------------+------+-----+---------+----------------+
| uid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| username | varchar(30) | NO | | NULL | |
| email | varchar(30) | YES | | NULL | |
| password | varchar(30) | NO | | NULL | |
+----------+------------------+------+-----+---------+----------------+
创建模型类测试
class User(Model):
"""用户表模型类"""
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
def main():
user = User(uid=1, name='hui', email='huidbk@163.com', password='123456')
user.save()
for i in range(2, 10):
user = User(
uid=i,
name=f'name{i}',
email=f'huidbk@16{i}.com',
password=f'12345{i}'
)
user.save()
if __name__ == '__main__':
main()
最后,查看数据库中 user 表的数据,确认插入是否成功。
mysql> select * from user;
+-----+----------+----------------+----------+
| uid | username | email | password |
+-----+----------+----------------+----------+
| 1 | hui | huidbk@163.com | 123456 |
| 2 | name2 | huidbk@162.com | 123452 |
| 3 | name3 | huidbk@163.com | 123453 |
| 4 | name4 | huidbk@164.com | 123454 |
| 5 | name5 | huidbk@165.com | 123455 |
| 6 | name6 | huidbk@166.com | 123456 |
| 7 | name7 | huidbk@167.com | 123457 |
| 8 | name8 | huidbk@168.com | 123458 |
| 9 | name9 | huidbk@169.com | 123459 |
+-----+----------+----------------+----------+
9 rows in set (0.00 sec)
源代码
完整源代码已上传至 Gitee 平台,仓库名为 PythonKnowledge: Python知识宝库,欢迎大家访问和参考。
以上就是用 Python 元类的特性实现 ORM 框架的详细内容,更多关于Python 实现 ORM 框架的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









