用 Python 元类的特性实现 ORM 框架


Posted in Python onMay 19, 2021

ORM是什么

O是 object,也就 类对象 的意思,R是 relation,翻译成中文是 关系,也就是关系数据库中 数据表 的意思,M是 mapping,是映射的意思。在ORM框架中,它帮我们把类和数据表进行了一个映射,可以让我们通过类和类对象就能操作它所对应的表格中的数据。ORM框架还有一个功能,它可以根据我们设计的类自动帮我们生成数据库中的表,省去了我们自己建表的过程。

一个句话理解就是:创建一个实例对象,用创建它的类名当做数据表名,用创建它的类属性对应数据表的字段,当对这个实例对象操作时,能够对应 MySQL 语句。

在 Django 中就内嵌了一个 ORM 框架,不需要直接面向数据库编程,而是定义模型类,通过模型类和对象完成数据表的增删改查操作。还有第三方库 sqlalchemy 都是 ORM框架。

用 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.属性=yyyy一样简单,这是开发ORM的初衷。

实现ORM中的insert功能

通过 Python 中 元类 简单实现 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 语句应该是:

insert into User (uid,username,email,password) values (123, 'hui', 'huidbk@163.com', '123456')

因此修改 User 类完善数据类型的检测

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 框架的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python中的代码编码格式转换问题
Jun 10 Python
Python的几个高级语法概念浅析(lambda表达式闭包装饰器)
May 28 Python
Python打包文件夹的方法小结(zip,tar,tar.gz等)
Sep 18 Python
Python实现ssh批量登录并执行命令
Oct 25 Python
Python 搭建Web站点之Web服务器与Web框架
Nov 06 Python
Python使用Selenium+BeautifulSoup爬取淘宝搜索页
Feb 24 Python
Python3.5.3下配置opencv3.2.0的操作方法
Apr 02 Python
Pytorch根据layers的name冻结训练方式
Jan 06 Python
python使用pymongo与MongoDB基本交互操作示例
Apr 09 Python
Python3 socket即时通讯脚本实现代码实例(threading多线程)
Jun 01 Python
python输入中文的实例方法
Sep 14 Python
基于Python实现nc批量转tif格式
Aug 14 Python
浅谈Python 中的复数问题
May 19 #Python
Python机器学习之基础概述
Python机器学习之PCA降维算法详解
Python 批量下载阴阳师网站壁纸
May 19 #Python
python 如何将两个实数矩阵合并为一个复数矩阵
May 19 #Python
python使用pywinauto驱动微信客户端实现公众号爬虫
python基于tkinter实现gif录屏功能
You might like
php小偷相关截取函数备忘
2010/11/28 PHP
修改php.ini不生效问题解决方法(上传大于8M的文件)
2013/06/14 PHP
JS解析XML文件和XML字符串详解
2015/04/17 Javascript
js如何判断访问是来自搜索引擎(蜘蛛人)还是直接访问
2015/09/14 Javascript
学习JavaScript设计模式(代理模式)
2015/12/03 Javascript
详解javascript实现瀑布流列式布局
2016/01/29 Javascript
jQuery插件AjaxFileUpload实现ajax文件上传
2016/05/05 Javascript
浅析jQuery Ajax通用js封装
2016/06/22 Javascript
基于HTML+CSS+JS实现增加删除修改tab导航特效代码
2016/08/05 Javascript
利用node.js实现自动生成前端项目组件的方法详解
2017/07/12 Javascript
详解bootstrap导航栏.nav与.navbar区别
2017/11/23 Javascript
基于Vue2.X的路由和钩子函数详解
2018/02/09 Javascript
浅谈Vue.js之初始化el以及数据的绑定说明
2019/11/14 Javascript
JS正则表达式验证端口范围(0-65535)
2020/01/06 Javascript
JS实现京东商品分类侧边栏
2020/12/11 Javascript
Python模块学习 datetime介绍
2012/08/27 Python
快速了解Python相对导入
2018/01/12 Python
使用python实现mqtt的发布和订阅
2019/05/05 Python
Python数据类型之String字符串实例详解
2019/05/08 Python
cProfile Python性能分析工具使用详解
2019/07/22 Python
Python学习笔记之文件的读写操作实例分析
2019/08/07 Python
Python time库基本使用方法分析
2019/12/13 Python
Python如何操作office实现自动化及win32com.client的运用
2020/04/01 Python
基于Python 的语音重采样函数解析
2020/07/06 Python
分享一枚pycharm激活码适用所有pycharm版本我的pycharm2020.2.3激活成功
2020/11/20 Python
英超联赛的首选足球:Mitre足球
2019/05/06 全球购物
一套SQL笔试题
2016/08/14 面试题
用C#语言写出与SQLSERVER访问时的具体过程
2013/04/16 面试题
工业自动化专业自荐信范文
2014/04/10 职场文书
2014中考励志标语
2014/06/05 职场文书
村班子对照检查材料
2014/08/18 职场文书
2015年六一儿童节活动总结
2015/02/11 职场文书
留学推荐信(中英文版)
2015/03/26 职场文书
使用numpy实现矩阵的翻转(flip)与旋转
2021/06/03 Python
vue+element ui实现锚点定位
2021/06/29 Vue.js
Python编写车票订购系统 Python实现快递收费系统
2022/08/14 Python