本文始发于个人公众号: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. Blazor WebAssembly 修仙之途 - 组件与数据绑定

    一.前言 在第一篇文章中,有提到过组件(Component)这个概念.组件在 Blazor 中是必不可少的,UI 全靠它组装起来,和前端的 JS 组件是一个意思,比如:vue component.re ...

  2. 多服务器 vps 管理方法,十款远程桌面软件测评分析

    作为运维工作者,从几年前巨大的工作量到如今有了极大的改善,这些年试用的远程桌面管理软件不在少数. 远程桌面是微软公司为了方便网络管理员管理维护服务器而推出的一项服务.从windows 2000 ser ...

  3. 管程(Monitor)概念及Java的实现原理

    互斥 互斥访问是并发编程要解决的核心问题之一. 有许多种方法可以满足临界区的互斥访问.大体上可以分为三种, 一种是软件方法,即由用户程序承担互斥访问的责任,而不需要依赖编程语言或操作系统,譬如Dekk ...

  4. Java 异常(一) 异常概述及其架构

    Java 异常(一) 异常概述及其架构 一.异常概述 (一).概述 Java异常是Java提供的一种识别及响应错误的一致性机制.异常指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常 ...

  5. Java实现 蓝桥杯 算法训练 前缀表达式

    算法训练 前缀表达式 时间限制:1.0s 内存限制:512.0MB 问题描述 编写一个程序,以字符串方式输入一个前缀表达式,然后计算它的值.输入格式为:"运算符 对象1 对象2", ...

  6. Java实现 LeetCode 274 H指数

    274. H指数 给定一位研究者论文被引用次数的数组(被引用次数是非负整数).编写一个方法,计算出研究者的 h 指数. h 指数的定义: "h 代表"高引用次数"(hig ...

  7. Java实现 洛谷 P1035 级数求和

    import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = ...

  8. Java实现迷宫城堡(强连通图的判定)

    1 问题描述 Problem Description 为了训练小希的方向感,Gardon建立了一座大城堡,里面有N个房间(N<=10000)和M条通道(M<=100000),每个通道都是单 ...

  9. java实现第39级台阶(三十九级台阶)

    标题: 第39级台阶 小明刚刚看完电影<第39级台阶>,离开电影院的时候,他数了数礼堂前的台阶数,恰好是39级! 站在台阶前,他突然又想着一个问题: 如果我每一步只能迈上1个或2个台阶.先 ...

  10. java实现第七届蓝桥杯七星填数

    七星填数 如图[图1.png]所示. 在七角星的14个节点上填入1~14 的数字,不重复,不遗漏. 要求每条直线上的四个数字之和必须相等. 图中已经给出了3个数字. 请计算其它位置要填充的数字,答案唯 ...