#31. Django ORM
Django ORM, об’єкти та quesrysets
План
I. Абстрактні, проксі та некеровані моделі
- Абстрактна модель
- Проксі модель
- Некерована модель
- Успадкування Meta
- Команда shell
II. CRUD
- R - Read
- 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 - Read (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 об’єкти
Документація тут
Як ви могли помітити, у випадку фільтрації, ми можемо вибрати об’єкти через логічне AND, за допомогою коми.
Comment.objects.filter(article__author__pseudonym='The king', article__genre=3)
У цьому випадку в нас є конструкція типу вибрати об’єкти у яких псевдонім автора статті це The king І жанр статті це цифра 3
Але що ж нам робити є нам потрібно використовувати логічне OR.
У цьому нам допоможе використання Q об’єкта.
Насправді, кожну з цих умов ми могли загорнути в такий об’єкт:
from django.db.models import Q
q1 = Q(article__author__pseudonym='The king')
q2 = Q(article__genre=3)
Тепер ми можемо явно використовувати логічні AND та OR.
Comment.objects.filter(q1 & q2) # AND
Comment.objects.filter(q1 | q2) # OR
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 і залити на Git лог із консолі в окремому файлі:
- Створити 5 коментарів із різним текстом, Хоча б один має починатися зі слова “Start”, хоч один у середині повинен мати слово “Middle”, хоч один має закінчуватися словом “Finish”.
- Отримати 5 останніх написаних коментарів (об’єкти)
- Отримати 5 останніх написаних коментарів (саме текст)
- Змінити коментарі зі спецсловами “Start”, “Middle”, “Finish”.
- Видалити всі коментарі, у яких у тексті є буква “k”, але не видаляти, якщо є буква “с”.
- Отримати перші 2 коментарі за датою створення до статті, у якій ім’я автора останнє за алфавітом.
II. Моделі
- Переписати метод
saveкоментаря так, щоб при створенні дата змінювалася б на рік назад (якщо сьогодні 10 вересня 2021 року, має виставлятися 10 вересня 2020 року), зміну коментарів не зачіпати.