Работа с запросами

После того как вы создали модели данных, Django автоматически предоставляет вам API-интерфейс для базы данных, который позволяет создавать, извлекать, обновлять и удалять объекты. Этот документ объясняет, как использовать этот API. Обратитесь к справочнику по модели данных для получения полной информации обо всех различных параметрах поиска модели.

Throughout this guide (and in the reference), we’ll refer to the following models, which comprise a blog application:

from datetime import date

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):
        return self.name

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField(default=date.today)
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField(default=0)
    number_of_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)

    def __str__(self):
        return self.headline

Создание объектов

Для представления данных таблицы базы данных в объектах Python Django использует интуитивно понятную систему: класс модели представляет таблицу базы данных, а экземпляр этого класса представляет конкретную запись в таблице базы данных.

Чтобы создать объект, создайте его экземпляр с помощью аргументов ключевого слова для класса модели, а затем вызовите save(), чтобы сохранить его в базе данных.

Предполагая, что модели находятся в файле mysite/blog/models.py:

>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

За кулисами выполняется SQL-оператор INSERT. Django не обращается в базу данных, пока вы не вызовете явно save().

Метод save() не имеет возвращаемого значения.

См.также

save() использует несколько дополнительных параметров, которые здесь не описаны. Смотрите документацию save() для получения полной информации.

Чтобы создать и сохранить объект за один шаг, используйте метод create().

Сохранение изменений в объектах

Чтобы сохранить изменения в объекте, который уже находится в базе данных, используйте save().

Для экземпляра Blog b5, который уже был сохранен в базе данных, этот пример меняет имя и обновляет запись в базе данных:

>>> b5.name = 'New name'
>>> b5.save()

Здесь выполняется оператор SQL UPDATE. Django не делает запрос в базу данных, пока вы не вызовете явно save().

Сохранение полей ForeignKey и ManyToManyField

Обновление поля ForeignKey работает точно так же, как и сохранение обычного поля - назначьте объект нужного типа соответствующему полю. В этом примере обновляется атрибут blog экземпляра Entry entry, при условии, что соответствующие экземпляры Entry и Blog уже сохранены в базе данных (поэтому мы можем получить их ниже):

>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

Обновление ManyToManyField работает немного иначе - используйте метод add() для добавления записи к отношению. Этот пример добавляет экземпляр Author joe к объекту entry:

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

Чтобы добавить несколько записей в ManyToManyField за один раз, включите в вызов несколько аргументов add(), например:

>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

Джанго сообщит, если вы попытаетесь назначить или добавить объект неправильного типа.

Получение объектов

Чтобы получить объекты из вашей базы данных, создайте QuerySet через Manager в своем классе модели.

QuerySet представляет коллекцию объектов из вашей базы данных. Может иметь ноль, один или несколько фильтров. Фильтры сужают результаты запроса на основе заданных параметров. В терминах SQL QuerySet приравнивается к оператору SELECT, а фильтр является ограничивающим предложением, таким как WHERE или LIMIT .

Вы получаете QuerySet, используя класс менеджер Manager вашей модели. Каждая модель имеет по крайней мере один Manager, и по умолчанию он называется objects. Доступ к нему напрямую через класс модели, например:

>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
    ...
AttributeError: "Manager isn't accessible via Blog instances."

Примечание

Managers доступны только через классы модели, а не из экземпляров модели, чтобы обеспечить разделение между операциями на уровне таблицы и операциями на уровне записи.

Manager является основным источником QuerySets для модели. Например, Blog.objects.all() возвращает QuerySet, который содержит все объекты Blog в базе данных.

Получение всех объектов

Самый простой способ извлечь объекты из таблицы - это получить их все. Для этого используйте метод all() в классе Manager:

>>> all_entries = Entry.objects.all()

Метод all() возвращает QuerySet всех объектов в базе данных.

Получение определенных объектов с помощью фильтров

QuerySet, возвращаемый all() описывает все объекты в таблице базы данных. Обычно нужно выбрать только подмножество полного набора объектов.

Чтобы создать такое подмножество, вы уточняете исходный QuerySet, добавляя условия фильтра. Два наиболее распространенных способа уточнить QuerySet:

filter(**kwargs)
Возвращает новый QuerySet, содержащий объекты, которые соответствуют заданным параметрам поиска.
exclude(**kwargs)
Возвращает новый QuerySet, содержащий объекты, которые не соответствуют указанным параметрам поиска.

Параметры поиска (**kwargs в приведенных выше определениях функций) должны быть в формате, описанном в разделе Поиск по полю.

Например, чтобы получить QuerySet записей блога за 2006 год, используйте filter() примерно так:

Entry.objects.filter(pub_date__year=2006)

С классом менеджера по умолчанию, он такой же, как:

Entry.objects.all().filter(pub_date__year=2006)

Цепочки фильтров

Результат уточнения QuerySet сам по себе является QuerySet, поэтому можно объединять уточнения вместе. Например:

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(
...     pub_date__gte=datetime.date(2005, 1, 30)
... )

Здесь берется начальный QuerySet всех записей в базе данных, добавляется фильтр, затем исключение, затем другой фильтр. Окончательный результат QuerySet , содержащий все записи с заголовком, начинающимся с «What», которые были опубликованы в период с 30 января 2005 года и по сегодняшний день.

Отфильтрованные QuerySet являются уникальными

