|
2011-08-07
Обсудить на форуме
Принять участие в разработке библиотеки/утилиты можно на GitHub.
DB_Type это фреймворк для преобразования сложных типов PostgreSQL 8.3+
в их аналоги на PHP и обратно. С ее помощью вы можете работать с полями сложного типа (к примеру,
двумерным массивом композитных типов) так же просто, как с привычными массивами PHP.
|
В будущем добавится поддержка и других СУБД, в которых имеются сложные типы. Пока же мы будем
говорить о PostgreSQL. (Кстати, в MySQL сложных типов нет, поэтому "низкоуровневые" типы
DB_Type, такие как Date, Timestamp и т. д., можно использовать в MySQL уже сейчас, если это
кому-то понадобится.)
|
Поддерживаются следующие типы данных и любые их вложенные комбинации:
- Массивы элементов произвольного типа (в том числе многомерные).
- Композитные типы и ROWTYPE (в частности, сами содержащие композитные поля или поля-массивы).
- Hstore (в том числе содержащие сложные элементы).
- Прочие типы: TIMESTAMP (преобразуется в Unix time), DATE, TIME, BOOLEAN и т. д.
Для чего это нужно?
PostgreSQL славится своей поддержкой сложных типов данных. Например, вы можете
определить столбец некоторой таблицы как двумерный массив строк:
CREATE TABLE something(
id INTEGER,
matrix TEXT[][]
);
INSERT INTO something(id, matrix) VALUES(
1, ARRAY[ARRAY['one','two'], ARRAY['three "3"','four']]
);
Однако в PHP-скрипте при попытке получить значение из такого столбца:
|
Листинг 1
|
$rs = $pdo->query("SELECT matrix FROM something WHERE id=1");
echo $rs->fetchColumn(); |
вы увидите лишь строковое представление этих данных, нечто вроде:
{{one,two},{"three \"3\"",four}}
DB_Type как раз и позволяет преобразовать выражения вида {{one,two},{"three \"3\"",four}} в
двумерный массив PHP (с учетом особенностей квотинга спец-последовательностей: кавычек, апострофов,
пустых строк, NULL и т. д.) Или обратно, если нужно записать двумерный массив в БД.
|
Кстати, в хранимых процедурах PostgreSQL работать со сложными типами легко и удобно.
|
Как использовать библиотеку?
Встроенные в PHP инструменты при работе со сложными типами возвращают данные
"упакованными", в виде специально сформированных строк, которые мы
далее будем называть "строковыми представлениями сложных типов". Выше мы уже
встречались с примером такой "упаковки": {{one,two},{"three \"3\"",four}}
для массива array(array("one", "two"), array('three "3"', "four")).
Библиотека же позволяет собирать объекты для разбора/построения сложных типов, как из конструктора.
Для каждого типа данных XXX имеется свой класс с именем DB_Type_XXX, объекты
которого умеют разбирать (input, "из PostgreSQL в PHP") и собирать (output, "в PostgreSQL из PHP")
соответствующие значения.
Сложные типы
Некоторые типы имеют сложную структуру, поэтому соответствующие классы принимают
в конструкторах уточняющую информацию о структуре вложенных элементов.
Тип "массив": Array
Тип массив
DB_Type_Pgsql_Array принимает первым параметром объект тип элемента массива:
|
Листинг 2: Массив строк
|
// Создаем парсер для типа "массив строк".
$parser = new DB_Type_Pgsql_Array(new DB_Type_String());
// Вернет array("one", "two")
$array = $parser->input('{one,two}'); |
А вот так можно разобрать двумерный массив строк из первого примера этой статьи:
|
Листинг 3: Двумерный массив строк
|
// Создаем парсер для типа "массив массивов строк".
$parser = new DB_Type_Pgsql_Array(
new DB_Type_Pgsql_Array(
new DB_Type_String()
)
);
// Вернет array(array("one", "two"), array('three "3"', "four"))
$array = $parser->input('{{"one",two},{"three \"3\"",four}}'); |
Можно обратно построить строку по массиву PHP для вставки в БД:
|
Листинг 4: Построение строкового представления массива
|
echo $parser->output($array); |
Тип "ROW": Row
Класс DB_Type_Pgsql_Row позволяет разбирать данные типа "строка таблицы" (ROWTYPE)
или, что практически то же самое,
композитный тип.
|
Листинг 5: Структура некоторого композитного типа
|
CREATE TYPE inventory_item AS (
name text,
supplier_id integer,
price numeric
);
CREATE TABLE on_hand (
item inventory_item,
count integer
);
INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000); |
|
Листинг 6: Разбор композитного типа
|
// Создаем парсер.
$parser = new DB_Type_Pgsql_Row(array(
'name' => new DB_Type_String(),
'supplier_id' => new DB_Type_Int(),
'price' => new DB_Type_Numeric(),
));
// Вернет array("name" => 'fuzzy dice', 'supplier_id' => 42, 'price' => 1.99)
$row = $parser->input('("fuzzy dice",42,1.99)');
// Или можно построить обратно строковое представление данных:
echo $parser->output($row); |
Что примечательно, в PostgreSQL можно создавать массивы элементов
композитного типа. Из разбор при помощи DB_Type не отличается от
разбора массива любого другого типа:
|
Листинг 7: Массив композитных типов
|
$parser = new DB_Type_Pgsql_Array(
new DB_Type_Pgsql_Row(array(
'name' => new DB_Type_String(),
'supplier_id' => new DB_Type_Int(),
'price' => new DB_Type_Numeric(),
)
); |
Видите, структура типа строится из классов, как из конструктора, и может быть любой.
Тип "ассоциативный массив": Hstore
HStore это тип из
contrib-расширения hstore,
позволяющий хранить в поле таблицы ассоциативный массив ("хэш") с произвольными элементами.
При работе с DB_Type_Pgsql_Hstore нужно лишь передать конструктору тип значений
хэша, а ключи ассоциативного массива могут быть произвольными.
|
Листинг 8: Хэш строковых значений
|
$parser = new DB_Type_Pgsql_Hstore(new DB_Type_String());
// Вернет array("aaa" => 'bq', 'b' => null, '' => 1)
$hash = $parser->input('aaa=>bq, b=>NULL, ""=>1'); |
Как водится, можно делать массивы типов hstore, либо же, наоборот,
hstore композитных типов и массивов. Можно даже работать с двумерными
хэшами (правда, внутри PostgreSQL хэш из величин произвольного типа
заставляет СУБД преобразовывать данные в строки и обратно, что не очень
выгодно сказывается на производительности).
Простые типа
Простые (или, как еще говорят, "скалярные") типы в DB_Type нужны для того,
чтобы на их основе строить сложные. Некоторые из таких типов совсем тривиальны
(например, строки и целые числа), другие же содержат в себе некоторую логику
разбора (TIMESTAMP, DATE и т. д.).
Тип "таймстэмп": Timestamp
При работе с TIMESTAMP-ом в PHP удобно получать время в формате "Unix epoch time",
т.е. число секунд, прошедших c 1 января 1970 года. Класс DB_Type_Timestamp
позволяет преобразовывать тип PostgreSQL TIMESTAMP в Unix epoch time
и обратно.
|
Листинг 9: Таймстэмп
|
$parser = new DB_Type_Timestamp();
// Вернет 1204450462.
echo $parser->input("2008-03-02 12:34:22");
// Построит timestamp обратно для вставки в БД.
echo $parser->output(1204450462); |
Естественно, можно объявлять массивы таймстэмпов, композитные типы с таймстэмпами и т. д.
Тип "дата": Date
Класс DB_Type_Date позволяет разбирать и строить значения типа PostgreSQL
DATE.
Тип "время": Time
Класс DB_Type_Time используется для разбора типа
TIME.
Тип "булевский": Boolean
В PostgreSQL значния булевских типов записываются как 't' (true) и 'f' (false).
Это порождает определенную путаницу в PHP, для которого 't' === true, но 'f' !== false.
Класс DB_Type_Boolean преобразовывает 't' в true, а 'f' и еще некоторые значения,
похожие на "ложь", в false.
Типы Int, Numeric, String
Имеются также классы для разбора и построения (а точнее, для определения при
создании сложных типов) совсем простых значений:
- DB_Type_String: строка.
- DB_Type_Numeric: десятичное число произвольной точности или же float-значение.
- DB_Type_Int: целое 32-битное число.
Модификаторы типов
Часто данные перед попаданием в БД (или, наоборот, после извлечения)
должны проходить некоторую минимальную обработку. В DB_Type есть
поддержка для некоторых наиболее частых операций; вы можете также написать
свои собственные "классы-модификаторы".
Отсечение пробелов: Trim
В подавляющем большинстве случае перед вставкой строковых данных в БД
из них хотелось бы вырезать ведущие и концевые пробелы. Чтобы делать это
автоматически, "оберните" тип DB_Type_String в объект класса Trim:
|
Листинг 10: Автоматическое отрезание пробелов
|
// Создаем парсер/построитель вида "массив строк с автоматическим
// отрезанием ведущих и концевых пробелов".
$parser = new DB_Type_Pgsql_Array(
new DB_Type_Wrapper_Trim(new DB_Type_String())
); |
Пустая строка в NULL
Следующая по популярности операция при попытке вставки значения ""
(пустая строка) записывать в БД величину NULL. Это можно сделать автоматически,
обернув тип String в объект класса EmptyNull:
|
Листинг 11: Автоматическое преобразование пустой строки в NULL
|
// Создаем парсер/построитель вида "массив строк с автоматическим
// отрезанием ведущих и концевых пробелов, где вместо пустых строк - NULL".
$parser = new DB_Type_Pgsql_Array(
new DB_Type_Wrapper_EmptyNull
new DB_Type_Trim(
new DB_Type_String()
)
)
); |
Отсечение части времени
Классу DB_Type_Date можно передать первым параметром "критерий отсечения",
если вы знаете, что значение нужно сократить, к примеру, до месяца (отбросив день).
Например, new DB_Type_Date(DB_Type_Date::TRUNC_MONTH), вызванный
для строки "2012-10-02", вернет "2012-10-01" сокращение даты до месяца.
Точно так же работают и сокращения для типа DB_Type_Time, только сокращать
можно до минут (TRUNC_MINUTE) и часов (TRUNC_HOUR).
Наконец, тип DB_Type_Timestamp поддерживает отсечение до минут, часов, дней, месяцев и лет.
Возврат константы: Constant
Если вызвать метод output() у объекта класса DB_Type_Constant,
то возвращаемое значение будет одно и то же вне зависимости от переданных
параметров.
|
Листинг 12: Константа
|
$parser = new DB_Type_Constant("value");
// Напечатает "value"
echp $parser->output('123'); |
Зачем это нужно? Для формировани "заглушечных" данных в композитных типов,
которые должны меняться вызывающей программой. В общем, это довольно сложно,
но если вы вдруг столкнетесь, то сразу поймете, зачем оно.
Валидаторы
Иногда требуется ограничить диапазон значений или формат данных,
которые подаются на вход методу output() классов DB_Type.
Ограничение на длину строки
Тип DB_Type_String принимает в конструкторе 2 необязательных параметра:
мнимальная и максимальная разрешенная длина строки. В случае, если строка не
удовлетворяет этим критериям, генерируется исключение.
|
Листинг 13: Ограничение длины строки
|
// Парсер/построитель вида "массив строк длины от 10 до 20 символов".
$parser = new DB_Type_Pgsql_Array(
new DB_Type_String(10, 20)
); |
Ту же самую функцию несет класс DB_Type_Wrapper_Length: он наклазывает
ограничение по длине на тип, переданный в первом параметре конструктора:
|
Листинг 14: Ограничение длины строки
|
// Создаем парсер/построитель вида "строка длины от 10 до 20 символов".
$parser = new DB_Type_Pgsql_Array(
new DB_Type_Wrapper_Length(new DB_Type_String(), 10, 20)
); |
Встроенные валидаторы
Нужно заметить, что некоторые ограничения уже встроены в типы Array,
Row и Hstore: попытка передачи в них значений неверного формата
приведет к исключению.
Также типы Int и Numeric генерируют исключение, если им передается нечисловое
значение. Тип Int дополнительно проверяет, что значение модет хранения в
32-битной ячейке памяти.
Другие типы (Time, Date и т. д.) также проверяют переданные данные
и генерируют исключения в случае несоответствия их формату.
Резюме
Семейство классов DB_Type позволяет задействовать мощь разнообразия
типов данных PostgreSQL в PHP-скриптах. Код классов покрыт юнит-тестами, поэтому
вы можете использовать преобразователи данных без опасений.
Зачем делать колонки сложных типов в БД? Давайте рассмотрим самый простой пример:
массив целых чисел. Довольно удобно хранить отношение "многие ко многим" не в виде
традиционной таблицы-связки, а в формате "массив идентификаторов":
|
Листинг 15
|
CREATE TABLE document(id INTEGER, data TEXT);
CREATE TABLE tag(id INTEGER, name TEXT, docs INTEGER[]); |
Конечно, при этом мы теряем возможность использовать внешние ключи для
ограничения целостности (впрочем, всегда можно реализовать эту проверку на
триггерах, а в одной из следующих версий PostgreSQL, вероятно, сделают
поддержку внешних ключей в полях-массивах). Но зато работа с такими данными
сильно упрощается (с точки зрения сокращения письма). В ряде случаев
удается достичь многократного прироста производительности (особенно с
использованием расширений
intarray
или патча dkLab PostgreSQL patch).
У такого метода "денормализации" есть, правда, и свои противники. Они советуют
"нормализовать все, что только можно". Что ж, их точка зрения заслуживает
уважения, но в каждом конкретном случае лучше все-таки смотреть по обстоятельствам,
какой метод удобнее и производительнее. Где-то хороша нормализация, а
где-то сложные типы.
|
|
Пользуетесь? Нравится?.. Пожертвуйте!
Пожертвование пойдет в качестве "спасибо" прямиком
на поддержание мотивации автора. А то он даже не знает, какое
количество людей пользуется той или иной библиотекой
или утилитой...
|