Django 使用心得 (三)利用 middleware 和 signal 實現資料的 「create_by」 記錄功能

elfgzp發表於2019-01-09

middleware 與 signal 使用小技巧

Django 使用心得 (三)利用 middleware 和 signal 實現資料的 「create_by」 記錄功能

記錄資料的建立者和更新者是一個在實際專案中非常常見的功能,但是若是在每個介面都增加這個業務邏輯就很不 Pythonic。

筆者在 Google 上無意發現了一種非常巧妙的實現方式,在這裡分享給大家。

Middleware 程式碼實現

在做了一些小修小改,處理掉了一些程式碼的版本問題後,最終的程式碼如下,我們一起來看看它是怎麼實現的。

create_by/middleware.py view raw
from django import conf
from django.db.models import signals
from django.core.exceptions import FieldDoesNotExist
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import curry


class WhoDidMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            if hasattr(request, 'user') and request.user.is_authenticated:
                user = request.user
            else:
                user = None

            mark_whodid = curry(self.mark_whodid, user)
            signals.pre_save.connect(mark_whodid, dispatch_uid=(self.__class__, request,), weak=False)

    def process_response(self, request, response):
        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            signals.pre_save.disconnect(dispatch_uid=(self.__class__, request,))
        return response

    def mark_whodid(self, user, sender, instance, **kwargs):
        create_by_field, update_by_field = conf.settings.CREATE_BY_FIELD, conf.settings.UPDATE_BY_FIELD

        try:
            instance._meta.get_field(create_by_field)
        except FieldDoesNotExist:
            pass
        else:
            if not getattr(instance, create_by_field):
                setattr(instance, create_by_field, user)

        try:
            instance._meta.get_field(update_by_field)
        except FieldDoesNotExist:
            pass
        else:
            setattr(instance, update_by_field, user)
複製程式碼

通過 WhoDidMiddleware 這個類繼承了 MiddlewareMixin 我們可以知道他的主要是通過 Django 的 middleware 來實現記錄 create_by(資料修改者)的。

...

class WhoDidMiddleware(MiddlewareMixin):
...
複製程式碼

由於 Django 呼叫到達模型層後,我們就無法獲取到當前 request(請求)的使用者,所以一般做法是在是檢視層在建立資料的時候將 request 所對應的使用者直接傳遞給模型, 用於建立資料。

這裡的 middleware 是用於處理 request,那麼 request的使用者該如何傳遞下去呢,我們繼續看程式碼,這裡出現了 signal(訊號)。

...
signals.pre_save.connect(mark_whodid, dispatch_uid=(self.__class__, request,), weak=False)
...
複製程式碼

Signal 在 Django 中常用來處理在某些資料之前我要做一些處理或者在某些資料處理之後我要執行些邏輯等等的業務需求。例如:

class Book(models.Model):
    name = models.CharField(max_length=32)
    author = models.ForeignKey(to=Author, on_delete=models.CASCADE, null=False)
    remark = models.CharField(max_length=32, null=True)

...

@receiver(pre_save, sender=Book)
def generate_book_remark(sender, instance, *args, **kwargs):
    print(instance)
    if not instance.remark:
        instance.remark = 'This is a book.'
複製程式碼

Signal connect 中的引數

那麼在這裡他又是如何使用的呢,我們需要去看看 Django 原始碼中 signals.pre_save.connect 這個函式的定義和引數。

django/dispatch/dispatcher.py view raw
    def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
        """
        Connect receiver to sender for signal.

        Arguments:

            receiver
                A function or an instance method which is to receive signals.
                Receivers must be hashable objects.

                If weak is True, then receiver must be weak referenceable.

                Receivers must be able to accept keyword arguments.

                If a receiver is connected with a dispatch_uid argument, it
                will not be added if another receiver was already connected
                with that dispatch_uid.

            sender
                The sender to which the receiver should respond. Must either be
                a Python object, or None to receive events from any sender.

            weak
                Whether to use weak references to the receiver. By default, the
                module will attempt to use weak references to the receiver
                objects. If this parameter is false, then strong references will
                be used.

            dispatch_uid
                An identifier used to uniquely identify a particular instance of
                a receiver. This will usually be a string, though it may be
                anything hashable.
        """
        ...
複製程式碼

