跟着廖大学python之orm框架实现

废话少说

之前自己主要是做web前端的,web后台接触的很少,最近在学习廖大的python教程,课程最后有一个小项目,即完成一个个人博客,所以借此项目了解了解web后台到底在做什么?而有关后台接触的第一个新概念就是ORM。

ORM是什么?

ORM 即Object Relational Mapping,全称对象关系映射
可是它到底是干啥的呢?
如果接触过一些web后台的化,我们知道web后台有很大一部分工作都是数据的增删改查,如果每次操作数据库都要连接数据库,构造sql语句,执行sql语句的话,那未免太麻烦了,所以我们可以将数据库中的表,字段,行,与我们的面向对象编程中的类,类的属性,以及对象建立一一对应的映射关系,这样我们便可以避免直接操作数据库,而只要调用相应的方法就可以了。
拿我过去的做法举个例子就明白了,比如实现一个用户注册,以前我是怎么做的,前台拿到数据,传给后台,然后后台字符串拼接形成sql语句,后台执行。
而有了ORM以后,我只需要用数据实例化一个User对象,然后调用该对象的save方法,便保存到了数据库,作为使用者的角度,不需要操作一句sql语句。
假设User类对应users表

user=User(id="100001",name="Andy",password="*****")
user.save()  //保存到数据库
user=User.findById("100001") #从数据库中找出id为"100001"的用户
user.update(password="*********")  #更改id为"100001"的用户密码
users=User.findAll() #取出users表中全部数据

我就问,这样用起来不爽吗?

注意

以下涉及的代码均为本人学习廖雪峰python3教程的课程项目实践代码
IO操作均为异步,用到的异步库为asyncio
链接的数据库为mysql 5.7,用到的mysql异步IO驱动为aiomysql

实现ORM的必要准备---封装数据库操作

创建数据库连接池

import asyncio
import aiomysql       
async def create_pool(**kw):
global __pool
__pool=await 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),  # 池中最多有10个链接对象
    minsize=kw.get('minsize',1),
)

封装select方法

async def select(sql,args,size=None): //size可以决定取几条
global __pool
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()
    return rs

除了select方法要返回查询内容,剩下的update,insert,delete均只需返回一个影响行数,所以可将它们三个封装为一个execute方法

封装execute方法(update,insert,delete)

def execute(sql,args):
global __pool
try:
    with (await __pool) as conn:
        cur=await conn.cursor()
        await cur.execute(sql.replace('?', '%s'), args)
        affected=cur.rowcount
        await cur.close()
except BaseException as e:
    raise e
return affected

开始动手实现ORM

编程中有个思想叫做”自顶向下“。所以当你对如何设计ORM无从下手的时候,你可以假设已经有一个ORM框架,你想怎么用?

class Model(object):
    async def find(self):
       pass
class User(Model):
    # 注意这里的都是类属性
    __table__="users"
    id=StringField(...)
    name=StringField(...)
user=User(id="10001",name="Andy")
user.save()

有没有发现,这样看User类,很清楚,对应user表,这个表有哪些字段,一目了然。然后让子类继承父类,实现对find,save...等方法的复用。真是完美,可是要怎么实现呢?

字段类的实现
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>' % (self.__class__.__name__,self.name)
  class StringField(Field):
    def __init__(self,name=None,primary_key=False,default=None,ddl='varchar(100)'):
      super(StringField,self).__init__(name,ddl,primary_key,default)
  # 其它字段略,一个道理,一个模式
Model 类的实现
# 让Model继承dict,主要是为了具备dict所有的功能,如get方法
# metaclass指定了Model类的元类为ModelMetaClass
class Model(dict,metaclass=ModelMetaClass):
  def __init__(self,**kw):
    super(Model,self).__init__(**kw)
  # 实现__getattr__与__setattr__方法,可以使引用属性像引用普通字段一样  如self['id']   
  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):
    value=getattr(self,key,None)
            return value
  # 取默认值,上面字段类不是有一个默认值属性嘛,默认值也可以是函数
  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
            setattr(self,key,value)
    return value
   # 一步异步,处处异步,所以这些方法都必须是一个协程
   #下面 self.__mappings__,self.__insert__等变量据是根据对应表的字段不同,而动态创建
   @asyncio.coroutine
   def save(self):  
      args=list(map(self.getValueOrDefault,self.__mappings__))
      yield from execute(self.__insert__,args)
   @asyncio.coroutine
   def remove(self):
      args=[]
      args.append(self[self.__primaryKey__])
      print(self.__delete__)
      yield from execute(self.__delete__,args)
   @asyncio.coroutine
   def update(self,**kw):
      print("enter update")
      args=[]
      for key in kw:
        if key not in self.__fields__:
            raise RuntimeError("field not found")
      for key in self.__fields__:
        if key in kw:
            args.append(kw[key])
        else:
            args.append(getattr(self,key,None))             
      args.append(getattr(self,self.__primaryKey__))
      yield from execute(self.__update__,args)
   # 类方法
   @classmethod 
   @asyncio.coroutine
   def find(cls,pk):        
      rs = yield from select('%s where `%s`=?' % (cls.__select__, cls.__primaryKey__), [pk], 1)
      if len(rs) == 0:
        return None
      return cls(**rs[0])  # 返回的是一个实例对象引用
   @classmethod
   @asyncio.coroutine
   def findAll(cls,where=None,args=None):
      sql=[cls.__select__]
      if where:
        sql.append('where')
        sql.append(where)
      if args is None:
        args=[]
      rs=yield from select(' '.join(sql),args)      
      return [cls(**r) for r in rs]
