廖雪峰实战之ORM框架个人学习笔记


ORM,建立类与数据库表的映射,ORM通过打开数据库连接,再由连接创建游标,通过游标执行一系列操作,将符合数据库表的格式的数据插入,或更新或其他操作,而实现类与数据库的映射的.

数据库连接池

博客的设计采用了异步io。一部异步,步步异步!

数据库连接池的好处在于提高服务器的性能的使用效率,减少占用!廖雪峰老师可能考虑的是些一个真正能用的工程项目,而不是一个写出来玩玩儿就完事儿的东西。之所以这么说,是因为他的这个博客设计思路有些地方真的是挺好的,同样也是我不曾涉及到的!

以下是百度百科对数据库连接池的解释:

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。 这项技术能明显提高对数据库操作的性能。

需要说明的是,廖雪峰老师在其网站上面的写法不再被新版的asyncio推荐的,而其github上面托管的写法就符合了!

修改的地方

1.我无法理解loop消息循环的用处,可能是多余的。简化删掉了!

数据库select

P.S.:根据菜鸟教程的SQL数据库相关知识。

SELECT 语句用于从数据库中选取数据。

SQL SELECT 语句

SELECT 语句用于从数据库中选取数据。结果被存储在一个结果表中,称为结果集。

SQL SELECT 语法

SELECT column_name,column_name
FROM table_name;

[](https://www.runoob.com/sql/sql-select.html)

# 将数据库的select操作封装在select函数中
# sql形参即为sql语句,args表示填入sql的选项值
# size用于指定最大的查询数量,不指定将返回所有查询结果
# 相关select语句的内容,在orm.md里面有介绍
async def select(sql,args,size=None):
    global __pool
    async with (await __pool) as conn:
        cur = await conn.cursor(aiomysql.DictCursor)
        ###用?占位符替换而非字符串拼接可防治sql注入
        await cur.execute(sql.replace('?','%s'),args)
        if size:
            rs =await cur.fetchmany(size)
        else:
            rs=await cur.fetchall()
        await cur.close()
        logging.info('rows returned:%s'%len(rs))
        return rs   ###返回查询内容

代码注解

cur = await conn.cursor(aiomysql.DictCursor)

该语句打开一个DictCursor,它与普通游标的不同在于,以dict(字典)形式返回结果。

await是异步写法,为什么这么写我暂时还没有去详细的阅读官方文档!

什么是游标?

以下是引用内容:[](https://blog.csdn.net/yxwb1253587469/article/details/52249174)

在数据库中,游标是一个十分重要的概念。游标提供了一种对从表中检索出的数据进行操作的灵活手段,就本质而言,游标实际上是一种能从包括多条数据记录的结果集中每次提取一条记录的机制。游标总是与一条SQL 选择语句相关联因为游标由结果集(可以是零条、一条或由相关的选择语句检索出的多条记录)和结果集中指向特定记录的游标位置组成。当决定对结果集进行处理时,必须声明一个指向该结果集的游标。如果曾经用 C 语言写过对文件进行处理的程序,那么游标就像您打开文件所得到的文件句柄一样,只要文件打开成功,该文件句柄就可代表该文件。对于游标而言,其道理是相同的。可见游标能够实现按与传统程序读取平面文件类似的方式处理来自基础表的结果集,从而把表中数据以平面文件的形式呈现给程序。

那我查阅了一些资料后,我个人理解游标其实就是一个和C语言中的指针有点儿相似的东西。我们就想象有一张表,表上面有一个类似指针的东西指向我们检索出来的记录吧!

下面是关于aiomysql.dictcursor介绍的一篇博客!要理解的话,不要忘了去看一下![](https://allenwind.github.io/2017/09/14/aiomysql%20%E7%AC%94%E8%AE%B0/)

下面就本函数截取部分:

Connection实例

  • conn.cursor(cursor=None) 参数指定游标类型,在aiomysql.cursors中

Cursor

游标cursor通过Connection.cursor()创建。它绑定与一个conn的整个生命周期,建立一个数据库会话,通过execute执行mysql命令。

游标实例cursor的属性:

  • execute(sql, args) args为元组类型,couroutine类型
  • fetchall()
  • fetchone()
  • fetchmany(size)

数据库的增删改execute函数

# 增删改都是对数据库的修改,因此封装到一个函数中
async def execute(sql,args):
    global __pool
    async with (await __pool)as conn:  # 从连接池中取出一条数据库连接
        # 若数据库的事务为非自动提交的,则调用协程启动连接
        if not conn.get_autocommit(): # 根据aiomysql文档,修改autocommit为obj.get_autocommit()
            await conn.begin()
        try:
            # 此处打开的是一个普通游标
            cur = await conn.cursor()
            await cur.execute(sql.replace('?','%s'),args)
            affected = cur.rowcount
            await cur.close()
            if not conn.get_autocommit(): # 同上, 事务非自动提交型的,手动调用协程提交增删改事务
                await conn.commit()
        except BaseException as e:
            if not conn.get_autocommit(): # 出错, 回滚事务到增删改之前
                await conn.rollback()
            raise e
        logging.info("数据库更新/插入/删除出错")
        return affected  ###insert,delete,update只需要返回影响的行数

代码注解

自动提交模式不足之处?

if not conn.get_autocommit(): # 根据aiomysql文档,修改autocommit为obj.get_autocommit()
 await conn.begin()

我在阅读完上面那篇博客后,想到我们设定默认的数据库修改为自动提交模式,还记得吗?就是create_pool函数里面有一句autocommit。

但是如果,会不会出现一种情况就是这个自动提交出了错(到底会出上面错我也不知道)。那我发现老师的代码里面就只能最后报一句错。所以,我就自己添加了几句代码。当发现事物提交的模式不是自动提交的时候,我就手动调用协程提交我们对mysql的增删改操作!

affected以及cur.execute

cur.exexute语句是这样的:

await cur.execute(sql.replace('?','%s'),args)

我们打开了一个普通游标后,在函数里面,我们已经将传来的链接中的需要对mysql操作的请求放到了args里面,并且用sql.replace里面的?占位符改为了%s,然后,我们执行args里面的语句就行了。

函数里面打开的是一个普通的游标cursor,普通游标不需要返回我们的sql操作的结果集。(之前的select里面我们的游标是一个字典类的游标,所以返回了查询到的size控制的结果)但是cursor可以返回一个影响的行数。即是rowcount。

详细可参考一下aiomysql的官方文档

关于Field

在整个程序中,首先定义了一个父类,父类默认继承于object。

假设一张完整的数据表中,有n行n列。数据表中的数据一般都是以行来记录的,也就是,每一行都是一次独立的数据记录。那我们在一行记录中需要包含什么呢?

我想到了常用的excel,还记得吗?excel里面我们每行都有序号,每一列都是对应了该列的数据,比如性别,姓名,身份证号码...;那么,在数据库中,或者说在廖老师设计的这个博客程序里面,我们每行记录需要的内容是什么呢?

需要每列的列名,每行确定的序号,每个方格里面的数据类型,以及方格里面的内容。

P.S:以上内容仅个人理解,可能有误!具体应该去了解数据库相关知识~

所以,我们写了一个数据表的默认格式(理解为模板?模板里面对应的每一项都有对应的几个选项):

##字段类实现
# 父域,可被其他域继承
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)

就好像,我们需要知道这一列是记录什么的?(性别?身份证号码?性别?)这一列对应的某行的序号是多少?这一列对应的某行有没有默认的输入?(比如默认为空?)以及这一列的某一行的数据时存储的什么类型?

数据库主键

数据库主键,指的是一个列或多列的组合,其值能唯一地标识表中的每一行,通过它可强制表的实体完整性。主键主要是用于其他表的外键关联,以及本记录的修改与删除。

主键参考一下百度百科吧

另外关于数据库列属性,可以参考一下简书的一篇文章

然后我们需要来写对应的数据类型:

我们给数据类型设置几个选项吧!(整数型,浮点型,布尔型,字符串型,文本型):

###各种字段类型子类
class StringField(Field):
    # ddl("data definition languages"),用于定义数据类型
    # varchar("variable char"), 可变长度的字符串,以下定义中的100表示最长长度,即字符串的可变范围为0~100
    # (char,为不可变长度字符串,会用空格字符补齐)
    def __init__(self, name=None, primary_key=False, default=None, ddl='varchar(100)'):
        super().__init__(name, ddl, primary_key, default)

class BooleanField(Field):
    def __init__(self, name=None, default=False):
        super().__init__(name, 'boolean', False, default)

class IntegerField(Field):
    def __init__(self, name=None, primary_key=False, default=0):
        super().__init__(name, 'bigint', primary_key, default)

class FloatField(Field):
    def __init__(self, name=None, primary_key=False, default=0.0):
        super().__init__(name, 'real', primary_key, default)

class TextField(Field):
    def __init__(self, name=None, default=None):
        super().__init__(name, 'text', False, default)

对于name,primary_key,default的个人解释在上面就不说了。

写的这几个子类,就是数据类型的选项,以及在子类中确定下来,每种数据类型对应的属性!

元类(忍不住多看你一眼)

参考资料

请一定要去看以下两篇文章:

请参考如下文章参考文章

不过我更建议去看廖雪峰老师的教程


我的理解

为了理解元类在orm中究竟做了什么,我想到通过整个完整的http请求来找到元类的作用!

但是在此之前我的去想清楚,整个Model是干嘛的!

Model的个人理解

如同上面Field一样,我依旧把Model放到Excel表格里面去理解!

Field定义了每张数据表里面字段内容的属性,Model则是用来定义多张数据表的!

我理解为,Model,其实就是整个webapp里面所有涉及到的数据表有哪几张。比如user,comments,这样的几张数据表。那么,这个Model一定是要单独出去写一个专门的地方的。

emm,我们假设要注册一个用户,我的数据库里面有一张数据表叫User,需要的字段是用户名,密码,电话号码。

好,我假设有一个web页面用于注册账号,我们假设web服务器已经将该页面的所有输入信息解析并封装完成,然后它会发送给我们wsgi,wsgi会把接收到的包再度解包,然后在路由表的作用下,把包内的信息发送给我们User类,Python会自动的给User类里面的各参数填充信息。然后为了完成注册过程,我们肯定会把收到的用户信息填到数据库里面去。

现在,python解释器会在User的父类(Model)去找metaclass,没有找到就会在User父类的父类去找metaclass,【如果还没找到就默认用type类】。现在找到了,python会把User中的所有信息打包成字典传入找到的metaclas里面,然后metaclass遍历字典信息,并且将这些信息填到Field定义的那些字段中,创建一个User实例,然后在返回给Model基类,最后在Model基类中,将信息拼接成一个SQL命令语句,然后只需要连接数据库,并执行语句就完成了。

P.S:上述过程可能说的有些啰嗦复杂,并且有些过程我也不完全了解,只能说个大概!

根据上文所述,元类的作用其实就是接收到Models模块里面某一个具体类的字典信息,并且动态构造一个类出来,然后传给Model基类,然后Model再去执行数据库的操作!

建议结合models.py来看元类!

代码注解

User模块

class User(Model):
    __table__ = 'users'

    id = StringField(primary_key=True, default=next_id, ddl='varchar(50)')
    email = StringField(ddl='varchar(50)')
    passwd = StringField(ddl='varchar(50)')
    admin = BooleanField()
    name = StringField(ddl='varchar(50)')
    image = StringField(ddl='varchar(500)')
    created_at = FloatField(default=time.time)

上面是在models.py里面的user类,我把它拿了出来,主要是为了配合modelmetaclass相互理解。

class ModelMetaClass(type):
    def __new__(cls,name,bases,attrs):

        # 元类必须实现__new__方法,当一个类指定通过某元类来创建,那么就会调用该元类的__new__方法
        # 该方法接收4个参数
        # cls为当前准备创建的类的对象
        # name为类的名字,创建User类,则name便是User
        # bases类继承的父类集合,创建User类,则base便是Model
        # attrs为类的属性/方法集合,创建User类,则attrs便是一个包含User类属性的dict
        # 排除Model类本身,因为Model类主要就是用来被继承的,其不存在与数据库表的映射
        if name=='Model':
            return type.__new__(cls,name,bases,attrs)

在上述代码中,def __new__(cls,name,bases,attrs):接收到传入的参数,然后自定义了一个__new__方法。我们知道Model基类是用来进行数据库操作的,所以不能生成Model类,所以,写函数if name=='Model'来排除对Model类的修改。

        # 以下是针对"Model"的子类的处理,将被用于子类的创建.metaclass将隐式地被继承
        # 取出表名,默认与类的名字相同
        tableName=attrs.get('__table__',None) or name
        logging.info('found model(找到模型): %s (table: %s)' % (name, tableName))
        # 获取所有的Field和主键名
        mappings=dict()       # 用于存储所有的字段,以及字段值
        fields=[]      # 仅用来存储非主键意外的其它字段,而且只存key
        primaryKey=None     # 仅保存主键的key


        # 遍历类的属性,找出定义的域(如StringField,字符串域)内的值,建立映射关系
        # k是属性名,v其实是定义域!请看name=StringField(ddl="varchar50")
        for k,v in attrs.items():
            # attrs同时还会拿到一些其它系统提供的类属性,我们只处理自定义的类属性,所以判断一下
            # isinstance 方法用于判断v是否是一个Field
            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)    # 将非主键的属性都加入fields列表中
        # 保证了必须有一个主键,没有找到主键也将报错,因为每张表有且仅有一个主键
        if not primaryKey:
            raise RuntimeError("Primary key not found(没有找到主键)")
        # 这里的目的是去除类属性,为什么要去除呢,因为我想知道的信息已经记录下来了。
        # 去除之后,就访问不到类属性了
        # 记录到了mappings,fields,等变量里,而我们实例化的时候,如
        # user=User(id='10001') ,为了防止这个实例变量与类属性冲突,所以将其去掉
        for k in mappings.keys():
            attrs.pop(k)
        # 以下都是要返回的东西了,刚刚记录下的东西,如果不返回给这个类,又谈得上什么动态创建呢?
        # 到此,动态创建便比较清晰了,各个子类根据自己的字段名不同,动态创建了自己

在上述代码中,做了这样一件事儿!在对当前类(如例子User)中查找定义的类属性,如果找到一个Field属性,就把他放到mappings里面,如果有非主键属性,则放到fields里面,接下来还有一件事儿,就是去除收到的类属性。为什么要去除呢?因为我们创建的实例里可能有变量名与类属性名重名,会把类属性名覆盖,就会报错!

说一下个人理解类属性和创建的实例:

利用metaclass的原因就是根据收到的请求,动态的创建一个实例去修改数据库。

而我们收到的请求信息会首先在Models.py里面写好的类里面装配一次,然后才会把Fields相关信息用一个字典装起来。为什么是Fields字段信息,是因为只有Fields字段信息拼接后就是我们的SQL操作语句。而其他的cls,name,bases只是函数用来定位模型和数据表的。

然而我们创建了成功一个实例后,我们要把创建的实例返回给orm里面的Model基类,让基类里面的函数去执行这个实例。

所以类属性就是我们在models.py里面定义类的字段属性,而根据这个属性,我们用Metaclass创建了一个实例。

        # 以下都是要返回的东西了,刚刚记录下的东西,如果不返回给这个类,又谈得上什么动态创建呢?
        # 到此,动态创建便比较清晰了,各个子类根据自己的字段名不同,动态创建了自己
        # 下面通过attrs返回的东西,在子类里都能通过实例拿到,如self

        # 将非主键的属性变形,放入escaped_fields中,方便增删改查语句的书写
        escaped_fields = list(map(lambda f: '`%s`' % f, fields))
        attrs['__mappings__']=mappings  # 保存属性和列的映射关系
        attrs['__table__']=tableName
        attrs['__primaryKey__']=primaryKey  # 主键属性名
        attrs['__fields__']=fields  # 除主键外的属性名

        # 构造默认的select, insert, update, delete语句,使用?作为占位符
        attrs['__select__'] = 'select `%s`, %s from `%s`' % (primaryKey, ', '.join(escaped_fields), tableName)
        # 此处利用create_args_string生成的若干个?占位
        # 插入数据时,要指定属性名,并对应的填入属性值
        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基类

Model类中,就可以定义各种操作数据库的方法,比如save()delete()find()update等等。

#Model基类
# ORM映射基类,继承自dict,通过ModelMetaclass元类来构造类
class Model(dict,metaclass=ModelMetaClass):
     # 初始化函数,调用其父类(dict)的方法
    def __init__(self,**kw):
        super(Model,self).__init__(**kw)
    ## 实现__getattr__与__setattr__方法,可以使引用属性像引用普通字段一样  如self['id']
    # 增加__getattr__方法,使获取属性更方便,即可通过"a.b"的形式
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model'对象没有'%s'属性" % key)
    # 增加__setattr__方法,使设置属性更方便,可通过"a.b=c"的形式
    def __setattr__(self, key, value):
        self[key]=value

    # 通过键取值,若值不存在,返回None
    def getValue(self,key):
        value = getattr(self,key,None)
        return value
    ##取默认值,上面定义了一个default属性,默认值也可以是一个函数
    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

# 一步异步,处处异步,所以这些方法都必须是一个协程
#下面 self.__mappings__,self.__insert__等变量据是根据对应表的字段不同,而动态创建
    async def save(self):
        # 我们在定义__insert__时,将主键放在了末尾.因为属性与值要一一对应,因此通过append的方式将主键加在最后
        args=list(map(self.getValueOrDefault,self.__mappings__))     #使用getValueOrDefault方法,可以调用time.time这样的函数来获取值
        args.append(self.getValueOrDefault(self.__primary_key__))
        rows = await execute(self.__insert__,args)
        if rows != 1:     #插入一条记录,结果影响的条数不等于1,肯定出错了
            logging.warning('failed to insert record: affected rows: %s' % rows)


    async def update(self):
        # 像time.time,next_id之类的函数在插入的时候已经调用过了,没有其他需要实时更新的值,因此调用getValue
        args = list(map(self.getValue, self.__mappings__))
        args.append(self.getValue(self.__primary_key__))
        rows = await execute(self.__update__, args)
        if rows !=1:
            logging.warning('failed to update by primary key: affected rows: %s' % rows)


    async def remove(self):
        args = [self.getValue(self.__primary_key__)]
        rows = await execute(self.__delete__, args)
        if rows != 1:
            logging.warning('failed to remove by primary key: affected rows: %s' % rows)


    # classmethod装饰器将方法定义为类方法
    # 对于查询相关的操作,我们都定义为类方法,就可以方便查询,而不必先创建实例再查询
    @classmethod
    # 我们之前已将将数据库的select操作封装在了select函数中,以下select的参数依次就是sql, args, size
    async def find(cls,pk):
        '按主键查找对象'
        rs = await select('%s where `%s`=?' % (cls.__select__, cls.__primaryKey__), [pk], 1)
        if len(rs) == 0:
            return None
        # 注意,我们在select函数中,打开的是DictCursor,它会以dict的形式返回结果
        return cls(**rs[0])  # 返回的是一个实例对象引用

    @classmethod
    async def findAll(cls,where=None,args=None,**kw):
        '按where子句查找对象'
        sql=[cls.__select__]
        # 我们定义的默认的select语句是通过主键查询的,并不包括where子句
        # 因此若指定有where,需要在select语句中追加关键字
        if where:
            sql.append('where')
            sql.append(where)
        if args is None:
            args=[]
        orderBy = kw.get("orderBy", None)
        # 解释同where, 此处orderBy通过关键字参数传入
        if orderBy:
            sql.append("order by")
            sql.append(orderBy)
        # 解释同where
        limit = kw.get("limit", None)
        if limit is not None:
            sql.append("limit")
            if isinstance(limit, int):
                sql.append("?")
                args.append(limit)
            elif isinstance(limit, tuple) and len(limit) == 2:
                sql.append("?, ?")
                args.extend(limit)
            else:
                raise ValueError("Invalid limit value: %s" % str(limit))
        rs = await select(' '.join(sql),args)    #没有指定size,因此会fetchall
        return [cls(**r) for r in rs]

    @classmethod
    async def findNumber(cls, selectField, where=None, args=None):
        '通过select和where查找号码。'
        sql = ['select %s _num_ from `%s`' % (selectField, cls.__table__)]
        if where:
            sql.append('where')
            sql.append(where)
        rs = await select(' '.join(sql), args, 1)
        if len(rs) == 0:
            return None
        return rs[0]['_num_']

声明:ITanger|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 廖雪峰实战之ORM框架个人学习笔记


Carpe Diem and Do what I like