Core Concepts¶
This chapter introduces the virtual fields that
ReverseRelationAdminMixin adds to a
ModelAdmin and how the mixin keeps single and
multi-select bindings in sync. For complete admin examples, see
Single binding (Company ↔ Department) and Multiple binding (Company ↔ Projects).
What the mixin injects¶
ReverseRelationAdminMixin introduces virtual form fields that proxy the
reverse side of ForeignKey and OneToOne relationships. Those fields are declared
in reverse_relations.
If your admin declares fieldsets or fields, you must include the virtual
names there so the Django template renders them. If your admin declares neither
fieldsets nor fields, Django renders all form fields by default and the
injected virtual fields will appear automatically. This
works because the mixin’s get_fields()
appends the virtual names for you and get_form()
injects the corresponding form fields dynamically.
Note
If you override get_fields()
without calling super(), or you hard-code fields/fieldsets and
omit the virtual names, the admin template will not render the virtual
fields. The form still contains them (the mixin injects them), but the layout
derives from get_fields/fieldsets.
During get_form()
the mixin removes those virtual names from the base form
(avoiding “unknown field” errors), creates
ModelChoiceField/ModelMultipleChoiceField
instances on the fly, and wires up any labels, help texts, or widgets defined on
ReverseRelationConfig.
See also
For visibility/editability at render time and layout rules, see Rendering & Visibility.
Request-aware querysets¶
Every virtual field resolves its queryset and initial selection for the current
request. limit_choices_to can be a callable that
receives (queryset, instance, request) and returns a scoped queryset. This
lets you present only the objects a user is allowed to bind while still showing
items already attached to the instance under edit.
Single vs. multiple selections¶
multiple determines whether the field captures a
single object or a synchronised set:
multiple=False(default) — behaves like a dropdown. The chosen object’s ForeignKey is set to the admin object, and any other rows pointing at it are unbound.multiple=True— represents the entire desired set. After form submission the mixin unbinds rows not in the selection before binding the chosen ones to the instance. The resulting database state matches the submitted list exactly.
Warning
Single-select unbinds all other objects pointing at the instance. Ensure the
reverse ForeignKey is null=True. If the relation must never be empty,
set required=True on the virtual field to prevent unbinding from raising
an IntegrityError.
Bulk operations and performance¶
ReverseRelationConfig supports an optional bulk parameter
that changes how bind/unbind operations are performed:
bulk=False(default) — uses individual model saves, triggering all Django model signals (pre_save,post_save, etc.) for each affected object.bulk=True— uses Django’s.update()method for better performance but bypasses model signals entirely.
Performance considerations:
Aspect |
Individual Saves ( |
Bulk Operations ( |
|---|---|---|
Database round-trips |
One per object |
One per operation type |
Model signals |
✅ Triggered normally |
❌ Bypassed entirely |
Performance |
Slower with large datasets |
✅ Significantly faster |
Error granularity |
✅ Per-object errors |
Batch-level errors only |
Memory usage |
Higher (object instantiation) |
✅ Lower (queryset operations) |
Best practices for bulk mode:
Enable bulk mode when managing hundreds or thousands of relationships
Ensure your application doesn’t depend on model signals for the reverse model
Use bulk mode consistently across related configurations for optimal performance
Consider the trade-off between performance and signal-based functionality
Warning
Bulk operations bypass Django’s model signal system. If your application relies
on pre_save, post_save, pre_delete, or other model signals for the
reverse relationship model, do not enable bulk mode.
Permissions interaction¶
When permission enforcement is enabled and a reverse field is rendered in
disable mode, the field becomes read-only and its POSTed value (if any) is
ignored. To avoid spurious validation errors, the mixin also forces
required=False on such disabled fields — this prevents Django from raising
“This field is required.” when there is no initial value and the browser omits
the disabled input from the submission.
See also
Architecture dives deeper into how the mixin hooks into the admin form lifecycle.
Data Integrity & Transactions explains the transaction model that keeps bindings consistent across fields.