Django ORM層日誌的兩種實現方式

最近開發一個內部的記錄系統,其中有一個需求要求將全部數據庫操做記錄下來,爲此想了一些方案.記錄一下.python

思路演化

這個需求出來的一瞬間我就否認了在業務邏輯層保存操做記錄的方案,我認爲這樣耦合度比較高,成本也過高. 代碼也會大量重複.
Django的ORM操做中,刪除操做會調用models.Modeldelete方法,增改會調用save方法,修改這些方法可以覆蓋除了查詢之外的全部ORM操做(查詢暫時跳過),修改savedelete的方法無外乎就是類繼承,裝飾器.數據庫

我也考慮了使用signal系統,可是這樣依然要在業務邏輯層處理髮送信號的問題,感受更復雜一些(好比對照修改先後的數據不如直接在Model中操做方便,Model的save方法在存盤以前,self是待存數據,根據self.pk從db中取出數據是舊數據方便對比.若是要在signal中拿到兩份數據比較麻煩,可能須要在業務層作更多的斟酌).django

繼承

我首先嚐試了類繼承的方法閉包

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()

我以爲這樣應該能夠的.然而在調用save()方法時出現錯誤OperationalError: no such table: logged_logger,能夠看出,我在原始model定義的Meta信息失效了,框架轉而在Logger類中尋找Meta,未找到的狀況下使用了框架的默認值.
我嘗試將super(Logger, self).save(*args, **kwargs)改爲super(self.__class__, self).save(*args, **kwargs),這樣super又成了調用TopSecret父類Logger的save(),如此反覆造成了循環調用報錯.
我仔細想了一下,Model類尋找Meta的邏輯是確定不去修改的,修改這個顯得不划算,也違反了不隨便改框架的基本原則,當時在此我轉向了裝飾器方法,而放棄了類繼承.
今天我寫這篇文章的時候隱約想起一件事情,Model好像有一個abstract的屬性,果真如此.定義這個Meta信息以後,框架會認爲這是一個抽象類,而不是數據模型,完美解決了問題.函數

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="測試內容")

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那裏調用save方法的方式比較彆扭,將實例方法當作靜態方法調用手動傳入實例有一種很違和的感受,不過總算是能工做了.spa

裝飾器

裝飾器是當時類繼承沒有成功,我走的另外一條路.日誌

首先由於咱們的裝飾器不可能裝到框架代碼裏去,只能在咱們定義的Model模型上使用類裝飾器.當時個人實現是使用django自帶的method_decorator.這個函數能夠將函數裝飾器變成方法裝飾器,裝飾到一個類的方法上,比較常見的用法是爲dispatch方法去除csrf保護.
可是使用這個方法會有一個問題,那就是寫一個函數裝飾器自己是不會取到類的實例自己的.還須要爲save方法傳入類的實例自己才能取到類數據進行日誌操做,不行,不夠優雅.code

怎麼辦?只能直接寫類裝飾器了.

以前沒寫過類裝飾器,其實類裝飾器和普通函數裝飾器同樣,思路和繼承的寫法也是同樣的.

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),這麼作會致使運行時調用當前實例的save方法,也就是_save自己,搞成無限遞歸
咱們分別打印一下 cbd_logger下這些方法的 id看一下
>>>obj.save()
save 90220624
obj.save 106285376
self.save 106285376
在裝飾後的save方法中, obj.save的地址和 self.save的地址是同樣的,這個save已經被裝飾器修改過了.
和下面這個閉包的原理差很少.
>>>fs = [lambda i:i*2 for i in range(3)]
>>>for f in fs:
...    print(f(1))
2
2
2

總結

類的繼承方法和裝飾器方法實際上都在作同一件事,就是在框架自己的savedelete方法外層增長日誌操做.可是需求尚未實現,咱們保存日誌的時候,不僅要知道數據變更,還要知道這些操做是誰作的,如何優雅的將這些信息傳遞給負責記錄的代碼?
目前咱們選擇的是在操做Model時,約定不使用objects,只使用TopSecret(name="",content="").save(request)這種方法,將request傳遞給save,再由以前實現的logger從request取出必要的信息進行記錄,好比IP,User,甚至UA等等.這麼作業務層須要多傳一個參數,仍是有了感知,可是也是沒辦法的事.對現有代碼的改動也是我知道的辦法中最小的. 這套下來感受django文檔中的給出的信息很充分,實現這個需求並不難.一開始出現的問題仍是由於對文檔印象不夠深,沒有第一時間解決問題.

相關文章
相關標籤/搜索