Каждый раз, когда вы уточняете QuerySet, вы получаете совершенно новый QuerySet, который никак не связан с предыдущим QuerySet. Каждое уточнение создает отдельный QuerySet, который можно хранить, использовать и переиспользовать повторно.

Пример:

>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())

Эти три QuerySets - разные. Первый - это базовый QuerySet, содержащий все записи, содержащие заголовок, начинающийся с «What». Второе - это подмножество первого, с дополнительными критериями, исключающими записи, чье pub_date сегодня или в будущем. Третий - это подмножество первого, с дополнительными критериями, которые выбирают только те записи, чьи pub_date находятся сегодня или в будущем. Начальный QuerySet (q1) не зависит от процесса уточнения.

`` QuerySet`` - ленивые

QuerySets ленивые - процесс создания QuerySet не связан с какими-либо действиями с базой данных. Вы можете складывать фильтры вместе весь день, и Django не будет выполнять запрос до тех пор, пока не будет вызван QuerySet. Посмотрите на этот пример:

>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)

Хотя это выглядит как три запроса в базу данных, на самом деле это только один запрос в базу данных, в последней строке (print(q)). В общем, результаты QuerySet не извлекаются из базы данных, пока вы не запросите их. Когда вы это делаете, QuerySet вызывается путем доступа к базе данных. Для получения более подробной информации о том, когда именно происходит вызов, смотрите Когда вычисляется QuerySet.

Получение одного объекта с помощью get()

filter() всегда возвращает QuerySet, даже если только один объект соответствует запросу - в этом случае это будет QuerySet, содержащий один элемент.

Если вы знаете, что только один объект соответствует вашему запросу, вы можете использовать метод get() из Manager который возвращает объект напрямую:

>>> one_entry = Entry.objects.get(pk=1)

Вы можете использовать любое выражение запроса с get(), так же как с filter(). Смотрите Поиск по полю.

Обратите внимание, что есть разница между использованием get() и использованием filter() с фрагментом [0]. Если нет результатов, соответствующих запросу, то get() вызовет исключение DoesNotExist. Это исключение является атрибутом класса модели, к которому выполняется запрос - поэтому в приведенном выше коде, если нет объекта Entry с первичным ключом 1, Django вызовет Entry.DoesNotExist.

Точно так же Django выдаст ошибку, если более чем один элемент соответствует запросу get(). В этом случае он вызовет MultipleObjectsReturned, который тоже является атрибутом самого класса модели.

Другие методы QuerySet

В большинстве случаев вы будете использовать all(), get(), filter() и exclude(), когда вам нужно искать объекты в базе данных. Однако это далеко не все; смотрите Справочник по API QuerySet для получения полного списка всех методов QuerySet.

Ограничение QuerySet

Используйте подмножество синтаксиса нарезки массивов в Python, чтобы ограничить ваш QuerySet определенным количеством результатов. Это эквивалент выражений SQL LIMIT и OFFSET.

Например, этот код возвращает первые 5 объектов (LIMIT 5):

>>> Entry.objects.all()[:5]

Это возвращает объекты с шестого по десятый (OFFSET 5 LIMIT 5):

>>> Entry.objects.all()[5:10]

Отрицательное индексирование (т.е. Entry.objects.all()[-1]) не поддерживается.

Как правило, такая выборка QuerySet возвращает новый QuerySet - он не вызывает запрос. Исключением является использование параметра «step» синтаксиса фрагмента Python. Например, этот код фактически выполнит запрос, чтобы вернуть список каждого второго объекта из первых 10:

>>> Entry.objects.all()[:10:2]

Дальнейшая фильтрация или упорядочение нарезанного набора запросов запрещены из-за неоднозначного характера того, как это может работать.

Чтобы извлечь один объект, а не список (например, SELECT foo FROM bar LIMIT 1), используйте простой индекс вместо среза. Например, этот код возвращает первый Entry в базе данных, после упорядочивания записей в алфавитном порядке по заголовку:

>>> Entry.objects.order_by('headline')[0]

Это примерно эквивалентно:

>>> Entry.objects.order_by('headline')[0:1].get()

Однако обратите внимание, что первый из них вызовет IndexError, а второй вызовет DoesNotExist, если ни один объект не соответствует заданным критериям. Смотрите get() для получения более подробной информации.

Поиск по полям

Поиск по полю - это то, как вы определяете содержание выражения SQL WHERE. Они указываются в качестве аргументов ключевых слов для методов filter(), exclude() и get() класса QuerySet.

Базовые аргументы поиска по ключевым словам имеют вид field__lookuptype=value. (Это двойное подчеркивание). Например:

>>> Entry.objects.filter(pub_date__lte='2006-01-01')

переводит (примерно) в следующий SQL:

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

Как это возможно

Python имеет возможность определять функции, которые принимают произвольные аргументы имя-значение, имена и значения которых оцениваются во время выполнения. Для получения дополнительной информации см. Keyword Arguments в официальном руководстве по Python.

Поле, указанное в поиске, должно быть именем поля модели. Однако есть одно исключение: в случае ForeignKey вы можете указать имя поля с суффиксом _id. В этом случае ожидается, что параметр value будет содержать необработанное значение первичного ключа внешней модели. Например:

>>> Entry.objects.filter(blog_id=4)

Если вы передадите неверный аргумент ключевого слова, функция поиска вызовет TypeError.

API базы данных поддерживает около двух десятков типов поиска; полная документация может быть найдена в справочнике поиска по полям. Чтобы дать вам представление о том, что доступно, вот некоторые из наиболее часто используемых поисков:

exact

«Точное» совпадение. Например:

>>> Entry.objects.get(headline__exact="Cat bites dog")

Будет генерировать SQL по этим строкам:

SELECT ... WHERE headline = 'Cat bites dog';

Если вы не предоставляете тип поиска - то есть, если аргумент ключевого слова не содержит двойного подчеркивания, - тип поиска считается точным.

Например, следующие два оператора эквивалентны:

>>> Blog.objects.get(id__exact=14)  # Explicit form
>>> Blog.objects.get(id=14)         # __exact is implied

Это сделано для удобства, потому что точный поиск является распространенным случаем.

iexact

Соответствие без учета регистра. Итак, запрос:

>>> Blog.objects.get(name__iexact="beatles blog")

Будет соответствовать Blog с названием "Beatles Blog", "beatles blog" или даже "BeAtlES blOG".

contains

Проверка содержимого в зависимости от регистра. Например:

Entry.objects.get(headline__contains='Lennon')

Примерно переводится в такой SQL:

SELECT ... WHERE headline LIKE '%Lennon%';

Обратите внимание, что это будет соответствовать заголовку 'Today Lennon honored', но не 'today lennon honored'.

Существует также версия без учета регистра icontains.

startswith, endswith
«Начинается с» и «заканчивается» соответственно. Существуют также версии без учета регистра istartswith и iendswith.

Опять же, это только поверхностное описание. Полное руководство можно найти в Справочнике поиска по полям.

Поиск, который использует отношения

Django предлагает мощный и интуитивно понятный способ «отслеживать» отношения в поисках, автоматически заботясь о SQL JOIN для вас, за кулисами. Чтобы охватить отношение, просто используйте имя поля связанных полей в моделях, разделенных двойным подчеркиванием, пока не дойдете до нужного поля.

В этом примере извлекаются все объекты Entry с помощью Blog, чье name равно 'Beatles Blog':

>>> Entry.objects.filter(blog__name='Beatles Blog')

Этот охват может быть настолько глубоким, насколько вы захотите.

Он работает и в обратном направлении. Хотя может быть настроен как, по умолчанию вы ссылаетесь на «обратную» связь в поиске, используя имя модели в нижнем регистре.

В этом примере извлекаются все объекты Blog, у которых есть хотя бы один Entry, чей headline содержит 'Lennon':

>>> Blog.objects.filter(entry__headline__contains='Lennon')

Если вы фильтруете по нескольким отношениям и одна из промежуточных моделей не имеет значения, которое удовлетворяет условию фильтра, Django будет обрабатывать его как пустой (все значения NULL), но допустимый объект там. Все это означает, что никакой ошибки не возникнет. Например, в этом фильтре:

Blog.objects.filter(entry__authors__name='Lennon')

(если была связанная Author модель), если бы не было author, связанного с записью, она будет обрабатываться так, как если бы к ней также не было прикреплено name, вместо того, чтобы выдавать ошибку из-за пропавшего author. Обычно это именно то, что вы хотите, чтобы произошло. Единственный случай, когда это может сбить с толку, это если вы используете isnull. Таким образом:

Blog.objects.filter(entry__authors__name__isnull=True)

вернет объекты Blog, которые имеют пустое name в author, а также те, которые имеют пустое author в entry. Если вы не хотите получать эти последние объекты, вы можете написать:

Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

Охватывающие многозначные отношения

При охвате ManyToManyField или обратном ForeignKey (например, от Blog до Entry) фильтрация по нескольким атрибутам поднимает вопрос о том, нужно ли требовать, чтобы каждый атрибут совпадал в одном и том же связанном объекте. Мы можем искать блоги, в которых есть запись 2008 года с «Lennon « в заголовке, или мы можем искать блоги, в которых есть просто любая запись 2008 года, а также более новая или старая запись с «Lennon « в заголовке.

To select all blogs containing at least one entry from 2008 having «Lennon» in its headline (the same entry satisfying both conditions), we would write:

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

Иначе, чтобы выполнить более свободный запрос, выбирающий все блоги, в которых есть только некоторая запись с «Lennon « в заголовке и некоторая запись от 2008 года, мы бы написали:

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

Предположим, что существует только один блог, в котором есть как записи, содержащие «Lennon «, так и записи 2008 года, но ни одна из записей 2008 года не содержит «Lennon «. Первый запрос не вернет ни одного блога, но второй запрос вернет этот единственный блог. (Это происходит потому, что записи, отобранные вторым фильтром, могут совпадать или не совпадать с записями в первом фильтре. Мы фильтруем элементы Blog с каждым оператором фильтра, а не элементы Entry). Короче говоря, если каждое условие должно соответствовать одному и тому же связанному объекту, то каждое из них должно содержаться в одном вызове filter().

Примечание

Поскольку второй (более разрешительный) запрос цепляет несколько фильтров, он выполняет несколько присоединений к первичной модели, потенциально давая дубликаты.

>>> from datetime import date
>>> beatles = Blog.objects.create(name='Beatles Blog')
>>> pop = Blog.objects.create(name='Pop Music Blog')
>>> Entry.objects.create(
...     blog=beatles,
...     headline='New Lennon Biography',
...     pub_date=date(2008, 6, 1),
... )
<Entry: New Lennon Biography>
>>> Entry.objects.create(
...     blog=beatles,
...     headline='New Lennon Biography in Paperback',
...     pub_date=date(2009, 6, 1),
... )
<Entry: New Lennon Biography in Paperback>
>>> Entry.objects.create(
...     blog=pop,
...     headline='Best Albums of 2008',
...     pub_date=date(2008, 12, 15),
... )
<Entry: Best Albums of 2008>
>>> Entry.objects.create(
...     blog=pop,
...     headline='Lennon Would Have Loved Hip Hop',
...     pub_date=date(2020, 4, 1),
... )
<Entry: Lennon Would Have Loved Hip Hop>
>>> Blog.objects.filter(
...     entry__headline__contains='Lennon',
...     entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>]>
>>> Blog.objects.filter(
...     entry__headline__contains='Lennon',
... ).filter(
...     entry__pub_date__year=2008,
... )
<QuerySet [<Blog: Beatles Blog>, <Blog: Beatles Blog>, <Blog: Pop Music Blog]>

Примечание

Поведение filter() для запросов, которые охватывают многозначные отношения, как описано выше, не реализовано эквивалентно для exclude(). Вместо этого условия в одном вызове exclude() не обязательно ссылаются на один и тот же элемент.

Например, следующий запрос исключит блоги, содержащие обе записи с «Lennon» в заголовке и записи, опубликованные в 2008 году:

Blog.objects.exclude(
    entry__headline__contains='Lennon',
    entry__pub_date__year=2008,
)

Однако, в отличие от поведения при использовании filter(), это не ограничивает блоги на основе записей, которые удовлетворяют обоим условиям. Чтобы сделать это, то есть, чтобы выбрать все блоги, которые не содержат записей, опубликованных с «Lennon», которые были опубликованы в 2008 году, вам нужно сделать два запроса:

Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains='Lennon',
        pub_date__year=2008,
    ),
)

Фильтры могут ссылаться на поля модели

В приведенных выше примерах мы создали фильтры, которые сравнивают значение модельного поля с константой. Но что, если вы хотите сравнить значение поля модели с другим полем той же модели?

Django предоставляет F выражения, чтобы разрешить такие сравнения. Экземпляры F() действуют как ссылка на поле модели в запросе. Затем эти ссылки можно использовать в фильтрах запросов для сравнения значений двух разных полей в одном экземпляре модели.

Например, чтобы найти список всех записей блога, у которых было больше комментариев, чем у пингбэков, мы создаем объект F() для ссылки на счетчик пингбэков и используем этот объект F() в запросе:

>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))

Django поддерживает использование сложения, вычитания, умножения, деления, модуля и степеней с объектами F(), как с константами, так и с другими объектами F(). Чтобы найти все записи в блоге с более чем вдвое большим количеством комментариев, чем пингбэками, мы модифицируем запрос:

>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks') * 2)

Чтобы найти все записи, в которых рейтинг записи меньше суммы пингбэков и комментариев, мы должны выполнить запрос:

>>> Entry.objects.filter(rating__lt=F('number_of_comments') + F('number_of_pingbacks'))

Вы также можете использовать нотацию двойного подчеркивания, чтобы охватить отношения в объекте F(). Объект F() с двойным подчеркиванием будет вводить любые объединения, необходимые для доступа к связанному объекту. Например, чтобы получить все записи, в которых имя автора совпадает с именем блога, мы можем выполнить запрос:

>>> Entry.objects.filter(authors__name=F('blog__name'))

Для полей даты и даты/времени вы можете добавить или вычесть объект timedelta. Следующее вернет все записи, которые были изменены более чем через 3 дня после их публикации:

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

Объекты F() поддерживают побитовые операции с помощью .bitand(), .bitor(), .bitxor(), .bitrightshift() и .bitleftshift(). Например:

>>> F('somefield').bitand(16)

Oracle

Oracle не поддерживает побитовую операцию XOR.

Выражения могут ссылаться на преобразования

Django поддерживает использование преобразований в выражениях.

For example, to find all Entry objects published in the same year as they were last modified:

>>> from django.db.models import F
>>> Entry.objects.filter(pub_date__year=F('mod_date__year'))

Чтобы найти самый ранний год публикации записи, мы можем выполнить запрос:

>>> from django.db.models import Min
>>> Entry.objects.aggregate(first_published_year=Min('pub_date__year'))

В этом примере определяется значение записи с наивысшим рейтингом и общее количество комментариев ко всем записям за каждый год:

>>> from django.db.models import OuterRef, Subquery, Sum
>>> Entry.objects.values('pub_date__year').annotate(
...     top_rating=Subquery(
...         Entry.objects.filter(
...             pub_date__year=OuterRef('pub_date__year'),
...         ).order_by('-rating').values('rating')[:1]
...     ),
...     total_comments=Sum('number_of_comments'),
... )

Использование сокращения pk

Для удобства Django предоставляет сокращение для поиска pk, который обозначает «первичный ключ».

В примере модели Blog первичным ключом является поле id, поэтому эти три оператора эквивалентны:

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

Использование pk не ограничивается __exact запросами - любое условие запроса может быть объединено с pk для выполнения запроса по первичному ключу модели:

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])

# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

Поиск pk также работает через объединения. Например, эти три утверждения эквивалентны:

>>> Entry.objects.filter(blog__id__exact=3) # Explicit form
>>> Entry.objects.filter(blog__id=3)        # __exact is implied
>>> Entry.objects.filter(blog__pk=3)        # __pk implies __id__exact

Экранирующие знаки процента и подчеркивания в выражениях LIKE

