Django-Admin.ru

get() в Django ORM: DoesNotExist и get_object_or_404

Как получить один объект в Django ORM: get() и его исключения, first() вместо try/except, get_object_or_404, get_or_create без гонок, aget() и get в DRF.

Редакция Django-Admin.ru
· 12 мин чтения

Содержание · 11 разделов
  1. Модели для примеров
  2. Как работает get() в Django ORM
  3. DoesNotExist и MultipleObjectsReturned
  4. Общие базовые классы
  5. ValueError вместо DoesNotExist
  6. get() или filter().first()
  7. Свой метод get_or_none()
  8. get_object_or_404 и get_list_or_404
  9. get_or_create(): получить или создать
  10. Гонка и уникальность
  11. aget() и другие методы в асинхронном коде
  12. Производительность get()
  13. Индексы
  14. Что значит LIMIT 21
  15. Лишние запросы
  16. get в Django REST Framework
  17. Частые вопросы
  18. Как получить объект или None без try/except?
  19. Почему DoesNotExist при обращении к связанному объекту?
  20. Можно ли вызвать get() без аргументов?
  21. Как проверить в тесте, что объекта нет?
  22. Чем get() отличается от latest() и earliest()?
  23. Как заблокировать строку, полученную через get()?
  24. Итог

Метод get() в Django ORM возвращает ровно один объект, подходящий под условие. Если записи нет, он выбрасывает Model.DoesNotExist, если записей несколько — Model.MultipleObjectsReturned. Во view вместо ручной обработки обычно пишут get_object_or_404(), а если отсутствие записи — нормальная ситуация, используют filter(...).first():

book = Book.objects.get(slug="idiot")               # объект или исключение
book = Book.objects.filter(slug="idiot").first()    # объект или None
book = get_object_or_404(Book, slug=slug)           # объект или ответ 404

Ниже — как работает get у Django QuerySet, какие тексты ошибок он выдаёт, чем отличается от first(), как избежать дублей в get_or_create(), что использовать в асинхронном коде и в Django REST Framework. Примеры проверены на Django 6.1 и SQLite, всё показанное работает и в Django 5.2 LTS. Остальные методы QuerySet собраны в шпаргалке по Django ORM.

Модели для примеров

# library/models.py
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=200)
    country = models.CharField(max_length=2, blank=True)

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=300)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    published = models.DateField()

    class Meta:
        ordering = ["title"]

    def __str__(self):
        return self.title


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

В базе три книги: «Война и мир» и «Анна Каренина» Льва Толстого, «Идиот» Фёдора Достоевского. Как описывать поля и Meta, разобрано в статье про модели Django.

Как работает get() в Django ORM

get() принимает те же условия, что и filter(): именованные аргументы с lookups и объекты Q. Подробно синтаксис условий разобран в статье про filter() и exclude().

from django.db.models import Q

Book.objects.get(pk=1)
Book.objects.get(slug="idiot")
Book.objects.get(title="Идиот", published__year=1869)
Book.objects.get(Q(title__startswith="Анна"), author__country="RU")

Позиционные Q пишутся перед именованными аргументами — иначе это синтаксическая ошибка Python.

В отличие от filter(), get() не ленивый: запрос выполняется сразу, и метод возвращает экземпляр модели, а не QuerySet. Его можно вызвать в конце цепочки: Book.objects.filter(author__country="RU").get(slug="idiot").

DoesNotExist и MultipleObjectsReturned

Оба исключения — атрибуты конкретной модели, импортировать их не нужно.

Book.objects.get(slug="master-i-margarita")
library.models.Book.DoesNotExist: Book matching query does not exist.
Book.objects.get(author__name="Лев Толстой")
library.models.Book.MultipleObjectsReturned: get() returned more than one Book -- it returned 2!

Строка «Model matching query does not exist» в трейсбеке означает одно: условие не нашло ни одной строки. Типичные причины — запись удалена, в URL пришёл чужой slug, условие собрано не из тех данных, или код обращается к другой базе (например, к пустой тестовой).

Обработка обоих случаев:

try:
    book = Book.objects.get(slug=slug)
except Book.DoesNotExist:
    book = None
except Book.MultipleObjectsReturned:
    # данные нарушают ожидаемую уникальность — стоит залогировать
    raise

Общие базовые классы

Book.DoesNotExist наследуется от django.core.exceptions.ObjectDoesNotExist, а Book.MultipleObjectsReturned — от django.core.exceptions.MultipleObjectsReturned. Исключения разных моделей не связаны между собой: except Author.DoesNotExist не поймает ошибку Book. Базовый класс нужен, когда модель заранее неизвестна:

