# 🏗️ Архитектура: Связь между Таблицами, Моделями, 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 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 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 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 get primaryKey => {id}; } ``` **Что это дает:** - ✅ Type-safe определение схемы БД - ✅ Автоматическая генерация миграций - ✅ Валидация на уровне компиляции --- ### 2️⃣ **MODELS** (Drift генерирует классы данных) **Расположение:** `lib/database/database.g.dart` (автогенерируемый файл) **Назначение:** Immutable классы данных для работы с записями из БД **Пример:** ```dart // Автогенерируется Drift из таблицы Users class User extends DataClass implements Insertable { final String id; final String externalUserId; final String? name; final String? email; final bool admin; final List 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 id; final Value externalUserId; final Value 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 with _$UserDaoMixin { UserDao(super.db); // GET operations Future getUserById(String id) { return (select(users)..where((u) => u.id.equals(id))) .getSingleOrNull(); } Future getUserByEmail(String email) { return (select(users)..where((u) => u.email.equals(email))) .getSingleOrNull(); } // CREATE operations Future createUser(UsersCompanion user) async { final inserted = await into(users).insertReturning(user); return inserted.id; } // UPDATE operations Future updateUser(User user) { return update(users).replace(user); } Future updateUserPartial(UsersCompanion updates) { final userId = updates.id.value; return (update(users)..where((u) => u.id.equals(userId))) .write(updates); } // JOIN queries Future 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 purchases; final UserSettings? userSettings; // Бизнес-логика UserDataModel? userData; CardPackModel? subscriptionModel; List 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 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 { final String id; final String externalUserId; final String? name; final String? email; final bool admin; final String? userSettings; final List 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 purchases; final String? userSettings; // ✅ Связанные объекты (загружаются отдельно) final List 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 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 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 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 packs; final bool? subscription; final Set 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 json) => _$UserDtoFromJson(json); Map toJson() => _$UserDtoToJson(this); } ``` **Конвертация Domain Model → DTO:** ```dart // lib/user/user_model.dart extension UserModelExtension on UserModel { Future 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 packs = []; // ✅ Полные объекты паков UserDataModel? userData; // ✅ Полный объект данных } ``` **UserDto:** ```dart class UserDto { bool? subscription; // ❌ Только boolean Set subscriptionFeatures; // ❌ Только фичи List packs; // ❌ Только ID строки UserDataDto? userDataDto; // ❌ Упрощенная версия } ``` #### Проблема 2: **Внутри backend нужна бизнес-логика** **Пример из реального кода (`lib/packs/pack_dto_converter.dart`):** ```dart Future 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, не объекты! ``` #### Проблема 3: **DTO уже вычислены и упрощены** **При создании DTO (`lib/user/user_model.dart`):** ```dart extension UserModelExtension on UserModel { Future 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 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 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 packs = []; // ✅ Полные объекты паков UserDataModel? userData; // ✅ Полный объект данных } ``` **UserDto:** ```dart class UserDto { bool? subscription; // ❌ Только boolean Set subscriptionFeatures; // ❌ Только фичи List packs; // ❌ Только ID строки UserDataDto? userDataDto; // ❌ Упрощенная версия } ``` #### Проблема 2: **Внутри backend нужна бизнес-логика** **Пример из реального кода:** ```dart // lib/packs/pack_dto_converter.dart Future 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, не объекты! ``` #### Проблема 3: **DTO уже вычислены и упрощены** **При создании DTO:** ```dart extension UserModelExtension on UserModel { Future 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 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 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 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 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 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** - легко добавлять новые сущности --- **Этот документ должен помочь понять, как данные проходят через все слои приложения!** 🚀