最近開發(fā)一個內(nèi)部的記錄系統(tǒng),其中有一個需求要求將所有數(shù)據(jù)庫操作記錄下來,為此想了一些方案.記錄一下.
思路演化
這個需求出來的一瞬間我就否定了在業(yè)務(wù)邏輯層保存操作記錄的方案,我認為這樣耦合度比較高,成本也太高. 代碼也會大量重復(fù).
Django的ORM操作中,刪除操作會調(diào)用models.Model的delete方法,增改會調(diào)用save方法,修改這些方法能夠覆蓋除了查詢以外的所有ORM操作(查詢暫時跳過),修改save和delete的方法無外乎就是類繼承,裝飾器.
我也考慮了使用signal系統(tǒng),但是這樣依然要在業(yè)務(wù)邏輯層處理發(fā)送信號的問題,感覺更復(fù)雜一些(比如對照修改前后的數(shù)據(jù)不如直接在Model中操作方便,Model的save方法在存盤之前,self是待存數(shù)據(jù),根據(jù)self.pk從db中可與取出數(shù)據(jù)是舊數(shù)據(jù)方便對比.如果要在signal中拿到兩份數(shù)據(jù)比較麻煩,可能需要在業(yè)務(wù)層做更多的斟酌).
繼承
我首先嘗試了類繼承的方法
class TopSecret(models.Model):
class Meta:
db_table = "絕密文件"
name = models.CharField(max_length=32)
content = models.TextField()
這是原始的model,我改寫成了如下
class Logger(models.Model):
def save(self, *args, **kwargs):
print("Do some log")
super(Logger, self).save(*args, **kwargs)
class TopSecret(Logger):
class Meta:
db_table = "絕密文件"
name = models.CharField(max_length=32)
content = models.TextField()
我覺得這樣應(yīng)該可以的.然而在調(diào)用save()方法時出現(xiàn)錯誤OperationalError: no such table: logged_logger,可以看出,我在原始model定義的Meta信息失效了,框架轉(zhuǎn)而在Logger類中尋找Meta,未找到的情況下使用了框架的默認值.
我嘗試將super(Logger, self).save(*args, **kwargs)改成super(self.__class__, self).save(*args, **kwargs),這樣super又成了調(diào)用TopSecret父類Logger的save(),如此反復(fù)形成了循環(huán)調(diào)用報錯.
我仔細想了一下,Model類尋找Meta的邏輯是肯定不去修改的,修改這個顯得不劃算,也違反了不隨便改框架的基本原則,當時在此我轉(zhuǎn)向了裝飾器方法,而放棄了類繼承.
今天我寫這篇文章的時候隱約想起一件事情,Model好像有一個abstract的屬性,果然如此.定義這個Meta信息之后,框架會認為這是一個抽象類,而不是數(shù)據(jù)模型,完美解決了問題.
class Logger(models.Model):
class Meta:
abstract = True
def save(self, *args, **kwargs):
print("Do some log")
super(Logger, self).save(*args, **kwargs)
class TopSecret(Logger):
class Meta:
db_table = "絕密文件"
name = models.CharField(max_length=32)
content = models.TextField()
In [1]: from logged.models import TopSecret
In [2]: obj = TopSecret(name="123",content="測試內(nèi)容")
In [3]: obj.save()
Do some log
In [4]:
在想起abstract之前我還想過其他的方案,比如單獨增加log類.這樣可以避免在Model父類和子類之間增加一層,解決了Meta信息的問題.
class Logger(object):
def save(self, *args, **kwargs):
print("Do some log")
models.Model.save(self, *args, **kwargs)
class TopSecret(Logger, models.Model):
class Meta:
db_table = "絕密文件"
name = models.CharField(max_length=32)
content = models.TextField()
這樣做有好處也有壞處,好處是Logger不再繼承Model,算是解耦合增加了代碼的可讀性,壞處是我看Logger那里調(diào)用save方法的方式比較別扭,將實例方法當做靜態(tài)方法調(diào)用手動傳入實例有一種很違和的感覺,不過總算是能工作了.
裝飾器
裝飾器是當時類繼承沒有成功,我走的另一條路.
首先因為我們的裝飾器不可能裝到框架代碼里去,只能在我們定義的Model模型上使用類裝飾器.當時我的實現(xiàn)是使用django自帶的method_decorator.這個函數(shù)可以將函數(shù)裝飾器變成方法裝飾器,裝飾到一個類的方法上,比較常見的用法是為dispatch方法去除csrf保護.
但是使用這個方法會有一個問題,那就是寫一個函數(shù)裝飾器本身是不會取到類的實例本身的.還需要為save方法傳入類的實例本身才能取到類數(shù)據(jù)進行日志操作,不行,不夠優(yōu)雅.
怎么辦?只能直接寫類裝飾器了.
之前沒寫過類裝飾器,其實類裝飾器和普通函數(shù)裝飾器一樣,思路和繼承的寫法也是一樣的.
def cbd_logger(obj):
if hasattr(obj, "save"):
save = obj.save
def _save(self, *args, **kwargs):
print "do some log %s" % self.name
return save(self, *args, **kwargs)
setattr(obj, "save", _save)
return obj
@cbd_logger
class TopSecret(models.Model):
class Meta:
db_table = "絕密文件"
name = models.CharField(max_length=32)
content = models.TextField()
值得注意的是,_save中不能直接
return obj.save(self,*args,**kwargs),這么做會導(dǎo)致運行時調(diào)用當前實例的save方法,也就是_save本身,搞成無限遞歸
我們分別打印一下cbd_logger下這些方法的id看一下
>>>obj.save()
save 90220624
obj.save 106285376
self.save 106285376
在裝飾后的save方法中,
obj.save的地址和self.save的地址是一樣的,這個save已經(jīng)被裝飾器修改過了.
和下面這個閉包的原理差不多.
>>>fs = [lambda i:i*2 for i in range(3)]
>>>for f in fs:
... print(f(1))
2
2
2
總結(jié)
類的繼承方法和裝飾器方法實際上都在做同一件事,就是在框架本身的save和delete方法外層增加日志操作.但是需求還沒有實現(xiàn),我們保存日志的時候,不只要知道數(shù)據(jù)變動,還要知道這些操作是誰做的,如何優(yōu)雅的將這些信息傳遞給負責記錄的代碼?
目前我們選擇的是在操作Model時,約定不使用objects,只使用TopSecret(name="",content="").save(request)這種方法,將request傳遞給save,再由之前實現(xiàn)的logger從request取出必要的信息進行記錄,比如IP,User,甚至UA等等.這么做業(yè)務(wù)層需要多傳一個參數(shù),還是有了感知,但是也是沒辦法的事.對現(xiàn)有代碼的改動也是我知道的辦法中最小的.
這套下來感覺django文檔中的給出的信息很充分,實現(xiàn)這個需求并不難.一開始出現(xiàn)的問題還是因為對文檔印象不夠深,沒有第一時間解決問題.