我正在为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_write
和allow_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
注意:此路由器与文档中显示的路由器非常相似,正如所述,它存在缺陷:
主/副本(某些数据库称为主/从(所描述的配置也有缺陷——它没有提供任何处理复制滞后(即查询不一致(的解决方案由于写入传播到副本(。它也没有考虑交易的相互作用数据库利用率策略。