前言: 我們在操作資料庫時候一般都是通過sql代碼來操作mysql資料庫中相關數據,這就需要懂得sql語句,那麼怎麼樣才能在不懂sql語句的情況下通過我們所學的python代碼來實現對mysql資料庫的操作? 當然有這種神奇的操作,其中之一就是今天深入瞭解的ORM對象關係映射(Object Rela ...
前言:
我們在操作資料庫時候一般都是通過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__方法相關聯。
改: