百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Python元类实战,通过元类实现数据库ORM框架

ccwgpt 2024-10-16 08:01 47 浏览 0 评论

今天是Python专题的第19篇文章,我们一起来用元类实现一个简易的ORM数据库框架。


本文主要是受到了廖雪峰老师Python3入门教程的启发,不过廖老师的博客有些精简,一些小白可能看起来比较吃力。我在他的基础上做了一些补充和注释,尽量写得浅显一些。


ORM框架是什么


如果是没有做过后端的小伙伴上来估计会有点蒙,这个ORM框架究竟是什么?ORM框架是后端工程师常用的一个框架,它的英文全称是Object Relational Mapping,即对象-关系映射框架。顾名思义就是把关系转化成对象的框架,关系这个词我们在哪里用的最多呢?


显然应该是数据库。之前我们在分布式的文章介绍关系型数据库和非关系型数据库的时候就着重介绍过关系的含义。我们常用的MySQL就是经典的关系型数据库,它存储的形式是表,但是表承载的数据其实是两个实体之间的"关系"。比如学生上课这个场景,学生和课程是两个主体(entity),我们要记录的是这两个主体之间的关系,也就是学生上课这件事。


而ORM框架做的事情是将这些关系映射成类,这样我们可以将这张表当中增删改查的功能抽象成类当中的方法。这样我们就可以通过调用类的方式来操作数据库了,从而达到高度抽象业务逻辑、降低用户使用难度的目的。


比如Java后端工程师常用的hibernate和ibatis都是用来做这件事情的,明确了框架的功能之后,我们先来设想一下最后的成果。假设我们现在开发出来了这么一套框架,那么它用起来的感觉应该是怎样的?


我们来看下廖老师博客里给的例子:


class User(Model):
    # 定义类的属性到列的映射:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')


User类代表了数据库当中的一张表,它有4个字段:id, name, email和password,我们在定义字段的同时也通过类别指定了它们的类型。这个应该不难理解,上面的这个类等价于我们在数据库当中执行了这么一段建表的SQL:


create table if not exists user (
 id int,
    name string,
    email string,
    password string
)


我们定义了表字段之后,接下来要做的就是根据字段创建数据了,其实也就是根据类创建实例。我们希望User类型的实例就对应User表当中的一条记录,并且我们可以通过调用实例当中的方法,来操作这张表进行增删改查。


# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()


那么,我们怎样可以实现这样的功能呢?


功能实现


我们先从简单的功能开始实现,首先是Field类,Field类表示数据库表当中一个字段的类型。这里的逻辑很容易理清楚,我们需要定义多种类型,比如IntegerField和StringField。我们可以对这些field类抽象出一个父类来:


class Field(object):
    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type
        
    def __str__(self):
        return '<{}:{}>'.format(self.__class__.__name__, self.name)


__str__方法当中打印出来的两个字段,分别是类别的名称和字段的名称,这段代码应该不难理解。


接着,我们实现它的两个子类,分别是IntegerField和StringField:


class StringField(Field):
    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(100)')
        
        
class IntegerField(Field):
    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')


这里也不难理解,只是一个简单的继承应用而已。


接下来就到了最关键的部分,也就是Model类的实现。我们先来分析一下我们希望Model这个类拥有的功能,由于它是我们定义出来的每一张表的父类,所以它应该能够获取子类当中的字段,并且将它存放在一个容器当中。由于我们需要存储的是字段名和类型的映射,所以将它存储在dict当中比较合理。


另外一个功能是我们希望它能够提供增删改查的接口,能够根据子类当中定义的字段自动生成相应的SQL语句去调用数据库。这个也是ORM框架的意义所在。


第二个功能容易实现,只要第一个功能搞定了,做一下字符串处理即可。但是第一个功能有些麻烦,它也是元类的意义所在。因为父类当中的方法是无法获取子类中定义的类属性的,只能通过元类,在构建类的时候可以拿到属性的信息。


所以我们已经很明确了,我们实现元类的目的就是为了实现这个功能。理清楚了之后,再来写代码就不难了。我们先来实现这个元类:


class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        # 创建model类的时候不做任何处理
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        # 打印表名的信息
        print('Found model: %s' % name)
        # mappings用来存储字段的信息
        mappings = dict()
        for k, v in attrs.items():
            # 判断v的类型,只有是Field的子类才会存储起来
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
        # 将mappings当中的数据从类属性当中移除,防止关键字冲突
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 保存属性和列的映射关系
        attrs['__table__'] = name # 假设表名和类名一致
        return type.__new__(cls, name, bases, attrs)

如果你看过之前的文章,对元类已经很熟悉了,那么这段代码对你来说应该不难理解。元类搞定了,剩下的Model就更简单了。按照规范,我们需要实现增删改查四个函数,但是这里我们只是为了展示,所以就只实现其中一个作为例子,其他几个都可以如法炮制。


class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kw):
        # 由于Model的基类是dict,所以创造Model的字段会被解析成dict的构造参数
        # 也就是说字段名和字段值的映射会存储在dict当中
        super(Model, self).__init__(**kw)
        
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            # fields存储字段名
            fields.append(v.name)
            # params填充问号
            params.append('?')
            # 获取字段的值
            args.append(getattr(self, k, None))
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))


Model当中的save方法不难看懂,但是前面的几个方法看起来有些多余。但实际上它们也很重要,这里有一个关键信息是Model类的父类是dict,我们在构建Model的时候传入的参数会被用来初始化一个dict。所以我们创建数据实例的时候数据的名称和数据值的映射会被存储在dict当中,所以我们在save方法当中才会从self的attr当中获取字段的值。并且我们在初始化User的时候,也必须要填写每个字段的名称,原因就在这里。


最后我们来运行一下:



