Django記錄所有ORM操作的探索

最近開發(fā)一個內(nèi)部的記錄系統(tǒng),其中有一個需求要求將所有數(shù)據(jù)庫操作記錄下來,為此想了一些方案.記錄一下.

思路演化

這個需求出來的一瞬間我就否定了在業(yè)務(wù)邏輯層保存操作記錄的方案,我認為這樣耦合度比較高,成本也太高. 代碼也會大量重復(fù).
Django的ORM操作中,刪除操作會調(diào)用models.Modeldelete方法,增改會調(diào)用save方法,修改這些方法能夠覆蓋除了查詢以外的所有ORM操作(查詢暫時跳過),修改savedelete的方法無外乎就是類繼承,裝飾器.

我也考慮了使用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é)

類的繼承方法和裝飾器方法實際上都在做同一件事,就是在框架本身的savedelete方法外層增加日志操作.但是需求還沒有實現(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)的問題還是因為對文檔印象不夠深,沒有第一時間解決問題.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,537評論 19 139
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 30,217評論 8 265
  • 剛山再往西三百五十里,是座英鞮山,山上生長著茂密的漆樹,山下蘊藏著豐富的金屬礦物和玉石,禽鳥野獸都是白色的。 涴水...
    本喜爾玉閱讀 753評論 0 4
  • 以下轉(zhuǎn)載水水的文章 因為喜歡,所以轉(zhuǎn)載 高曉松最近有一篇文被朋友圈刷屏《我主要教育女兒心安理得的混日子》。內(nèi)容是這...
    楓琳熹微閱讀 326評論 0 0
  • 春節(jié)顧名思義是春天的節(jié)日,時間是二十四節(jié)氣之一立春的分界點。春節(jié)期間最重要的事便是全家人除夕守歲,除去冬天的荒涼...
    霧欣雨鮮閱讀 830評論 1 10

友情鏈接更多精彩內(nèi)容