На перший погляд може здатися, що найбільшою проблемою в моделях взаємозв'язків є помилки через максимальну глибину рекурсії. Це часто перша проблема, з якою ви зіштовхнетеся при побудові моделей у SQL Alchemy, тому логічно, що ви захочете вирішити її першою при організації взаємозв'язків моделей.
Але я б стверджував, що більша проблема не пов'язана з явними помилками, логікою, помилками під час виконання чи чимось подібним. Вона прямо стосується самої моделі даних.
Якби я міг повернутися і подивитися на Flask і SQL Alchemy з новими очима, я б зосередився на цьому як ні на чому іншому.
Ось чому: хороші структури даних, продумані та логічні, можуть заощадити сотні рядків коду навіть у найпростіших проєктах.
З базовими, найпростішими правилами серіалізації, які проходять перевірку під час виконання, можна очікувати, що модель виглядатиме ось так.
Я хочу виокремити 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 годин, які я витратив на спроби змусити ці моделі працювати. Якщо ви хочете спробувати з'єднати ці набори даних, щоб отримати будь-яку значущу взаємодію на фронтенді, вам доведеться
- Завантажити обидва набори даних
- Зберегти їх в стані
- Передати їх як пропси
- Виконати перехресне співвіднесення даних ось так
// Симуляція кількох 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’. Звідти воно продовжить обробку даних, поки не знайде відношення до користувачів, і БУМ — досягнуто ліміту рекурсії.
КРАЩИЙ СПОСІБ
Розробник бекенду, який заслуговує на свою зарплату, повинен це розуміти.
Правильне визначення моделей зараз заощадить години головного болю в майбутньому.
Давайте подивимося на правильний спосіб обробки цих відносин.
serialize_rules = ('-project.collaborations', 'user',
'-user.collaborators.project')
Це скаже серіалізатору:
- виключити відношення до проекту ON
- виключити відношення до співробітників FROM
- виключити початкове відношення до користувачів FROM
- нашу таблицю ‘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