SQLALCHEMY SERIALIZER MIXIN — Пояснення для початківців

На перший погляд може здатися, що найбільшою проблемою в моделях взаємозв'язків є помилки через максимальну глибину рекурсії. Це часто перша проблема, з якою ви зіштовхнетеся при побудові моделей у SQL Alchemy, тому логічно, що ви захочете вирішити її першою при організації взаємозв'язків моделей.

Але я б стверджував, що більша проблема не пов'язана з явними помилками, логікою, помилками під час виконання чи чимось подібним. Вона прямо стосується самої моделі даних.

Якби я міг повернутися і подивитися на Flask і SQL Alchemy з новими очима, я б зосередився на цьому як ні на чому іншому.

Ось чому: хороші структури даних, продумані та логічні, можуть заощадити сотні рядків коду навіть у найпростіших проєктах.

З базовими, найпростішими правилами серіалізації, які проходять перевірку під час виконання, можна очікувати, що модель виглядатиме ось так.

pic

Я хочу виокремити 3 моделі. Ми можемо побачити користувачів, проєкти та таблицю "багато до багатьох", яка зв'язує ці дві, 'project_collaborators'.

Здається, все досить просто, давайте подивимося на приклад JSON-виводу з базовими правилами серіалізації обмеження глибини рекурсії на Python, показаними нижче.

class Project(db.Model, SerializerMixin):  
 __tablename__ = 'projects'  
 collaborations = db.relationship('ProjectCollaborators', back_populates = 'project')  
 serialize_rules = ('-users','-collaborations')  
 users = association_proxy('collaborations', 'user',  
 creator=lambda users_obj: ProjectCollaborators(user = users_obj))  


 class ProjectCollaborators(db.Model, SerializerMixin):  
 __tablename__ = 'project_collaborators'  
 project = db.relationship('Project', back_populates = 'collaborations')  
 serialize_rules = ('-project', '-user')

Цього достатньо, щоб уникнути помилки "max recursion limit reached", але це не дає нам корисної моделі.

Endpoint проєктів

"brand_name": "Jackson-Patrick",  
 "id": 1,  
 "logo": "there.png"  
 },  
 {  
 "brand_name": "Petersen-Hart",  
 "id": 2,  
 "logo": "Congress.png"  
 },

Endpoint колабораторів проєктів (багато до багатьох)

