#31. Django ORM

Django ORM, objects and quesrysets

enter image description here

План

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

  1. Абстрактная модель
  2. Прокси модель
  3. Неуправляемая модель
  4. Наследование Meta
    Команда shell

II. CRUD

  1. R - retrieve
  2. C - Create
  3. U - Update
  4. D - Delete

III. Сложные SQL конструкции

  1. Q объекты
  2. Aggregation
  3. 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.

enter image description here

objects

Для доступа или модификации любых данных, у каждой модели есть аттрибут objects, который позволяет производить любые манипуляции с данными. Он называется менеджер, и при желании его можно переопределить.

Рассмотрим весь CRUD и дополнительные особенности. Очень подробная информация по всем возможным операциям тут

Предварительно необходимо создать несколько объектов через админку.
Для доступа к моделям, их нужно импортировать, импортируем модель Comment

from myapp.models import Comment

enter image description here

CRUD

enter image description here

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, который указывается для полей связи, является обратной связью, и менеджером для объектов.
Например в нашей модели у поля 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 и залить на гит лог из консоли в отдельном файле:

  1. Создать 5 комментариев с разным текстом, Хотя бы один должен начинаться со слова “Start”, хоть один в середине должен иметь слово “Middle”, хоть один должен заканчиваться словом “Finish”.
  2. Получить 5 последних написанных комментариев (объекты)
  3. Получить 5 последних написанных комментариев (именно текст)
  4. Изменить комментарии со спец словами “Start”, “Middle”, “Finish”.
  5. Удалить все комментарии у которых в тексте есть буква “k”, но не удалять если есть буква “с”.
  6. Получить первые 2 коментария по дате создания к статье, у которой имя автора последнее по алфавиту.

II. Модели

  1. Переписать save комментария так, что бы при создании дата менялась бы на год назад (если сегодня 10 Сентября 2021, должна выставляться 10 Сентября 2020), изменение комментариев не затрагивать.

Литература (что почитать)

  1. Django ORM cookbook перевод на русский книги по ORM
  2. Работа с запросами