Django 3.2自关联ManyToMany关系如何在Admin中仅用1个Inline双向展示
问题根源
首先明确两个核心问题:
- 带自定义中间表的自关联多对多字段,Django会强制关闭
symmetrical属性,既不会自动创建反向关联记录,也不会在查询时自动合并双向关联,所以你在locale2上查不到locale1的关联 - 中间表存在两个指向
Locale的外键,Admin的Inline默认无法自动识别要绑定的外键,因此会抛出要求指定fk_name的报错
解决方案
第一步:调整模型逻辑实现关联对称性
不需要修改现有表结构,可通过信号自动同步反向关联记录,保证双向查询结果一致:
import uuid from django.db import models from django.db.models.signals import post_save, post_delete from django.dispatch import receiver class Locale(models.Model): """Locale to be used for everything on the Aspiria Campus""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # ... 其他字段 related_locales = models.ManyToManyField( "self", through='RelatedLocale', symmetrical=False) class Meta: db_table = 'locale' class RelatedLocale(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) locale = models.ForeignKey( Locale, on_delete=models.CASCADE, db_column='locale_id') related_locale = models.ForeignKey( Locale, related_name="related_locale", on_delete=models.CASCADE, db_column='related_locale_id') is_parent = models.BooleanField(default=False) class Meta: db_table = "related_locale" def __str__(self): return "%s related" % self.locale.name # 新增信号实现关联双向同步 @receiver(post_save, sender=RelatedLocale) def create_reverse_relation(sender, instance, created, **kwargs): if created: # 避免循环创建,先检查反向记录是否存在 RelatedLocale.objects.get_or_create( locale=instance.related_locale, related_locale=instance.locale, defaults={'is_parent': not instance.is_parent} ) @receiver(post_delete, sender=RelatedLocale) def delete_reverse_relation(sender, instance, **kwargs): # 同步删除反向关联 RelatedLocale.objects.filter( locale=instance.related_locale, related_locale=instance.locale ).delete()
第二步:配置单个Admin Inline实现双向展示
不需要创建两个Inline,只需要指定fk_name并重写查询逻辑合并双向记录即可:
from django.contrib import admin from django.db.models import Q from .models import Locale, RelatedLocale class RelatedLocaleInline(admin.TabularInline): model = RelatedLocale fk_name = 'locale' extra = 0 def get_queryset(self, request): qs = super().get_queryset(request) current_locale = self.parent_obj # 合并当前对象作为主关联和被关联的所有记录 return qs.filter(Q(locale=current_locale) | Q(related_locale=current_locale)) def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "related_locale": # 排除当前对象本身,避免自关联 kwargs["queryset"] = Locale.objects.exclude(id=self.parent_obj.id) return super().formfield_for_foreignkey(db_field, request, **kwargs) @admin.register(Locale) class LocaleAdmin(admin.ModelAdmin): inlines = [RelatedLocaleInline] # 其他Admin配置按需添加
效果验证
调整后执行shell命令即可得到预期结果:
>>> locale1.related_locales.set([locale2]) >>> locale1.related_locales.all() <QuerySet [<Locale: Locale object (<locale2>)>] >>> locale2.related_locales.all() <QuerySet [<Locale: Locale object (<locale1>)>]
Admin中不管打开locale1还是locale2的详情页,都可以在同一个Inline中看到全部关联记录。
内容的提问来源于stack exchange,提问作者ndyr
相关产品推荐
相关产品推荐

