Django:读取副本的自定义路由器在保存时引发OperationalError(与相关模型一起)



我正在为Django项目测试一个自定义数据库路由器,以支持读取副本(基于多个数据库上的Django文档(。当我创建包含对其他模型的引用的模型实例时,由于某种原因,save方法会尝试使用读取副本

我已经在DATABASE_ROUTERS设置中注册了以下路由器:

class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
return "replica"

这应该只将读取操作路由到副本,但由于某些原因,保存操作(在具有相关模型的模型上(似乎会触发副本的使用:

In [1]: from django.contrib.auth import User
In [2]: from myapp.models import BlogPost
In [3]: user = User.objects.first()
In [4]: post = BlogPost(user=user)
In [5]: post.save()
# ... <traceback>
OperationalError: (1142, "INSERT command denied to user 'readonly'@'localhost' for table 'myapp_blogpost'")

另一方面,在查询集(例如BlogPost.objects.create(user=user)(上使用create可以很好地工作,保存对相关模型没有FK引用的对象也是如此。

这是我的模型的简化版本,但我使用的模型只引用了User模型和一些基元字段,并且模型上没有自定义的save方法。

我是做错了什么,还是这种行为有记录?我找不到任何关于这个的参考资料。

FWIW,这是Django。2.2.9(也在2.2.22上测试(和MySQL 5.7

更新

在将db_for_write添加到路由器之后(正如abdul-aziz-barkat建议的那样,忽略路由器提示(,使其看起来像:

class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
return "replica"
def db_for_write(self, model, **hints):
return "default"

我遇到了相关对象的以下错误(可能是因为相关对象是从副本中提取的(:

/usr/local/lib/python3.6/dist-packages/django/db/models/base.py in __init__(self, *args, **kwargs)
481                 # checked) by the RelatedObjectDescriptor.
482                 if rel_obj is not _DEFERRED:
--> 483                     _setattr(self, field.name, rel_obj)
484             else:
485                 if val is not _DEFERRED:
/usr/local/lib/python3.6/dist-packages/django/db/models/fields/related_descriptors.py in __set__(self, instance, value)
218                 value._state.db = router.db_for_write(value.__class__, instance=instance)
219             if not router.allow_relation(value, instance):
--> 220                 raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
221
222         remote_field = self.field.remote_field
ValueError: Cannot assign "<User: fakeuser>": the current database router prevents this relation.

第二次更新

abdul-aziz-barkat更新了答案,建议定义allow_relation,从而解决了这个问题。

您的路由器仅定义db_for_read。虽然我不知道具体发生了什么,但我认为,发生的是外键的_state.db以某种方式复制到您正在创建的实例,导致主路由器尝试保存到replica。另一个你没有考虑过的明显问题是这样的事情:

user = User.objects.first()
user.save()

在上面的片段中,将保存到replica,而不是主数据库,因为模型是从副本中读取的,并且它的_state.db设置为replica,而您的路由器没有db_for_write,所以主路由器将使用该实例的提示,并决定它需要存储到replica。此外,由于路由器没有allow_relation方法,master数据库将始终假设从不同数据库提取的对象不能具有关系。

解决方案是,您需要将db_for_writeallow_relation方法添加到路由器:

class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
return "replica"

def db_for_write(self, model, **hints):
return "default" # Or whatever database you need to save to

def allow_relation(obj1, obj2, **hints):
db_list = ["default", 'replica']
if obj1._state.db in db_list and obj2._state.db in db_list:
return True
return False

注意:此路由器与文档中显示的路由器非常相似,正如所述,它存在缺陷:

主/副本(某些数据库称为主/从(所描述的配置也有缺陷——它没有提供任何处理复制滞后(即查询不一致(的解决方案由于写入传播到副本(。它也没有考虑交易的相互作用数据库利用率策略。

最新更新