[  
 {  
 "id": 1,  
 "project_id": 1,  
 "role": "Editor",  
 "user_id": 2  
 },  
 {  
 "id": 2,  
 "project_id": 1,  
 "role": "Editor",  
 "user_id": 3  
 },  
 {  
 "id": 3,  
 "project_id": 1,  
 "role": "Owner",  
 "user_id": 5  
 },

І ось найбільша шкода від тих 20 годин, які я витратив на спроби змусити ці моделі працювати. Якщо ви хочете спробувати з'єднати ці набори даних, щоб отримати будь-яку значущу взаємодію на фронтенді, вам доведеться

  1. Завантажити обидва набори даних
  2. Зберегти їх в стані
  3. Передати їх як пропси
  4. Виконати перехресне співвіднесення даних ось так
// Симуляція кількох API запитів  
 const fetchProjectDetails = (projectId) => {  
 return fetch(`/api/projects/${projectId}`).then(res => res.json());  
 };  

 const fetchCollaborators = (projectId) => {  
 return fetch(`/api/projects_collaborators?project_id=${projectId}`)  
.then(res => res.json());  
 };  

 // Неефективне об'єднання даних  
 fetchProjectDetails(1)  
 .then(project => {  
 return fetchCollaborators(1).then(collaborators => {  
 // Тепер вручну об'єднуємо або обробляємо дані про проєкт та колабораторів  
 console.log(`Project Name: ${project.brand_name}`);  
 collaborators.forEach(collaborator => {  
 console.log(`Collaborator Role: ${collaborator.role}`);  
 });  
 });  
 });

Результат — кодова складність, яку неймовірно важко керувати, неефективність, важкість у читанні, і це не дуже DRY.

До того ж, кожного разу, коли вам потрібно витягти нові дані або маніпулювати ними, вам доведеться викручувати код таким чином кожного разу.

Вкладені структури даних — кращий спосіб

А що якби ми зберігали взаємозв'язки моделей в одному маршруті (що працює для невеликих проєктів, як цей)?

Якщо ми хочемо отримати доступ до будь-яких даних наших моделей, ми можемо зробити це через SerializerMixin.
В результаті ми отримуємо щось подібне до цього:

/projects_collaborators

"id": 1,  
 "project": {  
 "brand_name": "Jackson-Patrick",  
 "id": 1,  
 "logo": "there.png"  
 },  
 "project_id": 1,  
 "role": "Editor",  
 "user": {  
 "collaborators": [  
 {  
 "id": 1,  
 "project_id": 1,  
 "role": "Editor",  
 "user_id": 2  
 }

Тепер ми можемо передати один набір даних, з одного запиту, і обробити його на клієнтській стороні. В результаті ми отримуємо набагато чистіший, ефективніший і DRY-код.

const projectName = response.project.brand_name;  
const collaborators = response.user.collaborators;
console.log(`Project Name: ${projectName}`);  
collaborators.forEach(collaborator => {  
 console.log(`Collaborator Role: ${collaborator.role}`);   
});

Отже, ми бачимо кращий спосіб. Як ми сюди потрапили? Ось тут і допомогли проби та помилки, документація SQLAlchemy, та трохи удачі.

Щоб серіалізувати наші дані таким чином, нам потрібно включити SerializerMixin в наші моделі. Краще консультуватися з документацією, але ви можете побачити простий приклад нижче.

from sqlalchemy_serializer import SerializerMixin  
class ProjectCollaborators(db.Model, SerializerMixin):  
 __tablename__ = 'project_collaborators'  
 id = db.Column(db.Integer, primary_key = True)  
 user_id = db.Column(db.Integer, ForeignKey('users.id'))  
 project_id = db.Column(db.Integer, ForeignKey('projects.id'))  
 role = db.Column(db.String, nullable=False)  


 user = db.relationship('User', back_populates = 'collaborators')  
 project = db.relationship('Project', back_populates = 'collaborations')  
 # serialize_rules = ('-project', '-user')  
 serialize_rules = ('-project.collaborations','user', '-user.collaborators.project')  
 __table_args__ = (  
 UniqueConstraint('user_id', 'project_id', name='uq_user_project'),  
 )

Розуміння Serializer Mixin

Цей пакет є досить об'ємним і має деякі цікаві можливості, але перше, що ми хочемо розглянути — це правила серіалізації (serialize rules). Це вказує серіалізатору, коли зупинятися.

Ми можемо додавати чи виключати елементи. Це визначається наступним чином:

Включити це відношення

‘user’

Виключити це відношення (за допомогою мінуса)

‘-user.collaborators’

І це дасть нам щось подібне:

{  
"project_id": 1,  
"role": "Editor",  
"id": 1,  
"project":  
 {"brand_name": "Jackson Patrick",  
 "id": 1,  
 "logo": "there.png"},  
"user_id": 2  
"user":  
 {"id": 2,  
 "name": "Theresa Mueller",  
 "profile_icon": "https://picsum.photos/100/100",  
 "username": "mroberts"},

І це дає нам основу для моделювання даних у SQLAlchemy. Воно слідує правилам до самого найспеціальнішого, і ми можемо побачити нашу модель ось так. Вона включає відношення користувача, але виключає частину user.collaborations, тому ми не бачимо цього в вкладеному JSON.

Якщо б ми зробили виключення рекурсивного відношення:

class ProjectCollaborators(db.Model, SerializerMixin):  
__tablename__ = 'project_collaborators'  
serialize_rules = ('-project', '-user')

Це вказує серіалізатору наступне:

Отримати дані з таблиці DB ‘projectcollaborators’ і включити всі дані та моделі відношень_ окрім відношень (‘-project’, та ‘-user’). Тому наша модель не матиме вкладених даних.

{"id": 1,  
"project_id": 1,  
"role": "Editor",  
"user_id": 2}

Якщо ви випадково залишите ці пункти, наприклад, і не обмежите інші моделі, то коли ви досягнете відношення користувача і почнете обробляти ці дані, воно знайде зворотнє посилання на таблицю ‘project_collaborators’. Звідти воно продовжить обробку даних, поки не знайде відношення до користувачів, і БУМ — досягнуто ліміту рекурсії.

pic

КРАЩИЙ СПОСІБ

Розробник бекенду, який заслуговує на свою зарплату, повинен це розуміти.
Правильне визначення моделей зараз заощадить години головного болю в майбутньому.

Давайте подивимося на правильний спосіб обробки цих відносин.

serialize_rules = ('-project.collaborations', 'user',  
 '-user.collaborators.project')

Це скаже серіалізатору:

  1. виключити відношення до проекту ON
  2. виключити відношення до співробітників FROM
  3. виключити початкове відношення до користувачів FROM
  4. нашу таблицю ‘project_collaborators’.

Отже, результат виглядатиме ось так:

"id": 1,  
"project": {  
 "brand_name": "Jackson-Patrick",  
 "id": 1,  
 "logo": "there.png"  
 },  
"project_id": 1,  
"role": "Editor",  
 "user": {  
 "collaborators": [  
 {  
 "id": 1,  
 "project_id": 1,  
 "role": "Editor",  
 "user_id": 2  
 }

Тепер, якщо ми хочемо отримати доступ до будь-яких даних відношень, ми можемо зробити це з одним запитом і меншою логікою на фронтенді. Чудова річ!

Перекладено з: SQLALCHEMY SERIALIZER MIXIN — Layman’s Guide

Leave a Reply

Your email address will not be published. Required fields are marked *