前言:
我們在操作數據庫時候一般都是通過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)
未完待續。。。