编写ORM-解析

个人笔记:仅做学习记录

    ORM-用来建立数据库和python的中间件,是任务逻辑层和数据库层的桥梁,就是用来简化对数据库进行的增删改查操作,将每一个表实例化为一个对象,每个这个对象中都有自己定义的增删改查的方法和各种属性,对这个表结构性操作的时候就只需要调用不同的操作方法就好了,不同的表就是不同的实例化对象。简化了数据库的学习,不需要创建数据库,不需要编写数据库访问层代码,执行数据库操作就是调用不同表的不同方法。具体解释见前一个博客。

    我需要实现的是可以实现如下代码:

    from orm import Model,StringField,IntegerField

    class User(Model):

        __table__ ='user'

        id = InterField(primary_key = True)  #类INtegerField的实例化对象

        name = StringField()

就是可以创建数据库的表对应类,这个类继承自Model基类,这个基类创建的过程又是根据metaclass=ModelMetaclass这个元类来创建的,每一个Model的子类也继承了这个元类,也就是在子类创建的时候,也会自动去调用metaclass这个属性,来修改这个子类的创建。上面orm模块就是自己定义的。所以我们需要做的就是:

创建元类ModelMetaclass:元类是所有后面的元类,比如Model,比如后面继承Model的子类,元类中包含所有设计到数据库操作的方法、属性。

再创建基类Model:Model继承自dict类,也就是所有数据库数据都是类似dict对象,所以需要继承这个类,然后又可以根据元类来对dict类中的相关属性、方法修改。

创建StringField,IntegerField类:指的后面表中(类中)不同属性的类型,比如整数类型,字符串类型。相当于之前建立一个整数型的id属性,每一个这个创建出的整数实际上就是int类的一个实例化对象(python中任何东西都是对象,包括元类也是自身的对象),我们这里就是需要自己定义int这个类。


数据库连接池

    由于大量的数据库的连接和关闭会导致资源损耗,如果可以在查询数据库之后 ,不关闭数据库连接,当别人使用时,把这个连接给别人用,就避免了一次建立数据库的连接。数据库连接池的基本思想是为数据库连接建立一个“缓冲池”。预先在缓冲池中建立一定数量的连接,当需要建立数据库连接时 ,只需要从缓冲池中取出一个,使用完毕后放回去,我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接(这里是网上搜索的关于连接池的解释)。但是这里的连接池并没有建立大量连接,好像是只要一个数据库连接?

传入参数pool和**kw,**kw指的接收不限个数个关键字参数,以dict形式存储参数,这里连接池创建函数传入的是host,port,user等连接的数据库信息,存入dict类型的kw变量中,再赋值给__pool全局变量。loop表示当前创建的事件循环。调用aiomysql.create_pool函数即可以连接到指定数据库上。返回给全局变量__pool存储连接池。这里就是只是打开一个连接,传入合适参数,后面其他地方需要打开数据库连接的时候,只用传入__pool全局变量即可,__pool就是一个已经打开的数据库连接,不需要每次都传入数据库参数,减少重复工作。


def log(sql,args=()):

    logging.info('SQL: %s' %sql)

定义log函数,传入的应该是数据库语句,这个函数在每个具体的数据库操作函数的开头调用,用来打印当前执行的sql语句。


select函数:数据库查询select函数,传入的参数是 sql语句,args参数是后面sql语句中占位符的参数列表,size是返回的结果行数。

 #获取数据库连接
    async with __pool.acquire() as conn:
        #获取游标,默认游标返回的结果为元组(每一项是另一个元组),这里可以通过aiomysql.DictCursor指定元组的元素为字典
        async with conn.cursor(aiomysql.DictCursor) as cur:
            #调用游标的execute()方法来执行sql语句,execute()接收两个参数,第一个为sql语句(可以包含占位符),第二个为占位符            #对应的值(也就是参数列表中传入的args),使用该形式可以避免直接使用字符串拼接出来的sql的注入攻击
            #sql语句的占位符为?,mysql里为%s,做替换
            await cur.execute(sql.replace('?', '%s'), args or ())
            #size有值就获取对应数量的数据
            if size:
                rs = await cur.fetchmany(size)
            else:
                #获取所有数据库中的所有数据,此处返回的是一个数组,数组元素为字典
                rs = await cur.fetchall()
        logging.info('rows returned: %s' % len(rs))
        #logging.info(rs)
        return rs

SQL注入攻击:要坚持使用带参数的SQL,为了防止SQL注入攻击,SQL注入即是指web应用程序对用户输入数据的合法性没有判断,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。使用带参数的SQL,就可以避免注入攻击。


