Привет, Хабр!
Сегодня рассмотрим PL/V8. Это расширение PostgreSQL, которое позволяет использовать JavaScript через движок V8 (тот самый, который стоит за Node.js). Это открывает массу возможностей:
Писать хранимые процедуры на JavaScript.
Выполнять сложную бизнес-логику прямо в базе данных.
Создавать триггеры, которые обрабатывают данные.
Звучит круто, но сразу уточним, где это применимо:
Когда вы работаете с JSON-структурами.
Если нужна динамическая обработка данных.
Когда хочется упростить и ускорить логику, не гоняя данные между базой и приложением.
Установим:
На Ubuntu/Debian:
sudo apt update
sudo apt install postgresql-plv8
Теперь подключаем расширение в базе:
CREATE EXTENSION plv8;
Проверим, всё ли работает:
SELECT plv8_version();
Если видите номер версии (например, 3.2.0
), значит, всё окей.
Для macOS/Linux: если нужно всё делать своими руками (или на системе нет готовых пакетов), PL/V8 можно собрать из исходников:
Скачиваем код:
git clone https://github.com/plv8/plv8
cd plv8
Устанавливаем зависимости:
sudo apt install build-essential cmake libstdc++-12-dev
Сборка:
make
sudo make install
После установки можно настроить некоторые параметры в postgresql.conf
:
plv8.execution_timeout
— время выполнения JS-кода в секундах. Значение по дефолту 300 секунд.plv8.memory_limit
— лимит памяти для JavaScript (в МБ). По умолчанию 256 МБ.
Пример настройки:
SET plv8.execution_timeout = 10; -- максимум 10 секунд
SET plv8.memory_limit = 128; -- максимум 128 МБ
Применение
Работа с JSON
Допустим, есть массивы ключей и значений, которые нужно объединить в JSON. Вот как это сделать:
CREATE FUNCTION json_from_arrays(keys TEXT[], values TEXT[]) RETURNS JSON AS $$
var result = {};
for (var i = 0; i < keys.length; i++) {
result[keys[i]] = values[i];
}
return result;
$$ LANGUAGE plv8 IMMUTABLE STRICT;
-- Проверим:
SELECT json_from_arrays(ARRAY['name', 'age'], ARRAY['Roman', '30']);
Результат:
{"name": "Roman", "age": "30"}
Подсчёт суммы в массиве
Пусть есть массив чисел, и нужно быстро посчитать их сумму:
CREATE FUNCTION array_sum(numbers plv8_int4array) RETURNS INT AS $$
var sum = 0;
for (var i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
$$ LANGUAGE plv8 IMMUTABLE STRICT;
-- Тестируем:
SELECT array_sum(ARRAY[1, 2, 3, 4, 5]); -- 15
Триггер для логирования изменений
Допустим, нужно логировать изменения в таблице. PL/V8 позволяет реализовать эту логику:
CREATE FUNCTION log_changes() RETURNS TRIGGER AS $$
plv8.elog(NOTICE, "Row changed: ", JSON.stringify(NEW));
return NEW;
$$ LANGUAGE plv8;
CREATE TRIGGER log_trigger
AFTER INSERT OR UPDATE
ON your_table
FOR EACH ROW
EXECUTE FUNCTION log_changes();
Теперь каждое изменение в таблице будет выводиться в лог PostgreSQL.
SPI и подготовленные запросы
PL/V8 имеет интерфейсы для работы с SQL-запросами. Пример использования подготовленных запросов:
CREATE FUNCTION find_users_by_age(age INT) RETURNS JSON AS $$
var plan = plv8.prepare("SELECT id, name FROM users WHERE age = $1", ['int']);
var result = plan.execute([age]);
plan.free();
return result;
$$ LANGUAGE plv8;
-- Используем:
SELECT find_users_by_age(30);
Это ускоряет выполнение часто используемых запросов, так как они заранее компилируются PostgreSQL.
Обработка данных с транзакциями
Предположим, есть таблица заказов, и нужно в одной транзакции обновить статусы всех заказов, а также записать лог об этом:
CREATE FUNCTION process_orders() RETURNS VOID AS $$
plv8.subtransaction(function () {
// Обновляем статусы заказов
plv8.execute("UPDATE orders SET status = 'processed' WHERE status = 'pending'");
// Логируем изменения
var log_message = "Processed orders at " + new Date().toISOString();
plv8.execute("INSERT INTO logs (message) VALUES ($1)", [log_message]);
});
$$ LANGUAGE plv8;
-- Запуск функции
SELECT process_orders();
Если одна из операций внутри транзакции не выполнится (например, ошибка в INSERT INTO logs
), все изменения будут откатаны.
Генерация отчёта с использованием JavaScript
Допустим, нужно сгенерировать отчёт в формате JSON, который объединяет данные из нескольких таблиц. PL/V8 позволяет это сделать без привлечения прочих инструментов:
CREATE FUNCTION generate_report() RETURNS JSON AS $$
// Получаем данные о пользователях
var users = plv8.execute("SELECT id, name FROM users");
// Получаем данные о заказах
var orders = plv8.execute("SELECT user_id, amount FROM orders");
// Объединяем данные
var report = {};
users.forEach(function(user) {
report[user.name] = orders
.filter(order => order.user_id === user.id)
.map(order => order.amount);
});
return report;
$$ LANGUAGE plv8;
-- Генерация отчёта
SELECT generate_report();
Прочие нюансы
-
Каждый вызов функции PL/V8 изолирован, но можно сохранять глобальные данные в объекте
plv8.global
.CREATE FUNCTION set_global_var(val TEXT) RETURNS VOID AS $$ plv8.global.my_var = val; $$ LANGUAGE plv8; CREATE FUNCTION get_global_var() RETURNS TEXT AS $$ return plv8.global.my_var || ' is global!'; $$ LANGUAGE plv8; -- Устанавливаем значение: SELECT set_global_var('Hello'); SELECT get_global_var(); -- "Hello is global!"
-
Избегайте использования
eval()
и динамических SQL-запросов. Лушче использовать параметры вместо конкатенации строк:plv8.elog(NOTICE, "Debug info: ", some_variable);
-
Для отладки используйте
plv8.elog
:plv8.elog(NOTICE, "Debug info: ", some_variable);
-
PL/V8 поддерживает выполнение JavaScript-кода на лету с помощью
plv8.run_script
. Полезно для выполнения динамических операций:DO $$ plv8.run_script('globalThis.dynamicVar = "I am dynamic!"'); plv8.elog(NOTICE, globalThis.dynamicVar); $$ LANGUAGE plv8;
Более подробно с библиотекой можно ознакомиться здесь.
В заключение рекомендую всем желающим обратить внимание на следующие открытые уроки:
10 февраля: «Почему ClickHouse становится выбором №1 при импортозамещении: сравнение с другими NoSQL решениями и практический кейс». Подробнее
11 февраля: «Запуск СУБД (MySQL, PostgreSQL) в Docker». Подробнее
Список всех бесплатных уроков по аналитике и анализу, а также по другим IT-направлениям, можно посмотреть в календаре.
Sabirman
>>
var plan = plv8.prepare("SELECT id, name FROM users WHERE age = $1", ['int']);
Выглядит, как будто план каждый раз новый создается.