skip to content

A Django admin course form hangs because its instructor select lists every user; how do autocomplete_fields and raw_id_fields fix it, and how do they differ?

level: middleimportance: should knowfreq 42%

answer

  1. the default widget loads every row
  2. search as you type
  3. the related admin must be searchable
  4. a primary key and a magnifying glass

basics

~20 s

The default ForeignKey widget is a select filled with every related row. autocomplete_fields swaps in a search-as-you-type box that queries the related ModelAdmin's search; raw_id_fields swaps in a plain id input with a lookup popup.

solid answer

~40 s

For a `ForeignKey` or `ManyToManyField`, the admin renders a `<select>` containing **every** related object, built from each row's `__str__()`. With 200,000 users that is a huge query and page, repeated in every inline row. `autocomplete_fields = ["instructor"]` replaces it with a Select2 box that fetches matches as you type from the admin's autocomplete view. That view uses the **related** `ModelAdmin`: it requires that model to be registered with `search_fields` (system checks `admin.E039`/`admin.E040`), applies its `get_queryset()`, `get_search_results()`, ordering and paginator, honours `limit_choices_to`, and requires view or change permission on the related model. `raw_id_fields = ["instructor"]` shows a text input holding the primary key (comma-separated ids for many-to-many) and a magnifying-glass popup of the related change list. Autocomplete is friendlier; raw id needs no search configuration and shows ids rather than names.

code

python · 17 lines
python
from django.contrib import admin

from courses.models import Course, Lesson


class LessonInline(admin.TabularInline):
    model = Lesson
    raw_id_fields = ["reviewer"]  # ops staff paste user ids


@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
    autocomplete_fields = ["instructor", "co_instructors"]
    inlines = [LessonInline]

# The user model's registered ModelAdmin must define search_fields
# (django.contrib.auth's UserAdmin does), or check admin.E040 fails.

go deeper

for a junior

Recall that the default related-field widget lists every row, and that autocomplete_fields and raw_id_fields replace it.

for a middle

Explain autocomplete's requirements (registered related admin with search_fields), what raw id shows, and why inlines multiply the cost.

for a senior

Show you diagnose slow change forms by widget, scope autocomplete via the related admin's get_queryset and permissions, and index searched columns.

for a principal

Decide how much of a huge related table editors should reach from a form, and whether selection belongs in a dedicated tool.

## Why the default widget breaks For a `ForeignKey`, the admin's form field is a `ModelChoiceField`, rendered as a `<select>` with one `<option>` per related row. To build it, Django loads every row of the related table and calls `__str__()` on each. That is fine for a `Category` table with 30 rows and a disaster for `instructor = ForeignKey(User)` when the user table has hundreds of thousands of rows: - one large query per widget, plus whatever each `__str__()` touches; - a page several megabytes big, slow to render in the browser; - multiplied by every inline row that repeats the widget, so a lesson inline with an `editor` foreign key and 30 lessons renders 30 of them. `ManyToManyField` has the same problem with a multi-select. ## autocomplete_fields `autocomplete_fields = ["instructor"]` renders a Select2 input. Nothing is loaded up front beyond the current value; as the editor types, the widget calls the admin's autocomplete JSON view, which returns a page of `{id, text}` results. That view is driven by the **related model's** `ModelAdmin`: 1. The related model must be registered on the same admin site, otherwise the system check `admin.E039` fails. 2. That `ModelAdmin` must define `search_fields`, otherwise `admin.E040` fails; the typed term is matched with its `get_search_results()`. 3. It starts from that admin's `get_queryset(request)`, so row scoping there also limits what can be picked. 4. It applies the field's `limit_choices_to`. 5. Ordering and pagination come from the related admin's `get_ordering()` and `get_paginator()`. 6. The user must have **view or change permission** on the related model, or the view returns permission denied, which prevents the widget from leaking data. The docs add a performance caveat: ordering a huge queryset and searching unindexed fields can still be slow, so index the `search_fields` columns of the related model. ## raw_id_fields `raw_id_fields = ["instructor"]` renders a text input for the primary key, or a comma-separated list of ids for a many-to-many field, plus a magnifying-glass button that opens the related model's change list in a popup to search and pick a row. Next to the input the admin shows the current object's label. ## Choosing between them | | `autocomplete_fields` | `raw_id_fields` | |---|---|---| | Up-front load | current value only | current value only | | Editor experience | type a name, pick from matches | type an id or use the popup | | Requirements | related model registered with `search_fields` | related model's change list for the popup | | Access control | view or change permission on the related model | the popup is an ordinary admin change list | | Works for | `ForeignKey`, `ManyToManyField` | `ForeignKey`, `ManyToManyField` | | Also in inlines | yes | yes | Autocomplete is the usual choice for editor-facing forms. Raw id fits internal tools where operators already work with ids, or when the related model cannot sensibly be searched. ## Customising the results Because the autocomplete view delegates to the related `ModelAdmin`, customisation happens there: - Override the user admin's `get_search_results()` to search the way editors think, for example an exact match on an e-mail address before falling back to name fragments; the docs suggest this for very large tables. - Give the related admin an `ordering` on an indexed column, or none at all, since sorting a huge candidate set is expensive. - Use `limit_choices_to` on the `ForeignKey` (for example only users in the Instructors group) so the choices are narrowed wherever the field appears, including the autocomplete. The label shown for each result is the object's `__str__()`, so make it distinguish people who share a name, for instance by including the e-mail. ## Related options - `formfield_for_foreignkey()` can narrow the default select's queryset instead, when a small subset is all that is valid (only users in the Instructors group). - `filter_horizontal`/`filter_vertical` improve many-to-many selects but still load every row.

  • A Django admin autocomplete for instructor returns nothing for some staff users; why might that be?
    The autocomplete view checks the staff user's view or change permission on the related model and returns permission denied without it. It also starts from the related `ModelAdmin.get_queryset(request)`, so row scoping there hides users, and it applies the field's `limit_choices_to`. Check all three before suspecting the search.
  • What system check fails if a Django ModelAdmin puts instructor in autocomplete_fields but the user admin lacks search_fields?
    `admin.E040`: the related model's `ModelAdmin` must define `search_fields` because the autocomplete search uses it. If the related model is not registered on the admin site at all, `admin.E039` fails instead. Both are raised at startup by the system checks, not when the page is opened.

The default select is a printed phone book stapled to the form: complete, heavy, and reprinted for every page view. Autocomplete is a directory-enquiries call that returns a few names matching what you say; raw id is being handed a customer number and a link to look it up.

saying these in an interview costs you the question

  • autocomplete_fields works without the related model being registered in the admin
  • raw_id_fields still loads every related row to validate the id
  • The autocomplete search uses the current ModelAdmin's search_fields
  • Autocomplete results ignore permissions, so any staff user sees every row
  • filter_horizontal avoids loading all related rows for many-to-many fields