這裡稍微解釋一下每個引數的含義:

  • receiver 接收到此訊號回撥函式
  • sender 這個訊號的傳送物件,若為空則可以為任意物件
  • weak 是否將 receiver 轉換成 弱引用物件,Signal 中預設 會將所有的 receiver 轉成弱引用,所以 如果你的receiver是個區域性物件的話, 那麼receiver 可能會被垃圾回收期回收,receiver 也就變成一個 dead_receiver 了,Signal 會在 connect 和 disconnect 方法呼叫的時候,清除 dead_receiver。
  • dispatch_uid 這個引數用於唯一標識這個 receiver 函式,主要的作用是防止 receiver 函式被註冊多次。

Middleware 程式碼分析

接下來我們一步步來分析 Middleware 中的程式碼

process_request 函式程式碼分析

當一個客戶端請求來到伺服器並經過 WoDidMiddleware 時,首先 request 會進入 process_request函式。

提取 request 中的 user

依照 RestFul 規範,我們先把非資料修改的方法都過濾掉,然後取出請求中的 user。

create_by/middleware.py view raw
def process_request(self, request):
    if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
        if hasattr(request, 'user') and request.user.is_authenticated:
            user = request.user
        else:
            user = None
    ...
複製程式碼

mark_whodid 函式程式碼分析

然後我們對 mark_whodid 函式做一些處理,在說明做了什麼處理之前,我們先看看 mark_whodid 函式實現了什麼邏輯。

create_by/middleware.py view raw
...
def mark_whodid(self, user, sender, instance, **kwargs):
    create_by_field, update_by_field = conf.settings.CREATE_BY_FIELD, conf.settings.UPDATE_BY_FIELD

    try:
        instance._meta.get_field(create_by_field)
    except FieldDoesNotExist:
        pass
    else:
        if not getattr(instance, create_by_field):
            setattr(instance, create_by_field, user)

    try:
        instance._meta.get_field(update_by_field)
    except FieldDoesNotExist:
        pass
    else:
        setattr(instance, update_by_field, user)
...
複製程式碼

這段程式碼非常的容易理解,主要功能就是根據 settings 中設定的 CREATE_BY_FIELDUPDATE_BY_FIELD(建立者欄位和更新者欄位)的名稱, 對 instance(資料物件例項)的對應的欄位附上使用者值。

curry 函式的作用

讓我們回到 process_request 這個函式,這裡對 mark_whodid 函式做了一個處理。

create_by/middleware.py view raw
...
mark_whodid = curry(self.mark_whodid, user)
...
複製程式碼

我這裡通過一個簡單的程式碼例子解釋一下 curry 函式的作用。

>>> from django.utils.functional import curry
>>> def p1(a, b, c):
…     print a, b, c
>>> p2 = curry(a, ‘a’, ‘b’)
>>> p2(‘c’)
a b c
複製程式碼

其實 curry 實現了類似裝飾器的功能,他將 p1 函式的引數通過 curry 函式設定了兩個預設值 ‘a’ 和 ‘b’ 分別按順序賦值給 a 和 b,產生了一個新的函式 p2。 這樣相當於我們如果要呼叫 p1 函式只想傳入 c 引數但是 a 和 b 引數並沒有設定預設值,我們就可以用 curry 函式封裝成一個新的函式 p2 來呼叫。

所以上面的程式碼中,我們將 mark_whodid 的 user 引數的預設值設定為從 request 中獲取的 user,並且再次生成一個 mark_whodid 函式。

註冊 signal

從程式碼我們可以知道 connect 函式傳入了 receiver、dispatch_uid 和 weak 引數,每個引數的作用上文中已經說明了,sender 引數為空則所有 models 的 pre_save(在資料儲存之前)都會觸發 receiver,也就是我們的 mark_whodid 函式。

create_by/middleware.py view raw
...
signals.pre_save.connect(mark_whodid, dispatch_uid=(self.__class__, request,), weak=False)
...
複製程式碼

process_response 函式程式碼分析

完成客戶端的請求處理,當然是要返回 response(服務端響應)了。因為我們在 process_request 函式註冊了訊號,我們用完當然要把訊號登出。

這裡就用到了 disconnect 函式,由於我們在註冊時傳入了 dispatch_uid 所以我們不需要過多的引數,對這個函式感興趣的可以看一看官方的文件 Disconnecting signals

create_by/middleware.py view raw
...
def process_response(self, request, response):
    if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
        signals.pre_save.disconnect(dispatch_uid=(self.__class__, request,))
    return response
...
複製程式碼

總結

這個實現方式非常巧妙,同時也可以學習到 Django 中的 Middleware 和 Signal 的簡單用法,以上就是本文的主要內容,希望能給大家帶來幫助。

參考文章

django get current user in model save - Django: Populate user ID when saving a model

Django學習 curry 函式

Django Signal 解析


相關文章