get() в Django ORM: DoesNotExist и get_object_or_404
Как получить один объект в Django ORM: get() и его исключения, first() вместо try/except, get_object_or_404, get_or_create без гонок, aget() и get в DRF.
Содержание · 11 разделов
- Модели для примеров
- Как работает get() в Django ORM
- DoesNotExist и MultipleObjectsReturned
- Общие базовые классы
- ValueError вместо DoesNotExist
- get() или filter().first()
- Свой метод get_or_none()
- get_object_or_404 и get_list_or_404
- get_or_create(): получить или создать
- Гонка и уникальность
- aget() и другие методы в асинхронном коде
- Производительность get()
- Индексы
- Что значит LIMIT 21
- Лишние запросы
- get в Django REST Framework
- Частые вопросы
- Как получить объект или None без try/except?
- Почему DoesNotExist при обращении к связанному объекту?
- Можно ли вызвать get() без аргументов?
- Как проверить в тесте, что объекта нет?
- Чем get() отличается от latest() и earliest()?
- Как заблокировать строку, полученную через get()?
- Итог
Метод 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() |
|---|---|---|
| Запись найдена | объект | объект |
| Записи нет | DoesNotExist | None |
| Подходит несколько записей | 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