сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Базы данных в Python

Django ORM

Django ORM: F(), bulk, values, get

Django ORM полон удобных сокращений, у каждого из которых есть подвох, который любят на собесе: F() против гонок, bulk_create против сигналов, get() против None. Знание этих граней отличает того, кто писал прод, от того, кто прошёл туториал.

Типовые формулировки: «как безопасно инкрементировать счётчик под конкуренцией?», «чем плох bulk_create?», «что вернёт get() на пустой результат».

F() против гонок, bulk и values для скорости

F() ссылается на значение поля на СТОРОНЕ БД: F('count')+1 превращается в UPDATE SET count=count+1 одним запросом. Это снимает гонку read-modify-write - в отличие от obj.count += 1; obj.save(), где два параллельных воркера прочитают одно значение и один инкремент потеряется.

bulk_create вставляет пачку объектов одним или несколькими INSERT вместо перебора save() - на порядки быстрее на больших вставках. Цена: save() и сигналы НЕ вызываются, а в части бэкендов не проставляется PK (primary key). .values() возвращает словари перечисленных полей вместо инстансов модели - меньше памяти и работы, когда нужны сырые данные, а не методы модели.

// Общий мотив: перекладывай работу на БД (F, bulk) и не тащи в Python лишнее (values), но помни, что теряется (сигналы, методы).

# гонка:  obj.count += 1; obj.save()
# атомарно, без гонки:
Row.objects.filter(id=pk).update(count=F('count') + 1)
F()
выражение над полем на стороне БД, без чтения в Python
bulk_create
пакетная вставка вместо перебора save()

get() бросает, а не возвращает None

get() возвращает РОВНО один объект или бросает исключение: DoesNotExist, если записи нет, и MultipleObjectsReturned, если их больше одной. None он не возвращает никогда. Кто ждёт «объект или None», ловит необработанный DoesNotExist в проде.

«Объект или None» это filter(...).first(): вернёт первый подходящий или None, не бросая. Выбор между ними - про то, ожидаешь ли ты отсутствие как нормальный случай (first) или как ошибку (get с обработкой DoesNotExist).

// .values() возвращает словари, а не инстансы, поэтому методы модели на них не вызвать это осознанный размен на скорость и память.

.values()
выборка словарей полей вместо инстансов модели
DoesNotExist
исключение get() при отсутствии записи

Как отвечать: «Как безопасно увеличить счётчик под конкуренцией?»

Через F(), а не чтением в Python. Если сделать obj.count += 1 и obj.save(), то под конкуренцией два воркера прочитают одно и то же значение, каждый прибавит единицу и запишет - один инкремент потеряется, это классическая гонка read-modify-write. F('count')+1 вместо этого генерирует UPDATE SET count = count + 1, то есть прибавление считает сама БД атомарно в одном запросе, ничего не читая в Python. Параллельные апдейты сериализует база на уровне строки, и ни один инкремент не пропадёт. Для более сложных инвариантов можно ещё взять SELECT FOR UPDATE, но для счётчика достаточно F().

Названа конкретная гонка, показан механизм F() (счёт на стороне БД одним UPDATE), объяснено почему это снимает потерю обновлений и упомянут FOR UPDATE для сложных случаев.

На чём валят

  • obj.count += 1; obj.save() под конкуренцией теряет обновления - нужен F() или атомарный UPDATE.
  • Ждать от get() None на пусто: он бросает DoesNotExist; «объект или None» - filter().first().
  • bulk_create молча пропускает save()/сигналы и в части бэкендов не проставляет PK.
  • Тянуть инстансы модели, когда нужны только пара полей - .values() экономит память и время.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.

  1. #orm_django1 / 5
    Нужно вставить тысячу объектов Django одним махом. Что взять?
    A)get_or_create в цикле, чтобы избежать дубликатов при вставке
    B)Цикл с .save() на каждом объекте — Django сам их сгруппирует
    C)bulk_create — один запрос вместо тысячи save()
    D)transaction.atomic вокруг цикла — это и есть пакетная вставка
    показать ответ и разбор
    +C)bulk_create — один запрос вместо тысячи save()

    // разбор: bulk_create отправляет пакет объектов одним (или несколькими батч-) INSERT, минуя перебор save(). На тысячах строк это на порядки быстрее. Плата: не вызываются save()/сигналы и (в части бэкендов) не проставляются PK — учитывай это в логике.

  2. #orm_django2 / 5
    Чем .values() отличается от обычной выборки объектов модели в Django ORM?
    A)values() умеет выбирать только по первичному ключу таблицы
    B)values() возвращает те же инстансы, только доступные для чтения
    C)values() быстрее, потому что кэширует набор в памяти
    D)values() отдаёт словари полей, а не инстансы модели
    показать ответ и разбор
    +D)values() отдаёт словари полей, а не инстансы модели

    // разбор: values() возвращает QuerySet словарей (только перечисленные колонки), а не инстансы модели — не создаются объекты, недоступны методы модели, зато меньше памяти и работы. Годится, когда нужны сырые данные для сериализации или агрегатов, а не поведение модели.

  3. #orm_django3 / 5
    Model.objects.get(id=1) для несуществующей записи — что вернёт?
    A)Бросит DoesNotExist, а не вернёт None
    B)Создаст новую пустую запись с этим id автоматически
    C)Вернёт пустой QuerySet, по которому можно итерироваться
    D)Вернёт None, как это делает словарь при отсутствии ключа
    показать ответ и разбор
    +A)Бросит DoesNotExist, а не вернёт None

    // разбор: get() возвращает ровно один объект или бросает DoesNotExist, если ничего не нашёл (и MultipleObjectsReturned, если нашёл больше одного). None он не возвращает — это частая ошибка ожиданий. Нужен «объект или None» — берут filter(...).first().

  4. #orm_django4 / 5
    Django ORM реализует паттерн Active Record. Что это значит?
    A)Объект модели сам умеет сохраняться и удаляться: order.save(), order.delete()
    B)Модель хранит только активные (не удалённые) строки, скрывая помеченные на удаление
    C)Модели полностью отделены от сохранения, которым занимается отдельный репозиторий
    D)Все записи таблицы держатся загруженными в памяти процесса
    показать ответ и разбор
    +A)Объект модели сам умеет сохраняться и удаляться: order.save(), order.delete()

    // разбор: Active Record: объект-запись несёт и данные, и поведение персистентности — model_instance.save() вставляет/обновляет строку, .delete() удаляет. Это лаконично и быстро для типичного CRUD (сильная сторона Django). Обратная сторона — домен срастается с ORM: бизнес-логика в моделях тяжелее тестируется без БД, отсюда в сложных системах вводят сервисный слой поверх.

  5. #orm_django5 / 5
    Чем отличаются Model.objects.get() и Model.objects.filter() при поиске одной записи?
    A)Разницы нет, это взаимозаменяемые способы получить запись по заданному условию
    B)Оба возвращают ровно один объект, просто get быстрее за счёт использования индекса
    C)get возвращает список из одного элемента, а filter — сам объект напрямую без обёртки
    D)get вернёт один объект или бросит исключение; filter — всегда QuerySet (0..N)
    показать ответ и разбор
    +D)get вернёт один объект или бросит исключение; filter — всегда QuerySet (0..N)

    // разбор: get() ожидает ровно одну запись: ноль — DoesNotExist, больше одной — MultipleObjectsReturned. filter() возвращает QuerySet (ленивый набор), пустой или с элементами, без исключений. Поэтому get удобен для поиска по уникальному ключу с явной обработкой отсутствия, а filter — когда результатов может быть несколько или ни одного и это нормально.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.