В черговій статті серії про кращі практики програмування з Django розглянемо роботу з базою даних та Django моделями. Поділюсь досвідом нашої команди та розкажу про:
- Django аплікації, що полегшують життя при роботі з даними;
- як організовувати код моделей в проекті;
- роботу з чистими SQL запитами;
- індексування та пошук даних;
- унаслідування у класах моделей;
- міграцію даних;
- дизайн бази даних та моделей;
- менеджери в Django ORM.
Якщо ви створюєте веб-аплікації із Django фреймворком, тоді дана стаття буде для вас ‘must to read’…
Перед тим як переходити до конкретних порад, хочу наголосити на одному загальному, проте дуже важливому правилі:
Перед тим як писати ваші класи моделей, добряче продумайте структуру вашої бази даних. Маючи специфікації проекту, розуміючи, які об’єкти будуть задіяні у вашій аплікації, ви маєте можливість відмалювати таблиці даних і зв’язки між ними. Вже базуючись на даній структурі починайте розробляти класи моделей. Це допоможе ефективніше і правильніше скористатись Django ORM та в майбутньому уникнути багатьох проблем при скейлінгу проекта.
Бібліотеки та аплікації
Кілька пакетів та бібліотек, які допомагають нам у щоденній роботі із моделями в Django:
Колишній пакет South у новій версії Django перенесли в його ядро, і даний інструмент є одним із найважливіших при роботі з моделями в Django. Без нього годі й уявити подальші зміни в структурі даних, класах Django моделей і оновлення на продакшині. Далі в статті наведу кілька практик при роботі із міграціями моделей і даних.
В пакеті django-model-utils зібрано великий набір утиліт та інструментів для роботи із моделями в Django. Додаткові поля, моделі типу TimeStamped, набір додаткових менеджерів моделей, класи для роботи з полями Choice, для трекання змін в даних моделі і ще багато іншого – все це вкладено у пакеті django-model-utils.
django-extensions є ще однією бібліотекою додаткових фіч для Django моделей. Ось лише невелика частина того, що ви там знайдете: маса заготовок для створення кастомних компонент вашого проекту, додаткові корисні менеджмент команди (типу shell_plus), розширений набір полів, інструмент створення графів для ваших моделей, автоматизація завдань (job scheduling), і багато іншого…
Якщо багато моделей…
З книги “Two scoops of Django” ми перебрали наступне правило:
Якщо у вашій аплікації є більше, ніж 20 моделей, подумайте про те, щоб розбити її на дрібніші аплікації. Швидше за все ваша аплікація виконує занадто багато роботи, як на одну аплікацію. І загалом, намагайтесь тримати не більше, ніж 5 моделей на одну Django аплікацію.
SQL запити
Django ORM є надзвичайно потужним і, в той же час, простим інструментом для роботи з базою даних не виходячи за рамки Python. Крім того, що Django ORM дозволяє зробити більшість необхідних дій, які ваша аплікація може потребувати, ви також не думаєте про те, з якою конкретно базою даних ви працюєте (MySQL, PostgreSQL чи ін.).
Проте в окремих випадках вам, все ж таки, може знадобитись напряму робити запити в базу даних готуючи власні SQL команди. Давайте розберемо кілька випадків, коли варто переходити з ORM на SQL синтаксис, а коли ні:
- в складних запитах, де ORM зумовлює ваш код робити кілька різних запитів з джоінами на великих об’ємах даних, а потім ви у своєму Python коді ще додатково обробляєте та об’єднуєте певним чином результати даних запитів – тут краще переглянути свій підхід; можливо ви не на повну використовуєте Django ORM, а можливо таки потрібно перейти на SQL запит і зробити все одним запитом; це покращить читабельність вашого коду та його швидкодію;
- допоки швидкодія і складність коду з використанням Django ORM вас задовільнять – не переходіть на Raw SQL; такі запити важче мігрувати при зміні бази даних;
- якщо вже приходиться писати SQL команду, краще використовувати raw() метод, а не extra();
- якщо SQL для ваc в конкретному випадку буде написати значно швидше, ніж придумувати складний ORM варіант – використовуйте SQL.
Індекси
Індекси в базі даних, ми знаємо, є для пришвидшення пошуку даних. Правило роботи із індексами доволі просте: по-замовчуванню поле не робимо індексом, а встановлюємо йому “db_index=True” лише при потребі. Тобто лише, коли настав момент і ми вирішили, що нам потрібно шукати по даному полі в таблиці.
Додавати індекси усім полям не є оптимально, адже кожен такий індекс сповільнює час запису в базу. Для індексів база змушена створювати і підтримувати додаткову таблицю з індексами при кожному записі в базу.
І ще кілька детальніших правиль додавання індекса:
- якщо поле використовується хоча б у 10-25% усіх запитів в базу;
- ми можемо запустити тести і перевірити, що з індексом наша аплікація/в’юшка/певна функція дає суттєве пришвидшення;
- коли ми маємо наближені до реальних продакшин даних, на яких додавання індекса дає нам також пришвидшення.
При роботі з базою даних PostgreSQL pg_stat_activity дає нам список індексів, які справді використовуються.
Унаслідування класів моделей
Взагалі, унаслідування моделей в Django є доволі слизькою темою. Фреймворк надає три види унаслідування: абстрактні класи, проксі моделі та мульти-табличне (кілька таблиць) унаслідування.
Якщо вам потрібно додати одне-два поля до різних моделей, краще додайте їх у кожну із цих моделей. У даному випадку краще обійтись без унаслідування. Якщо ж потрібно надати різним моделям більше полів чи однакову поведінку, тоді вже варто звертатись до унаслідування.
Якщо вам потрібно надати однакову множину полів різним моделям і все, тоді абстракниий клас буде вашим помічником. Тут є один нюанс: ви не можете напряму використати абстрактинй клас як кінцеву модель, і вам прийдеться створити нову модель, навіть, якщо дана модель міститиме точну копію усіх полів абстрактного класу.
Мульти-табличне унаслідування створює три таблиці, якщо ви працюєте із двома кінцевими (child) класами, та одним кореневим (parent) класом, від якого унаслідуєте 2 child класи. В основній (parent) таблиці містяться спільні дані, в двох інших – специфічні для обох child класів. Робота з подібними моделями спричиняє до складніших запитів у бази, щоб об’єднувати дві таблиці (parent i child). Загалом, не рекомендується використовувати даний метод унаслідування. Хоча він і буває доволі зручним для девелопера в певних випадках. Особливо, коли потрібно працювати окремо із специфічними кінцевими даними child класу, з якого при потребі можна далі дістати решту основних даних. Проте, навіть у цьому випадку, краще просто створити дві моделі і надати їм зв’язок через OneToOneField поле.
Проксі моделі варто використовувати, коли кілька моделей мають ідентичний набір полів, проте різну поведінку. У всіх інших випадках кращим варіантом буде абстрактний клас. Проксі моделі також класно юзати, якщо працюєте із класом, на який зав’язаний існуючий функціонал, який не хочете ламати. Наприклад, якщо хочете розширити додатковою поведінкою вбудований User клас.
Нотатки по міграції
Кілька корисних нотаток по роботі із міграцією ваших моделей і даних в базі:
- як тільки створили нову модель чи Django аплікацію, не полінуйтесь та створіть для неї нову міграцію;
- завжди намагайтесь писати reverse міграції та тестувати їх; ви ніколи не можете бути певні, що чергові зміни не поламають ваш код та базу, і в таких випадках можливість відкотити назад ваші зміни буде неоціненною;
- при роботі з репозиторієм коду, закидуйте в коміт рівно ті міграції, які відповідають поточним потребам; на продакшині ви не хочете мати можливі майбутні міграції, які пропрацювали на своєму локальному розробницькому середовищі;
- ніколи не видаляйте ті міграції, які вже були запущені на продакшині; плясайте від них!
- якщо маєте величезну базу на продакшині, обов’язково потестуйте ваші міграції на стейджинг інстансі, де у вас приблизно того ж розміру база даних.
Дизайн моделей
Якщо ви ще не знаєте, що таке нормалізація баз даних, тоді дуже рекомендую ознайомитись. І хоча Django ORM робить більшість речей за вас для нормалізації вашої бази даних, вам, як професійному розробнику, який працює із реляційними базами даних, просто необхідно бути в курсі нормалізації баз даних.
Нормалізація
Правило номер один: завжди починайти із нормалізованої конфігурації ваших моделей. Особливо, переконайтесь, що жодна модель не містить даних, які уже містяться у інших моделях.
Притримуйтесь правил нормалізації і не ламайте їх без крайньої необхідності. На даному етапі використовуйте поля співвідношень (relationship) без вагань.
Кешування
Якщо ваша правильна (нормалізована) структура даних виявляється занадто повільною на продакшин базі, перед тим як починати її переробляти і ламати правила нормалізації – розгляньте кешування на рівні Django ORM. В більшості випадків кешування в конкретних проблемних місцях буде достатнім.
В одній із наступних статтей ми розглянемо кращі практики кешування запитів у базу даних.
Денормалізація
Якщо все спробували, і кешування також не допомагає, і якщо ви добре розумієте, що робите, лише тоді розглядайте варіант денормалізації вашої бази даних.
Денормалізація даних найчастіше означає наступні практики:
- використання внутрішніх допоміжних моделей для зберігання та пошуку даних швидше; тобто дублювання даних в базі;
- можливість встановлення множинних значень одному атрибуту;
- можливість тримати дублюючі дані в одній і ті й же ж таблиці;
- встановлення зв’язків між різними рядками однієї таблиці;
- обмежене використання relationship полів, і натомість штучна прив’язка між записами і внутрішніми об’єктами;
- часто для розширеної денормалізації бази даних приходиться використовувати RAW SQL, з допомогою якого можемо обійти обмеження Django ORM.
Null vs Blank
Це окрема тема. Часто плутають атрибути полів null та blank.
Null – вказує на те, чи може дане поле в таблиці бути порожнім (не встановленим). Тобто даний атрибут налаштовує колонку в таблиці бази даних. Він працює на рівні бази даних.
Blank – вказує на те, чи поле є обов’язковим на рівні інтерфейсу користувача. Тобто на формах редагування та додавання моделі.
По дефолту обидві опції встановлені Django ORM у False.
Взалежності від того, з яким типом поля використовується той чи інший атрибут, є певні правила і рекомендації по використанню. Наприклад, із булеанівськими полями завжди рекомендується використовувати null=False i blank=False. Якщо ж хочете мати порожнє значення в базі на місці булеанівської колонки, скористайтесь іншим типом поля: NullBooleanField.
Подібних правил є дуже багато. Для детального ознайомлення рекомендую дану гілку на stackoverflow.
Менеджери моделей
Django ORM оперує базою через так звані менеджери моделей (model manager). Кожна модель має дефолтний менеджер встановлений під атрибутом objects. Через нього маємо доступ до бази даних. В деяких випадках нам може знадобитись наш кастомний менеджер для полегшення роботи з моделями і уникнення дублювання логіки на рівні в’юшок.
В цьому випадку даю лише одну пораду. Замість встановлення вашого кастомного менеджера на місце дефолтного (тобто під атрибутом objects), встановіть його у кастомний атрибут. Наприклад, кастомний менеджер моделей для полегшення управління класом статтей можна встановити на атрибут: Article.articles.
При цьому ви надалі матимете доступ до дефолтного менеджера при потребі, через атрибут objects. Також при унаслідуванні моделей, матимете доступ до обидвох. І не забувайте, що objects атрибут потрібно повторно перебивати першим у вашій кастомній моделі, перед присвоєнням вашого кастомного менеджера моделей.
***
На сьогодні все, а наступного разу розглянемо в’юшки в Django.
Якщо маєте, що додати до вищенаведених порад – скористайтесь формою коментування внизу!
Привіт, класна актуальна стаття!
До речі, щодо extra() то все ж джанго дуже б хотіло його deprecate-нути,
тому раджу використовувати суперові
django.db.models.expressions
таdjango.db.models.functions
, де вже є описані або можна самому обернути складні SQL конструкції в ORM. З власного досвіду 70% запитів можна помістити в ORM, ще 10% – запхати (буквально), а проблеми починаються коли треба кастомні JOIN-и по кількох полях (на звичайні JOIN-и теж є ORM обгортки) , nested SELECT і WITH .. SELECT.Щодо – pg_stat_activity, можливо тут помилка.
pg_stat_activity
показує, які процеси працюють з базою, для індексів є pg_stat_all_indexes, pg_stat_user_indexes & friends.Ще б згадав що в питанні доцільності індексів велику роль грає селективність виразу/поля по якому індексуємо, відтак наприклад boolean майже ніколи не варто індексувати бо лише два можливих значення. Принято, що індекс варто робити коли запит вибирає не більше, ніж 15% всіх рядків.
Класне чтиво!