从结果上来看,我们输出了User这个类的插入SQL以及它的字段的值。只需要链接一下数据库,我们的这个ORM框架就可以真正投入使用了。


总结


在整个ORM框架实现的过程当中,最重要的是我们对Model这个类创建了元类,但是真正应用的地方却是在Model的子类。实际上在实际创建User类的时候,解释器会先搜索User内部是否定义了元类,如果没有,会上一层去往User的父类也就是Model类搜索元类,如果找到了元类,就会使用元类来创建User。相当于元类被隐形地继承了下来,但是我们在使用子类的时候却感知不到。


对于框架的使用者来说,也的确不需要了解框架内部的实现机制,只需要明白使用方法,照着使用就行了。虽然元类的实现和理解很复杂,但是使用起来却很简单,这也是它的一个显著特点。


最后,本文的代码示例源于廖雪峰老师的博客,向廖雪峰老师致敬。想要查看廖老师博客原文的,请点击查看原文。


如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。


本文始发于公众号:TechFlow

相关推荐

用Steam启动Epic游戏会更快吗?(epic怎么用steam启动)

Epic商店很香,但也有不少抱怨,其中一条是启动游戏太慢。那么,如果让Steam启动Epic游戏,会不会速度更快?众所周知,Steam可以启动非Steam游戏,方法是在客户端左下方点击“添加游戏”,然...

Docker看这一篇入门就够了(dockerl)

安装DockerLinux:$curl-fsSLhttps://get.docker.com-oget-docker.sh$sudoshget-docker.sh注意:如果安装了旧版...

AYUI 炫丽PC开发UI框架2016年6月15日对外免费开发使用 [1]

2016年6月15日,我AY对外发布AYUI(WPF4.0开发)的UI框架,开发时候,你可以无任何影响的去开发PC电脑上的软件exe程序。AYUI兼容XP操作系统,在Win7/8/8.1/10上都顺利...

别再说C#/C++套壳方案多了!Tauri这“借壳生蛋”你可能没看懂!

浏览器套壳方案,C#和C++有更多,你说的没错,从数量和历史积淀来看,C#和C++确实有不少方式来套壳浏览器,让Web内容在桌面应用里跑起来。但咱们得把这套壳二字掰扯清楚,因为这里面学问可大了!不同的...

OneCode 核心概念解析——Page(页面)

在接触到OneCode最先接触到的就是,Page页面,在低代码引擎中,页面(Page)设计的灵活性是平衡“快速开发”与“复杂需求适配”的关键。以下从架构设计、组件系统、配置能力等维度,解析确...

React是最后的前端框架吗,为什么这么说的?

油管上有一位叫Theo的博主说,React是终极前端框架,为什么这么说呢?让我们来看看其逻辑:这个标题看起来像假的,对吧?React之后明明有无数新框架诞生,凭什么说它是最后一个?我说的“最后一个”不...

面试辅导(二):2025前端面试密码:用3个底层逻辑征服技术官

面试官放下简历,手指在桌上敲了三下:"你上次解决的技术难题,现在回头看有什么不足?"眼前的候选人瞬间僵住——这是上周真实发生在蚂蚁金服终面的场景。2025年的前端战场早已不是框架熟练...

前端新星崛起!Astro框架能否终结React的霸主地位?

引言:当"背着背包的全能选手"遇上"轻装上阵的短跑冠军"如果你是一名前端开发者,2024年的框架之争绝对让你眼花缭乱——一边是React这位"背着全家桶的全能选...

基于函数计算的 BFF 架构(基于函数计算的 bff 架构是什么)

什么是BFFBFF全称是BackendsForFrontends(服务于前端的后端),起源于2015年SamNewman一篇博客文章《Pattern:BackendsFor...

谷歌 Prompt Engineering 白皮书:2025年 AI 提示词工程的 10 个技巧

在AI技术飞速发展的当下,如何更高效地与大语言模型(LLM)沟通,以获取更准确、更有价值的输出,成为了一个备受关注的问题。谷歌最新发布的《PromptEngineering》白皮书,为这一问题提供了...

光的艺术:灯具创意设计(灯光艺术作品展示)

本文转自|艺术与设计微信号|artdesign_org_cn“光”是文明的起源,是思维的开端,同样也是人类睁眼的开始。每个人在出生一刻,便接受了光的照耀和洗礼。远古时候,人们将光奉为神明,用火来...

MoE模型已成新风口,AI基础设施竞速升级

机器之心报道编辑:Panda因为基准测试成绩与实际表现相差较大,近期开源的Llama4系列模型正陷入争议的漩涡之中,但有一点却毫无疑问:MoE(混合专家)定然是未来AI大模型的主流范式之一。...

Meta Spatial SDK重大改进:重塑Horizon OS应用开发格局

由文心大模型生成的文章摘要Meta持续深耕SpatialSDK技术生态,提供开自去年9月正式推出以来,Meta持续深耕其SpatialSDK技术生态,通过一系列重大迭代与功能增强,不断革新H...

&quot;上云&quot;到底是个啥?用&quot;租房&quot;给你讲明白IaaS/PaaS/SaaS的区别

半夜三点被机房报警电话惊醒,顶着黑眼圈排查服务器故障——这是十年前互联网公司运维的日常。而现在,程序员小王正敷着面膜刷剧,因为公司的系统全"搬"到了云上。"部署到云上"...

php宝塔搭建部署thinkphp机械设备响应式企业网站php源码

大家好啊,欢迎来到web测评。本期给大家带来一套php开发的机械设备响应式企业网站php源码,上次是谁要的系统项目啊,帮你找到了,还说不会搭建,让我帮忙录制一期教程,趁着今天有空,简单的录制测试了一下...

取消回复欢迎 发表评论: