Джино: хостинг и веб-сервисы

Система Orphus
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | DB_Type: преобразование сложных типов PostgreSQL (ARRAY, ROW, HSTORE) в PHP и обратно 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


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).

У такого метода "денормализации" есть, правда, и свои противники. Они советуют "нормализовать все, что только можно". Что ж, их точка зрения заслуживает уважения, но в каждом конкретном случае лучше все-таки смотреть по обстоятельствам, какой метод удобнее и производительнее. Где-то хороша нормализация, а где-то — сложные типы.







Дмитрий Котеров, Лаборатория dk. ©1999-2016
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com