Django多数据库场景下Admin保存关联对象触发ValueError错误
Django多数据库配置下Admin保存关联对象时的ValueError问题
错误场景
在Django Admin中保存关联了UserRoleAssociation的User对象时触发ValueError,项目配置了default和members两个数据库,User、UserRoleAssociation、Role模型均应使用members数据库。
错误信息:
ValueError: Cannot assign "<Role: Role object (67c43a2e-c4e7-4846-bd31-700bf5d35e82)>": the current database router prevents this relation.
模型定义
# models.py class Role(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) role_name = models.CharField(max_length=255, unique=True) class Meta: db_table = "roles" class UserRoleAssociation(models.Model): user = models.ForeignKey("User", on_delete=models.CASCADE) role = models.ForeignKey("Role", on_delete=models.CASCADE) def __str__(self): return f"{self.role.role_name}" class Meta: unique_together = ["user", "role"] db_table = "user_role_association" class User(models.Model): phone_number = models.CharField(max_length=15) is_premium = models.BooleanField(default=False) roles = models.ManyToManyField("Role", through=UserRoleAssociation, related_name="users") class Meta: db_table = "users" app_label = "members" base_manager_name = 'objects' default_manager_name = 'objects'
Admin配置
自定义MultiDBModelAdmin和MultiDBTabularInline强制使用members数据库:
# admin.py class MultiDBModelAdmin(admin.ModelAdmin): using = "members" def save_model(self, request, obj, form, change): obj.save(using=self.using) def delete_model(self, request, obj): obj._state.db = self.using obj.delete(using=self.using) def delete_queryset(self, request, queryset): queryset.using(self.using).delete() def get_queryset(self, request): return super().get_queryset(request).using(self.using) def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, using=self.using, **kwargs) def formfield_for_manytomany(self, db_field, request, **kwargs): return super().formfield_for_manytomany(db_field, request, using=self.using, **kwargs) def _save_related(self, request, form, formsets, change): """Ensure related objects are saved in the correct database.""" for formset in formsets: formset.instance._state.db = self.using # Force the database super()._save_related(request, form, formsets, change) class MultiDBTabularInline(admin.StackedInline): using = "members" def get_queryset(self, request): return super().get_queryset(request).using(self.using) def formfield_for_manytomany(self, db_field, request, **kwargs): # Tell Django to populate ManyToMany widgets using a query # on the 'members' database. return super().formfield_for_manytomany( db_field, request, using=self.using, **kwargs ) class UserRoleAssociationInline(MultiDBTabularInline): model = UserRoleAssociation extra = 1 @admin.register(User) class MemberAdmin(MultiDBModelAdmin): list_display = ["id"] inlines = [UserRoleAssociationInline]
解决方案
1. 完善数据库路由配置
创建数据库路由器,明确将members应用下所有模型路由到members数据库:
# routers.py class MembersRouter: def db_for_read(self, model, **hints): if model._meta.app_label == 'members': return 'members' return None def db_for_write(self, model, **hints): if model._meta.app_label == 'members': return 'members' return None def allow_relation(self, obj1, obj2, **hints): if obj1._meta.app_label == 'members' and obj2._meta.app_label == 'members': return True return None def allow_migrate(self, db, app_label, model_name=None, **hints): if app_label == 'members': return db == 'members' return None
在settings.py中配置路由:
DATABASE_ROUTERS = ['your_project.routers.MembersRouter']
2. 修复Inline类的数据库处理
补充MultiDBTabularInline的外键字段处理方法,并确保关联对象保存到指定数据库:
class MultiDBTabularInline(admin.StackedInline): using = "members" def get_queryset(self, request): return super().get_queryset(request).using(self.using) def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, using=self.using, **kwargs) def formfield_for_manytomany(self, db_field, request, **kwargs): return super().formfield_for_manytomany( db_field, request, using=self.using, **kwargs ) def save_formset(self, request, form, formset, change): instances = formset.save(commit=False) for instance in instances: instance._state.db = self.using instance.save() formset.save_m2m()
3. 统一模型的app_label
给Role和UserRoleAssociation模型添加app_label,确保路由器能识别它们属于members应用:
class Role(models.Model): # ... 字段定义 ... class Meta: db_table = "roles" app_label = "members" # 新增 class UserRoleAssociation(models.Model): # ... 字段定义 ... class Meta: unique_together = ["user", "role"] db_table = "user_role_association" app_label = "members" # 新增
4. 强制关联对象的数据库状态
在MultiDBModelAdmin的_save_related方法中,确保Role实例的数据库状态正确:
class MultiDBModelAdmin(admin.ModelAdmin): # ... 其他方法 ... def _save_related(self, request, form, formsets, change): for formset in formsets: formset.instance._state.db = self.using # 处理formset中的外键对象 for form in formset.forms: role = form.cleaned_data.get('role') if role: role._state.db = self.using super()._save_related(request, form, formsets, change)
内容的提问来源于stack exchange,提问作者mirodil
相关产品推荐
相关产品推荐

