|
| 1 | +# Адаптеры баз данных (DB Adapters) |
| 2 | + |
| 3 | +Фреймворк umbot не знает, используете вы SQL, NoSQL или файловую систему. Он оперирует абстрактными объектами IQuery и IQueryData. Ваша задача как разработчика адаптера — написать "транслятор", который превращает эти абстракции в реальные запросы к вашей СУБД. |
| 4 | + |
| 5 | +## Архитектура: Template Method |
| 6 | + |
| 7 | +Базовый класс `BaseDbAdapter` (из `umbot/plugins`) берет на себя рутину: |
| 8 | + |
| 9 | +- Замер времени выполнения запросов (метрики EMetric.DB_SELECT, DB_INSERT и т.д.). |
| 10 | +- Управление жизненным циклом (вызов connect при старте). |
| 11 | +- Обертки над вашими методами (публичные `select`, `insert` вызывают ваши `_select`, `_insert`). |
| 12 | + |
| 13 | +### Почему мы переопределяем \_select, а не select? |
| 14 | + |
| 15 | +Публичные методы (`select`, `insert`, `update`, `remove`) в `BaseDbAdapter` уже написаны. Они оборачивают ваши внутренние методы (`_select`, `_insert`), чтобы замерять время выполнения и логировать метрики. Если вы переопределите select(), вы сломаете сбор метрик и логику повторных подключений. Вы всегда реализуете только методы с подчеркиванием. |
| 16 | + |
| 17 | +## Обязательный контракт (что нужно реализовать) |
| 18 | + |
| 19 | +Наследуемся от `BaseDbAdapter` и реализуем: |
| 20 | + |
| 21 | +1. connect(): Promise<boolean> — Устанавливаете соединение с БД. |
| 22 | +2. isConnected(): Promise<boolean> — Проверяете, живо ли соединение (например, делаете ping БД). |
| 23 | +3. \_select(selectData: IQuery, where: IQueryData | null, isOne: boolean): Promise<IModelRes> — Поиск. |
| 24 | +4. \_insert(insertData: IQuery): Promise<boolean> — Добавление. |
| 25 | +5. \_update(updateData: IQuery): Promise<boolean> — Обновление. |
| 26 | +6. \_remove(removeData: IQuery): Promise<boolean> — Удаление. |
| 27 | +7. destroy(): Promise<void> — Закрываете пул соединений при остановке приложения. |
| 28 | + |
| 29 | +## Форматы данных (Шпаргалка): |
| 30 | + |
| 31 | +### Вход (IQuery): То, что фреймворк передает вам. |
| 32 | + |
| 33 | +Это объект, который фреймворк передает в ваши методы \_select, \_insert и т.д. |
| 34 | + |
| 35 | +```ts |
| 36 | +{ |
| 37 | + tableName: 'UsersData', // Имя таблицы/коллекции |
| 38 | + primaryKeyName: 'userId', // Первичный ключ |
| 39 | + query: { userId: '123' }, // Условия WHERE (может быть null) |
| 40 | + data: { name: 'John' }, // Данные для SET (может быть null) |
| 41 | + rules: [{ name: ['name'], type: 'string', max: 50 }] // Правила валидации |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +### Условия и данные (IQueryData) |
| 46 | + |
| 47 | +Формат query и data внутри IQuery. |
| 48 | +Важно: Значения могут быть не только примитивами, но и объектами с операторами. Фреймворк не навязывает конкретный диалект (например, $gt для Mongo или > для SQL). Адаптер сам решает, как интерпретировать эти операторы. |
| 49 | + |
| 50 | +```ts |
| 51 | +// Простое условие (равенство) |
| 52 | +{ userId: '123', platform: 'alisa' } |
| 53 | + |
| 54 | +// Условие с оператором (адаптер должен сам распарсить это в SQL `age > 18` или Mongo `$gt`) |
| 55 | +{ age: { $gt: 18 }, status: 'active' } |
| 56 | +``` |
| 57 | + |
| 58 | +### Выходные данные (IModelRes) |
| 59 | + |
| 60 | +То, что вы обязаны вернуть из метода `_select`. |
| 61 | + |
| 62 | +```ts |
| 63 | +// Успех (даже если ничего не найдено, status должен быть true, а data - пустым массивом или null) |
| 64 | +{ status: true, data: { userId: '123', name: 'John' } } |
| 65 | +{ status: true, data: [] } |
| 66 | + |
| 67 | +// Ошибка (сбой подключения, синтаксическая ошибка и т.д.) |
| 68 | +{ status: false, error: 'Connection timeout' } |
| 69 | +``` |
| 70 | + |
| 71 | +Для методов `_insert`, `_update`, `_remove` вы возвращаете просто boolean (true при успехе, false при ошибке). |
| 72 | + |
| 73 | +## Критические нюансы (Скрытые контракты) |
| 74 | + |
| 75 | +### 1. Валидация данных |
| 76 | + |
| 77 | +В базовом классе `BaseDbAdapter` нет встроенного метода `validate()`. |
| 78 | +Однако в `MongoAdapter` он реализован для валидации данных по правилам модели (`IModelRules`). |
| 79 | + |
| 80 | +Если вы хотите, чтобы ваш адаптер также валидировал данные (обрезал строки по `max`, |
| 81 | +приводил типы), реализуйте метод `validate()` в своём классе: |
| 82 | + |
| 83 | +```ts |
| 84 | +public validate(query: IQuery, element: IQueryData | null): IQueryData { |
| 85 | + if (!element) return {}; |
| 86 | + |
| 87 | + const rules = query.rules; |
| 88 | + if (rules) { |
| 89 | + rules.forEach((rule) => { |
| 90 | + rule.name.forEach((fieldName) => { |
| 91 | + if (rule.type === 'string' || rule.type === 'text') { |
| 92 | + if (rule.max !== undefined) { |
| 93 | + element[fieldName] = Text.resize(element[fieldName] as string, rule.max); |
| 94 | + } |
| 95 | + element[fieldName] = this.escapeString(element[fieldName] as string); |
| 96 | + } else if (rule.type === 'integer' || rule.type === 'int') { |
| 97 | + element[fieldName] = +(element[fieldName] as number); |
| 98 | + } |
| 99 | + }); |
| 100 | + }); |
| 101 | + } |
| 102 | + return element; |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +Затем вызывайте его в `_insert()` и `_update()`: |
| 107 | + |
| 108 | +```ts |
| 109 | +public async _insert(insertData: IQuery): Promise<boolean> { |
| 110 | + const validData = this.validate(insertData, insertData.data); |
| 111 | + // ... выполнение запроса с validData |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +**Примечание**: Валидация в модели (`Model.validate()`) и в адаптере (`validate()`) — это разные вещи. |
| 116 | +Модель валидирует свои данные перед сохранением, а адаптер валидирует данные по правилам `IModelRules` |
| 117 | +перед выполнением запроса к БД. |
| 118 | + |
| 119 | +_Зачем тогда в `IQuery` передаются `rules`?_ |
| 120 | +Они нужны вам для **маппинга типов** специфичных для вашей СУБД. Например, если вы пишете SQL-адаптер, вы можете использовать `rules`, чтобы понять, что поле с `type: 'object'` нужно сериализовать в JSON-строку перед вставкой, а `max: 150` использовать для динамического создания `VARCHAR(150)`. |
| 121 | + |
| 122 | +### 2. Хранение подключения (Connection Pool) |
| 123 | + |
| 124 | +Чтобы не создавать новое подключение к БД на каждый запрос, фреймворк предоставляет синглтон-хранилище. |
| 125 | +При успешном `connect()` вы должны сохранить пул соединений в `this._appContext.database.databaseInfo`. |
| 126 | + |
| 127 | +```ts |
| 128 | +async connect(): Promise<boolean> { |
| 129 | + const pool = await createMyDbPool(this._dbOptions); |
| 130 | + // Сохраняем пул, чтобы использовать его в _select/_insert |
| 131 | + this._appContext.database.databaseInfo = { myDbPool: pool }; |
| 132 | + return true; |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +### 3. Произвольные запросы (\_query) |
| 137 | + |
| 138 | +Если разработчику приложения нужно выполнить "сырой" SQL-запрос или агрегацию, он использует метод `model.query(callback)`. |
| 139 | +В `BaseDbAdapter` публичный `query` просто вызывает `_query`. По умолчанию `_query` возвращает `null`. Если вы хотите поддержать кастомные запросы, переопределите `_query`, передав в callback ваше подключение. |
| 140 | + |
| 141 | +### 4. Обработка ошибок |
| 142 | + |
| 143 | +Не бросайте исключения (`throw new Error`) из методов `_select`, `_insert`, `_update`, `_remove`. |
| 144 | +Фреймворк ожидает, что вы сами обработаете ошибки внутри метода и вернете `false` или `{ status: false, error: ... }`. |
| 145 | + |
| 146 | +## Универсальный пример реализации (Псевдокод) |
| 147 | + |
| 148 | +```ts |
| 149 | +import { BaseDbAdapter } from 'umbot/plugins'; |
| 150 | +import { IQuery, IQueryData, IModelRes, TQueryCb } from 'umbot'; |
| 151 | + |
| 152 | +export class MyCustomDbAdapter extends BaseDbAdapter { |
| 153 | + async connect(): Promise<boolean> { |
| 154 | + try { |
| 155 | + // 1. Создаем пул соединений |
| 156 | + const pool = await myDbDriver.connect(this._dbOptions); |
| 157 | + // 2. Сохраняем его в контекст |
| 158 | + this._appContext.database.databaseInfo = { pool }; |
| 159 | + return true; |
| 160 | + } catch (err) { |
| 161 | + return false; |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + async isConnected(): Promise<boolean> { |
| 166 | + const pool = this._appContext.database.databaseInfo?.pool; |
| 167 | + return pool ? await pool.ping() : false; |
| 168 | + } |
| 169 | + |
| 170 | + async _select( |
| 171 | + selectData: IQuery, |
| 172 | + where: IQueryData | null, |
| 173 | + isOne: boolean, |
| 174 | + ): Promise<IModelRes> { |
| 175 | + const pool = this._appContext.database.databaseInfo?.pool; |
| 176 | + if (!pool) return { status: false, error: 'No DB connection' }; |
| 177 | + |
| 178 | + try { |
| 179 | + // 1. Парсим абстрактные условия where в SQL/NoSQL запрос |
| 180 | + const sqlQuery = this.buildSelectQuery(selectData.tableName, where, isOne); |
| 181 | + |
| 182 | + // 2. Выполняем запрос |
| 183 | + const result = await pool.execute(sqlQuery); |
| 184 | + |
| 185 | + // 3. Возвращаем в формате IModelRes |
| 186 | + return { status: true, data: result }; |
| 187 | + } catch (err) { |
| 188 | + // Не бросаем исключение, а возвращаем статус false |
| 189 | + return { status: false, error: (err as Error).message }; |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + async _insert(insertData: IQuery): Promise<boolean> { |
| 194 | + const pool = this._appContext.database.databaseInfo?.pool; |
| 195 | + if (!pool) return false; |
| 196 | + |
| 197 | + try { |
| 198 | + // Валидация (так как в BaseDbAdapter её нет, используем свою) |
| 199 | + const validData = this.validate(insertData, insertData.data); |
| 200 | + const sqlQuery = this.buildInsertQuery(insertData.tableName, validData); |
| 201 | + await pool.execute(sqlQuery); |
| 202 | + return true; |
| 203 | + } catch (err) { |
| 204 | + return false; |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + // Переопределяем _query, чтобы поддержать сырые запросы от разработчика |
| 209 | + public async _query(callback: TQueryCb): Promise<unknown> { |
| 210 | + const pool = this._appContext.database.databaseInfo?.pool; |
| 211 | + if (pool) { |
| 212 | + // Передаем пул в callback разработчика |
| 213 | + return await callback(pool, pool); |
| 214 | + } |
| 215 | + return null; |
| 216 | + } |
| 217 | + |
| 218 | + async destroy(): Promise<void> { |
| 219 | + const pool = this._appContext.database.databaseInfo?.pool; |
| 220 | + if (pool) await pool.close(); |
| 221 | + } |
| 222 | + |
| 223 | + // --- Вспомогательные методы --- |
| 224 | + |
| 225 | + // Своя валидация |
| 226 | + private validate(query: IQuery, data: IQueryData | null): IQueryData { |
| 227 | + if (!data) return {}; |
| 228 | + // Здесь можно пройтись по query.rules и обрезать строки по max |
| 229 | + return data; |
| 230 | + } |
| 231 | + |
| 232 | + // Транслятор IQueryData в SQL (упрощенно) |
| 233 | + private buildSelectQuery(table: string, where: IQueryData | null, isOne: boolean): string { |
| 234 | + let sql = `SELECT * FROM ${table}`; |
| 235 | + if (where) { |
| 236 | + const conditions = Object.keys(where).map((key) => { |
| 237 | + const val = where[key]; |
| 238 | + // Поддержка операторов |
| 239 | + if (typeof val === 'object' && val !== null && val.$gt !== undefined) { |
| 240 | + return `${key} > ${val.$gt}`; |
| 241 | + } |
| 242 | + return `${key} = '${val}'`; |
| 243 | + }); |
| 244 | + sql += ` WHERE ${conditions.join(' AND ')}`; |
| 245 | + } |
| 246 | + if (isOne) sql += ' LIMIT 1'; |
| 247 | + return sql; |
| 248 | + } |
| 249 | + |
| 250 | + private buildInsertQuery(table: string, data: IQueryData): string { |
| 251 | + // ... логика формирования INSERT |
| 252 | + return ''; |
| 253 | + } |
| 254 | +} |
| 255 | +``` |
0 commit comments