execute函数:数据库修改操作(update,insert,delete),修改之后需要提交到数据库中,所以需要用try...except...进行错误判断,如果出现错误(任何错误都是基于BaseExcept)则判断是否autocommit,没有的话就回滚rollback到修改操作之前,在try中的操作是执行的修改操作,修改操作后会autocommit,如果没有则手动提交conn.commit。

async def execute(sql, args, autocommit=True):
    log(sql)
    async with __pool.acquire() as conn:
        if not autocommit:
            #如果不是自动提交事务,需要手动启动,但是我发现这个是可以省略的
            await conn.begin()
        try:
            async with conn.cursor(aiomysql.DictCursor) as cur:
                await cur.execute(sql.replace('?', '%s'), args)
                #获取增删改影响的行数
                affected = cur.rowcount            #没有自动提交则手动执行commit()命令
            if not autocommit:
                await conn.commit()
        except BaseException as e:
            if not autocommit:
                #回滚,在执行commit()之前如果出现错误,就回滚到执行事务前的状态,以免影响数据库的完整性
                await conn.rollback()
            raise
        return affected

参数中的autocommit设置为True表示是否自动提交事务,如果为True,就不需要再用commit来提交事务:就是所有的语句都是需要commit后,才会在真实数据库中生效。如果不是自动提交事务,就需要手动开始启动conn.begin()。

rollback   回滚就是数据库里做修改后 ( update  ,insert  , delete),未commit之前使用rollback,可以恢复数据到修改之前。


def create_args_string(num):
    L = []
    for n in range(num):
        L.append('?')
    return ', '.join(L#创建拥有几个占位符的字符串(???用在哪????)用在比如后面的insert中的(值1, 值2,....)这个部分就要根据传入的参数替换,最多有总列数的参数,所以需要提前用有总列数num个占位符'?'的字符串放在(值1, 值2,....)相应的位置。


Field 类:为了保存数据库列名和类型的基类(所有表中field的类型的基类)

我们构建每个表都是先构建整个表的框架,每个表的类中需要放入的就是有哪些列,这些列的名称也不同,并且有特定的类型,不同的列名可以根据这个列中的数据的数据类型选择不同的Field类型(比如intField)来实例化,不同的列的类型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)

基类先设置每个列都需要的属性,比如是否主键,默认值;其他继承的不同Field再根据自己的类型重写初始化参数。每个表的列名实例后,每个表的实例化就相当于在表中添加具体的每一行。

__init__函数传入列名,类型,是否为主键,默认值;__str__函数(在使用print语句时被调用,调整输出语句的格式)打印的是

<表名,列类型:列名>

StringField类:具体的列名的数据类型,映射varchar的StringField。ddl指的表中这一列的数据的类型设置(字符型varchar)

FloatField类:浮点数的类型的column_type为何设置的'real'?


元类ModelMetaclass:所有自己的定义类的元类,在其他设置了metaclass="元类名称"的地方会拦截类对象的创建,调用Metaclass类,根据元类设置修改类定义,返回修改后的类。

tableName = attrs.get('__table__', None) or nam

tableName就是保存表名,通过get获取dict对象attr中的__table__属性,如果为真则输出结果,否则把类名name作为表名。

其中完美用到了短路逻辑:

    or运算符:表达式从左到右进行运算,若or左侧True,则短路后面所有的表达式(不管是or还是and,因为只要有or,左侧为真则所有为真,赋值的话就只输出左侧表达式即可),若左侧为False,则不能使用短路逻辑。

    and运算符:表达式从左到右进行运算,若and左侧为False,则短路后面所有and表达式(因为一侧为假则所有为假),直到有or出现,输出and左侧表达式到or左侧,参与接下来运算。

#保存列类型的对象
mappings = dict()

mapping:保存列名和类型的字典类型,这里存放的是attrs.items()中取出的键值元组数组,这个键值数组指的是(列名:列类型)组。即v指的比如StringField类,所以才有v.primary_type。

#保存列名的数组
fields = []

