В прошлой части я говорил про генерацию модели, но так и не осветил как соединить генерацию моделей и работу с БД. Сейчас это исправлю.
Все примеры Sql буду приводить с использованием PostgreSQL (epgsql) и драйвер для tq_db tq_postgres_driver.
Meta
Для того, чтобы управлять моделями требуется большое количество мета информации. Есть много способов ее хранить но я выбрал оптимальный для себя – функция $meta/1.
Она аргументом принимает ключ запрашиваемых данных.
Первым символом идет знак “$” (из-за этого приходится весь атом обрамлять в одинарные кавычки), это сделано для того чтобы подчеркнуть, что функция является системной и ее не желательно использовать в бизнес логике.
Также определена функция $meta/2, которая просто пробрасывает вызов на $meta/1, но нужна для работы механизма вызова через кортеж.
1 2 3 4 5 | |
1 2 3 4 5 6 | |
Опции которые по умолчанию задает tq_transform:
module– возвращает имя модуля для модели{record_index,Field}– позиция поля Field в модели
tq_db расширяет этот список еще несколькими:
table– имя таблицы Sqlindexes– список индексных полей{db_type, Field}– тип поля Field в базе данных{db_alias, Field}– имя поля Field в базе данных{db_fields, r}– список полей, которые разрешено писать в БД{db_fields, w}– список полей, которые разрешено читать из БД
Для генерации tq_db необходимо использовать tq_sqlmodel_transform parse_transform, обязательно должна быть указана опция модели table, заданы БД типы для полей и хотя бы одно из них должно быть индексное.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
Sql injection
Метод защиты от sql инъекций будет достаточно простой: каждому аргументу, который будет подставлен в результирующий Sql запрос в пару должно быть дописано описание типа, которое укажет драйверу БД как работать с этими данными. Драйвер должен взять на себя всю ответственность по проверке и экранированию данных.
Например:
1
| |
Connection pool
Для работы с БД желательно использовать пул подключений. Имя пула может быть явно указан при генерации модели.
1 2 3 | |
По умолчанию используется пул с именем db.
Логика работы пула должна быть полностью реализована драйвером.
Select
Вспомним структуру модели db_user.
Типичный запрос для получения данных выглядит так:
1 2 | |
Теперь в Rows лежит кортеж {Login, Email, Password, Salt}.
Теперь понадобится функция для преобразования этого кортежа в нашу модель.
1 2 3 4 5 6 | |
Можно ввести дополнительную функцию, которая будет принимать запрос и функцию конструктор и возвращать список полученных моделей:
1 2 3 | |
и использовать ее так:
1 2 | |
Функция to_model/1 получилась не универсальной, т.к. фиксировано количество полей, которые должны возвращаться из БД. Устраним этот досадный недостаток, введя функции field_constructor(FieldName), которая возвращает Setter для поля, и constructor(Fields), которая принимает список полей модели и возвращает аналог to_model/1 для преобразования кортежа в объект db_user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Кода больше, но он более универсален.
1 2 3 4 5 6 7 8 | |
Если при выгрузке данных потребуется сделать преобразование поля from_db, то следует изменить конструктор поля, к примеру:
1 2 3 4 5 6 7 | |
tq_sqlmodel_transform может генерировать функции get и find для модели. Для этого надо явно указать через опцию модели
1 2 3 4 | |
get – Арность функции равна количеству индексных полей модели.
find(Query, Args) – принимает в качестве параметров запрос Query, который представляет собой dsl и должен обрабатываться далее. Поговорим об это позже.
1 2 3 4 5 6 7 | |
Помимо представленных, для получения данных из БД можно воспользоваться более низкоуровневыми функциями из модулей tq_sql и tq_dsl.
Save
Для сохранения модели понадобится флаг is_new, который показывает является ли она новой или нет. В конструкторе должны быть сброшены флаг is_new и флаги изменения полей (_changed).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
Так же добавим функцию is_new/1
1 2 | |
Наконец-таки перейдем к сохранению модели.
Необходимо проверить является ли модель новой и, в зависимости от результата, выполнить insert или update.
1 2 3 4 5 6 7 | |
Я не хочу отправлять в БД все поля, буду передавать только реально измененные:
1 2 3 4 5 6 7 8 9 10 11 | |
В списке должны быть только те поля, которые храняться в БД.
Если понадобиться перед сохранением сделать преобразвание полея to_db, то следует изменить реализацию этой функции на следующую:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Я не буду приводить тут пример как реализовать в общем виде генерацию Sql для insert и update, имея всю мета информацию и данные – это будет тривиальным заданием. Кому интересно – могут посмотреть пример реализации из postgres драйвера: insert, update
Необходимость генерации функции save нужно указать через опцию модели generate.
1 2 3 4 | |
Hooks
Полезно иметь возможность задавать хуки для операций save и delete. Для этого в реализацию методов save добавляются пред и пост условия.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Обратите внимание, что хук after_save_hook отличается количеством аргументов. Иногда бывает полезно иметь доступ к изначальному состоянию модели, к примеру, для проверки выполнялось ли первое сохранение модели или же обновление старой.
В tq_db хуки задаются через опции модели:
1 2 3 4 5 6 7 | |
SQL DSL.
Посмотрим на запрос на выборку данных
1 2 3 | |
Что сразу мне не нравится:
- Я должен вручную писать alias поля в “select” части Sql запроса и имена полей в конструкторе и вручную контролировать корректность их заполения;
- в “where” части я должен вручную писать alias-ы полей;
- должен вручную вписывать типы аргументов для Args;
- вручную указывать в запросе имя таблицы.
Я решил это дело автоматизировать путем введения дополнительных конструкций непосредственно в Sql.
Получились такие такое расширение:
$model.field_nameили$field_name– в sql вместо этой конструкции будет подставлен alias поля, полученный черезmodel:'$meta'({db_alias, field_name}).~model.field_nameили~field_name– в местах этих конструкций будет вставлен аргумент из Args. Для аргумента будет автоматически подставлен тип model:$meta'({db_type, field_name}). (Например для~id, из аргумента 10 получится [{integer, 10}]).#model– подстановка имени таблицы для модели model изmodel:'$meta'({db_alias, field_name}).@model.field_nameили@field_name– в sql вместо этой конструкции будет подставлен alias поля, полученный черезmodel:'$meta'({db_alias, field_name}),field_nameтакже будет подставлен вconstructor.@model.field_name(...)или@field_name(...)–field_nameбудет подставлен вconstructor, но alias не попадет в sql. Например, для запроса<<"SELECT @id(1)">>, в конструктор в качестве значения для поля id попадет 1.@model.*или@*– извлечь все поля (которые помечены как read)@model...или@...– извлечь поля, которые ранее не упоминались в этом sql запросе (которые помечены как read)
Т.к. часто возникают коллизии по именам, их иногда следует писать в виде “table_alias.field”, для указания синонимов таблицы при подстановке алиасов полей можно использовать фигурные скобки:
${table_alias}model.field_nameили${table_alias}field_name@{table_alias}model.field_nameили@{table_alias}field_name@{table_alias}model...или${table_alias}...@{table_alias}model.*или${table_alias}*Весь представленый dsl был реализован для удовлетворения личных потребностей по работе с Sql. DSL реализуется драйвером, так что, при желании, его можно легко его заменить на свой.
Для работы с новым языком запросов используется модуль tq_dsl.
1
| |
Usage
Все готово к тому, чтобы привести комплексный пример. Перепишем модуль db_user по всем правилам, т.е. будем солить пароль при сохранении в БД, искать пользователя по связке login + password и т.п.
Первое что нужно сделать – добавить драйвер tq_db в зависимости проекта, в моем случае это tq_postgres_driver:
1 2 3 | |
Далее необходимо запутить драйвер и задать параметры пула подключений
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
описать модель
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
и, для полноты картины, валидаторы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | |
tq_db не создает схему таблицы в БД, так что вам придется завести ее самим.
Пример использования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
На сегодня все. Но хочу сказать, что предстоит еще много работы для продолжения которой мне очень нужна обратная связь от сообщества, так что пишите мне, задавайте вопросы, говорите пожелания и давайте рекомендации… Спасибо за внимание.