При разработке большого приложения, бизнес-логика которого реализована на базе хранимых процедур, часто возникает вопрос о том, как лучше хранить и использовать константы в приложении. Ниже рассматриваются 4 различных подхода к решению этого вопроса:
Под этим подходом понимается использование значений, жестко закодированных в код хранимок.
Пример
...
SELECT * FROM customers WHERE state = 1;
...
В данном примере производится фильтрация клиентов по полю state, которое имеет значение 1. 1 - это, вероятнее всего, какое-то определенное состояние клиента. И клиентов с таким состоянием будут обработаны ниже в коде хранимой процедуры.
Данный подход не требует каких-то дополнительных трудозатрат и интуитивно является самым быстрым решением.
Об использовании магических значений уже много написано книг и статей. Магические значения плохо влияют на поддерживаемость проекта в целом.
Константы приложения при этом подходе хранятся в настройках СУБД в файле postgresql.conf. Доступ к константам осуществляется с помощью существующих стандартных функций.
...
SELECT * FROM customers WHERE state = current_setting('myapp.active_state')::int;
...
Данный подход является отличным способом использования уже существующего функционала. Он гарантирует высокую производительность кода и отсутствие сложно воспроизводимых ошибок.
Как плюсом, так минусом можно назвать отсутствие легкого введение новых констант в приложение. Так как для того, чтобы создать константу, надо отредактировать файл postgresql.conf и перезагрузить настройки сервера.
В этом случае на каждую константу создаётся хранимая процедура, которая просто банально возвращает значение этой константы. Доступ к константе в этом случае осуществляется через вызов хранимой процедуры.
CREATE OR REPLACE FUNCTION active_state() RETURNS int AS
$$
SELECT 1::int
$$ LANGUAGE sql IMMUTABLE;
SELECT * FROM customers WHERE state = active_state();
С точки зрения разработчика этот подход является очень простым и понятным, а так же гарантируется хорошую скорость выполнения.
Этот способ организации доступа к константам приложения с точки зрения его поддержки (просмотр, изменения, удаление) является сложным в обслуживании, так как требует использования нестандартного специализированного функционала (к примеру, для генерации хранимой процедуры, которая будет возвращать значение новой константы).
Для хранения констант используется простая таблица (вида ключ-значение).
CREATE TABLE constants (
key varchar PRIMARY KEY,
value varchar
);
а для доступа к этим константам используются хранимые процедуры.
CREATE OR REPLACE FUNCTION get_constant(_key varchar) RETURNS varchar AS
$$
SELECT value FROM constants WHERE key = _key;
$$ LANGUAGE sql IMMUTABLE;
SELECT * FROM customers WHERE state = get_constant('active.state')::int;
Данный подход является самым легким в поддержке - константы изменяются, создаются и их можно все просмотреть в одном месте.
Необходимо создать хоть и простой, но специализированный функционал для поддержки данного способа.
Чтобы окончательно сделать выбор необходимо иметь представление о производительности, которую даёт каждый из вышеописанных способов.
Скрипт с необходимыми тестами.
-- NULL
CREATE OR REPLACE FUNCTION test_null_performance() RETURNS void AS
$$
DECLARE
_i int;
_value varchar;
_key varchar;
BEGIN
_key := 'myapp.var1';
FOR _i IN SELECT generate_series(1, 10000000)
LOOP
NULL;
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
SELECT test_null_performance();
-- Test inline constants
CREATE OR REPLACE FUNCTION test_constants_inline_performance() RETURNS void AS
$$
DECLARE
_i int;
_value varchar;
_key varchar;
BEGIN
_key := 'myapp.var1';
FOR _i IN SELECT generate_series(1, 10000000)
LOOP
_value := 'value1';
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
SELECT test_constants_inline_performance();
-- Test setting constants
CREATE OR REPLACE FUNCTION test_constants_settings_performance() RETURNS void AS
$$
DECLARE
_i int;
_value varchar;
_key varchar;
BEGIN
_key := 'myapp.var1';
FOR _i IN SELECT generate_series(1, 10000000)
LOOP
_value := current_setting(_key);
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
SELECT test_constants_settings_performance();
-- Test immutable function constants
CREATE OR REPLACE FUNCTION get_constant_value() RETURNS varchar AS
$$
SELECT 'value1'::varchar
$$ LANGUAGE sql IMMUTABLE;
CREATE OR REPLACE FUNCTION test_constants_function_performance() RETURNS void AS
$$
DECLARE
_i int;
_value varchar;
BEGIN
FOR _i IN SELECT generate_series(1, 10000000)
LOOP
_value := get_constant_value();
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
SELECT test_constants_function_performance();
-- Test stable function constants
CREATE OR REPLACE FUNCTION get_constant_stable_value() RETURNS varchar AS
$$
SELECT 'value1'::varchar
$$ LANGUAGE sql STABLE;
CREATE OR REPLACE FUNCTION test_constants_stable_function_performance() RETURNS void AS
$$
DECLARE
_i int;
_value varchar;
BEGIN
FOR _i IN SELECT generate_series(1, 10000000)
LOOP
_value := get_constant_stable_value();
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
SELECT test_constants_stable_function_performance();
-- Test table constants
CREATE TABLE constants (
key varchar PRIMARY KEY,
value varchar
);
INSERT INTO constants(key, value) VALUES('myapp.var1', 'value1');
CREATE OR REPLACE FUNCTION get_constant_value(_key varchar) RETURNS varchar AS
$$
SELECT value FROM constants WHERE key = _key;
$$ LANGUAGE sql IMMUTABLE;
CREATE OR REPLACE FUNCTION test_constants_table_performance() RETURNS void AS
$$
DECLARE
_i int;
_value varchar;
BEGIN
FOR _i IN SELECT generate_series(1, 10000000)
LOOP
_value := get_constant_value('myapp.var1');
END LOOP;
END
$$ LANGUAGE plpgsql VOLATILE;
SELECT test_constants_table_performance();
denis=# \timing on
Timing is on.
denis=# SELECT test_constants_inline_performance();
test_constants_inline_performance
-----------------------------------
(1 row)
Time: 4168.470 ms
denis=# SELECT test_constants_settings_performance();
test_constants_settings_performance
-------------------------------------
(1 row)
Time: 6251.461 ms
denis=# SELECT test_constants_function_performance();
test_constants_function_performance
-------------------------------------
(1 row)
Time: 2733.908 ms
denis=# SELECT test_constants_stable_function_performance();
test_constants_stable_function_performance
--------------------------------------------
(1 row)
Time: 2750.599 ms
denis=# SELECT test_constants_table_performance();
test_constants_table_performance
----------------------------------
(1 row)
Time: 2702.855 ms
denis=# SELECT test_null_performance();
test_null_performance
-----------------------
(1 row)
Time: 1135.609 ms
На данном синтетическом тесте неожиданно производительность кода при использовании магических значений оказалась не самой высокой. Использование настроек по производительности в два раза проиграли лидерам тестирования. Одинаково высокие результаты показали: хранение констант в хранимых процедурах и функционал с хранением констант в таблицах.
Возможно, магические значения проиграли другим способам из-за какого-то кеширования внутри PL/PgSQL движка, но в целом результаты показывают, что все способы обладают достаточной скоростью и выбор конкретного способа должен осуществляться исходя из других критериев (лёгкости поддержки, к примеру)