python 编写ORM

在一个Web App中,所有数据,包括用户信息、发布的日志、评论等,都存储在数据库中。在awesome-python3-webapp中,我们选择MySQL作为数据库。

Web App里面有很多地方都要访问数据库。访问数据库需要创建数据库连接、游标对象,然后执行SQL语句,最后处理异常,清理资源。这些访问数据库的代码如果分散到各个函数中,势必无法维护,也不利于代码复用。

所以,我们要首先把常用的SELECT、INSERT、UPDATE和DELETE操作用函数封装起来。

由于Web框架使用了基于asyncio的aiohttp,这是基于协程的异步模型。在协程中,不能调用普通的同步IO操作,因为所有用户都是由一个线程服务的,协程的执行速度必须非常快,才能处理大量用户的请求。而耗时的IO操作不能在协程中以同步的方式调用,否则,等待一个IO操作时,系统无法响应任何其他用户。

这就是异步编程的一个原则:一旦决定使用异步,则系统每一层都必须是异步,“开弓没有回头箭”。

幸运的是aiomysql为MySQL数据库提供了异步IO的驱动。

创建连接池

我们需要创建一个全局的连接池,每个HTTP请求都可以从连接池中直接获取数据库连接。使用连接池的好处是不必频繁地打开和关闭数据库连接,而是能复用就尽量复用。

连接池由全局变量__pool存储,缺省情况下将编码设置为utf8,自动提交事务:

@asyncio.coroutine
def create_pool(loop, **kw):
    logging.info(‘create database connection pool...‘)
    global __pool
    __pool = yield from aiomysql.create_pool(
        host=kw.get(‘host‘, ‘localhost‘),
        port=kw.get(‘port‘, 3306),
        user=kw[‘user‘],
        password=kw[‘password‘],
        db=kw[‘db‘],
        charset=kw.get(‘charset‘, ‘utf8‘),
        autocommit=kw.get(‘autocommit‘, True),
        maxsize=kw.get(‘maxsize‘, 10),
        minsize=kw.get(‘minsize‘, 1),
        loop=loop
    )

Select

要执行SELECT语句,我们用select函数执行,需要传入SQL语句和SQL参数:

@asyncio.coroutine
def select(sql, args, size=None):
    log(sql, args)
    global __pool
    with (yield from __pool) as conn:
        cur = yield from conn.cursor(aiomysql.DictCursor)
        yield from cur.execute(sql.replace(‘?‘, ‘%s‘), args or ())
        if size:
            rs = yield from cur.fetchmany(size)
        else:
            rs = yield from cur.fetchall()
        yield from cur.close()
        logging.info(‘rows returned: %s‘ % len(rs))
        return rs

SQL语句的占位符是?,而MySQL的占位符是%sselect()函数在内部自动替换。注意要始终坚持使用带参数的SQL,而不是自己拼接SQL字符串,这样可以防止SQL注入攻击。

注意到yield from将调用一个子协程(也就是在一个协程中调用另一个协程)并直接获得子协程的返回结果。

如果传入size参数,就通过fetchmany()获取最多指定数量的记录,否则,通过fetchall()获取所有记录。

Insert, Update, Delete

要执行INSERT、UPDATE、DELETE语句,可以定义一个通用的execute()函数,因为这3种SQL的执行都需要相同的参数,以及返回一个整数表示影响的行数:

@asyncio.coroutine
def execute(sql, args):
    log(sql)
    with (yield from __pool) as conn:
        try:
            cur = yield from conn.cursor()
            yield from cur.execute(sql.replace(‘?‘, ‘%s‘), args)
            affected = cur.rowcount
            yield from cur.close()
        except BaseException as e:
            raise
        return affected

execute()函数和select()函数所不同的是,cursor对象不返回结果集,而是通过rowcount返回结果数。

ORM

有了基本的select()execute()函数,我们就可以开始编写一个简单的ORM了。

设计ORM需要从上层调用者角度来设计。

我们先考虑如何定义一个User对象,然后把数据库表users和它关联起来。

from orm import Model, StringField, IntegerField

class User(Model):
    __table__ = ‘users‘

    id = IntegerField(primary_key=True)
    name = StringField()

注意到定义在User类中的__table__idname是类的属性,不是实例的属性。所以,在类级别上定义的属性用来描述User对象和表的映射关系,而实例属性必须通过__init__()方法去初始化,所以两者互不干扰:

# 创建实例:
user = User(id=123, name=‘Michael‘)
# 存入数据库:
user.insert()
# 查询所有User对象:
users = User.findAll()

定义Model

首先要定义的是所有ORM映射的基类Model

class Model(dict, metaclass=ModelMetaclass):

    def __init__(self, **kw):
        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 getValue(self, key):
        return getattr(self, key, None)

    def getValueOrDefault(self, key):
        value = getattr(self, key, None)
        if value is None:
            field = self.__mappings__[key]
            if field.default is not None:
                value = field.default() if callable(field.default) else field.default
                logging.debug(‘using default value for %s: %s‘ % (key, str(value)))
                setattr(self, key, value)
        return value