from django.core.exceptions import ObjectDoesNotExist


def find(model, **lookup):
    try:
        return model.objects.get(**lookup)
    except ObjectDoesNotExist:
        return None

ValueError вместо DoesNotExist

Если значение нельзя привести к типу поля, до запроса дело не доходит:

Book.objects.get(pk="abc")
ValueError: Field 'id' expected a number but got 'abc'.

Во view это ответ 500, а не 404. Защититесь конвертером в URL — <int:pk> вместо <str:pk> — или проверьте значение заранее.

get() или filter().first()

first() возвращает первый объект или None и никогда не выбрасывает исключение. Парный метод last() берёт последний объект.

Book.objects.filter(slug="idiot").first()       # → <Book: Идиот>
Book.objects.filter(slug="nope").first()        # → None
Book.objects.filter(author__name="Лев Толстой").order_by("-published").first()
# → <Book: Анна Каренина>

Разница видна в SQL. get() сбрасывает сортировку и ограничивает выборку 21 строкой, first() всегда сортирует: по Meta.ordering, по явному order_by() или, если сортировки нет, по первичному ключу.

-- Book.objects.get(slug="idiot")
SELECT ... FROM "library_book" WHERE "library_book"."slug" = 'idiot' LIMIT 21

-- Book.objects.filter(slug="idiot").first()
SELECT ... FROM "library_book" WHERE "library_book"."slug" = 'idiot'
ORDER BY "library_book"."title" ASC LIMIT 1

-- Author.objects.filter(country="RU").first()  (модель без Meta.ordering)
SELECT ... FROM "library_author" WHERE "library_author"."country" = 'RU'
ORDER BY "library_author"."id" ASC LIMIT 1
Ситуацияget()filter().first()
Запись найденаобъектобъект
Записи нетDoesNotExistNone
Подходит несколько записейMultipleObjectsReturnedпервая по сортировке
Сортировка в SQLубираетсяесть всегда
Когда выбиратьпоиск по pk или уникальному полюзапись может отсутствовать, нужна «любая первая»

Правило выбора простое. Если по смыслу запись одна (pk, slug, email с unique=True) — get(): он сообщит о нарушении уникальности, а first() молча вернёт случайную из дублей. Если записи может не быть или их может быть много, а нужна одна по порядку — first().

ORDER BY в first() по уникальному индексу почти ничего не стоит. На большой таблице с фильтром по неиндексированному полю сортировка может заметно замедлить запрос.

Свой метод get_or_none()

Чтобы не писать try/except в каждом view, добавьте метод в собственный QuerySet:

# library/models.py
class BookQuerySet(models.QuerySet):
    def get_or_none(self, *args, **kwargs):
        try:
            return self.get(*args, **kwargs)
        except self.model.DoesNotExist:
            return None


class Book(models.Model):
    ...
    objects = BookQuerySet.as_manager()
Book.objects.get_or_none(slug="nope")                                  # → None
Book.objects.filter(author__country="RU").get_or_none(slug="idiot")    # → <Book: Идиот>

В отличие от first(), такой метод сохраняет проверку на дубли: при двух подходящих записях он выбросит MultipleObjectsReturned.

get_object_or_404 и get_list_or_404

django.shortcuts.get_object_or_404() вызывает get() и превращает DoesNotExist в Http404 — Django отдаёт страницу 404 вместо ошибки сервера.

from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render

from .models import Book


def book_detail(request, slug):
    book = get_object_or_404(Book, slug=slug)
    return render(request, "library/book_detail.html", {"book": book})

Текст исключения, который видно в режиме DEBUG = True:

django.http.response.Http404: No Book matches the given query.

Первым аргументом можно передать модель, менеджер или QuerySet. QuerySet нужен, чтобы подгрузить связи или ограничить доступ:

# автор книги в том же запросе, без отдельного SELECT
book = get_object_or_404(Book.objects.select_related("author"), pk=pk)

# заказ другого пользователя даст 404
order = get_object_or_404(Order, pk=pk, user=request.user)

Два ограничения стоит помнить:

  • MultipleObjectsReturned функция не перехватывает — при неуникальном условии будет ошибка 500;
  • ValueError из-за значения неверного типа тоже пройдёт насквозь, поэтому в URL нужен подходящий конвертер.

get_list_or_404() работает через filter(): возвращает список (не QuerySet) и выбрасывает Http404, если он пуст.

from django.shortcuts import get_list_or_404


def author_books(request, author_id):
    books = get_list_or_404(Book, author_id=author_id)
    return HttpResponse(", ".join(b.title for b in books))

Для каталога или поиска пустой список — нормальный ответ с кодом 200. get_list_or_404() уместен, когда пустая выборка означает несуществующий ресурс, например страницу автора без книг.

get_or_create(): получить или создать

get_or_create() ищет объект по условию и создаёт его, если не нашёл. Возвращает кортеж (объект, создан_ли):

tag, created = Tag.objects.get_or_create(name="классика")      # → классика True
tag, created = Tag.objects.get_or_create(name="классика")      # → классика False

author, created = Author.objects.get_or_create(
    name="Антон Чехов",
    defaults={"country": "RU"},
)

Поля из defaults используются только при создании и в поиске не участвуют. Условия с lookups (name__iexact) в создаваемый объект не попадают — нужное значение надо продублировать в defaults:

Author.objects.get_or_create(
    name__iexact="charles dickens",
    defaults={"name": "Charles Dickens", "country": "GB"},
)
# без defaults объект создастся с пустым name

Если не нужен флаг, распакуйте его в _: tag, _ = Tag.objects.get_or_create(name="классика"). Для обновления существующей записи есть update_or_create() — она разобрана в статье про обновление записей в Django.

Гонка и уникальность

Внутри get_or_create() сначала выполняется SELECT ... LIMIT 21, а если ничего не найдено — INSERT в отдельной транзакции. Между SELECT и INSERT другой запрос может успеть создать такую же запись. Что будет дальше, зависит от ограничения в базе:

Поиск по полюДва одновременных запроса
unique=True или UniqueConstraintвторой INSERT падает с IntegrityError, Django перехватывает его, повторяет get() и возвращает существующий объект с created=False
без ограничения уникальностив таблице появляются две одинаковые строки

Во втором случае дубль проявится позже. Для модели Label — копии Tag без unique=True — следующий get_or_create() по тому же условию упал:

Label.MultipleObjectsReturned: get() returned more than one Label -- it returned 2!

Вывод: вызывайте get_or_create() только по полям, уникальность которых гарантирует база. Для набора полей используйте UniqueConstraint:

class Subscription(models.Model):
    user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["user", "author"], name="unique_subscription"),
        ]


Subscription.objects.get_or_create(user=request.user, author=author)

Ограничение добавляется миграцией; если в таблице уже есть дубли, миграция не применится, пока их не удалить.

aget() и другие методы в асинхронном коде

В асинхронном view синхронный get() вызвать нельзя:

django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

У всех методов есть асинхронные версии с префиксом a: aget(), afirst(), alast(), aget_or_create(), aupdate_or_create(). Для ответа 404 — aget_object_or_404() и aget_list_or_404(). Исключения те же самые.

from django.http import HttpResponse
from django.shortcuts import aget_object_or_404

from .models import Book


async def book_detail(request, slug):
    book = await aget_object_or_404(Book.objects.select_related("author"), slug=slug)
    return HttpResponse(f"{book.title} — {book.author.name}")


async def find_book(slug):
    try:
        return await Book.objects.aget(slug=slug)
    except Book.DoesNotExist:
        return None

select_related() здесь обязателен. Обращение к book.author у объекта без подгруженной связи выполняет запрос к базе и в асинхронном коде даёт ту же SynchronousOnlyOperation. Основы async и await — в статье про асинхронность в Python.

Производительность get()

Индексы

get() быстрый, если условие попадает в индекс. Первичный ключ и поля с unique=True индексируются автоматически. План запроса показывает explain() — его вызывают у filter() с тем же условием, потому что get() возвращает объект:

print(Book.objects.filter(slug="idiot").explain())
print(Book.objects.filter(title="Идиот").explain())
4 0 39 SEARCH library_book USING INDEX sqlite_autoindex_library_book_1 (slug=?)
3 0 216 SCAN library_book

Поиск по slug идёт по индексу, по title — полным перебором таблицы. Если объект часто ищут по неуникальному полю, добавьте db_index=True или models.Index в Meta.indexes. На PostgreSQL и MySQL формат вывода explain() другой, но строки Index Scan и Seq Scan читаются так же.

Что значит LIMIT 21

Чтобы при ошибочном условии не загружать в память всю таблицу, get() добавляет LIMIT 21 и читает не больше 21 строки. Отсюда формулировка в сообщении: если подходящих записей 21 и больше, Django не знает точного числа.

library.models.Author.MultipleObjectsReturned: get() returned more than one Author -- it returned more than 20!

Сортировку из Meta.ordering get() убирает — в SQL выше нет ORDER BY, хотя у Book задан ordering = ["title"]. Исключение — select_for_update() на базах, где блокировка несовместима с LIMIT: там ограничение не добавляется.

