前言:
我们在操作数据库时候一般都是通过sql代码来操作mysql数据库中相关数据,这就需要懂得sql语句,那么怎么样才能在不懂sql语句的情况下通过我们所学的python代码来实现对mysql数据库的操作?
当然有这种神奇的操作,其中之一就是今天深入了解的ORM对象关系映射(Object Relational Mapping),本文主要通过python代码来自己实现mysql数据库的对象关系映射,达到对前面所学知识的巩固与加深。
先来说说具体映射关系:(记住这个关系,在后面具体代码实现的时候会用到)
ORM:对象关系映射: 类 =======> 数据库的一张表 对象 =======> 表的一条记录 对象点属性 =======> 记录某一个字段对应的值
上面关系分析:
通过python中创建类来实现对数据库一张表结构的关系产生一种一一对应关系
通过python中对创建的类实例化的对象操作对数据库一张表进行表记录的操作的一一对应关系
通过python中对象点属性的方式来操作记录表中某一字段的对应值,的一一对应操作关系
首先来通过代码层面来映射数据库表字段的类型:
# 定义一个类,在映射数据库中的表结构: class Field(object): # 先定义一个表结构字段类,比如 字段名name、字段类型column_type、字段是否为主键primary_key、字段默认值default 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 # 当然字段的类型很多,可以单独设特殊的字段类:比如varchar、int字段类型,让它继承FIeld类就行 class StringField(Field): # 定义字段类型varchar # 将字段类型指定为:varchar(255),主键默认为False,默认值为None def __init__(self, name, column_type='varchar(255)', primary_key=False, default=None): # 让它重写__init__的基础上其他地方继承它的基类Field里面的__init__方法 super().__init__(name, column_type, primary_key, default) class IntegerField(Field): # 定义字段类型int def __init__(self, name, column_type='int', primary_key=False, default=None): super().__init__(name, column_type, primary_key, default)
暂时先创建2种常见类型的字段类型类
接着来看看如何映射数据库表的结构:
# 创建一个字典对象的过程:t1 = dict(name='sgt', age=18, sex = 'male') # 让Models类继承字典这个类,这样Models类就继承了dict类的方法(把一堆关键字参数传进去,返回一个字典)的实例化过程 class Models(dict): def __init__(self, **kwargs): super().__init__(**kwargs) # 除了继承dict类的方法,我们还需要拥有更多方法,比如当传入的参数通过对象点(传入参数关键字名)的方法得到参数的 # 关键字值,通过点参数名=参数值来新增传入的关键字参数 # 继续分析:传入参数是关键字形式(name='sgt',age = 18...),但是参数不是类中属性,如果想要通过实例化出的对象点 # 这个参数key的方式得到value值的目的,可以使用__getattr__来实现,也就是说,实例化出的对象在点这个key # 时候,触发了__getattr__方法,方法返回self.get(key),这里的self就是继承dict类通过传入关键字参数返回的字典 # 类型的的对象,通过点get()就能获得对应的value值。 def __getattr__(self, item): # 在对象获取它没有的属性和方法的时候触发 return self.get(item) # item就是传入参数的k # 既然可以点k的方式得到value,那么还可以点新key=值的方法来增加传入的关键字参数 def __setattr__(self, key, value): # 在对象点属性=值的时候自动触发 self[key] = value # 通过上面的__getattr__和__setattr__的方法实现了实例化出对象的方式让传入的参数返回给对象一个字典的 # 同时又可以让这个对象点关键字中的key得到value值,点key=value值来新增或者设置值的目的 # 这里插一嘴:为何要实现这个目的?因为我们通过pymysql模块实现操作数据库返回来的数据类型基本都是字典类型外面 # 套列表的形式,那么如果想办法将查询的结果也变成一个字典对象,那么查询里面的key(字段名)和value(字段记录值) # 就特别方便了,同时在新增和插入数据时候会用到这个方法,达到更简单明了的目的。
上面只是实现了我么在操作表记录方面的某些功能,但是我么知道还没有达到映射数据库表结构的目的
怎么做呢?想想我们的目的:在映射表结构的时候这个表结构应该有哪些东西?
回答:表的字段名们,表的主键是哪个字段,当然还有表名,好像有了这3个关键性的因素映射数据库表结构就差不多达到目的了。
那么如何才能实现我们在创建一个映射表结构的一个类的同时这些我们想要的因素都能自动产生呢?
说到自动,又说道创建类的时候,我想我们可以往元类上面想了,前面学习元类的时候我们就可以拦截类的创建过程,在这个过程中加入或者修改,达到我们想要的目的。
所以说拦截类的创建过程是关键,类创建过程会触发啥?答案是:元类的__new__方法
既然要拦截,肯定是不让元类的__new__生效,让我们自己定义一个__new__或者说在元类的__new__触发之前自己通过自定义__new__来加入一些我们需要的然后再走元类的__new__,此时就能达到目的了。
# 对指定Models的元类为MyMeta class MyMeta(type): # 自定义元类必须继承type才能算是元类,否则就是普通的类 def __new__(cls, *args, **kwargs): print(cls) print(args) print(kwargs) class Models(dict, metaclass=MyMeta): def __init__(self, **kwargs): super().__init__(**kwargs) def __getattr__(self, item): return self.get(item) def __setattr__(self, key, value): self[key] = value Myname = 'sgt'
# 这里创建了类Models的时候,就触发了我们自定义元类中的__new__方法,所以右键就会执行打印,结果依次是 # <class '__main__.MyMeta'> # ('Models', (<class 'dict'>,), {'__module__': '__main__', '__qualname__': 'Models', # '__init__': <function Models.__init__ at 0x0000025A17B19BF8>, '__getattr__': # <function Models.__getattr__ at 0x0000025A17B19C80>, '__setattr__': <function Models.__setattr__ at # 0x0000025A17B19D08>, 'Myname': 'sgt', '__classcell__': <cell at 0x0000025A17A87618: empty>}) # {} # 第一行打印的是Models的类 # 仔细看第二行:第一个是Models--类名,第二个是dict这个类--也就是Models的基类,第三个是个字典,看看字典里的 # 内容,一眼瞅过去好像是一个类里面的内置属性和自定义属性(因为看到了Myname这个变量) # 最后一行就{},关键字参数没传啥。 # 最后分析一下:创建类的时候我们拦截了类的创建过程,自定义了元类,在类创建的时候让它走了我们自定义元类里面的 # __new__方法,这样,Models这个类一'class'开始申明就开始准备走__new__方法,接着我们看了打印的各个参数: # 分别是cls-创建的类自己、类名、类的基类们、类属性字典,所以既然类在创建时候会在__new__传入这些参数,那么我们
# 将这些参数进一步明了化一下: class MyMeta(type): # 自定义元类必须继承type才能算是元类,否则就是普通的类 def __new__(cls, class_name, class_bases, class_attrs): print(cls) print(class_name) print(class_bases) print(class_attrs) class Models(dict, metaclass=MyMeta): def __init__(self, **kwargs): super().__init__(**kwargs) def __getattr__(self, item): return self.get(item) def __setattr__(self, key, value): self[key] = value Myname = 'sgt' # 右键再次运行一下,发现打印的结果一模一样,至此我们进一步明确化了__new__的实质了,接下来开始实现我们的初衷 # 在类创建的时候为这个类添加默认的属性:映射表名、映射表的主键字段名、映射表的自定义属性(字段名、对应字段值)
拦截类的创建,加入默认表结构属性
开始拦截类的创建(表结构映射的创建)
class MyMeta(type): def __new__(cls, class_name, class_bases, class_attrs): # 我们要知道一件事:我们只需要设置我们自己定义(创建类时候你写的属性)属性,其他建类时候默认的一些内置属性 # 我们是不需要的,或者说我们可以将自己定义属性集中在一个字典中,这个字典我们起个名字:mappings # # __new__拦截了哪些类的创建:Models、Models的子类,很显然Models类我们无需拦截,因为我们创建表结构映射的类 # 并不是Models,而应该是继承了Models的一个类,所以需要排除Models if class_name == 'Models': return type.__new__(cls, class_name, class_bases, class_attrs) # 开始部署自定义的类属性: # 表名:我们在创建类体代码的时候会设置个属性叫table_name=***,如果没有设置,默认为类名 table_name = class_attrs.get('table_name', class_name) primary_key = None # 后面要找出主键是哪个字段名,这里先设置个空 mappings = {} # 这个mappings就是我们需要找出的自定义字段名和对应相关参数 # class_attr={'table_name':'user','id':IntegerField(name='id', primary_key=True),'name':StringField(name='name')...) for k, v in class_attrs.items(): if isinstance(v, Field): # 用字段类型是否属于它的基类Field来过滤得到我们想要的自定义的属性 mappings[k] = v if v.primary_key: # 如果该字段类型中primary_key=True # 在for循环中,因为最初primary_key是None,当第一次找到primary_key时候,将primary_key赋值给该字段名,当下次在for循环 # 中找到primary_key同时primary_key不为空时候就代表又找到了第二个primary_key,此时必须的抛异常,因为一张表不能有2个primary_key if primary_key: raise TypeError('一张表只能有一个主键') primary_key = v.name for k in mappings.keys(): # 代码健壮性行为 class_attrs.pop(k) # 前面将表中自定义字段集中在mappings里了,此时外面的class_attrs中的内置默认属性中的自定义字段 # 为了避免重复需要删除。 if not primary_key: # 如果遍历完了class_attrs还没有找到primary_key,也需要抛异常,一张表必须要有一个主键 raise TypeError('一张表必须有一个主键') # 最后将我们自定义的属性(表名、字段名和字段类型的类对象、主键字段名)加入class_attrs(创建这个类的初始属性中) class_attrs['table_name'] = table_name class_attrs['primary_key'] = primary_key class_attrs['mappings'] = mappings # 加进去之后,我们仅仅是拦截__new__来达到这个目的,关于创建类的其他全部过程还是该怎么走怎么在,交个元类去做 return type.__new__(cls, class_name, class_bases, class_attrs) class Models(dict, metaclass=MyMeta): def __init__(self, **kwargs): super().__init__(**kwargs) # 要搞清楚这里的self是啥?我们肯定知道是个对象,这个对象有点特别,他是个字典对象,因为Models继承了dict的方法 def __getattr__(self, item): return self.get(item) def __setattr__(self, key, value): self[key] = value
接下来开始实现对表的查、改、增:新建一个python文件:mysql_singleton
import pymysql class Mysql(object): def __init__(self): self.conn = pymysql.connect( # 建立数据库连接 host='127.0.0.1', port=3306, user='root', password='123', database='youku_01', charset='utf8', autocommit=True ) self.cursor = self.conn.cursor(pymysql.cursors.DictCursor) # 建立游标连接 def close_db(self): # 健壮性补充 self.cursor.close() self.conn.close() def select(self, sql, args=None): # 查 self.cursor.execute(sql, args) res = self.cursor.fetchall() return res # 返回结果为:[{},{},{}...] def execute(self, sql, args): # insert、update操作方法,查最多是查不到结果为空,但是改和增的话如果 # 出问题的话有可能达不到我们想要的结果,所以需要捕获异常,让我们能知道修改成功与否 try: self.cursor.execute(sql, args) except BaseException as e: print(e) _instance = None @classmethod # 实现单例,减小内存空间的占用 def singleton(cls): if not cls._instance: cls._instance = cls() return cls._instance
然后封装一个个方法:
查:
from mysql_singleton import Mysql # 导入刚才新建的文件中的类 @classmethod def select(cls, **kwargs): # cls:创建的表结构关系映射的类 ms = Mysql.singleton() # 创建一个Mysql单例对象ms,通过ms来点类中方法实施对应操作 if not kwargs: # 如果不传关键字参数,说明查询是select * from 表名 sql = 'select * from %s' % cls.table_name res = ms.select(sql) # 如果传入关键字参数:select * from 表名 where 字段名k=传入字段v else: k = list(kwargs.keys())[0] # 拿出关键字中的k v = kwargs.get(k) sql = 'select * from %s where %s = ?' % (cls.table_name, k) # 此处最后一个?就是要传入的v,这里用?占位主要为了避免sql注入问题 sql = sql.replace('?', '%s') # 将?变成%s,解决sql注入问题不传,后面cursor.execute会自动识别%s传参 res = ms.select(sql, v) if res: return [cls(**i) for i in res] # 这里这样做的目的:res是查询到的结果[{},{},..],将其遍历,然后打散成x=1,y=2的形式当做参数传入 # 类参与实例化的过程得到一个对象(因为该类继承dict,所以得到的是个字典对象),这样就与我们一开始为何要将类 # 继承dict为何要在Models中写__getattr__和__setattr__方法相关联。
改:
修改表,和查一样,都是对sql语句的组合拼接,然后调用Mysql中的execute方法,来达到修改目的,这里需要注意一点,修改的操作都是建立在已经查到数据对象的基础上的,因为你查不到就无从改起。
class Models(dict): # 这里不需要使用类方法,因为前面查询数据时候我们是通过关键字参数来查询表记录的 # 也就是用类直接点select方法去直接查找表记录,无需实例化出对象,因为对象就是我们映射的表记录, # 而此时我们就是需要提前找到表记录,然后在表记录基础上修改某个字段对应的值,所以用对象方法是最恰当的 def update(self): # 需要注意的是此时的self是前面已经通过select方法找到的一表记录对象。 # 这个self.mappings就是这条表记录的所有字段和字段属性的字典(不是字段值,别弄错了) # 而这个self我们可以看做是个字典对象(self = {'id':1,'name':'sgt','passord':'123'....) # 记住,它是一条映射表中一条记录的字典对象(因为它的类继承了dict的方法),同时它的基类中 # 还有__getattr__和__setattr__两个方法,这样我们就能通过self点self中的key,来得到对应的value, # 通过self点key=新value来设置新的值,然后用这个修改了的对象来update。 ms = Mysql.singleton() keys = [] vaules = [] pr = None for k, v in self.mappings.items(): keys.append(v.name+'=?') # 得到该对象所有字段名(包括主键,因为修改记录也可以改id值),用'=?'占位 vaules.append(getattr(self, v.name)) #使用getattr方法得到所以对象所有字段名对应的值 if v.primary_key: # 找到主键,这里我们默认用主键来定位修改数据。 pr = getattr(self, v.name) # 使用getattr方法得到对象对应的字符串对应的属性值——主键的值 # 所有数据已经获取完毕,接下来开始拼接sql语句: # 先来看看update正常语句样式:update table set name='666',gender='male' where id=2; sql = 'update %s set %s where %s=%s' % (self.table_name, ','.join(keys), self.primary_key, pr) # 注意上面几个百分号对应的格式化输出参数的形式,第二个将keys列表通过‘,’.join方法,将列表用逗号隔开,就达到了set后面 # 的name='666'的样式,当然,这里的666被我们暂时用?占位了,因为?的地方不能直接就拼接上去,需要考虑下sql的注入问题。 # 到这里sql语句成这样了:sql = updata 表名 set name=?,gender=? where id=2; # 这里的问号继续用replace替换成%s就能开始使用sql方法修改数据了。这里细心的人肯定会问,我什么修改操作都没做,怎么做到修改字段值 # 的操作呢? # 其实这里面还有一步操作不是在这个方法里面做的,也就是修改字段名对应字段值的操作,继续分析修改的过程: # 先获取到一条记录的对象,这个对象是一个字典对象,由于继承了Models的所有方法(包括Models继承的dict方法,和Models内的__getattr__ # 和__setattr__方法),接下来所以我们可以通过点某个字段名=新字段值的过程来将已经获取到的记录的对象修改某个字段名对应的值。 # 当把需要修改的字段名替换完了之后,这个对象就是一个‘新的已经修改好了的对象’,它包含所有的字段值,不管是修改的还是没有修改的。 # 接下来将这个对象用上面的sql方法的语句全部update。没有修改的字段继续不变,修改的字段发生变化。从而达到update的目的。 sql = sql.replace('?', '%s') ms.execute(sql, vaules)
未完待续。。。