閱讀源碼分析使用 restframework-bulk 包進行`批量`操作中的 `批量更新` 失敗問題

django-restframework-icon.png

背景

之前寫過一篇文章 《Django restframework實現(xiàn)批量操作》 介紹了兩種方案實現(xiàn) Django批量操作板丽,其中一種是通過 restframework-bulk 來實現(xiàn)拜姿,但是當時遇到一個 QuerySet object has no attribute pk 問題沒有搞明白木柬。

今天看到博客中有朋友提問,就打開源碼分析debug了一下赡矢,找到了根本愿意所在

先說結果

User模型中的name字段使用 unique=True愿卸, 程序在運行過程中就進行 UniqueValidator嚴重听诸,根源是這個類中exclude_current_instance 方法沒有對參數(shù)instance 做類型判斷抄瓦,在批量 操作的時候instance 是 List 類型潮瓶,不是對象類型陶冷,所以報錯沒有pk屬性

穿插

如果想更好的理解和實戰(zhàn)Django restframework實現(xiàn)批量操作 涉及的知識點钙姊,可以獲取該案例的源碼

下載地址 django multi operations demo code

分析

1)、批量更新接口http://127.0.0.1:8002/api/v2/users/ 定位到 urls.py 文件中相關代碼

from demoapp.views import UserViewSet
router.register('users', UserViewSet)

2)埂伦、看到 views.py 文件中 UserViewSet的定義

# Create your views here.
class UserViewSet(BulkModelViewSet):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    filter_backends = (DjangoFilterBackend, )
    filterset_class = UserFilterSet

    def allow_bulk_destroy(self, qs, filtered):
        print("here: qs-> ", qs, "\n filtered-> ", filtered)
        return qs is not filtered

3)煞额、ctrl+鼠標右鍵 BulkModelViewSet 上找到 restframework_bulk/generic.py 看到

class BulkModelViewSet(bulk_mixins.BulkCreateModelMixin,
                       bulk_mixins.BulkUpdateModelMixin,
                       bulk_mixins.BulkDestroyModelMixin,
                       ModelViewSet):
    pass

這里我們分析 bulk_mixins.BulkUpdateModelMixin

4)、同樣的方式定位到 restframework_bulk/drf3/mixins.pyBulkUpdateModelMixin 的實現(xiàn)

class BulkUpdateModelMixin(object):
    ... ...
    def bulk_update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        serializer = self.get_serializer(
            self.filter_queryset(self.get_queryset()),
            data=request.data,
            many=True,
            partial=partial,
        )
        # Attention: 
        # 1、注意上面的 self.get_serializer() 中有了many=True膊毁,所以不是序列化是沒有問題
        # 2胀莹、但是從錯誤中知道這里 is_valid() 觸發(fā)了異常
        # 3、那么就需要根據(jù)錯誤去追 rest_framework中對應 serializers 相關的邏輯
        serializer.is_valid(raise_exception=True)
        self.perform_bulk_update(serializer)
        return Response(serializer.data, status=status.HTTP_200_OK)

Attention:

  • 1婚温、注意上面的 self.get_serializer() 中有了many=True描焰,所以不是序列化是沒有問題
  • 2、但是從錯誤中知道這里 is_valid() 觸發(fā)了異常
  • 3栅螟、那么就需要根據(jù)錯誤去追 rest_framework中對應 serializers 相關的邏輯

5)荆秦、從錯誤中有如下信息

... ...
File "/Users/colinspace/.pyenv/versions/py2022/lib/python3.9/site-packages/rest_framework/serializers.py", line 756, in is_valid
    self._validated_data = self.run_validation(self.initial_data)
.... ...

所以去 rest_framework/serializers.py 中 756行檢查,源碼中可以得知是定位到 ListSerializer類中的is_valid() 方法力图,然后調用相關的內部方法步绸,這里列舉出 調用順序,具體源碼太多這里不做展示

is_valid() -> self.run_validation() -> self.to_internal_value(data) -> self.child.run_validation(item) -> Serializer類中的self.to_internal_value(data)

這里需要把 Serializer類中的self.to_internal_value(data) 展開說明下

fields = self._writable_fields
for field in fields:
    validate_method = getattr(self, 'validate_' + field.field_name, None)
    primitive_value = field.get_value(data)
    try:
        validated_value = field.run_validation(primitive_value)
        if validate_method is not None:
            validated_value = validate_method(validated_value)