Лишние запросы

Каждый вызов get() — отдельный запрос, результат не кэшируется. Типичные потери:

  • get() в цикле по списку идентификаторов. Замените на Book.objects.in_bulk(ids) или filter(pk__in=ids) — один запрос вместо N;
  • book.author.name после get() — второй запрос. Добавьте select_related("author") перед get(); подробнее в статье про N+1 в Django ORM;
  • get() только ради проверки существования. Используйте filter(...).exists() — база не передаёт строку целиком.

get в Django REST Framework

В DRF объект для RetrieveAPIView, UpdateAPIView и ModelViewSet достаёт метод get_object(). Он берёт get_queryset(), применяет фильтры и вызывает get_object_or_404() по полю lookup_field (по умолчанию pk), затем проверяет права на объект.

from rest_framework import generics

from .models import Book
from .serializers import BookSerializer


class BookDetail(generics.RetrieveAPIView):
    queryset = Book.objects.select_related("author")
    serializer_class = BookSerializer
    lookup_field = "slug"   # URL: path("api/books/<slug:slug>/", BookDetail.as_view())

Ответы на существующий и несуществующий slug, а также на нечисловой pk во view с lookup_field по умолчанию:

GET /api/books/idiot/  → 200 {"id":3,"title":"Идиот","slug":"idiot"}
GET /api/books/nope/   → 404 {"detail":"No Book matches the given query."}
GET /api/pk/abc/       → 404 {"detail":"Not found."}

Версия get_object_or_404() из DRF дополнительно перехватывает ValueError, TypeError и ValidationError, поэтому abc вместо числового pk даёт 404, а не 500. MultipleObjectsReturned она не обрабатывает: если lookup_field указывает на неуникальное поле, API упадёт с 500. Чтобы ограничить объекты пользователем, фильтруйте get_queryset() — get_object() ищет только внутри него. Общая настройка DRF разобрана в руководстве по Django REST Framework.

Частые вопросы

Как получить объект или None без try/except?

Book.objects.filter(slug=slug).first(). Если по условию может найтись больше одной записи и это ошибка данных, лучше свой метод get_or_none() из раздела выше — он сохраняет проверку на дубли.

Почему DoesNotExist при обращении к связанному объекту?

Обратная связь OneToOneField без записи выбрасывает исключение при чтении атрибута: author.profile даст Author.profile.RelatedObjectDoesNotExist: Author has no profile. Оно наследуется от Profile.DoesNotExist, так что подходит обычный except Profile.DoesNotExist. Короткая проверка — hasattr(author, "profile").

Можно ли вызвать get() без аргументов?

Да. Model.objects.get() вернёт единственную запись таблицы или выбросит одно из двух исключений. Так иногда хранят настройки сайта в модели с одной строкой.

Как проверить в тесте, что объекта нет?

Используйте self.assertRaises(Book.DoesNotExist) в TestCase или pytest.raises(Book.DoesNotExist) — пример окружения в статье про тесты Django на pytest. Для get_object_or_404() проверяйте ответ клиента: assert response.status_code == 404.

Чем get() отличается от latest() и earliest()?

latest("published") и earliest("published") возвращают крайний объект по полю даты (или по Meta.get_latest_by) и выбрасывают DoesNotExist на пустой выборке, но не проверяют уникальность. Аналог без исключения — order_by("-published").first().

Как заблокировать строку, полученную через get()?

Вызовите select_for_update() перед get() внутри транзакции. Строка останется заблокированной для других транзакций до выхода из блока:

from django.db import transaction

with transaction.atomic():
    book = Book.objects.select_for_update().get(slug="idiot")
    ...  # изменения, которые нельзя перемешивать с параллельными запросами
    book.save()

Итог

  • get() — для поиска по pk и уникальным полям: объект, DoesNotExist или MultipleObjectsReturned, запрос с LIMIT 21 без сортировки.
  • filter().first() — когда записи может не быть: вернёт None, при дублях молча возьмёт первую.
  • get_object_or_404() во view превращает отсутствие записи в 404; QuerySet первым аргументом подгружает связи и ограничивает доступ.
  • get_or_create() защищён от гонок только при уникальном ограничении в базе.
  • В асинхронном коде — aget(), afirst(), aget_object_or_404() и select_related() для связей.
  • В DRF объект достаёт get_object(), а ошибки типа значения дают 404.

Полное описание методов — в справочнике QuerySet API.

  • #Django ORM
  • #QuerySet
  • #get
  • #get_object_or_404
  • #DRF
↑ ↓ выборEnter открытьEsc закрыть