Django框架ORM操作數(shù)據(jù)庫不生效問題示例解決方法
本文詳細(xì)描述使用Django 的ORM框架操作PostgreSQL數(shù)據(jù)庫刪除不生效問題的定位過程及解決方案,并總結(jié)使用ORM框架操作數(shù)據(jù)庫不生效的問題的通用定位方法
問題描述
最近使用Django 的ORM框架操作PostgreSQL數(shù)據(jù)庫總是出現(xiàn)刪除不生效(尤其是在并發(fā)的時候)。業(yè)務(wù)代碼中也沒有任何報錯。
定位過程 首先,我們懷疑是SQL語句拼裝錯誤(比如ID不對),導(dǎo)致了刪除不生效
通過在Python日志中打印ORM框架的SQL以及返回的操作結(jié)果,發(fā)現(xiàn)delete操作返回的記錄數(shù)是1。且SQL中的ID符合業(yè)務(wù)邏輯,說明相應(yīng)SQL語句是執(zhí)行成功的。排除了這條猜測
接著我們懷疑DELETE操作后,數(shù)據(jù)又被其他業(yè)務(wù)CREATE回來了
通過在數(shù)據(jù)庫中增加觸發(fā)器,將nfinst表的寫操作記錄到nfinst_audit表,發(fā)現(xiàn)沒有刪除操作。排除了這條猜測
create table nfinst_audit( operation char(1) not null, stamp timestamp not null, userid text not null, nfinstid text not null, order_id SERIAL , addr text not null, port text not null ); --將DELETE、UPDATE、INSERT操作記錄到nfinst_audit表中 create or replace function process_nfinst_audit() returns trigger as $nfinst_audit$ begin if (TG_OP = 'DELETE') then insert into nfinst_audit(operation,stamp,userid,nfinstid,addr,port) VALUES('D',now(),user,old.nfinstid,inet_client_addr(),inet_client_port()); return old; elsif (TG_OP = 'UPDATE') then insert into nfinst_audit(operation,stamp,userid,nfinstid,addr,port) VALUES('U',now(),user,new.nfinstid,inet_client_addr(),inet_client_port()); return new; elsif (TG_OP = 'INSERT') then insert into nfinst_audit(operation,stamp,userid,nfinstid,addr,port) VALUES('I',now(),user,new.nfinstid,inet_client_addr(),inet_client_port()); return new; end if; return null; end; $nfinst_audit$ language plpgsql; --創(chuàng)建觸發(fā)器 create trigger nfinst_audit before insert or update or delete on nfinst for each row execute procedure process_nfinst_audit();
- 結(jié)合以上2點,猜測是事務(wù)沒有commit導(dǎo)致
Django默認(rèn)的事務(wù)模式是autocommit,每一次數(shù)據(jù)庫操作執(zhí)行后都會自動提交。項目使用的SQLAlchemy庫的StaticPool連接池,配合gevent使用,一個進(jìn)程中的所有協(xié)程串行復(fù)用一個數(shù)據(jù)庫連接。
(這里解釋一下為什么要一個進(jìn)程中的所有協(xié)程復(fù)用一個連接,因為Python的PostgreSQL驅(qū)動pyscopg2是由c語言編寫,協(xié)程在與數(shù)據(jù)庫交互時,并不會因為io操作而切走,所以即使使用多個連接,也無法帶來并發(fā)能力的提升,反而會增加維護(hù)多個連接的消耗)
查看delete操作的源碼,delete操作是在一個事務(wù)中執(zhí)行了pre_delete signal、刪除表記錄、post_delete signal等操作,執(zhí)行完成后自動commit或者rollback。
def delete(self): for model, instances in self.data.items(): self.data[model] = sorted(instances, key=attrgetter("pk")) self.sort() deleted_counter = Counter() # 開啟事務(wù),語句塊執(zhí)行結(jié)束后會根據(jù)執(zhí)行結(jié)果選擇commit或者rollback with transaction.atomic(using=self.using, savepoint=False): for model, obj in self.instances_with_model(): if not model._meta.auto_created: signals.pre_delete.send( sender=model, instance=obj, using=self.using ) for qs in self.fast_deletes: count = qs._raw_delete(using=self.using) deleted_counter[qs.model._meta.label] += count for model, instances_for_fieldvalues in six.iteritems(self.field_updates): query = sql.UpdateQuery(model) for (field, value), instances in six.iteritems(instances_for_fieldvalues): query.update_batch([obj.pk for obj in instances], {field.name: value}, self.using) for instances in six.itervalues(self.data): instances.reverse() for model, instances in six.iteritems(self.data): query = sql.DeleteQuery(model) pk_list = [obj.pk for obj in instances] count = query.delete_batch(pk_list, self.using) deleted_counter[model._meta.label] += count if not model._meta.auto_created: for obj in instances: # 執(zhí)行post_delete后置處理 signals.post_delete.send( sender=model, instance=obj, using=self.using )
這里的pre_delete signal跟post_delete signal類似于數(shù)據(jù)庫的觸發(fā)器,不過是在Python代碼層面實現(xiàn)的。問題就出在這個post_delete signal上面,出錯的數(shù)據(jù)表注冊了post_delete signal,并在其中調(diào)用了REST接口,而調(diào)用REST接口會導(dǎo)致協(xié)程發(fā)生切換,如果切換后的協(xié)程也操作了數(shù)據(jù)庫,會將現(xiàn)有的事務(wù)回滾。(因為從連接池新拿到的連接,應(yīng)該保證是沒有事務(wù)在執(zhí)行的,如果有,就認(rèn)為該連接上一次被使用時出現(xiàn)了異常,需回滾事務(wù))
將post_delete相關(guān)邏輯注掉后,問題消失
解決方案
解決方法有如下幾種:
- 直接修改Django源碼,將post_delete signal的邏輯移除到事務(wù)外面(Django將post_delete的邏輯放在事務(wù)里確認(rèn)有點坑,一旦post_delete出現(xiàn)異常就會導(dǎo)致事務(wù)回滾,并且事務(wù)過長也會消耗數(shù)據(jù)庫資源)
- 修改業(yè)務(wù)代碼,將delete成功后的處理邏輯由使用signal完成,改為重寫Django Model的delete方法(先調(diào)用父類的delete方法,成功后再執(zhí)行后置處理邏輯)
- 重寫signal機(jī)制,post_delete使用自己實現(xiàn)的signal機(jī)制
最終綜合考慮對業(yè)務(wù)代碼的侵入性,以及后續(xù)的可維護(hù)性,我們選擇了方案3來解決數(shù)據(jù)庫刪除不生效的問題。但在實施的時候,又發(fā)現(xiàn)了新的問題:django從數(shù)據(jù)庫刪除完數(shù)據(jù)后,會將Model對象也刪除,從而導(dǎo)致post_delete無對象可操作??紤]到delete操作幾乎不會出現(xiàn)rollback的情況,將post_delete移到了實際delete操作前面,類似于pre_delete。沒有直接使用pre_delete是為了減少對業(yè)務(wù)代碼的入侵。另外django自帶的pre_delete也在事務(wù)中,而我們的改法是將signal操作移到事務(wù)外,以降低數(shù)據(jù)庫壓力
在models.py中做了如下修改
- 定義了自己的post_delete,并將業(yè)務(wù)代碼中注冊post_delete信號量改為從models.py導(dǎo)入post_delete變量
post_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True)
Django Model有2種方式進(jìn)行刪除操作,分別是直接對一條Model記錄刪除,以及對QuerySet進(jìn)行刪除。所以需要定義自己的Model類以及QuerySet基類,并讓需要進(jìn)行post_delete操作的Model類繼承前面自定義的基類
class CModel_QuerySet(models.query.QuerySet): def delete(self): # 將post_delete信號量觸發(fā)操作移到了事務(wù)外面 for inst in self: post_delete.send( sender=self.model, instance=inst, using=None ) super(CModel_QuerySet, self).delete() class CModel_CustomManager(models.Manager): # custom QuerySet for snap QuerySet.update operations def get_queryset(self): return CModel_QuerySet(self.model, using=self._db) # 自定義的Model基類 class CModelWithUpdateSignal(models.Model): class Meta: abstract = True # custom models.Manager for snap QuerySet.update operations objects = CModel_CustomManager() def delete(self, *args, **kwargs): # 將post_delete信號量觸發(fā)操作移到了事務(wù)外面 post_delete.send( sender=self.__class__, instance=self, using=None ) super(CModelWithUpdateSignal, self).delete(*args, **kwargs) # 需要進(jìn)行post_delete操作的Model類 class NfInstModel(CModelWithUpdateSignal): ……
總結(jié)
ORM框架操作數(shù)據(jù)庫,可以抽象至如下流程
如果出現(xiàn)操作數(shù)據(jù)庫不生效,但是也沒有報錯的情況。可以從以下幾個方面來排查問題
是否SQL本身執(zhí)行未生效(通常是業(yè)務(wù)邏輯導(dǎo)致,比如DELETE操作傳錯了ID),可以在ORM框架源碼中加日志,將SQL執(zhí)行結(jié)果打印出來
是否本次操作被其他操作覆蓋,可以對數(shù)據(jù)表增加觸發(fā)器,將CREATE、UPDATE、DELETE操作記錄到另一張表。通過查看操作記錄來確認(rèn)是否是業(yè)務(wù)邏輯覆蓋的問題
是否是事務(wù)沒有COMMIT,可以在ORM框架源碼中COMMIT操作前后增加日志,如發(fā)現(xiàn)確實沒有COMMIT,需要排查在事務(wù)執(zhí)行過程(包含前置signal、執(zhí)行SQL、后置signal等處理)中,是否出現(xiàn)異常,以及數(shù)據(jù)庫連接在中途有沒有被其他線程使用
到此這篇關(guān)于Django框架ORM操作數(shù)據(jù)庫不生效問題的定位示例的文章就介紹到這了,更多相關(guān)django orm操作數(shù)據(jù)庫不生效內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python進(jìn)程管理工具supervisor安裝使用
supervisor是一個用python語言編寫的進(jìn)程管理工具,它可以很方便的監(jiān)聽、啟動、停止、重啟一個或多個進(jìn)程,本文給大家介紹python進(jìn)程管理工具supervisor安裝使用配置教程,感興趣的朋友一起看看吧2023-08-08詳解Python 使用 selenium 進(jìn)行自動化測試或者協(xié)助日常工作
這篇文章主要介紹了Python 使用 selenium 進(jìn)行自動化測試 或者協(xié)助日常工作,我們可以使用 selenium 來幫助我們進(jìn)行自動化的 Web 測試,也可以通過 selenium 操作瀏覽器做一些重復(fù)的,簡單的事情,來減輕我們的工作2021-09-09Pandas對每個分組應(yīng)用apply函數(shù)的實現(xiàn)
這篇文章主要介紹了Pandas對每個分組應(yīng)用apply函數(shù)的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Python?創(chuàng)建或讀取?Excel?文件的操作代碼
Excel是一種常用的電子表格軟件,廣泛應(yīng)用于金融、商業(yè)和教育等領(lǐng)域,本文介紹Python?創(chuàng)建或讀取?Excel?文件的操作代碼,感興趣的朋友一起看看吧2023-09-09PyTorch中的squeeze()和unsqueeze()解析與應(yīng)用案例
這篇文章主要介紹了PyTorch中的squeeze()和unsqueeze()解析與應(yīng)用案例,文章內(nèi)容介紹詳細(xì),需要的小伙伴可以參考一下,希望對你有所幫助2022-03-03