mnemo_cards/mnemo_cards_backend/docs/ARCHITECTURE_MODELS.md
2026-01-03 16:14:27 +03:00

1505 lines
57 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🏗️ Архитектура: Связь между Таблицами, Моделями, DTO и DAO
Документ объясняет, как связаны компоненты данных в проекте Mnemo Cards Backend.
---
## 📊 Общая схема связей
```
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ users table │ │ user_datas │ │ card_packs │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↕ (SQL queries)
┌─────────────────────────────────────────────────────────────────┐
│ Drift ORM (Code Generation) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. TABLES (lib/database/tables/*.dart) │ │
│ │ - Определение структуры таблиц │ │
│ │ class Users extends Table { ... } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↕ (build_runner) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 2. MODELS (database.g.dart) │ │
│ │ - Автогенерация классов данных │ │
│ │ class User extends DataClass { ... } │ │
│ │ class UsersCompanion { ... } (для insert/update) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↕ (DAO methods) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 3. DAO (lib/database/daos/*_dao.dart) │ │
│ │ - Методы для работы с БД │ │
│ │ class UserDao { Future<User?> getUserById(...) } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↕ (Extensions)
┌─────────────────────────────────────────────────────────────────┐
│ Domain Models (mnemo_cards_common_backend) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 4. Domain Models (UserModel, UserDataModel) │ │
│ │ - Бизнес-логика приложения │ │
│ │ - Расширения для конвертации │ │
│ │ extension UserToUserModel on User { ... } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↕ (toDto())
┌─────────────────────────────────────────────────────────────────┐
│ DTO (mnemo_cards_common) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 5. DTO (UserDto, CardPackDto, etc.) │ │
│ │ - JSON сериализуемые объекты для API │ │
│ │ - Только данные, без логики │ │
│ │ extension UserModelExtension { UserDto toDto() } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↕ (toJson())
┌─────────────────────────────────────────────────────────────────┐
│ API Layer (Shelf) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 6. API Endpoints │ │
│ │ Response.ok(userDto.toJson()) │ │
│ │ - HTTP JSON responses │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 🔄 Детальный поток данных
### Пример: Получение пользователя через API
```dart
// 1. HTTP Request
GET /api/v2/users/me
// 2. API Endpoint (users_api_v2.dart)
@Route.get('/users/me')
Future<Response> getCurrentUser(Request request) async {
final user = request.user; // UserModel (domain model)
// 3. Конвертация в DTO
final dto = await user.toDto(); // UserModel → UserDto
// 4. JSON Response
return _json(dto.toJson()); // UserDto → JSON
}
```
### Пример: Получение данных из БД
```dart
// 1. Manager (user_manager.dart)
Future<UserModel?> fetchUser(String id) async {
// 2. DAO обращается к БД через Drift
final user = await _db.userDao.getUserById(id); // Returns: User (Drift model)
// 3. Конвертация Drift User → Domain UserModel
return await user?.toUserModel(); // User → UserModel
}
```
### Пример: Создание нового пользователя
```dart
// 1. API получает данные
POST /api/v2/auth/register
{ "email": "user@example.com", "password": "..." }
// 2. API вызывает Manager
await _userManager.createUser(email, password);
// 3. Manager использует DAO
final companion = UsersCompanion.insert(
email: email,
externalUserId: generateId(),
// ...
);
final userId = await _db.userDao.createUser(companion); // Insert в БД
// 4. Возврат созданной модели
final user = await _db.userDao.getUserById(userId);
return await user.toUserModel();
```
---
## 📦 Компоненты детально
### 1⃣ **TABLES** (Определение структуры БД)
**Расположение:** `lib/database/tables/*.dart`
**Назначение:** Определяют структуру таблиц PostgreSQL через Drift
**Пример:**
```dart
// lib/database/tables/users.dart
class Users extends Table {
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get name => text().nullable()();
TextColumn get email => text().nullable()();
BoolColumn get admin => boolean()
.withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime()
.withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
```
**Что это дает:**
- ✅ Type-safe определение схемы БД
- ✅ Автоматическая генерация миграций
- ✅ Валидация на уровне компиляции
---
### 2⃣ **MODELS** (Drift генерирует классы данных)
**Расположение:** `lib/database/database.g.dart` (автогенерируемый файл)
**Назначение:** Immutable классы данных для работы с записями из БД
**Пример:**
```dart
// Автогенерируется Drift из таблицы Users
class User extends DataClass implements Insertable<User> {
final String id;
final String externalUserId;
final String? name;
final String? email;
final bool admin;
final List<String> purchases;
final PgDateTime createdAt;
final PgDateTime updatedAt;
final bool isDeleted;
const User({
required this.id,
required this.externalUserId,
this.name,
this.email,
required this.admin,
// ...
});
// Автоматически генерируются:
// - toJson()
// - fromJson()
// - toCompanion()
// - copyWith()
}
```
**Companion классы** (для insert/update):
```dart
// Для создания/обновления записей
class UsersCompanion {
final Value<String> id;
final Value<String> externalUserId;
final Value<String?> name;
// ...
UsersCompanion.insert({
this.id = const Value.absent(), // auto-generated
required String externalUserId,
String? name,
// ...
}) : externalUserId = Value(externalUserId),
name = Value(name);
}
```
**Ключевые особенности:**
- ✅ Immutable (неизменяемые после создания)
- ✅ Type-safe (все типы проверяются компилятором)
- ✅ Автоматическая сериализация JSON
---
### 3⃣ **DAO** (Data Access Objects)
**Расположение:** `lib/database/daos/*_dao.dart`
**Назначение:** Методы для CRUD операций с БД через Drift
**Пример:**
```dart
// lib/database/daos/user_dao.dart
@DriftAccessor(tables: [Users, UserDatas, Tokens, RefreshTokens, UserPacks])
class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
UserDao(super.db);
// GET operations
Future<User?> getUserById(String id) {
return (select(users)..where((u) => u.id.equals(id)))
.getSingleOrNull();
}
Future<User?> getUserByEmail(String email) {
return (select(users)..where((u) => u.email.equals(email)))
.getSingleOrNull();
}
// CREATE operations
Future<String> createUser(UsersCompanion user) async {
final inserted = await into(users).insertReturning(user);
return inserted.id;
}
// UPDATE operations
Future<bool> updateUser(User user) {
return update(users).replace(user);
}
Future<void> updateUserPartial(UsersCompanion updates) {
final userId = updates.id.value;
return (update(users)..where((u) => u.id.equals(userId)))
.write(updates);
}
// JOIN queries
Future<UserWithData?> getUserWithDataById(String id) async {
final query = select(users).join([
leftOuterJoin(userDatas, userDatas.userId.equalsExp(users.id)),
])..where(users.id.equals(id));
final result = await query.getSingleOrNull();
if (result == null) return null;
return UserWithData(
user: result.readTable(users),
userData: result.readTableOrNull(userDatas),
);
}
}
```
**Ключевые особенности:**
- ✅ Type-safe запросы (проверяются компилятором)
- ✅ Абстракция над SQL
- ✅ Автоматическая обработка транзакций
- ✅ Поддержка JOIN операций
---
### 4⃣ **Domain Models** (Бизнес-логика)
**Расположение:** `mnemo_cards_common_backend/lib/src/models/`
**Назначение:** Бизнес-модели с логикой приложения
**Пример:**
```dart
// mnemo_cards_common_backend/lib/src/models/user/user_model.dart
class UserModel {
final String? id;
final String? name;
final String? email;
final bool admin;
final List<String> purchases;
final UserSettings? userSettings;
// Бизнес-логика
UserDataModel? userData;
CardPackModel? subscriptionModel;
List<CardPackModel> packs = [];
// Методы бизнес-логики
bool get hasActiveSubscription =>
subscriptionModel != null && subscriptionModel!.isActive;
bool canAccessPack(String packId) {
return packs.any((p) => p.id == packId) || admin;
}
}
```
**Конвертация Drift → Domain Model:**
```dart
// lib/user/user_drift_extension.dart
extension UserToUserModel on User {
Future<UserModel> toUserModel() async {
return UserModel(
id: id,
name: name,
email: email,
admin: admin,
purchases: purchases,
userSettings: userSettings != null
? UserSettings.fromJson(userSettings!)
: null,
);
}
}
```
**Обратная конвертация (Domain → Drift):**
```dart
extension UserModelToUser on UserModel {
UsersCompanion toUsersCompanion() {
return UsersCompanion(
id: id != null ? Value(id!) : const Value.absent(),
name: Value(name),
email: Value(email),
admin: Value(admin),
purchases: Value(purchases),
userSettings: Value(userSettings?.toJson()),
);
}
}
```
**Ключевые особенности:**
- ✅ Содержат бизнес-логику
- ✅ Могут иметь связанные объекты (UserData, Packs, etc.)
- ✅ Независимы от слоя БД
---
## 🔄 Drift Models vs Domain Models - В чем разница?
**Это критически важное различие!** Давайте разберем детально.
### 📊 Сравнение
| Характеристика | Drift Models (из DAO) | Domain Models |
|----------------|----------------------|---------------|
| **Источник** | Генерируются Drift из таблиц БД | Создаются вручную для бизнес-логики |
| **Соответствие БД** | Точное соответствие структуре таблиц | Может отличаться от структуры БД |
| **Связанные объекты** | ❌ Нет (только FK в таблицах) | ✅ Да (загружаются отдельно) |
| **Бизнес-логика** | ❌ Нет | ✅ Да (методы, проверки) |
| **Где используются** | Только в слое БД (DAO) | В бизнес-логике (Managers, API) |
| **Изменения** | Автогенерируются при изменении таблиц | Изменяются вручную |
| **Пример** | `User`, `UserData` (из database.g.dart) | `UserModel`, `UserDataModel` |
### 🔍 Детальное сравнение на примере User
#### Drift Model: `User` (из DAO)
```dart
// lib/database/database.g.dart (автогенерируется Drift)
class User extends DataClass implements Insertable<User> {
final String id;
final String externalUserId;
final String? name;
final String? email;
final bool admin;
final String? userSettings;
final List<String> purchases;
final PgDateTime createdAt;
final PgDateTime updatedAt;
final bool isDeleted;
// ❌ НЕТ связанных объектов
// ❌ НЕТ бизнес-логики
// ❌ Только данные, точно соответствующие таблице users
}
```
**Особенности:**
- ✅ Точное соответствие таблице `users` в PostgreSQL
-Все поля из БД присутствуют
- ❌ Нет связанных объектов (packs, subscriptionModel, userData)
- ❌ Нет методов бизнес-логики
- ❌ Используется только для работы с БД
**Откуда берется:**
```dart
// DAO возвращает Drift Model
final user = await _db.userDao.getUserById(id); // Returns: User (Drift)
```
#### Domain Model: `UserModel`
```dart
// mnemo_cards_common_backend/lib/src/models/user/user_model.dart
class UserModel {
String? id;
final String? name;
final String? email;
final bool admin;
final List<String> purchases;
final String? userSettings;
// ✅ Связанные объекты (загружаются отдельно)
final List<CardPackModel> packs = [];
UserDataModel? userData;
UserSubscriptionModel? subscriptionModel;
// ✅ Может содержать бизнес-логику через extensions
}
```
**Особенности:**
- ✅ Может отличаться от структуры БД
- ✅ Содержит связанные объекты (загружаются отдельно)
- ✅ Используется для бизнес-логики
- ✅ Может иметь методы (через extensions)
- ✅ Независим от структуры БД
**Откуда берется:**
```dart
// Конвертация Drift → Domain Model
final user = await _db.userDao.getUserById(id); // User (Drift)
final userModel = await user.toUserModel(); // UserModel (Domain)
```
### 🔄 Процесс конвертации
#### Шаг 1: Получение Drift Model из БД
```dart
// DAO возвращает Drift Model
final user = await _db.userDao.getUserById(userId);
// user это: User (Drift Model)
// Содержит только поля из таблицы users
// user.id, user.name, user.email, etc.
// НО: НЕТ user.packs, НЕТ user.subscriptionModel
```
#### Шаг 2: Конвертация в Domain Model
```dart
// lib/user/user_drift_extension.dart
extension UserToUserModel on User {
Future<UserModel> toUserModel() async {
// Создаем Domain Model из Drift Model
final userModel = UserModel(
id: id,
name: name,
email: email,
admin: admin,
purchases: purchases,
userSettings: userSettings,
);
// ⚠️ На этом этапе связанные объекты еще НЕ загружены!
// packs, userData, subscriptionModel будут null/пустыми
return userModel;
}
}
```
#### Шаг 3: Загрузка связанных объектов (опционально)
```dart
// Если нужны связанные объекты, загружаем их отдельно
final userModel = await user.toUserModel();
// Загружаем паки пользователя
final userPacks = await _db.userDao.getUserPacks(userId);
userModel.packs.addAll(userPacks.map((p) => await p.toCardPackModel()));
// Загружаем подписку
final subscription = await _db.subscriptionDao.getActiveSubscription(userId);
userModel.subscriptionModel = subscription?.toUserSubscriptionModel();
// Загружаем userData
final userData = await _db.userDao.getUserData(userId);
userModel.userData = await userData?.toUserDataModel();
```
### 💡 Почему нужны оба типа?
#### ❌ Если использовать только Drift Models:
```dart
// ❌ Проблема: Нет связанных объектов
final user = await _db.userDao.getUserById(id);
// ❌ Нельзя проверить доступ к паку
if (user.packs.contains(pack)) { ... } // НЕТ такого поля!
// ❌ Нельзя проверить подписку
if (user.subscriptionModel?.isActive) { ... } // НЕТ такого поля!
```
#### ✅ С Domain Models:
```dart
// ✅ Получаем Domain Model
final userModel = await _userManager.fetchUser(id);
// ✅ Можем работать со связанными объектами
if (userModel.packs.contains(pack)) { ... } // Работает!
// ✅ Можем проверять подписку
if (userModel.subscriptionModel?.isActive) { ... } // Работает!
```
### 📋 Сравнение на конкретных примерах
#### Пример 1: Структура данных
**Drift Model (User):**
```dart
class User {
final String id;
final String name;
final String email;
// Только поля из таблицы users
// НЕТ packs
// НЕТ subscriptionModel
// НЕТ userData
}
```
**Domain Model (UserModel):**
```dart
class UserModel {
final String? id;
final String? name;
final String? email;
// ✅ Может иметь связанные объекты
final List<CardPackModel> packs = [];
UserSubscriptionModel? subscriptionModel;
UserDataModel? userData;
}
```
#### Пример 2: Использование в коде
**С Drift Model:**
```dart
// ❌ Ограничено только данными из таблицы
final user = await _db.userDao.getUserById(id);
print(user.name); // ✅ Работает
print(user.packs); // ❌ Нет такого поля
```
**С Domain Model:**
```dart
// ✅ Можем использовать связанные объекты
final userModel = await _userManager.fetchUser(id);
print(userModel.name); // ✅ Работает
print(userModel.packs.length); // ✅ Работает
print(userModel.subscriptionModel); // ✅ Работает
```
#### Пример 3: Бизнес-логика
**Drift Model - только данные:**
```dart
class User {
// Только поля, никакой логики
final bool admin;
// ❌ Нет методов бизнес-логики
}
```
**Domain Model - может иметь логику:**
```dart
class UserModel {
final bool admin;
final List<CardPackModel> packs = [];
// ✅ Можем добавить методы через extensions
bool canAccessPack(String packId) {
return packs.any((p) => p.id == packId) || admin;
}
}
```
### 🎯 Когда использовать что?
#### Используйте Drift Models когда:
1. ✅ Работаете напрямую с БД (в DAO)
2. ✅ Делаете простые CRUD операции
3. ✅ Нужны только данные из одной таблицы
4.Не нужны связанные объекты
```dart
// Пример: Простое обновление
final user = await _db.userDao.getUserById(id);
await _db.userDao.updateUser(user.copyWith(name: 'New Name'));
```
#### Используйте Domain Models когда:
1. ✅ Реализуете бизнес-логику
2. ✅ Нужны связанные объекты
3. ✅ Работаете в Managers или API
4. ✅ Нужны методы и проверки
```dart
// Пример: Бизнес-логика
final userModel = await _userManager.fetchUser(id);
if (userModel.subscriptionModel?.isActive == true) {
// Логика с подпиской
}
```
### 🔄 Типичный поток работы
```dart
// 1. DAO возвращает Drift Model (только данные из БД)
final user = await _db.userDao.getUserById(id); // User (Drift)
// 2. Конвертируем в Domain Model (базовая конвертация)
final userModel = await user.toUserModel(); // UserModel (Domain)
// 3. При необходимости загружаем связанные объекты
if (needFullData) {
final packs = await _db.userDao.getUserPacks(id);
userModel.packs.addAll(/* конвертируем packs */);
final subscription = await _db.subscriptionDao.getActiveSubscription(id);
userModel.subscriptionModel = /* конвертируем subscription */;
}
// 4. Используем Domain Model для бизнес-логики
if (userModel.canAccessPack(packId)) {
// ...
}
// 5. Конвертируем в DTO для API
final dto = await userModel.toDto();
```
### 🎓 Ключевые выводы
1. **Drift Models** = прямые маппинги строк БД
- Только данные из таблиц
- Нет связанных объектов
- Используются только в слое БД
2. **Domain Models** = модели для бизнес-логики
- Могут иметь связанные объекты
- Используются в бизнес-логике
- Независимы от структуры БД
3. **Конвертация** всегда идет Drift → Domain
- DAO возвращает Drift Models
- Конвертируем в Domain Models через extensions
- Domain Models используются в остальном коде
4. **Разделение ответственности:**
- **Drift Models** = работа с БД
- **Domain Models** = бизнес-логика
- **DTO** = передача данных через API
---
### 5⃣ **DTO** (Data Transfer Objects)
**Расположение:** `mnemo_cards_common/lib/src/dtos/`
**Назначение:** Объекты для передачи данных через API (JSON)
**Пример:**
```dart
// mnemo_cards_common/lib/src/dtos/user/user_dto.dart
@JsonSerializable()
@CopyWith()
class UserDto {
String? id;
final String? name;
final String? email;
final bool admin;
final List<String> packs;
final bool? subscription;
final Set<SubscriptionFeatureEnum> subscriptionFeatures;
final UserDataDto? userDataDto;
UserDto({
this.id,
this.name,
this.email,
this.admin = false,
this.packs = const [],
this.subscription = false,
this.subscriptionFeatures = const {},
this.userDataDto,
});
// JSON сериализация
factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
Map<String, Object?> toJson() => _$UserDtoToJson(this);
}
```
**Конвертация Domain Model → DTO:**
```dart
// lib/user/user_model.dart
extension UserModelExtension on UserModel {
Future<UserDto> toDto() async {
final activeSubscription = subscriptionModel != null
&& subscriptionModel!.isActive;
return UserDto(
id: id,
name: name,
email: email,
admin: admin,
packs: packs.map((e) => e.id?.toString())
.whereNotNull()
.toList(),
subscription: activeSubscription,
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
userDataDto: userData?.toDto(),
);
}
}
```
**Ключевые особенности:**
- ✅ Только данные (no logic)
- ✅ JSON сериализуемые
- ✅ Могут отличаться от структуры БД
- ✅ Версионирование API через DTO
---
## 🤔 Зачем нужны Domain Models, если есть DTO?
**Это частый вопрос!** Давайте разберемся на конкретных примерах из кода.
### ❌ Почему нельзя использовать только DTO?
#### Проблема 1: **DTO не содержат связанные объекты**
**UserModel (Domain):**
```dart
class UserModel {
UserSubscriptionModel? subscriptionModel; // ✅ Полный объект подписки
List<CardPackModel> packs = []; // ✅ Полные объекты паков
UserDataModel? userData; // ✅ Полный объект данных
}
```
**UserDto:**
```dart
class UserDto {
bool? subscription; // ❌ Только boolean
Set<SubscriptionFeatureEnum> subscriptionFeatures; // ❌ Только фичи
List<String> packs; // ❌ Только ID строки
UserDataDto? userDataDto; // ❌ Упрощенная версия
}
```
#### Проблема 2: **Внутри backend нужна бизнес-логика**
**Пример из реального кода (`lib/packs/pack_dto_converter.dart`):**
```dart
Future<CardPackPreviewDto> toCardPackPreviewDto(
CardPackModel model,
UserModel? userModel, // ← Domain Model!
) async {
// Проверка доступа через связанные объекты
bool available = model.users.contains(userModel);
if (userModel != null && !available) {
// Используем subscriptionModel с методами
available = userModel.subscriptionModel?.features
.contains(SubscriptionFeatureEnum.packs) == true;
}
// Вычисляем цену через UserModel
String? price = model.price;
if (userModel != null && !available) {
price = await _productsPriceResolver.userPackPrice(userModel, model);
}
// ...
}
```
**С DTO это невозможно:**
```dart
// ❌ Невозможно - DTO не имеет subscriptionModel
available = userDto.subscriptionModel?.features... // Нет такого поля!
// ❌ Невозможно - DTO содержит только ID паков
userDto.packs.contains(model) // packs это List<String>, не объекты!
```
#### Проблема 3: **DTO уже вычислены и упрощены**
**При создании DTO (`lib/user/user_model.dart`):**
```dart
extension UserModelExtension on UserModel {
Future<UserDto> toDto() async {
// Вычисляем активность подписки из Domain Model
final activeSubscription = subscriptionModel != null
&& subscriptionModel!.isActive; // ← Используем Domain Model!
// Извлекаем только ID из паков
final packIds = packs.map((e) => e.id?.toString())
.whereNotNull()
.toList(); // ← Упрощаем список объектов
return UserDto(
subscription: activeSubscription, // Предвычисленное значение
packs: packIds, // Только строки
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
);
}
}
```
**DTO - это "моментальный снимок" состояния Domain Model для клиента.**
### ✅ Правильное использование
```
┌─────────────────────────────────────────────────────────┐
│ Backend Business Logic │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Domain Models (UserModel) │ │
│ │ - Связанные объекты (subscriptionModel, packs) │ │
│ │ - Бизнес-логика │ │
│ │ - Методы и проверки │ │
│ └──────────────────────────────────────────────────┘ │
│ ↕ (toDto()) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DTO (UserDto) │ │
│ │ - Только примитивы │ │
│ │ - Предвычисленные значения │ │
│ │ - Упрощенная структура │ │
│ └──────────────────────────────────────────────────┘ │
│ ↕ (toJson()) │
└─────────────────────────────────────────────────────────┘
JSON Response
Client App
```
### 📊 Сравнение на практике
| Задача | Domain Model (UserModel) | DTO (UserDto) |
|--------|-------------------------|---------------|
| **Проверить доступ к паку** | ✅ `user.packs.contains(pack)` | ❌ Нет объектов паков, только ID |
| **Проверить активную подписку** | ✅ `user.subscriptionModel?.isActive` | ❌ Только boolean `subscription` |
| **Получить фичи подписки** | ✅ `user.subscriptionModel?.features` | ✅ `userDto.subscriptionFeatures` |
| **Вычислить цену с учетом скидок** | ✅ Использует `user.userData` | ❌ Нет userData для логики |
| **Отправить через API** | ❌ Слишком сложная структура | ✅ Упрощенная структура |
| **JSON сериализация** | ❌ Может содержать циклические ссылки | ✅ Безопасная сериализация |
### 💡 Реальные примеры из кода
#### Пример 1: Проверка доступа через подписку
```dart
// ✅ С Domain Model - работает
// lib/packs/pack_dto_converter.dart
bool canAccess = userModel.subscriptionModel?.features
.contains(SubscriptionFeatureEnum.packs) == true;
// ❌ С DTO - невозможно (нет subscriptionModel)
// Можно только проверить subscriptionFeatures, но логика сложнее
```
#### Пример 2: Вычисление цены со скидкой
```dart
// ✅ С Domain Model - используем userData для скидок
// lib/packs/products_price_resolver.dart
Future<String> userPackPrice(
UserModel userModel, // Domain Model
CardPackModel model,
) async {
if (userModel.id != null) {
final userData = await _db.userDao.getUserData(userModel.id!);
final maxDiscount = await _discountsManager.getProductDiscount(
model,
userData, // Используем UserDataModel для логики
);
// ... вычисление скидки
}
}
// ❌ С DTO - нет доступа к userData для вычислений
```
#### Пример 3: Конвертация в DTO использует Domain Model
```dart
// Этот код находится ВНУТРИ backend и использует Domain Model
extension UserModelExtension on UserModel {
Future<UserDto> toDto() async {
// Используем Domain Model для вычислений
final activeSubscription = subscriptionModel != null
&& subscriptionModel!.isActive;
// Извлекаем данные из связанных объектов
final packIds = packs.map((e) => e.id?.toString())
.whereNotNull()
.toList();
// Создаем упрощенный DTO для клиента
return UserDto(
subscription: activeSubscription, // Предвычисленное
packs: packIds, // Упрощенное
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
);
}
}
```
### 🎯 Вывод
**Domain Models нужны для:**
1.**Бизнес-логики внутри backend**
- Проверки доступа
- Вычисления цен
- Валидация данных
- Работа со связанными объектами
2.**Работы со сложными структурами**
- Связанные объекты (subscriptionModel, packs, userData)
- Методы и computed properties
- Циклические связи
3.**Независимости от API**
- Domain Models не зависят от формата API
- Можно менять API без изменения бизнес-логики
- Переиспользование в разных контекстах
**DTO нужны для:**
1.**Передачи данных через API**
- Упрощенная структура
- Только примитивы
- Без циклических ссылок
2.**Версионирования API**
- Можно менять DTO без изменения Domain Models
- Обратная совместимость
3.**Оптимизации**
- Отправляем только нужные данные
- Предвычисленные значения
### 🔄 Типичный поток
```dart
// 1. Получаем Domain Model из БД
final user = await _userManager.fetchUser(userId); // UserModel
// 2. Используем Domain Model для бизнес-логики
if (user.subscriptionModel?.isActive == true) {
// Логика с использованием subscriptionModel
}
// 3. Конвертируем в DTO для API
final dto = await user.toDto(); // UserDto
// 4. Возвращаем JSON клиенту
return Response.ok(dto.toJson());
```
**Внутри backend работаем с Domain Models, клиенту отправляем DTO!** 🎯
---
## 🤔 Зачем нужны Domain Models, если есть DTO?
**Это частый вопрос!** Давайте разберемся на конкретных примерах.
### ❌ Почему нельзя использовать только DTO?
#### Проблема 1: **DTO не содержат связанные объекты**
**UserModel (Domain):**
```dart
class UserModel {
UserSubscriptionModel? subscriptionModel; // ✅ Полный объект подписки
List<CardPackModel> packs = []; // ✅ Полные объекты паков
UserDataModel? userData; // ✅ Полный объект данных
}
```
**UserDto:**
```dart
class UserDto {
bool? subscription; // ❌ Только boolean
Set<SubscriptionFeatureEnum> subscriptionFeatures; // ❌ Только фичи
List<String> packs; // ❌ Только ID строки
UserDataDto? userDataDto; // ❌ Упрощенная версия
}
```
#### Проблема 2: **Внутри backend нужна бизнес-логика**
**Пример из реального кода:**
```dart
// lib/packs/pack_dto_converter.dart
Future<CardPackPreviewDto> toCardPackPreviewDto(
CardPackModel model,
UserModel? userModel, // ← Domain Model!
) async {
// Проверка доступа через связанные объекты
bool available = model.users.contains(userModel);
if (userModel != null && !available) {
// Используем subscriptionModel с методами
available = userModel.subscriptionModel?.features
.contains(SubscriptionFeatureEnum.packs) == true;
}
// Вычисляем цену через UserModel
String? price = model.price;
if (userModel != null && !available) {
price = await _productsPriceResolver.userPackPrice(userModel, model);
}
// ...
}
```
**С DTO это невозможно:**
```dart
// ❌ Невозможно - DTO не имеет subscriptionModel
available = userDto.subscriptionModel?.features... // Нет такого поля!
// ❌ Невозможно - DTO содержит только ID паков
userDto.packs.contains(model) // packs это List<String>, не объекты!
```
#### Проблема 3: **DTO уже вычислены и упрощены**
**При создании DTO:**
```dart
extension UserModelExtension on UserModel {
Future<UserDto> toDto() async {
// Вычисляем активность подписки
final activeSubscription = subscriptionModel != null
&& subscriptionModel!.isActive; // ← Используем Domain Model!
// Извлекаем только ID из паков
final packIds = packs.map((e) => e.id?.toString())
.whereNotNull()
.toList(); // ← Упрощаем список объектов
return UserDto(
subscription: activeSubscription, // Предвычисленное значение
packs: packIds, // Только строки
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
);
}
}
```
**DTO - это "моментальный снимок" состояния Domain Model для клиента.**
### ✅ Правильное использование
```
┌─────────────────────────────────────────────────────────┐
│ Backend Business Logic │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Domain Models (UserModel) │ │
│ │ - Связанные объекты (subscriptionModel, packs) │ │
│ │ - Бизнес-логика │ │
│ │ - Методы и проверки │ │
│ └──────────────────────────────────────────────────┘ │
│ ↕ (toDto()) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DTO (UserDto) │ │
│ │ - Только примитивы │ │
│ │ - Предвычисленные значения │ │
│ │ - Упрощенная структура │ │
│ └──────────────────────────────────────────────────┘ │
│ ↕ (toJson()) │
└─────────────────────────────────────────────────────────┘
JSON Response
Client App
```
### 📊 Сравнение на практике
| Задача | Domain Model (UserModel) | DTO (UserDto) |
|--------|-------------------------|---------------|
| **Проверить доступ к паку** | ✅ `user.packs.contains(pack)` | ❌ Нет объектов паков, только ID |
| **Проверить активную подписку** | ✅ `user.subscriptionModel?.isActive` | ❌ Только boolean `subscription` |
| **Получить фичи подписки** | ✅ `user.subscriptionModel?.features` | ✅ `userDto.subscriptionFeatures` |
| **Вычислить цену с учетом скидок** | ✅ Использует `user.userData` | ❌ Нет userData для логики |
| **Отправить через API** | ❌ Слишком сложная структура | ✅ Упрощенная структура |
| **JSON сериализация** | ❌ Может содержать циклические ссылки | ✅ Безопасная сериализация |
### 💡 Реальные примеры из кода
#### Пример 1: Проверка доступа через подписку
```dart
// ✅ С Domain Model - работает
bool canAccess = userModel.subscriptionModel?.features
.contains(SubscriptionFeatureEnum.packs) == true;
// ❌ С DTO - невозможно (нет subscriptionModel)
// Можно только проверить subscriptionFeatures, но логика сложнее
```
#### Пример 2: Вычисление цены со скидкой
```dart
// ✅ С Domain Model - используем userData для скидок
Future<String> userPackPrice(
UserModel userModel, // Domain Model
CardPackModel model,
) async {
if (userModel.id != null) {
final userData = await _db.userDao.getUserData(userModel.id!);
final maxDiscount = await _discountsManager.getProductDiscount(
model,
userData, // Используем UserDataModel для логики
);
// ... вычисление скидки
}
}
// ❌ С DTO - нет доступа к userData для вычислений
```
#### Пример 3: Конвертация в DTO использует Domain Model
```dart
// Этот код находится ВНУТРИ backend и использует Domain Model
extension UserModelExtension on UserModel {
Future<UserDto> toDto() async {
// Используем Domain Model для вычислений
final activeSubscription = subscriptionModel != null
&& subscriptionModel!.isActive;
// Извлекаем данные из связанных объектов
final packIds = packs.map((e) => e.id?.toString())
.whereNotNull()
.toList();
// Создаем упрощенный DTO для клиента
return UserDto(
subscription: activeSubscription, // Предвычисленное
packs: packIds, // Упрощенное
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
);
}
}
```
### 🎯 Вывод
**Domain Models нужны для:**
1.**Бизнес-логики внутри backend**
- Проверки доступа
- Вычисления цен
- Валидация данных
- Работа со связанными объектами
2.**Работы со сложными структурами**
- Связанные объекты (subscriptionModel, packs, userData)
- Методы и computed properties
- Циклические связи
3.**Независимости от API**
- Domain Models не зависят от формата API
- Можно менять API без изменения бизнес-логики
- Переиспользование в разных контекстах
**DTO нужны для:**
1.**Передачи данных через API**
- Упрощенная структура
- Только примитивы
- Без циклических ссылок
2.**Версионирования API**
- Можно менять DTO без изменения Domain Models
- Обратная совместимость
3.**Оптимизации**
- Отправляем только нужные данные
- Предвычисленные значения
### 🔄 Типичный поток
```dart
// 1. Получаем Domain Model из БД
final user = await _userManager.fetchUser(userId); // UserModel
// 2. Используем Domain Model для бизнес-логики
if (user.subscriptionModel?.isActive == true) {
// Логика с использованием subscriptionModel
}
// 3. Конвертируем в DTO для API
final dto = await user.toDto(); // UserDto
// 4. Возвращаем JSON клиенту
return Response.ok(dto.toJson());
```
**Внутри backend работаем с Domain Models, клиенту отправляем DTO!** 🎯
---
### 6⃣ **API Layer** (HTTP Endpoints)
**Расположение:** `lib/api/v2/*_api_v2.dart`
**Назначение:** HTTP endpoints, которые возвращают JSON
**Пример:**
```dart
// lib/api/v2/users_api_v2.dart
@lazySingleton
class UsersApiV2 {
final UserManager _userManager;
final AppDatabase _db;
@Route.get('/users/me')
Future<Response> getCurrentUser(Request request) async {
final user = request.user; // UserModel (domain)
if (user == null) {
return _unauthorized();
}
// Конвертация: UserModel → UserDto
final dto = await user.toDto();
// Конвертация: UserDto → JSON
return _json(dto.toJson());
}
@Route('PATCH', '/users/me')
Future<Response> updateCurrentUser(Request request) async {
final user = request.user;
final body = jsonDecode(await request.readAsString());
// Обновление через DAO
await _db.userDao.updateUserPartial(
UsersCompanion(
id: Value(user.id!),
name: body['name'] != null
? Value(body['name'])
: const Value.absent(),
email: body['email'] != null
? Value(body['email'])
: const Value.absent(),
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
// Возврат обновленных данных
final refreshedUser = await _userManager.fetchUser(user.id!);
final dto = await refreshedUser.toDto();
return _json(dto.toJson());
}
}
```
---
## 🔄 Полный цикл: Создание пользователя
```dart
// 1. HTTP Request
POST /api/v2/auth/register
{ "email": "test@example.com", "password": "secret123" }
// 2. API Handler (auth_api_v2.dart)
final userModel = await _userManager.createUser(email, password);
// 3. UserManager (user_manager.dart)
Future<UserModel> createUser(String email, String password) async {
// Создание Drift Companion для insert
final companion = UsersCompanion.insert(
externalUserId: generateId(),
email: email,
// ...
);
// 4. DAO Insert (user_dao.dart)
final userId = await _db.userDao.createUser(companion);
// 5. Получение созданного User (Drift model)
final user = await _db.userDao.getUserById(userId);
// 6. Конвертация User → UserModel
return await user!.toUserModel();
}
// 7. API Response: UserModel → UserDto → JSON
final dto = await userModel.toDto();
return Response.ok(dto.toJson());
```
**Что происходит в БД:**
```sql
-- Drift автоматически генерирует SQL:
INSERT INTO users (id, external_user_id, email, created_at, updated_at)
VALUES (
gen_random_uuid()::text, -- auto-generated UUID
'generated-id-123',
'test@example.com',
NOW(),
NOW()
)
RETURNING *;
```
---
## 📋 Сравнительная таблица
| Компонент | Расположение | Тип данных | Назначение | Жизненный цикл |
|-----------|--------------|------------|------------|----------------|
| **Tables** | `lib/database/tables/` | Class extends `Table` | Определение схемы БД | Статичен (изменяется миграциями) |
| **Models (Drift)** | `lib/database/database.g.dart` | Immutable `DataClass` | Данные из БД | Генерируется Drift |
| **DAO** | `lib/database/daos/` | Methods | CRUD операции | Статичен |
| **Domain Models** | `mnemo_cards_common_backend/models/` | Classes with logic | Бизнес-логика | Может содержать состояние |
| **DTO** | `mnemo_cards_common/dtos/` | JSON-serializable | API responses | Версионируется |
| **API** | `lib/api/v2/` | HTTP handlers | Endpoints | Обрабатывает запросы |
---
## 🔑 Ключевые принципы
### 1. **Разделение ответственности**
- **Tables** → Определяют структуру БД
- **Drift Models** → Представляют данные из БД
- **DAO** → Абстракция над SQL запросами
- **Domain Models** → Содержат бизнес-логику
- **DTO** → Для передачи данных через API
- **API** → HTTP endpoints
### 2. **Направление конвертации**
```
Drift Model (User)
→ Domain Model (UserModel) [через extension]
→ DTO (UserDto) [через toDto()]
→ JSON [через toJson()]
```
### 3. **Type Safety**
Все преобразования типизированы на уровне компилятора:
- ✅ Drift проверяет запросы к БД
- ✅ Dart проверяет конвертации
- ✅ JSON сериализация с кодогенерацией
### 4. **Независимость слоев**
- **Domain Models** не зависят от БД
- **DTO** не зависят от Domain Models (но конвертируются из них)
- **API** использует Domain Models и DTO
---
## 💡 Практические примеры
### Пример 1: Получение пользователя с данными
```dart
// DAO возвращает User (Drift) + UserData (Drift)
final userWithData = await _db.userDao.getUserWithDataById(userId);
// Конвертация в Domain Models
final userModel = await userWithData.user.toUserModel();
final userDataModel = userWithData.userData?.toUserDataModel();
// Связывание
userModel.userData = userDataModel;
// Конвертация в DTO для API
final dto = await userModel.toDto(); // Включает userDataDto
```
### Пример 2: Создание записи с транзакцией
```dart
// Использование Companion для insert
await _db.transaction(() async {
// Создать пользователя
final userCompanion = UsersCompanion.insert(
externalUserId: 'telegram_123',
name: 'John Doe',
);
final userId = await _db.userDao.createUser(userCompanion);
// Создать UserData
final userDataCompanion = UserDatasCompanion.insert(
userId: userId,
totalStudyTimeMinutes: 0,
);
await _db.userDao.createUserData(userDataCompanion);
});
```
### Пример 3: Обновление частичных данных
```dart
// Использование Companion для partial update
await _db.userDao.updateUserPartial(
UsersCompanion(
id: Value(userId),
name: Value('New Name'), // Обновить только name
// email остается без изменений (Value.absent())
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
```
---
## 🎯 Итоги
### Связи:
1. **Tables** → определяют структуру БД
2. **Drift Models** → автогенерируются из Tables
3. **DAO** → используют Drift Models для работы с БД
4. **Domain Models** → конвертируются из Drift Models через extensions
5. **DTO** → конвертируются из Domain Models через toDto()
6. **API** → используют Domain Models и возвращают DTO как JSON
### Преимущества этой архитектуры:
**Type Safety** - все проверяется компилятором
**Separation of Concerns** - каждый слой решает свою задачу
**Maintainability** - легко изменять отдельные слои
**Testability** - можно мокировать любой слой
**Scalability** - легко добавлять новые сущности
---
**Этот документ должен помочь понять, как данные проходят через все слои приложения!** 🚀