Поиск в полях, эквивалентный операторам LIKE (iexact, contains, icontains, startswith, istartswith, endswith and iendswith) автоматически уберет два специальных символа, используемых в операторах LIKE - знак процента и подчеркивание. (В операторе LIKE знак процента означает подстановочный знак из нескольких символов, а подчеркивание означает подстановочный знак из одного символа.)

Это означает, что все должно работать интуитивно, чтобы абстракция не просачивалась. Например, чтобы получить все записи, содержащие знак процента, просто используйте знак процента, как и любой другой символ:

>>> Entry.objects.filter(headline__contains='%')

Джанго заботится об экранировании для вас; результирующий SQL будет выглядеть примерно так:

SELECT ... WHERE headline LIKE '%\%%';

То же самое касается подчеркивания. Оба знака процента и подчеркивания обрабатываются для вас прозрачно.

Кэширование и QuerySet

Каждый класс QuerySet содержит кэш для минимизации доступа к базе данных. Понимание того, как это работает, позволит вам написать наиболее эффективный код.

Во вновь созданном QuerySet кеш пуст. Первый раз QuerySet оценивается и, следовательно, происходит запрос к базе данных - Django сохраняет результаты запроса QuerySet в кэш и возвращает результаты, которые были явно запрошены (например, следующий элемент, если итерация происходит через QuerySet). Последующие оценки QuerySet повторно используют кэшированные результаты.

Помните об этом поведении кэширования, потому что оно может удивить вас, если вы не используете правильно QuerySet. Например, следующее создаст два QuerySet, оцените их:

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

Это означает, что один и тот же запрос к базе данных будет выполнен дважды, что фактически удваивает нагрузку на вашу базу данных. Кроме того, существует вероятность того, что два списка могут не включать в себя одни и те же записи базы данных, потому что «запись» может быть добавлена или удалена за доли секунды между двумя запросами.

Чтобы избежать этой проблемы, просто сохраните QuerySet и повторно используйте его:

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Reuse the cache from the evaluation.

Когда QuerySet не кэшируется

