В прошлой части я говорил про генерацию модели, но так и не осветил как соединить генерацию моделей и работу с БД. Сейчас это исправлю.
Все примеры 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 |
|
На сегодня все. Но хочу сказать, что предстоит еще много работы для продолжения которой мне очень нужна обратная связь от сообщества, так что пишите мне, задавайте вопросы, говорите пожелания и давайте рекомендации… Спасибо за внимание.