Modeldict继承,所以具备所有dict的功能,同时又实现了特殊方法__getattr__()__setattr__(),因此又可以像引用普通字段那样写:

>>> user[‘id‘]
123
>>> user.id
123

以及Field和各种Field子类:

class Field(object):

    def __init__(self, name, column_type, primary_key, default):
        self.name = name
        self.column_type = column_type
        self.primary_key = primary_key
        self.default = default

    def __str__(self):
        return ‘<%s, %s:%s>‘ % (self.__class__.__name__, self.column_type, self.name)

映射varcharStringField

class StringField(Field):

    def __init__(self, name=None, primary_key=False, default=None, ddl=‘varchar(100)‘):
        super().__init__(name, ddl, primary_key, default)

注意到Model只是一个基类,如何将具体的子类如User的映射信息读取出来呢?答案就是通过metaclass:ModelMetaclass

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        # 排除Model类本身:
        if name==‘Model‘:
            return type.__new__(cls, name, bases, attrs)
        # 获取table名称:
        tableName = attrs.get(‘__table__‘, None) or name
        logging.info(‘found model: %s (table: %s)‘ % (name, tableName))
        # 获取所有的Field和主键名:
        mappings = dict()
        fields = []
        primaryKey = None
        for k, v in attrs.items():
            if isinstance(v, Field):
                logging.info(‘  found mapping: %s ==> %s‘ % (k, v))
                mappings[k] = v
                if v.primary_key:
                    # 找到主键:
                    if primaryKey:
                        raise RuntimeError(‘Duplicate primary key for field: %s‘ % k)
                    primaryKey = k
                else:
                    fields.append(k)
        if not primaryKey:
            raise RuntimeError(‘Primary key not found.‘)
        for k in mappings.keys():
            attrs.pop(k)
        escaped_fields = list(map(lambda f: ‘`%s`‘ % f, fields))
        attrs[‘__mappings__‘] = mappings # 保存属性和列的映射关系
        attrs[‘__table__‘] = tableName
        attrs[‘__primary_key__‘] = primaryKey # 主键属性名
        attrs[‘__fields__‘] = fields # 除主键外的属性名
        # 构造默认的SELECT, INSERT, UPDATE和DELETE语句:
        attrs[‘__select__‘] = ‘select `%s`, %s from `%s`‘ % (primaryKey, ‘, ‘.join(escaped_fields), tableName)
        attrs[‘__insert__‘] = ‘insert into `%s` (%s, `%s`) values (%s)‘ % (tableName, ‘, ‘.join(escaped_fields), primaryKey, create_args_string(len(escaped_fields) + 1))
        attrs[‘__update__‘] = ‘update `%s` set %s where `%s`=?‘ % (tableName, ‘, ‘.join(map(lambda f: ‘`%s`=?‘ % (mappings.get(f).name or f), fields)), primaryKey)
        attrs[‘__delete__‘] = ‘delete from `%s` where `%s`=?‘ % (tableName, primaryKey)
        return type.__new__(cls, name, bases, attrs)

这样,任何继承自Model的类(比如User),会自动通过ModelMetaclass扫描映射关系,并存储到自身的类属性如__table____mappings__中。

然后,我们往Model类添加class方法,就可以让所有子类调用class方法:

class Model(dict):

    ...

    @classmethod
    @asyncio.coroutine
    def find(cls, pk):
        ‘ find object by primary key. ‘
        rs = yield from select(‘%s where `%s`=?‘ % (cls.__select__, cls.__primary_key__), [pk], 1)
        if len(rs) == 0:
            return None
        return cls(**rs[0])

User类现在就可以通过类方法实现主键查找:

user = yield from User.find(‘123‘)

往Model类添加实例方法,就可以让所有子类调用实例方法:

class Model(dict):

    ...

    @asyncio.coroutine
    def save(self):
        args = list(map(self.getValueOrDefault, self.__fields__))
        args.append(self.getValueOrDefault(self.__primary_key__))
        rows = yield from execute(self.__insert__, args)
        if rows != 1:
            logging.warn(‘failed to insert record: affected rows: %s‘ % rows)

这样,就可以把一个User实例存入数据库:

user = User(id=123, name=‘Michael‘)
yield from user.save()

最后一步是完善ORM,对于查找,我们可以实现以下方法:

  • findAll() - 根据WHERE条件查找;
  • findNumber() - 根据WHERE条件查找,但返回的是整数,适用于select count(*)类型的SQL。

以及update()remove()方法。

所有这些方法都必须用@asyncio.coroutine装饰,变成一个协程。

调用时需要特别注意:

user.save()

没有任何效果,因为调用save()仅仅是创建了一个协程,并没有执行它。一定要用:

yield from user.save()

才真正执行了INSERT操作。