fields:放入的是列名,也就是这个表中所有的列(除主键外)。

        #存放主键的属性,初始值为None        primaryKey = None        dict对象的items中为键值对,循环取出存放在k(键),v(值)中
        for k, v in attrs.items():
            # 因为attrs中传入的是类的方法属性的集合,在属性中有id=IntergerField这样的属性,            # 其中id就是k,IntergerField就是v下面的判断是判断传入参数,            # 将field类型的属性(也就是列名(列名都是基于field类)和值)保存下来            if isinstance(v, Field):
                logging.info('  found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
                if v.primary_key:   #因为在field中需要传入是不是主键的参数属性primary_key,所以可以利用这个判断
                    # 找到主键,进入到这里就表示当前为主键,判断在此之前primaryKey是否为真,为真则表示主键不唯一错误。
                    if primaryKey:
                        raise StandardError('Duplicate primary key for field: %s' % k)
                    primaryKey = k
                else:
                    #保存非主键的列名
                    fields.append(k)
        if not primaryKey:
            raise StandardError('Primary key not found.')

        for k in mappings.keys():     #循环所有的键
            attrs.pop(k)              #删除所有键,也就是列名
??为什么删除所有键为什么???看看后面用到mapping的地方。

escaped_fields = list(map(lambda f: '`%s`' % f, fields))

['`列名`','`列名`','`列名`','`列名`']这个语句就是将fields中的转换为以下左边list的形式。本来fields也是list,在每个类名外加了反引号。作用????

attrs['__mappings__'] = mappings # 保存属性和列的映射关系

保存对象的属性到内置的属性名称中。


attrs['__select__'] = 'select `%s`, %s from `%s`' % (primaryKey, ', '.join(escaped_fields), tableName)

select 列名称 from 表名称

构造SELECT语句(要执行SELECT语句,就要用之前设置的select函数执行,select函数传入的参数是SQL语句和SQL参数,其中主要涉及的语句是:yield from cur.execute(sql.replace('?','%s'),args or ()),这个语句是用当前函数传入的sql语句替换SQL语句中的'%s'参数)??所以这句SQL语句中的%s就是需要替换的地方??加了反引号的`%s`就是传入的主键和表名。反引号的作用是为了避免与sql关键字冲突。因为列名称这个位置放入的是所有的列名,escaped_fields中缺少主键,所以黄字部分表示的是全部的列名称(每个列名之间用,间隔)。


 attrs['__insert__'] = 'insert into `%s` (%s, `%s`) values (%s)' % (tableName, ', '.join(escaped_fields), primaryKey, create_args_string(len(escaped_fields) + 1))

INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....)

红字的部分是插入的值,如果黄字部分有替换的参数的话,就在相应的列中插入红字中的值。insert语句

create_args_string(len(escaped_fields) + 1)) 这个语句是调用的前面创建占位符的函数,创建有num个占位符的字符串,num也就是总列数。


 attrs['__update__'] = 'update `%s` set %s where `%s`=?' % (tableName, ', '.join(map(lambda f: '`%s`=?' % (mappings.get(f).name or f), fields)), primaryKey)

UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值

','.join(map(lambda f: '`%s`=?' % (mappings.get(f).name or f),fields))这里返回的是总行数-1(因为update操作不能更新主键)个'`%s`=?'对,每一个'`%s`=?'对用,隔开,`%s`就是列类型的名字(也就是比如StringField的name属性,其实就是列名,默认值为None),如果没有就返回f,也是列名。WHERE后面是查找更新的是哪一行,用主键最方便。


类方法:@classmethod

类方法可以通过类调用,实例方法通过实例调用,但是实例方法也是所有子类都可以直接继承的啊?那类方法的优点是什么?用在什么地方比较合适,为什么有的地方用类方法,有的用实例方法。后面的具体见下节。


运行测试

关于orm.py运行测试,需要先建立数据库mySchool和表firstSchool,命令如下:

service mysql start

mysql -u root -p

#输入密码,进入mysql工作区

mysql> create database mySchool;

Query OK, 1 row affected (0.00 sec)

#建立数据库

mysql> USE mySchool

Database changed

#建立表,括号中为field和类型

mysql> create table teacher(id int(3),name char(10)); 

Query OK, 0 rows affected (0.08 sec)

或者可以利用前面的教程数据库mysql中利用python连接数据库,建立表,插入数据。

1、按代码运行出现如下错误:

AttributeError: 'Connection' object has no attribute '_writer'

charset=kwargs.get('charset', 'utf8')

这里修改utf-8为utf8,是数据库错误据说。

2、重新运行出现如下界面:

heh:[{'name': 'hubu', 'id': '1'}]

...

RuntimeError: Event loop is closed

其实这个也不算错误,如果想看到没有提示错误的运行结果可以在orm.py最后加句 loop.run_forever(),指一直运行协程,就不会出现报错循环关闭的情况。

后面其实可以写一个main函数调用前面的各种方法来进行数据库的操作。

PS:

建表:model实例化类,再实例化对象(表)

表中数据:各种field的实例化

你可能感兴趣的:(编写ORM-解析)