суббота, 6 июля 2019 г.

Перенос данных нескольких полей в единое поле hstore

Сегодня опишу вам как решить следующую сверхспецифическую задачу: у нас в БД PostgreSQL есть таблица некоторой структуры:

id | name | attr_1 |  attr_2 |  attr_3 |
---------------------------------------|
 1 | NAME |     v1 |      v2 |      v3 |


Вследствие каких-то причин в проекте решено соединить данные полей attr_1, attr_2, attr_3 в одно поле attrs типа hstore.

Таким образом, новая таблица и запись в ней выглядит так:

id | name |                                        attrs |
---------------------------------------------------------|
 1 | NAME |     attr_1 => v1, attr_2 => v2, attr_3 => v3 |


Естественно, будем все максимально автоматизировать. Для работы нам пригодятся некоторые функции для работы с hstore и немного php, просто чтобы сформировать общий текст запроса. Общий текст запроса выглядит вот так:

UPDATE tableName
SET attrs =
    hstore(
        string_to_array(
            rtrim(
                (CASE WHEN (attr_1 IS NOT NULL) THEN ('attr_1' || '~~~' || attr_1 || '~~~') ELSE '' END)
                ||
                (CASE WHEN (attr_2 IS NOT NULL) THEN ('attr_2' || '~~~' || attr_2 || '~~~') ELSE '' END)
                ||
                (CASE WHEN (attr_3 IS NOT NULL) THEN ('attr_3' || '~~~' || attr_3 || '~~~') ELSE '' END),
                '~'
            ),
            '~~~'
        )
    )


Итак, что же здесь происходит? Начнем с внутренней части.

1. Для значения каждого из полей attr_1, attr_2, attr_3 мы создаем строку вида НазваниеПоля~~~ЗначениеПоля~~~ или просто пустую строку, если значение поля NULL. Все эти строки объединяем в одну результрующую.

2. Далее нам требуется избавиться от ~ в конце объединенной строки. В этом нам помогает rtrim.

3. Потом из объединенной строки мы создаем массив, разбивая строку по разделителю ~~~.

4. И, наконец, полученный массив передаем в метод hstore. Готово.

Отдельно замечу, что разделителем выбран ~~~ потому, что встретить его в значениях полей attr_1, attr_2, attr_3 невозможно. Если в ваших данных может встречаться такой набор символов - используйте другой разделитель из более "странных" символов.

С использованием php можно создать такой скрипт генерации и выполнения запроса:

$fields = [
    'attr_1',
    'attr_2',
    'attr_3',
    // еще поля
];
$glue = '~~~';
$selectPattern = "(CASE WHEN (%s IS NOT NULL) THEN ('%s' || '$glue' || %s || '$glue') ELSE '' END)";
$select = [];
foreach ($fields as $field) {
    $select[] = vsprintf($selectPattern, array_fill(0, 3, $field);
}
$select = implode(' || ', $select);

$this->runSql("
    UPDATE {$table}
    SET attrs = hstore(string_to_array(rtrim(({$select}), '~'), '{$glue}'))
");


По ссылке - улучшенный гист с кодом, обрабатывающим даже поле типа datetime.