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=False)

Bulk Operations (bulk=True)

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