元类的理解

根据上面的理解,因为数据库中每张表的字段都不一样,所以我们需要动态的生成类。python作为一门动态语言,可以很容易实现动态的创建类。实现动态创建类有俩种方法,一个是通过type()函数,另一个是通过元类
类是对象的模板,元类是类的模板。我们的User类继承自Model类,而Model类的模板是元类ModelMetaClass,所以当使用者实例化一个User对象的时候,User会根据Model去创建,而Model则根据ModelMetaClass动态创建,所以user对象间接的根据ModelMetaClass创建。

实现ModelMetaClass类
class ModelMetaClass(type):
    # 元类必须实现__new__方法,当一个类指定通过某元类来创建,那么就会调用该元类的__new__方法
    # 该方法接收4个参数
    # cls为当前准备创建的类的对象 
    # name为类的名字,创建User类,则name便是User
    # bases类继承的父类集合,创建User类,则base便是Model
    # attrs为类的属性/方法集合,创建User类,则attrs便是一个包含User类属性的dict
def __new__(cls,name,bases,attrs): 
    # 因为Model类是基类,所以排除掉,如果你print(name)的话,会依次打印出Model,User,Blog,即
    # 所有的Model子类,因为这些子类通过Model间接继承元类
    if name=="Model":
        return type.__new__(cls,name,bases,attrs)
    # 取出表名,默认与类的名字相同
    tableName=attrs.get('__table__',None) or name
    logging.info('found model: %s (table: %s)' % (name, tableName))
    # 用于存储所有的字段,以及字段值
    mappings=dict()
    # 仅用来存储非主键意外的其它字段,而且只存key
    fields=[]
    # 仅保存主键的key
    primaryKey=None
    # 注意这里attrs的key是字段名,value是字段实例,不是字段的具体值
    # 比如User类的id=StringField(...) 这个value就是这个StringField的一个实例,而不是实例化
    # 的时候传进去的具体id值
    for k,v in attrs.items(): 
        # attrs同时还会拿到一些其它系统提供的类属性,我们只处理自定义的类属性,所以判断一下
        # isinstance 方法用于判断v是否是一个Field 
        if isinstance(v,Field):
            mappings[k]=v
            if v.primary_key:
                if primaryKey:
                    raise RuntimeError("Douplicate primary key for field :%s" % key)
                primaryKey=k
            else:
                fields.append(k)
    # 保证了必须有一个主键
    if not primaryKey:
        raise RuntimeError("Primary key not found")
    # 这里的目的是去除类属性,为什么要去除呢,因为我想知道的信息已经记录下来了。
    # 去除之后,就访问不到类属性了,如图
image.png
    # 记录到了mappings,fields,等变量里,而我们实例化的时候,如
    # user=User(id='10001') ,为了防止这个实例变量与类属性冲突,所以将其去掉
    for k in mappings.keys():
        attrs.pop(k)
    # 以下都是要返回的东西了,刚刚记录下的东西,如果不返回给这个类,又谈得上什么动态创建呢?
    # 到此,动态创建便比较清晰了,各个子类根据自己的字段名不同,动态创建了自己
    # 下面通过attrs返回的东西,在子类里都能通过实例拿到,如self
    attrs['__mappings__']=mappings
    attrs['__table__']=tableName
    attrs['__primaryKey__']=primaryKey
    attrs['__fields__']=fields
    # 只是为了Model编写方便,放在元类里和放在Model里都可以
    attrs['__select__']="select %s ,%s from %s " % (primaryKey,','.join(map(lambda f: '%s' % (mappings.get(f).name or f ),fields )),tableName)
    attrs['__update__']="update %s set %s where %s=?"  % (tableName,', '.join(map(lambda f: '`%s`=?' % (mappings.get(f).name or f), fields)),primaryKey)
    attrs['__insert__']="insert into %s (%s,%s) values (%s);" % (tableName,primaryKey,','.join(map(lambda f: '%s' % (mappings.get(f).name or f),fields)),create_args_string(len(fields)+1))
    attrs['__delete__']="delete from %s where %s= ? ;" % (tableName,primaryKey)
    return type.__new__(cls,name,bases,attrs)

我的疑问

关于元类这块的代码,我只是理解了廖大教程的代码,并且跟着教程自己实现了一遍,但是让我自己去写,ORM我肯定想不到用元类什么的。其实我一直有个疑问,因为我觉得仅仅通过子类继承父类就可以实现,为何一定要用元类呢?就是按照廖大教程的思路走下来,用元类很好,没问题,很清晰,但是下来想一想,我觉得只需要用继承就可实现,。大概像下图这样,不知道这样有什么大问题?

image.png

更新

针对上述问题,我在知乎上请教了python大神,回答如下:

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399
  • 我觉得人,能够吃,真的是一件非常幸福的事情。 今天阴天,不知道怎么回事我突然想起来很久之前我吃过的一顿饭。我记得非...
    你说的黑是什么黑阅读 251评论 1 1
  • 一念起,万水千山。 一念灭,沧海桑田。 一念堕,万劫不复。 一念生,百废俱兴。 一念存,亘古不灭。 一念善,四海升...
    演員Wyh阅读 263评论 0 1
  • 今天是上班以来感觉最累的一天。也许是事情实在太多,又或许是上上级总是很莫名奇妙交代一些任务,而这些任务其实并不在职...
    Anges阅读 228评论 0 0