Queryset`ы не всегда кэшируют свои результаты. При оценке только части набора запросов проверяется кэш, но если он не заполняется, элементы, возвращаемые последующим запросом, не кэшируются. В частности, это означает, что ограничение набора запросов с использованием среза массива или индекса не заполнит кэш.

Например, многократное получение определенного индекса в объекте набора запросов будет каждый раз запрашивать базу данных:

>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # Queries the database
>>> print(queryset[5]) # Queries the database again

Однако, если весь набор запросов уже был оценен, вместо этого будет проверяться кеш:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print(queryset[5]) # Uses cache
>>> print(queryset[5]) # Uses cache

Вот несколько примеров других действий, которые приведут к оценке всего набора запросов и, следовательно, заполнению кеша:

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

Примечание

Простой вывод набора запросов не заполнит кэш. Это связано с тем, что вызов __repr__() возвращает только часть всего набора запросов.

Асинхронные запросы

New in Django Development version.

Если вы пишете асинхронные представления или код, вы не можете использовать ORM для запросов совсем так, как мы описали выше, поскольку вы не можете вызывать блокирующий синхронный код из асинхронного кода - он заблокирует цикл событий (или, что более вероятно, Django заметит и поднимет SynchronousOnlyOperation, чтобы предотвратить это).

К счастью, вы можете выполнять многие запросы, используя API асинхронных запросов Django. Каждый метод, который может заблокироваться - например, get() или delete() - имеет асинхронный вариант (aget() или adelete()), а когда вы выполняете итерацию результатов, вы можете использовать асинхронную итерацию (async for).

Итерация запросов

New in Django Development version.

Стандартный способ итерации запроса - с помощью for - приводит к блокировке запроса к базе данных за кулисами, поскольку Django загружает результаты во время итерации. Чтобы исправить это, вы можете перейти на async for:

async for entry in Authors.objects.filter(name__startswith="A"):
    ...

Имейте в виду, что вы также не можете делать другие действия, которые могут привести к итерации по кверисету, например, обернуть list() вокруг него для принудительной оценки (вы можете использовать async for в comprehension, если вам это нужно).

Поскольку методы QuerySet, такие как filter() и exclude(), фактически не выполняют запрос - они настраивают кверисет на выполнение при итерации, - вы можете свободно использовать их в асинхронном коде. О том, какие методы можно продолжать использовать подобным образом, а какие имеют асинхронные версии, читайте в следующем разделе.

QuerySet and manager methods

New in Django Development version.

Некоторые методы менеджеров и кверисетов - например, get() и first() - принудительно выполняют кверисет и являются блокирующими. Некоторые, например filter() и exclude(), не принуждают к выполнению и поэтому безопасны для запуска из асинхронного кода. Но как же отличить их друг от друга?

Хотя вы можете порыться и посмотреть, есть ли версия метода с префиксом a (например, у нас есть aget(), но нет afilter()), есть более логичный способ - посмотреть, что это за метод в QuerySet reference.

В нем вы найдете методы по QuerySets, сгруппированные в два раздела:

  • Методы, возвращающие новые наборы запросов: Это неблокирующие методы, у них нет асинхронных версий. Вы можете использовать их в любой ситуации, хотя перед использованием прочитайте примечания к defer() и only().
  • Методы, которые не возвращают кверисетов: Это блокирующие методы, и у них есть асинхронные версии - асинхронное имя для каждого из них указано в документации, хотя наш стандартный шаблон - добавление префикса a.

Используя это различие, вы можете определить, когда вам нужно использовать асинхронные версии, а когда нет. Например, вот правильный асинхронный запрос:

user = await User.objects.filter(username=my_input).afirst()

filter() возвращает кверисет, и поэтому его можно продолжать передавать по цепочке в асинхронной среде, тогда как first() оценивает и возвращает экземпляр модели - таким образом, мы переходим на afirst() и используем await перед всем выражением, чтобы вызвать его асинхронным способом.

Примечание

Если вы забудете вставить часть await, вы можете увидеть ошибки типа «объект coroutine не имеет атрибута x « или «<coroutine …>» строки вместо экземпляров вашей модели. Если вы видите такие ошибки, значит, где-то не хватает await, чтобы превратить объект coroutine в реальное значение.

Транзакции

New in Django Development version.

Транзакции не в настоящее время поддерживаются с асинхронными запросами и обновлениями. Вы обнаружите, что при попытке использовать одну из них возникает ошибка SynchronousOnlyOperation.

Если вы хотите использовать транзакцию, мы предлагаем вам написать ваш код ORM в отдельной синхронной функции и затем вызвать ее с помощью sync_to_async - см. подробнее Поддержка асинхронного режима.

Запросы к JSONField

Реализация поисков отличается в JSONField, главным образом из-за существования ключевых преобразований. Для демонстрации мы будем использовать следующий пример модели:

from django.db import models

class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)

    def __str__(self):
        return self.name

Хранение и запрос для `` None``

As with other fields, storing None as the field’s value will store it as SQL NULL. While not recommended, it is possible to store JSON scalar null instead of SQL NULL by using Value(None, JSONField()).

Независимо от того, какие значения сохранены, при извлечении из базы данных представление Python скалярного JSON null совпадает с SQL NULL, т.е. None. Поэтому может быть трудно различить их.

Это относится только к None как значению верхнего уровня поля. Если None находится внутри list или dict, он всегда будет интерпретироваться как JSON null.

При запросе значение None всегда будет интерпретироваться как JSON null. Чтобы запросить SQL NULL, используйте isnull:

>>> Dog.objects.create(name='Max', data=None)  # SQL NULL.
<Dog: Max>
>>> Dog.objects.create(
...     name='Archie', data=Value(None, JSONField())  # JSON null.
... )
<Dog: Archie>
>>> Dog.objects.filter(data=None)
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=Value(None, JSONField())
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
<QuerySet [<Dog: Max>]>
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>

Если вы не уверены, что хотите работать со значениями SQL NULL, рассмотрите возможность установки null=False и обеспечения подходящего значения по умолчанию для пустых значений, например default=dict.

Примечание

Хранение JSON скаляра null не нарушает null=False.

Changed in Django 4.2:

Добавлена поддержка выражения JSON null с помощью Value(None, JSONField()).

Не рекомендуется, начиная с версии 4.2: Передача Value("null") для выражения JSON null устарела.

Преобразование ключа, индекса и пути

Для запроса на основе заданного ключа словаря используйте этот ключ в качестве имени поиска:

>>> Dog.objects.create(name='Rufus', data={
...     'breed': 'labrador',
...     'owner': {
...         'name': 'Bob',
...         'other_pets': [{
...             'name': 'Fishy',
...         }],
...     },
... })
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': None})
<Dog: Meg>
>>> Dog.objects.filter(data__breed='collie')
<QuerySet [<Dog: Meg>]>

Несколько ключей могут быть объединены в цепочку для формирования пути поиска:

>>> Dog.objects.filter(data__owner__name='Bob')
<QuerySet [<Dog: Rufus>]>

Если ключ является целым числом, он будет интерпретирован как преобразование индекса в массиве:

>>> Dog.objects.filter(data__owner__other_pets__0__name='Fishy')
<QuerySet [<Dog: Rufus>]>

Если ключ, который вы хотите запросить, конфликтует с именем другого поиска, используйте вместо этого поиск contains.

Чтобы запросить отсутствующие ключи, используйте поиск isnull:

>>> Dog.objects.create(name='Shep', data={'breed': 'collie'})
<Dog: Shep>
>>> Dog.objects.filter(data__owner__isnull=True)
<QuerySet [<Dog: Shep>]>

Примечание

В приведенных выше примерах поиска неявно используется поиск exact. Преобразования ключей, индекса и пути также могут быть связаны с: icontains, endswith, iendswith, iexact, regex, iregex, startswith, istartswith, lt, lte, gt, и gte, также как с Сдерживание и ключевые поиски.

KT() выражения

New in Django 4.2.
class KT(lookup)

Представляет текстовое значение преобразования ключа, индекса или пути JSONField. Вы можете использовать нотацию двойного подчеркивания в lookup для цепочки ключевых и индексных преобразований словаря.

Например:

>>> from django.db.models.fields.json import KT
>>> Dog.objects.create(name="Shep", data={
...     "owner": {"name": "Bob"},
...     "breed": ["collie", "lhasa apso"],
... })
<Dog: Shep>
>>> Dogs.objects.annotate(
...     first_breed=KT("data__breed__1"),
...     owner_name=KT("data__owner__name")
... ).filter(first_breed__startswith="lhasa", owner_name="Bob")
<QuerySet [<Dog: Shep>]>

Примечание

Из-за способа работы запросов по ключевым путям exclude() и filter() не гарантируются производить исчерпывающие наборы. Если вы хотите включить объекты, у которых нет пути, добавьте поиск isnull.

Предупреждение

Поскольку любая строка может быть ключом в объекте JSON, любой поиск, кроме перечисленных ниже, будет интерпретироваться как поиск ключа. Ошибок не возникает. Будьте особенно осторожны с опечатками и всегда проверяйте, работают ли ваши запросы так, как вы хотите.

Пользователи MariaDB и Oracle

Использование order_by() для преобразований ключа, индекса или пути отсортирует объекты с использованием строкового представления значений. Это связано с тем, что MariaDB и Oracle Database не предоставляют функцию, которая преобразует значения JSON в их эквивалентные значения SQL.

Пользователи Oracle

В Oracle Database использование None в качестве значения для поиска в запросе exclude() вернет объекты, которые не имеют null в качестве значения в заданный путь, включая объекты, которые не имеют пути. На других серверах базы данных запрос будет возвращать объекты, которые имеют путь, и значение не равно null.

Пользователи PostgreSQL

В PostgreSQL, если используется только один ключ или индекс, используется оператор SQL ->. Если используется несколько операторов, то используется оператор #>.

SQLite users

В SQLite строковые значения "true", "false" и "null" всегда будут интерпретироваться как True, False и JSON null соответственно.

Сдерживание и ключевые поиски

contains

Поиск contains переопределяется в JSONField. Возвращаемые объекты - это те, в которых заданный dict пар ключ-значение содержится в верхнем уровне поля. Например:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contains={'owner': 'Bob'})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>
>>> Dog.objects.filter(data__contains={'breed': 'collie'})
<QuerySet [<Dog: Meg>]>

Oracle и SQLite

contains не поддерживается в Oracle и SQLite.

contained_by

Это обратное значение contains - возвращаемые объекты будут теми, где пары ключ-значение на объекте являются подмножеством пар в переданном значении. Например:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie', 'owner': 'Bob'})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
<QuerySet [<Dog: Fred>]>

Oracle и SQLite

contains_by не поддерживается в Oracle и SQLite.

has_key

Возвращает объекты, в которых данный ключ находится на верхнем уровне данных. Например:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_key='owner')
<QuerySet [<Dog: Meg>]>

has_keys

Возвращает объекты, в которых все данные ключи находятся на верхнем уровне данных. Например:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_keys=['breed', 'owner'])
<QuerySet [<Dog: Meg>]>

has_any_keys

Возвращает объекты, где любой из указанных ключей находится на верхнем уровне данных. Например:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_any_keys=['owner', 'breed'])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

Сложные поиски с объектами Q

Запросы ключевых аргументов в filter() и т.д. - объединяются вместе «AND». Если вам нужно выполнить более сложные запросы (например, запросы с помощью операторов OR), вы можете использовать Q объекты.

Q объект (django.db.models.Q) - это объект, используемый для инкапсуляции набора ключевых аргументов. Эти ключевые аргументы указаны также как и в «Поиск по полям» выше.

Например, этот объект Q инкапсулирует один запрос LIKE:

from django.db.models import Q
Q(question__startswith='What')

Q objects can be combined using the &, |, and ^ operators. When an operator is used on two Q objects, it yields a new Q object.

Например, этот оператор выдает один объект Q, который представляет «OR» двух запросов "question__startswith":

Q(question__startswith='Who') | Q(question__startswith='What')

Это эквивалентно следующему оператору SQL WHERE:

WHERE question LIKE 'Who%' OR question LIKE 'What%'

You can compose statements of arbitrary complexity by combining Q objects with the &, |, and ^ operators and use parenthetical grouping. Also, Q objects can be negated using the ~ operator, allowing for combined lookups that combine both a normal query and a negated (NOT) query:

Q(question__startswith='Who') | ~Q(pub_date__year=2005)

Каждая функция поиска, которая принимает ключевые слова-аргументы (например filter(), exclude(), get()) также может быть передан один или несколько объектов Q в качестве позиционных (неименованных) аргументов. Если вы предоставляете несколько аргументов объекта Q для функции поиска, аргументы будут объединены «AND». Например:

Poll.objects.get(
    Q(question__startswith='Who'),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

… примерно переводится в SQL как:

SELECT * from polls WHERE question LIKE 'Who%'
    AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

Функции поиска могут смешивать использование Q объектов и ключевых аргументов. Все аргументы, предоставляемые функции поиска (будь то аргументы с ключевыми словами или объекты Q), соединяются «AND». Однако, если предоставляется объект Q, он должен предшествовать определению любых аргументов ключевого слова. Например:

Poll.objects.get(
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
    question__startswith='Who',
)

… будет правильным запросом, эквивалентным предыдущему примеру, но:

# INVALID QUERY
Poll.objects.get(
    question__startswith='Who',
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

… не будет верным.

См.также

примеры поиска с OR в модульных тестах Django показывают некоторые возможные варианты использования Q.

Changed in Django Development version:

Support for the ^ (XOR) operator was added.

Сравнение объектов

Чтобы сравнить два экземпляра модели, просто используйте стандартный оператор сравнения Python, знак двойного равенства: ==. За кулисами сравниваются значения первичных ключей двух моделей.

Используя приведенный выше пример Entry, следующие два оператора эквивалентны:

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id

Если первичный ключ модели не называется id, нет проблем. Сравнения всегда будут использовать первичный ключ, как бы он ни назывался. Например, если поле первичного ключа модели называется name, эти два оператора эквивалентны:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name

Удаление объектов

The delete method, conveniently, is named delete(). This method immediately deletes the object and returns the number of objects deleted and a dictionary with the number of deletions per object type. Example:

>>> e.delete()
(1, {'blog.Entry': 1})

Вы также можете удалить объекты массово. Каждый метод QuerySet имеет метод delete(), который удаляет все элементы этого класса QuerySet.

Например, это удаляет все объекты Entry с pub_date 2005 года:

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

Имейте в виду, что это, когда это возможно, будет выполняться исключительно в SQL, и поэтому методы delete() отдельных экземпляров объекта не обязательно будут вызываться во время процесса. Если вы предоставили пользовательский метод delete() для класса модели и хотите убедиться, что он вызывается, вам нужно будет «вручную» удалить экземпляры этой модели (например, путем итерации по QuerySet и вызывать delete() для каждого объекта в отдельности), а не с использованием метода массового delete() класса QuerySet.

Когда Django удаляет объект, по умолчанию он эмулирует поведение ограничения SQL ON DELETE CASCADE - другими словами, любые объекты, имеющие внешние ключи, указывающие на объект, который будет удален, будут удалены вместе с ним. Например:

b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

Это каскадное поведение настраивается с помощью аргумента on_delete для ForeignKey.

Обратите внимание, что delete() является единственным методом QuerySet, который не предоставляется классу Manager сам. Это механизм безопасности, который предотвращает случайный запрос Entry.objects.delete() и удаление всех записей. Если вы хотите хотите удалить все объекты, вам нужно явно запросить полный набор запросов:

Entry.objects.all().delete()

Копирование экземпляров модели

Although there is no built-in method for copying model instances, it is possible to easily create new instance with all fields“ values copied. In the simplest case, you can set pk to None and _state.adding to True. Using our blog example:

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save() # blog.pk == 1

blog.pk = None
blog._state.adding = True
blog.save() # blog.pk == 2

Все становится сложнее, если вы используете наследование. Рассмотрим подкласс Blog:

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)

django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
django_blog.save() # django_blog.pk == 3

Due to how inheritance works, you have to set both pk and id to None, and _state.adding to True:

django_blog.pk = None
django_blog.id = None
django_blog._state.adding = True
django_blog.save() # django_blog.pk == 4

Этот процесс не копирует отношения, которые не являются частью таблицы базы данных модели. Например, Entry имеет ManyToManyField для Author. После дублирования записи вы должны установить отношения «многие ко многим» для новой записи:

entry = Entry.objects.all()[0] # some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry._state.adding = True
entry.save()
entry.authors.set(old_authors)

Для OneToOneField вы должны продублировать связанный объект и назначить его полю нового объекта, чтобы избежать нарушения однозначного уникального ограничения. Например, предполагая, что entry уже продублировано, как указано выше:

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail._state.adding = True
detail.entry = entry
detail.save()

Обновление нескольких объектов одновременно

Иногда вы хотите установить для поля определенное значение для всех объектов в QuerySet. Вы можете сделать это с помощью метода update(). Например:

# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

Используя этот метод, вы можете устанавливать только нереляционные поля и поля ForeignKey. Чтобы обновить нереляционное поле, укажите новое значение как константу. Чтобы обновить ForeignKey поля, установите новое значение в качестве нового экземпляра модели, на который вы хотите указать. Например:

>>> b = Blog.objects.get(pk=1)

# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.update(blog=b)

Метод update() применяется мгновенно и возвращает количество строк, соответствующих запросу (которое может быть не равно количеству обновленных строк, если некоторые строки уже имеют новое значение). Единственное ограничение на обновляемый QuerySet состоит в том, что он может получить доступ только к одной таблице базы данных: главной таблице модели. Вы можете фильтровать на основе связанных полей, но вы можете обновить только столбцы в основной таблице модели. Пример:

>>> b = Blog.objects.get(pk=1)

# Update all the headlines belonging to this Blog.
>>> Entry.objects.filter(blog=b).update(headline='Everything is the same')

Помните, что метод update() преобразуется непосредственно в оператор SQL. Это массовая операция для прямых обновлений. Он не запускает какие-либо методы save() на ваших моделях или сигналы pre_save или post_save (которые являются следствием вызова save()), и не обновляет поля auto_now. Если вы хотите сохранить каждый элемент в QuerySet и убедиться, что метод save() вызывается в каждом экземпляре Вам не нужны никакие специальные функции для этого. Просто зациклите их и вызовите save():

for item in my_queryset:
    item.save()

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

>>> Entry.objects.update(number_of_pingbacks=F('number_of_pingbacks') + 1)

Однако, в отличие от объектов F() в filter и exclude, вы не можете вводить объединения при использовании объектов F() в обновлении - вы можете ссылаться только на поля, локальные для обновляемой модели. Если вы попытаетесь ввести объединение с объектом F(), будет вызвано FieldError:

# This will raise a FieldError
>>> Entry.objects.update(headline=F('blog__name'))

Откат к сырому SQL

Если вам понадобится написать SQL-запрос, который слишком сложен для обработки базы данных Django, вы можете вернуться к написанию SQL вручную. У Django есть несколько вариантов написания необработанных SQL-запросов; смотрите Выполнение необработанных SQL-запросов.

Наконец, важно отметить, что уровень базы данных Django - это просто интерфейс к вашей базе данных. Вы можете получить доступ к вашей базе данных с помощью других инструментов, языков программирования или фреймворков баз данных; нет ничего специфичного для Django в вашей базе данных.

Вернуться на верх