Эта статья перевод. Оригинал: Using Django querysets effectively.
Системы объектно-реляционного отображения (ORM) делают взаимодействие с базой данных SQL намного легче, но имеют репутацию неэффективных и значительно более медленных решений, чем «сырые» SQL запросы.
Эффективное использование ORM подразумевает некоторое понимание того, как система строит запросы к базе данных. В этой статье я опишу способы эффективного использования Django ORM системы для средних и огромных наборов данных.
Django QuerySet-ы ленивы
QuerySet в Django является представлением некоторого числа строк в базе данных, опционально отфильтрованных посредством запроса. Например, следующий код является представлением всех людей в базе данных по имени ‘Dave’:
1 |
person_set = Person.objects. filter (first_name = "Dave" ) |
Приведенный выше код не запускает какие-либо запросы к базе данных. Вы можете можете взять person_set
и применить дополнительные фильтры, или передать его в функцию, и ничего не будет отправлено в базу данных. Это хорошо, потому что запросы к базе данных являются одной из вещей, которые существенно замедляют веб-приложения.
Для выборки данных из базы данных необходимо осуществить перебор по QuerySet:
1 |
for person in person_set: |
2 |
print (person.last_name) |
У Django QuerySets есть кэш
Как только вы начнете осуществлять перебор по QuerySet, все строки соответствующие QuerySet извлекутся из базы данных и будут преобразованы в объекты моделей Django. Это называется вычисление (evaluation). Затем эти модели сохранятся во встроенный кэш QuerySet, так что если вы осуществите перебор по QuerySet снова, вы не выполните тот же запрос повторно.
Например, следующий код выполнит только один запрос к базе данных:
1 |
pet_set = Pet.objects. filter (species = "Dog" ) |
if-выражения вызывают вычисление QuerySet
Самым полезным в кэше QuerySet является то, что он позволяет эффективно проверить, содержит ли QuerySet строки, а затем только осуществить перебор по ним, если хотя бы одна строка была найдена:
1 |
restaurant_set = Restaurant.objects. filter (cuisine = "Indian" ) |
5 |
for restaurant in restaurant_set: |
Кэш QuerySet является проблемой, если вам не нужны все результаты выборки
Иногда, вместо того чтобы осуществлять перебор по результатам выборки, вы просто хотите убедиться, что существует по крайней мере один результат. В этом случае, простое использование if-выражения с QuerySet по-прежнему будет полностью вычислять QuerySet и заполнять его кэш. Даже если вы никогда не планируете использовать эти результаты!
1 |
city_set = City.objects. filter (name = "Cambridge" ) |
6 |
print ( "At least one city called Cambridge still stands!" ) |
Чтобы избежать этого, используйте метод exists()
, дабы проверить, была ли найдена хотя бы одна соответствующая строка:
1 |
tree_set = Tree.objects. filter ( type = "deciduous" ) |
6 |
print ( "There are still hardwood trees in the world!" ) |
Кэш QuerySet является проблемой, если ваш QuerySet огромен
Если вы имеете дело с тысячами строк данных, единовременное помещение их всех в память может быть очень расточительно. Хуже того, огромный QuerySet может заблокировать серверные процессы, вызвав останов всего веб-приложения.
Для предотвращения переполнения кэша QuerySet, но сохранения возможности осуществления перебора по всем результатам выборки, используйте метод iterator()
для извлечения данных частями, и отбрасывайте старые строки, когда они были обработаны.
1 |
star_set = Star.objects. all () |
4 |
for star in star_set.iterator(): |
Конечно, использование метода iterator()
, чтобы избежать переполнения кэша QuerySet, означает, что повторный перебор по тому же QuerySet снова будет выполнять запрос к базе данных. Так что используйте iterator()
с осторожностью, и убедитесь, что ваш код организован так, чтобы избежать повторного вычисления того же огромного QuerySet.
if-выражение является проблемой, если ваш QuerySet огромен
Как было показано ранее, кэш QuerySet отлично подходит для объединения if-выражением с for-выражением. Это позволяет осуществлять условный перебор по QuerySet. Для огромных QuerySet-ов, однако, заполнение кэша QuerySet это не вариант.
Самым простым решением является объединение exists()
с iterator()
, позволяющее избегать заполнения кэша QuerySet за счет запуска двух запросов к базе данных.
1 |
molecule_set = Molecule.objects. all () |
3 |
if molecule_set.exists(): |
5 |
for molecule in molecule_set.iterator(): |
6 |
print (molecule.velocity) |
Более сложным решением является использование передовых методов перебора в языке Python чтобы выбрать первую позицию в iterator()
, прежде чем решить, следует ли продолжать итерации.
01 |
atom_set = Atom.objects. all () |
03 |
atom_iterator = atom_set.iterator() |
06 |
first_atom = next (atom_iterator) |
13 |
from itertools import chain |
14 |
for atom in chain([first_atom], atom_iterator): |
Остерегайтесь наивной оптимизации
Кэш QuerySet существует для того, чтобы уменьшить количество запросов к базе данных, сделанных вашим приложением, и при нормальном использовании гарантирует, что ваша база данных будет запрошена только в случае необходимости.
Использование методов exists()
и iterator()
позволяет оптимизировать использование памяти вашего приложения. Однако, так как они не заполняют кэш QuerySet, они могут привести к дополнительным запросам к базе данных.
Так что пишите код внимательно, и если приложение начинает замедляться, взгляните на узкие места в коде. Возможно, небольшая оптимизации QuerySet сможет вам помочь.