#31. Django ORM
Django ORM, objects and quesrysets
План
I. Абстрактные, прокси и неуправляемые модели
- Абстрактная модель
- Прокси модель
- Неуправляемая модель
- Наследование Meta
Команда shell
II. CRUD
- R - retrieve
- C - Create
- U - Update
- D - Delete
III. Сложные SQL конструкции
- Q объекты
- Aggregation
- F - выражения
IV. Домашнее задание / Практика:
V. Литература (что почитать)
Django ORM (Object Relational Mapping) является одной из самых мощных особенностей Django. Это позволяет нам взаимодействовать с базой данных, используя код Python, а не SQL.
Мы уже знаем про то как хранить данные, и как связать таблицы между собой, давайте научимся, извлекать, модифицировать и удалять данные при помощи кода.
Допустим, что модель выглядит так:
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.translation import gettext as _
class Author(models.Model):
pseudonym = models.CharField(max_length=120, blank=True, null=True)
name = models.CharField(max_length=120)
def __str__(self):
return self.name
class Article(models.Model):
NOT_SELECTED = 1
COMEDY = 2
ACTION = 3
BEAUTY = 4
OTHER = 5
GENRE_CHOICES = (
(NOT_SELECTED, _("Not selected")),
(COMEDY, _("Comedy")),
(ACTION, _("Action")),
(BEAUTY, _("Beauty")),
(OTHER, _("Other"))
)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
null=True,
related_name='articles',
)
text = models.TextField(null=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(default=timezone.now)
genre = models.IntegerField(choices=GENRE_CHOICES, default=NOT_SELECTED)
def __str__(self):
return f'Author - {self.author.name}, genre - {self.genre}, id - {self.id}'
class Comment(models.Model):
text = models.CharField(max_length=1000)
article = models.ForeignKey(Article, on_delete=models.DO_NOTHING)
comment = models.ForeignKey(
'myapp.Comment',
null=True, blank=True,
on_delete=models.DO_NOTHING,
related_name='comments',
)
user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
def __str__(self):
return f'{self.text} by {self.user.username}'
class Like(models.Model):
user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
article = models.ForeignKey(Article, on_delete=models.DO_NOTHING)
def __str__(self):
return f'By user {self.user.username} to article {self.article.id}'
Что бы модели появились в админке, их необходимо зарегистрировать в admin.py
:
from django.contrib import admin
from .models import Article, Author, Comment, Like
admin.site.register(Author)
admin.site.register(Article)
admin.site.register(Comment)
admin.site.register(Like)
Рассмотрим некоторые новые возможности
from django.contrib.auth.models import User
Это модель встроенного в Django пользователя, её мы рассмотрим немного позже.
from django.utils.translation import gettext as _
Стандартная функция перевода языка для Django, допустим что ваш сайт имеет функцию переключения языка, с русского, украинского и английского, эта функция поможет нам в будущем указать значения для всех трех языков. Подробнее тут
NOT_SELECTED = 1
COMEDY = 2
ACTION = 3
BEAUTY = 4
OTHER = 5
GENRE_CHOICES = (
(NOT_SELECTED, _("Not selected")),
(COMEDY, _("Comedy")),
(ACTION, _("Action")),
(BEAUTY, _("Beauty")),
(OTHER, _("Other"))
)
Переменная состоящая из коллекции коллекций, нужна для использования choices
значений, используется для хранения выбора чего-либо, в нашем случае жанра, в базе будет храниться только число, а пользователю будет выводиться уже текст.
Рассмотрим вот эту строку
def __str__(self):
return f'Author - {self.author.name}, genre - {self.genre}, id - {self.id}'
self.author.name
- в базе по значению FK
хранится id,
но в коде мы можем получить доступ к значениям связанной модели, конкретно в этой ситуации, мы берем значение поля name
из связанной модели author
.
Рассмотрим вот эту строку:
comment = models.ForeignKey(
'myapp.Comment',
null=True, blank=True,
on_delete=models.DO_NOTHING,
related_name='comments',
)
Модель можно передать не только как класс, но и по имени модели указав приложение appname.Modelname
При такой записи мы создаём связь один ко многим к самому себе, указав при этом black=True, null=True
. Можно создать коммент без указания родительского комментария, а если создать комментарий со ссылкой на другой, это будет комментарий к комментарию, причем это можно сделать любой вложенности.
related_name
- в этой записи нужен для того, что бы получить выборку всех вложенных объектов, мы рассмотрим их немного позже.
Абстрактные, прокси и неуправляемые модели
Абстрактная модель (Абстрактный базовый класс)
class Meta:
abstract = True
Абстрактные базовые классы полезны, когда вы хотите поместить некоторую общую информацию в ряд других моделей. Вы пишете свой базовый класс и помещаете abstract=True
в класс Meta
. Эта модель не будет использоваться для создания таблицы базы данных. Вместо этого, когда класс используется в качестве базового класса для других моделей, его поля будут добавлены к полям дочернего класса.
Пример:
from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
class Student(CommonInfo):
home_group = models.CharField(max_length=5)
Таблица для CommonInfo
не будет созданна!
Прокси модель
class Meta:
proxy = True
Модель, которая создаётся на уровне языка программирования, но не на уровне базы данных. Используется если нужно добавить метод, изменить поведение менеджера итд.
Синтаксис:
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
class MyPerson(Person):
class Meta:
proxy = True
def do_something(self):
pass
В базе будет храниться одна таблица, в Django два класса.
Часто используется для отображения в админке нескольких таблиц для одного объекта.
Неуправляемая модель
class Meta:
managed=False
По умолчанию True
, то есть Django создаст соответствующие таблицы базы данных в migrate
или как часть миграции и удалит их как часть команды управления flush
. То есть Django управляет жизненными циклами таблиц базы данных.
Если установлено значение False
, для этой модели не будут выполняться операции создания, изменения или удаления таблицы базы данных. Это полезно, если модель представляет существующую таблицу или представление базы данных, созданное другими способами. Это единственная разница, когда managed=False
. Все остальные аспекты работы с моделью такие же, как обычно.
Наследование Meta
Когда создается абстрактный базовый класс, Django делает любой Meta внутренний класс, который вы объявили в базовом классе, доступным как атрибут. Если дочерний класс не объявляет свой собственный класс Meta, он наследует родительский класс Meta. Если наследник хочет расширить класс родителя Meta, он может сделать его подклассом.
Например:
from django.db import models
class CommonInfo(models.Model):
# ...
class Meta:
abstract = True
ordering = ['name']
class Student(CommonInfo):
# ...
class Meta(CommonInfo.Meta):
db_table = 'student_info'
Django вносит одну корректировку в класс Meta
абстрактного базового класса: перед установкой атрибута Meta
он устанавливает abstract=False
. Это означает, что потомки абстрактных базовых классов сами не становятся автоматически абстрактными классами. Чтобы создать абстрактный базовый класс, который наследуется от другого абстрактного базового класса, вам необходимо явно установить abstract=True
для дочернего элемента.
Некоторые атрибуты не имеет смысла включать в класс Meta
абстрактного базового класса. Например, включение db_table
будет означать, что все дочерние классы (те, которые не указывают свои собственные Meta
) будут использовать одну и ту же таблицу базы данных, которая почти наверняка не та, которая ожидается.
Благодаря тому, как работает наследование Python, если дочерний класс наследует от нескольких абстрактных базовых классов, по умолчанию будут наследоваться только параметры Meta
из первого перечисленного класса. Чтобы наследовать Meta
от нескольких абстрактных базовых классов, вы должны явно объявить наследование Meta
.
Например:
from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
ordering = ['name']
class Unmanaged(models.Model):
class Meta:
abstract = True
managed = False
class Student(CommonInfo, Unmanaged):
home_group = models.CharField(max_length=5)
class Meta(CommonInfo.Meta, Unmanaged.Meta):
pass
Команда shell
python manage.py shell
Эта команда открывает нам консоль с уже импортированными всеми стандартными, но не самописными, модулями Django.
objects
Для доступа или модификации любых данных, у каждой модели есть аттрибут objects
, который позволяет производить любые манипуляции с данными. Он называется менеджер, и при желании его можно переопределить.
Рассмотрим весь CRUD и дополнительные особенности. Очень подробная информация по всем возможным операциям тут
Предварительно необходимо создать несколько объектов через админку.
Для доступа к моделям, их нужно импортировать, импортируем модель Comment
from myapp.models import Comment
CRUD
R - retrieve
Функции для получения объектов в Django могут возвращать, два типа данных, объект модели и queryset
Объект - это единичный элемент.
QuerySet - это список объектов, со своими встроенными методами. Queryset позволяет читать данные из базы данных, фильтровать и изменять их порядок. Queryset очень поход на обычный list
, можно получать элементы колекции по индексы, делать срезы, итерировать в цикле for
.
Каждый queryset является уникальным.
Очень часто пишут цепочки querysets:
qs = Model.objects.all()
qs = qs.filter(id__in=[2,4,6])
qs = qs.filter(user__id=5)
print(qs[2:])
for obj in qs:
print(obj.id)
По отношению к базе данных, являются ‘ленивыми’, то есть реальный запрос выполнится в самую последнюю очередь, когда приложению уже необходимы будут данные.
Методы, возвращающие queryset
all
Для получения всех данных используется метод all()
, возвращает queryset со всеми существующими объектами этой модели.
>>> Comment.objects.all()
<QuerySet [<Comment: Good article by admin>, <Comment: so so by admin>]>
filter
Для получения отфильтрованных данных используется метод filter()
Если указать фильтр без параметров, то он сделает, то же самое что и all.
Можно указать любые поля для фильтрации, даже из смежных таблиц.
Например фильтр по полю текст.
Comment.objects.filter(text='so so')
Фильтр по вложенным объектам, выполняется через двойное нижнее подчеркивание.
Фильтр по жанру статьи комментария.
Comment.objects.filter(article__genre=3)
По псевдониму автора.
Comment.objects.filter(article__author__pseudonym='The king')
По псевдониму автора и жанру.
Comment.objects.filter(article__author__pseudonym='The king', article__genre=3)
Так же у каждого поля существуют встроенные системы поиска (lookup
), пишутся с таким же синтаксисом как и доступ к ForeignKey
field__lookuptype=value
Стандартные lookups:
lte
- меньше или равно
gte
- больше или равно
lt
- меньше
gt
- больше
startswith
- начинается с
istartswith
- начинается с, без учёта регистра
endswith
- заканчивается на
iendswith
- заканчивается на, без учёта регистра
range
- находится в рамках
week_day
- день недели (для дат)
year
- год (для дат)
isnull
- является наном
contains
- частично содержит учитывая регистр
icontains
- то же самое, но регистро независимо
exact
- совпадает (не обязательный лукап, делает то же, что и знак равно)
iexact
- совпадает регистро независимо (по запросу “привет” найдет и “Привет” и “прИвЕт”)
in
- содержится в каком то списке
Их намного больше, детальнее тут
Примеры:
Псевдоним содержит слово ‘king’
Comment.objects.filter(article__author__pseudonym__icontains='king')
Комменты к статье созданной не позднее чем вчера
Comment.objects.filter(article__created_at__gte=date.today() - timedelta(days=1))
Комменты к статьям с жанрами под номерами 2 и 3
Comment.objects.filter(article__genre__in=[2, 3])
exclude
Функция, обратная функции filter
, вытащит всё что не попадает выборку
Например все комменты к статьям у которых жанр не 2 и не 3
Comment.objects.exclude(article__genre__in=[2, 3])
filter
и exclude
можно совмещать. К любому кверисету можно применить фильтр или ексклюд еще раз.
Например, все комменты к статьям, созданным не позже чем вчера, с жанрами не 2 и не 3
Comment.objects.filter(article__created_at__gte=date.today() - timedelta(days=1)).exclude(article__genre__in=[2, 3])
order_by
По умолчанию кверисеты не имеют сортировки. Это всязано именно с СУБД, которая так данные отдает. Если необзодимо явно отсортировать данные, используйте order_by()
Comment.objects.filter(article__created_at__gte=date.today() - timedelta(days=1)).order_by('-article__created_at').all()
Все методы кверисетов тут.
Особое внимание стоит уделить на методы distinct
, reverse
, values
, difference
Так же во все фильтры можно вставлять целые объекты, например
article = Article.objects.get(id=2)
comments = Comment.object.exclude(article=article)
Методы, возвращающие объект модели
get
В отличие от filter
и exclude
, get
получает сразу объект, работает только, если можно определить объект однозначно и он существует.
Можно применять те же условия, что и для filter
и exclude
Например, получения объекта по id
Comment.objects.get(id=3)
Если объект не найден или найдено больше одного объекта по заданным параметрам, вы получите исключение, которое можно обрабатывать.
Исключения уже находятся в самой модели.
try:
Comment.objects.get(article__genre__in=[2, 3])
except Comment.DoesNotExist:
return "Can't find object"
except Comment.MultipleObjectsReturned:
return "More than one object"
first и last
К кверисету можно применять методы first
и last
что бы получить первый или последний элемент кверисета
Например, получить первый коммент написанный за вчера:
Comment.objects.filter(article__created_at__gte=date.today() - timedelta(days=1)).first()
Информация по всем остальным методам тут
related_name
Атрибут related_name
, который указывается для полей связи, является обратной связью, и менеджером для объектов.
Например в нашей модели у поля author
модели Article
есть related_name=articles
:
a = Author.objects.first()
articles = a.articles.all() # тут будут все статьи конкретного автора в виде кверисета, т.к. all() возвращает кверисет
Можно ли получить объекты обратной связи без указания related_name? Можно. Связь появляется автоматически даже без указания этого атрибута.
Обратный менеджер формируется из названия модели и конструкции _set
, допустим у поля article
модели Comment
не указан related_name:
a = Article.objects.first()
a.comment_set.all() # такой же менеджер как в прошлом примере, вернёт кверисет коментариев относящихся к этой статье.
C - Create
Для создания новых объектов используется два возможных варианта, через метод, create
или метод save
Создадим двух новых авторов, при помощи разных методом
Author.objects.create(name='New author by create', pseudonym="Awesome author")
# OR
a = Author(name="Another new author", pseudonym="Gomer")
a.save()
В чём же разница? В том, что в первом случае, при выполнении этой строки запрос в базу будет отправлен сразу, во втором, только при вызове метода save()
Метод save так же является и методом для апдейта, если применяется для уже существующего объекта, рассмотрим его подробнее немного дальше.
U - Update
Для апдейта используется метод update()
Применяется только к кверисетам, к объекту применить нельзя
Например обновим текст в комментарии с id=3
Comment.objects.filter(id=3).update(text='bla-bla')
# OR
c = Comment.objects.get(id=3)
c.text = 'bla-bla'
c.save()
D - Delete
Для удаления используется метод delete()
, тоже применяется только к кверисетам.
Удалить все комменты от пользователя с id=2
Comment.objects.filter(user__id=2).delete()
Совмещенные методы
get_or_create(), update_or_create(), bulk_create(), bulk_update()
get_or_create
- метод, который попытается создать новый объект если не сможет найти нужный в базе
update_or_create
- обновит если объект существует, создаст если не существует
bulk_create
- массовое создание
bulk_update
- массовое обновление (отличие от обычного в том, что при обычном на каждый объект создается запрос, в этом случае запрос делается массово)
Подробно почитать про них тут
Сложные SQL конструкции
На самом деле мы не ограниченны стандартными конструкциями, мы можем применять предвычисления на уровне базы, добавлять логические конструкции и т.д., давайте рассмотри подробнее.
Q объекты
Документация тут
Как вы могли заметит, в случае фильтрации, мы можем выбрать объекты через логическое И, при помощи запятой.
Comment.objects.filter(article__author__pseudonym='The king', article__genre=3)
В этом случае у нас есть конструкция типа выбрать объекты у которых псевдоним автора статьи это The king
И жанр статьи это цифра 3
Но что же нам делать есть нам нужно использовать логическое ИЛИ.
В этом нам поможет использование Q объекта.
На самом деле, каждое из этих условий мы могли завернуть в такой объект:
from django.db.models import Q
q1 = Q(article__author__pseudonym='The king')
q2 = Q(article__genre=3)
Теперь мы можем явно использовать логические И и ИЛИ.
Comment.objects.filter(q1 & q2) # И
Comment.objects.filter(q1 | q2) # ИЛИ
Aggregation
Документация тут
Агрегация в Django - это по сути предвычисления.
Допустим что у нас есть модели:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
pubdate = models.DateField()
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
Мы можем совершить предвычисления каких либо средних, минимальных, максимальных значений, вычислить сумму и т.д.
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg('price'))
{'price__avg': 34.35}
На самом деле all() не несёт пользы в этом примере
>>> Book.objects.aggregate(Avg('price'))
{'price__avg': 34.35}
Значение можно именовать
>>> Book.objects.aggregate(average_price=Avg('price'))
{'average_price': 34.35}
Можно вносить больше одной агрегации за раз
>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}
Если нам нужно, что бы подсчитанное значение было у каждого объекта модели, мы используем метод annotate
# Build an annotated queryset
from django.db.models import Count
q = Book.objects.annotate(Count('authors'))
# Get the first object in the queryset
q[0]
<Book: The Definitive Guide to Django >
q[0].authors__count
2
# Get the second object in the queryset
q[1]
< Book: Practical Django Projects >
q[1].authors__count
1
Их тоже может быть больше одного
book = Book.objects.first()
book.authors.count()
2
book.store_set.count()
3
q = Book.objects.annotate(Count('authors'), Count('store'))
q[0].authors__count
6
q[0].store__count
6
Все эти вещи можно комбинировать
highly_rated = Count('book', filter=Q(book__rating__gte=7))
Author.objects.annotate(num_books=Count('book'), highly_rated_books=highly_rated)
C ордерингом
>>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors')
F - выражения
Документация по этому разделу тут
F выражения нужны для получения значения полей, и оптимизации записи.
Допустим нам нужно увеличить определенному объекту в базе значение какого либо поля на 1
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed += 1
reporter.save()
На самом деле в этот момент мы получаем значение из базы в память, обрабатываем, и записываем в базу
Есть другой путь:
from django.db.models import F
reporter = Reporters.objects.get(name='Tintin')
reporter.stories_filed = F('stories_filed') + 1
reporter.save()
Преимущества под капотом, но давайте предположим, что нам нужно сделать эту же операцию массово
reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)
Такие объекты можно использовать и в аннотации и в фильтрах и во многих других местах.
Домашнее задание / Практика:
I. Запросы
Выполнить всё через shell
и залить на гит лог из консоли в отдельном файле:
- Создать 5 комментариев с разным текстом, Хотя бы один должен начинаться со слова “Start”, хоть один в середине должен иметь слово “Middle”, хоть один должен заканчиваться словом “Finish”.
- Получить 5 последних написанных комментариев (объекты)
- Получить 5 последних написанных комментариев (именно текст)
- Изменить комментарии со спец словами “Start”, “Middle”, “Finish”.
- Удалить все комментарии у которых в тексте есть буква “k”, но не удалять если есть буква “с”.
- Получить первые 2 коментария по дате создания к статье, у которой имя автора последнее по алфавиту.
II. Модели
- Переписать
save
комментария так, что бы при создании дата менялась бы на год назад (если сегодня 10 Сентября 2021, должна выставляться 10 Сентября 2020), изменение комментариев не затрагивать.
Литература (что почитать)
- Django ORM cookbook перевод на русский книги по ORM
- Работа с запросами