本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是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。相当于元类被隐形地继承了下来,但是我们在使用子类的时候却感知不到。

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

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

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

本文使用 mdnice 排版

Python元类实战,通过元类实现数据库ORM框架的更多相关文章

  1. appium+python自动化项目实战(一):引入nose和allure框架

    本文将介绍一套比较完整的appium自动化框架,以python为编写脚本语言,是因为python有强大的库,同时易学易懂. 最终的测试框架代码,将在jenkins项目中一键构建,执行自动化测试用例,并 ...

  2. Python学习_13_继承和元类

    继承 继承的含义就是子类继承父类的命名空间,子类中可以调用父类的属性和方法,由于命名空间的查找方式,当子类中定义和父类同名属性或者方法时,子类的实例调用的是子类中的属性,而不是父类,这就形成了pyth ...

  3. 通过 python的 __call__ 函数与元类 实现单例模式

    简单一句话,当一个类实现__call__方法时,这个类的实例就会变成可调用对象. 直接上测试代码 class ClassA: def __call__(self, *args, **kwargs): ...

  4. 【python进阶】详解元类及其应用1

    前言 元类在python中是很重要的一部分,我将分两次去讲解元类及其应用,此篇为详解元类及其应用第一篇,下面开始今天的说明~~~ 1. 类也是对象 在⼤多数编程语⾔中,类就是⼀组⽤来描述如何⽣成⼀个对 ...

  5. 【python进阶】详解元类及其应用2

    前言 在上一篇文章[python进阶]详解元类及其应用1中,我们提到了关于元类的一些前置知识,介绍了类对象,动态创建类,使用type创建类,这一节我们将继续接着上文来讲~~~ 5.使⽤type创建带有 ...

  6. python中的单例模式、元类

    单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场. ...

  7. [转]深刻理解Python中的元类(metaclass)以及元类实现单例模式

    使用元类 深刻理解Python中的元类(metaclass)以及元类实现单例模式 在看一些框架源代码的过程中碰到很多元类的实例,看起来很吃力很晦涩:在看python cookbook中关于元类创建单例 ...

  8. python面试题~反射,元类,单例

    1 什么是反射?以及应用场景? test.py def f1(): print('f1') def f2(): print('f2') def f3(): print('f3') def f4(): ...

  9. 深刻理解Python中的元类(metaclass)以及元类实现单例模式

    在理解元类之前,你需要先掌握Python中的类.Python中类的概念借鉴于Smalltalk,这显得有些奇特.在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段.在Python中这一点仍 ...

随机推荐

  1. Java动态代理与静态代理以及它能为我们做什么

    相信我们在网上和平时学习和工作中或多或少都接触过Java的代理模式,经常听到什么静态代理.动态代理的一些名词.但我们是否真的很清楚这些呢?至少我在面试时,发现很多人并不很清楚. 首先代理比较好理解,就 ...

  2. 【Java8新特性】接口中的默认方法和静态方法,你都掌握了吗?

    写在前面 在Java8之前的版本中,接口中只能声明常量和抽象方法,接口的实现类中必须实现接口中所有的抽象方法.而在Java8中,接口中可以声明默认方法和静态方法,本文,我们就一起探讨下接口中的默认方法 ...

  3. IDEA,PyCharm系列软件常用快捷键

    首先介绍一下小编常用的快捷键: 注释 ctrl+/ 当想看某个类或者某个方法的时候 ctrl+鼠标左键单击 运行程序 ctrl+shift+f10 调试程序 ctrl+shift+f9 撤销 ctrl ...

  4. 减少if...的使用

    最近维护一批代码,其中包括一堆if...的使用,多的情况嵌套8.9层,痛苦不堪,所以搜寻一些可以降低if...else的方法来改善一下代码,写个简单总结. 第一种: 优化前 if (measuredV ...

  5. iOS-自定义Model转场动画-仿酷我音乐播放器效果

    周末,闲来无事,仿写了酷我音乐播放器效果: 效果图如下: 实现思路: 1.实现手势处理视图旋转 2.自定义Model动画: 1.手势是利用了一个UIPanGestureRecognizer手势: 注意 ...

  6. javaweb之Servlet,http协议以及请求转发和重定向

    本文是作者原创,版权归作者所有.若要转载,请注明出处. 一直用的框架开发,快连Servlet都忘了,此文旨在帮自己和大家回忆一下Servlet主要知识点.话不多说开始吧 用idea构建Servlet项 ...

  7. ScrollView嵌套ConstraintLayout导致最后一项显示不全

    原因:scrollView不受ConstraintLayout的约束布局影响 解决方法: 保持scrollview的宽高为0dp,设置其相对ConstraintLayout相对约束 <andro ...

  8. 【C#】AutoMapper 使用手册

    目录 1 入门例子 2 注册 2.1 Profile 3 配置 3.1 命名约定 3.2 配置可见性 3.3 全局属性/字段过滤 3.4 识别前缀和后缀 3.5 替换字符 4 调用构造函数 5 数组和 ...

  9. Chrome扩展移植到Edge浏览器教程

    微软在推出Edge浏览器之初,就把能够使用扩展(extension)作为一个重要功能.在Win10一周年更新版(1607)中,这项功能正式向广大用户推出(当然,Insider用户早就测试了一段时间了) ...

  10. 给博客使用Butterfly主题并部署到GitHub服务器

    目录 前言 一.安装Butterfly主题 二.将本地博客部署到GitHub服务器 三.将个人域名与GitHub绑定 前言 安装完Hexo框架后,自带的主题在thems文件夹下可以查看,应用后界面: ...