這里是對模型序列化中的可寫字段進行run_validation 處理吃媒,然后就跳到了 rest_framework/fields.py中的 CharField類的run_validation() -> Field類的run_validation() -> Field類的 run_validators()

def run_validators(self, value):
    errors = []
    for validator in self.validators:
        try:
            if getattr(validator, 'requires_context', False):
                validator(value, self)
            else:
                validator(value)

前面說過 Unique=True 會進行 UniqueValidator 驗證瓤介,UniqueValidator 中 requires_context=True ,

這里執(zhí)行validator(value, self) 實際就是執(zhí)行 UniqueValidator 類的__call__(self, value, serializer_field) 方法

def __call__(self, value, serializer_field):
    field_name = serializer_field.source_attrs[-1]
    instance = getattr(serializer_field.parent, 'instance', None)

    queryset = self.queryset
    queryset = self.filter_queryset(value, queryset, field_name)
    queryset = self.exclude_current_instance(queryset, instance)
    if qs_exists(queryset):
        raise ValidationError(self.message, code='unique')

這里 serializer_field 就是UserSerializer的name字段, serializer_field.parent就是UserSerializer本身,它的instance 在批量操作的情況就是一個List類型赘那,而不是單一對象刑桑,這里可以 輸出類型很容易驗證

最后知道錯誤發(fā)送在 exclude_current_instance 方法,不就是 UniqueValidator 中的方法么募舟,

def exclude_current_instance(self, queryset, instance):
    if instance is not None:
        return queryset.exclude(pk=instance.pk)
    return queryset

既然 instance 此時是List類型漾月,所以 instance.pk 必然報錯

所以需要修改這里的源碼為如下:

def exclude_current_instance(self, queryset, instance):
    """
    If an instance is being updated, then do not include
    that instance itself as a uniqueness conflict.
    """
    if instance is not None:
        if isinstance(instance, list):
            for item in instance:
                queryset.exclude(pk=instance.pk)
            return queryset
        # return queryset.exclude(pk=instance.pk)
    return queryset

然后執(zhí)行批量更新,沒有報錯胃珍,也確實按照實際期望執(zhí)行了


ok梁肿,今天的源碼分析就到這里,比較冗長也比較細觅彰,就要是帶著大家都一遍吩蔑,了解如何去分析閱讀源碼。這樣以后出現(xiàn)問題的時候填抬,就知道怎么去分析解決問題了烛芬。

如果喜歡文章, 歡迎點贊飒责、關注赘娄、留言交流 1209755822


博客首頁: http://blog.colinspace.com
csdn首頁: https://blog.csdn.net/eagle5063
簡書首頁: http://www.reibang.com/u/6d793fbacc88
知乎首頁: https://www.zhihu.com/people/colin-31-49

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市宏蛉,隨后出現(xiàn)的幾起案子遣臼,更是在濱河造成了極大的恐慌,老刑警劉巖拾并,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揍堰,死亡現(xiàn)場離奇詭異鹏浅,居然都是意外死亡,警方通過查閱死者的電腦和手機屏歹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門隐砸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蝙眶,你說我怎么就攤上這事季希。” “怎么了幽纷?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵胖眷,是天一觀的道長。 經常有香客問我霹崎,道長珊搀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任尾菇,我火速辦了婚禮境析,結果婚禮上,老公的妹妹穿的比我還像新娘派诬。我一直安慰自己劳淆,他們只是感情好,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布默赂。 她就那樣靜靜地躺著沛鸵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缆八。 梳的紋絲不亂的頭發(fā)上曲掰,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音奈辰,去河邊找鬼栏妖。 笑死,一個胖子當著我的面吹牛奖恰,可吹牛的內容都是我干的吊趾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼瑟啃,長吁一口氣:“原來是場噩夢啊……” “哼论泛!你這毒婦竟也來了?” 一聲冷哼從身側響起蛹屿,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤屁奏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜡峰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體了袁,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年湿颅,在試婚紗的時候發(fā)現(xiàn)自己被綠了载绿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡油航,死狀恐怖崭庸,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情谊囚,我是刑警寧澤怕享,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站镰踏,受9級特大地震影響函筋,放射性物質發(fā)生泄漏。R本人自食惡果不足惜奠伪,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一跌帐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绊率,春花似錦谨敛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至藐俺,卻和暖如春炊甲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欲芹。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工蜜葱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耀石。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓牵囤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親滞伟。 傳聞我的和親對象是個殘疾皇子揭鳞,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容