最后看看我们实现的ORM模块一共多少行代码?累计不到300多行。用Python写一个ORM是不是很容易呢?

http://www.pdfxs.com/cpgjhegfgncpcfefdfcfebegcfebdbcfefdfcfebdecfecdecfefdfcfdjefcfdidecfefdgcfdjdgcfebee/

时间: 2025-01-17 06:03:57

python 编写ORM的相关文章

python编写登录接口(上)

中途经过了好几天都没有动手了,得坚持下去啊刚看了Alex老师的视频,其中有个题目如下:编写登录接口-输入用户密码-认证成功后显示欢迎信息-输错三次后锁定 # -*- coding: cp936 -*-#用户名保存在一个文件名为user.txt文件中import os,stringtmp=file('C:\Users\hityxg\Desktop\user.txt')username=file('C:\Users\hityxg\Desktop\username.txt','w')b=tmp.rea

python编写登录接口

python编写登录接口 一.需求 编写登录接口: 1.输入用户名和密码登录 2.输错三次锁定账户 3.下次登录还是上次的账户,提示锁定,直接退出(用到文件读写) 4.成功 后显示登录成功 二.需求流程图 三.代码示例 例1: #!/bin/bash/env python #_*_ coding:utf-8 _*_ #python version:3.6 ''' 编写登录接口:     1.输入用户名和密码登录     2.输错三次锁定账户     3.下次登录还是上次的账户,提示锁定,直接退出

Gvim入门(3)——简易配置python编写环境

用Gvim配置python编写环境最重要的也是最基本的一项就是要在Gvim的配置启动文件中,也就是在安装目录下的_vimrc中添加配置python解释器的代码.然而前提是要有配置好环境变量. 总结一下就是: (1)配置环境变量: 将python的路径添加到系统的环境变量的path里面,注意加分号.不会的就去百度里面google一下. (2)测试python配置环境变量是否成功: 在win+r,键入cmd,在dos(命令提示符)中键入python,如果结果是下图的情况,表示python环境变量配置

用Python编写博客导出工具

用Python编写博客导出工具 罗朝辉 (http://kesalin.github.io/) CC 许可,转载请注明出处 写在前面的话 我在 github 上用 octopress 搭建了个人博客,octopress 使用Markdown语法编写博文.之前我在CSDN博客上也写过不少的技术博文,都说自己的孩子再丑也是个宝,所以就起了把CSDN博客里面的文章导出到个人博客上的念头.刚开始想找个工具把CSDN博客导出为xml或文本,然后再把xml或文本转换为Markdown博文.可惜搜了一下现有博

python编写简单的html登陆页面(3)

1  在python编写简单的html登陆页面(2)的基础上在延伸一下: 可以将静态分配数据,建立表格,存放学生信息 2  加载到静态数据 3  html的编写直接在表格里添加一组数据就行了 4  VS Code上面跟前面的后台类似,添加一个content再链接到html就可以了 @app.route('/content')def content(): return render_template('content.html') return 'content.....'

python编写网络抓包分析脚本

python编写网络抓包分析脚本 写网络抓包分析脚本,一个称手的sniffer工具是必不可少的,我习惯用Ethereal,简单,易用,基于winpcap的一个开源的软件 Ethereal自带许多协议的 decoder,但是它的架构并不灵活,如果你要加入一个自己定义的的解码器,得去修改Ethereal的代码,再重新编译,相当的烦琐 对于一般的明文协议,没有什么问题,但是对于加密协议,比如网络游戏,客户端程序一般会在刚连接上的时候,发送一个随机密钥,而后的报文都会用这个密钥进 行加密,诸如此类.要想

用python编写ios工程自动编译、打包ipa等脚本

第一次使用python请先看:http://my.oschina.net/LangZiAiFer/blog/298763 代码管理我用的是Git: 工程clone如下: def gitClone(): os.system ('git clone https://[email protected]/xxx/goccia.git ~/desktop/20140804') return https://[email protected]/xxx/goccia.git是git服务器路径,不必care:~

Python 编写通过post方式提交的接口测试代码

第一次用python编写程序 是照着视频里编写的 但是期间也出现了很多错误 视频内用了是Python2 版本的 而我的是python 3版本的 写这两程序时就发现有很多不同 首先 python3没有urllib2这个组件 现在是urllib.request 所以在文件前面引入 import urllib.request urllib2.Request  更换为  urllib.request.Request urllib2.urlopen 更换为 urllib.request.urlopen 在

Python编写简易木马程序(转载乌云)

Python编写简易木马程序 light · 2015/01/26 10:07 0x00 准备 文章内容仅供学习研究.切勿用于非法用途! 这次我们使用Python编写一个具有键盘记录.截屏以及通信功能的简易木马.依然选用Sublime text2 +JEDI(python自动补全插件)来撸代码,安装配置JEDI插件可以参照这里: /tips/?id=4413 首先准备好我们需要的依赖库,python hook和pythoncom. 下载安装python hook 下载安装pythoncom模块: