Для приготовления виджета Государственного Адресного Реестра сначала нужно его (ГАР) загрузить. При инициализации базы были созданы не только таблицы для загрузки в них ГАР, но также и таблица и функции для виджета. В этой статье остановимся на них подробнее.
Итак, для виджета будем использовать следующую иерархическую таблицу (и индексы), в которую поместим все актуальные данные по всем регионам:
CREATE TABLE IF NOT EXISTS gar ( -- создаём таблицу если её ещё нет
-- первичный ключ
id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
parent uuid, -- родитель
name text NOT NULL, -- наименование
short text NOT NULL, -- краткий тип
type text NOT NULL, -- полный тип
post text, -- почтовый индекс
region smallint NOT NULL -- код региона
);
CREATE INDEX IF NOT EXISTS gar_parent_idx ON gar USING btree (parent);
CREATE INDEX IF NOT EXISTS gar_name_idx ON gar USING btree (name);
CREATE INDEX IF NOT EXISTS gar_short_idx ON gar USING btree (short);
CREATE INDEX IF NOT EXISTS gar_type_idx ON gar USING btree (type);
CREATE INDEX IF NOT EXISTS gar_region_idx ON gar USING btree (region);
Далее определим вспомогательную функцию, которую будем использовать для получения полного наименования
-- создаём или меняем функцию от наименования, краткого и полного типа
CREATE OR REPLACE FUNCTION gar_text(name text, short text, type text)
RETURNS text -- возвращающую полное наименование
LANGUAGE sql -- использующую язык sql
IMMUTABLE AS $body$ -- зависящую только от своих параметров
select case -- в случае
-- когда полный тип не определён, возвратить наименование
when gar_text.type in ('Не определено') then gar_text.name
-- когда полный тип - Чувашия, возвратить наименование и полный тип
when gar_text.type in ('Чувашия') then gar_text.name||' '||gar_text.type
-- когда в наименовании содержится полный тип, возвратить только наименование
when gar_text.name ilike '%'||gar_text.type||'%' then gar_text.name
-- иначе возвратить краткий тип точка наименование
else gar_text.short||'.'||gar_text.name
end;
$body$;
Также создадим представление с добавлением этой функции в качестве колонки
CREATE OR REPLACE VIEW gar_view AS -- создаём или меняем представление
SELECT gar.*, -- выбирая всё
gar_text(name, short, type) AS text -- добавляя полное наименование
from gar; -- из ГАРа
Далее определим вспомогательную функцию для подсчёта количества дочерних элементов
-- создаём или меняем функцию от идентификатора
CREATE OR REPLACE FUNCTION gar_child(id uuid)
-- возвращающую количесвтво его дочерних элементов
RETURNS bigint LANGUAGE sql STABLE AS $body$
select count(1) from gar where parent = gar_child.id;
$body$;
Также, вспомогательную функцию для выбора, если задан массив идентификаторов
-- создаём или меняем функцию от массива идентификаторов
CREATE OR REPLACE FUNCTION gar_select(id uuid[])
RETURNS SETOF gar_view -- возвращающую набор строк из представления
LANGUAGE sql STABLE AS $body$
-- выбираем всё, в т.ч. и полное наименование
select gar.*, gar_text(name, short, type) AS text from gar
-- в указанном агрументом функции порядке
inner join (select unnest(gar_select.id) as id,
generate_subscripts(gar_select.id, 1) as i) as _ on _.id = gar.id
-- для указанных в аргументе функции идентификаторов
where gar.id = any(gar_select.id) order by i;
$body$;
И ещё вспомогательную функцию для рекурсивного выбора вверх родителей указанного идентификатора, ограничиваясь указанным родительским идентификатором
CREATE OR REPLACE FUNCTION gar_select( -- создаём или меняем функцию от
id uuid, -- идентификатора
parent uuid DEFAULT NULL -- и необязательного родительского идентификатора
) RETURNS SETOF gar_view -- возвращающую набор строк представления
LANGUAGE sql STABLE AS $body$
with recursive _ as ( -- рекурсивно
-- сначала выбирая по указанному идентификатору
select gar.*, 0 as i from gar where id = gar_select.id
union
-- а потом выбирая родителей вверх до указанного родителя или до самого верха
select gar.*, _.i + 1 as i from gar inner join _ on (_.parent = gar.id)
where gar_select.parent is null or _.parent != gar_select.parent
) select id, parent, name, short, type, post, region, gar_text(name, short, type) AS text
from _ order by i desc;
$body$;
Также вспомогательную функцию для поиска
CREATE OR REPLACE FUNCTION gar_select( -- создаём или меняем функцию
parent uuid, -- от родителя
name text, -- наименования
short text, -- краткого типа
type text, -- полного типа
post text, -- почтового индекса
region text -- кода региона
) RETURNS SETOF gar_view -- возвращающую набор строк представления
LANGUAGE sql STABLE AS $body$
-- выбираем всё, где
select gar.*, gar_text(gar.name, gar.short, gar.type) AS text from gar where true
-- если не задан родитель то без родителя, а если задан - то по нему
and ((gar_select.parent is null and parent is null) or parent = gar_select.parent)
-- если задано наимнование, то ищем по нему сначала или после пробела или после дефиса или после точки
and (gar_select.name is null or name ilike gar_select.name||'%' or name ilike '% '||gar_select.name||'%' or name ilike '%-'||gar_select.name||'%' or name ilike '%.'||gar_select.name||'%')
-- если задан кратки тип, то ищем по нему
and (gar_select.short is null or short ilike gar_select.short)
-- если задан полный тип и это массив - то учитываем все элементы массива, а если не массив, то сначала
and (gar_select.type is null or case when gar_select.type ilike '{%}' then type = any(gar_select.type::text[]) else type ilike gar_select.type||'%' end)
-- если задан почтовый индекс, то ищем по нему сначала
and (gar_select.post is null or post ilike gar_select.post||'%')
-- если задан код региона и это массив - то учитываем все элементы массива, а если не массив - то ищем по нему
and (gar_select.region is null or case when gar_select.region ilike '{%}' then region = any(gar_select.region::smallint[]) else region = gar_select.region::smallint end)
-- сортируем сначала по численному наименованию, а потом по наименованию
order by to_number('0'||name, '999999999'), name;
$body$;
И ещё вспомогательную функцию для поиска родителей
-- создаём или меняем функцию от родителя, наименования, ...
CREATE OR REPLACE FUNCTION gar_select_parent(parent uuid, name text, short text, type text, post text, region text) RETURNS SETOF gar_view LANGUAGE sql STABLE AS $body$
-- выбираем всё
select gar.*, gar_text(gar.name, gar.short, gar.type) AS text from gar
-- где полный тип из указанного массива
where type = any(gar_select_parent.type::text[])
-- и если заданно наименование , то ищем по нему или после пробела или после дефиса или после точки
and (gar_select_parent.name is null or name ilike gar_select_parent.name||'%' or name ilike '% '||gar_select_parent.name||'%' or name ilike '%-'||gar_select_parent.name||'%' or name ilike '%.'||gar_select_parent.name||'%')
-- сортируя по глубине вложенности
order by (select count(id) from gar_select(id, gar_select_parent.parent)), to_number('0'||name, '999999999'), name;
$body$;
Ну и теперь, главную функцию для виджета
-- создаём или меняем функцию от json, возвращающую json
CREATE OR REPLACE FUNCTION gar_select(INOUT json json) RETURNS json LANGUAGE plpgsql STABLE AS $body$ <<local>> declare
id text default nullif(trim(gar_select.json->>'id'), ''); -- уид
parent text default nullif(trim(gar_select.json->>'parent'), ''); -- уид родителя
name text default nullif(trim(gar_select.json->>'name'), ''); -- наименование
short text default nullif(trim(gar_select.json->>'short'), ''); -- кратко
type text default nullif(trim(gar_select.json->>'type'), ''); -- тип
post text default nullif(trim(gar_select.json->>'port'), ''); -- индекс
region text default nullif(trim(gar_select.json->>'region'), ''); -- регион
text text default nullif(trim(gar_select.json->>'text'), ''); -- строка поиска
offset int default coalesce(nullif(trim(gar_select.json->>'offset'), '')::int, 0); -- офсет
limit int default coalesce(nullif(trim(gar_select.json->>'limit'), '')::int, 10); -- лимит
"full" boolean default coalesce(nullif(trim(gar_select.json->>'full'), '')::boolean, false); -- все?
child boolean default coalesce(nullif(trim(gar_select.json->>'child'), '')::boolean, false); -- дети?
begin
if local.id is not null then -- если задан id
local.id = translate(local.id, '[]','{}');
if local.id ilike '{%}' then -- если id - массив
if local.full then -- если все результаты
with _ as (
with _ as (
select * from gar_select(local.id::uuid[])
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select * from _ offset local.offset limit local.limit
) select coalesce(json_agg((select json_agg(_) from (
select *, case when local.child then gar_child(_.id) end as child from gar_select(_.id::uuid)
) as _)), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict gar_select.json;
else -- иначе - не все результаты
with _ as (
with _ as (
select * from gar_select(local.id::uuid[])
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select *, case when local.child then gar_child(_.id) end as child from _ offset local.offset limit local.limit
) select coalesce(json_agg(_), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict gar_select.json;
end if;
else -- иначе id - не массив
if local.full then -- если все результаты
with _ as (
with _ as (
select * from gar_select(local.id::uuid)
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select * from _ offset local.offset limit local.limit
) select coalesce(json_agg((select json_agg(_) from (
select *, case when local.child then gar_child(_.id) end as child from gar_select(_.id::uuid)
) as _)), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict gar_select.json;
else -- иначе - не все результаты
with _ as (
with _ as (
select * from gar_select(local.id::uuid)
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select *, case when local.child then gar_child(_.id) end as child from _ offset local.offset limit local.limit
) select coalesce(json_agg(_), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict gar_select.json;
end if;
end if;
else -- иначе - не задан id
if local.text is not null then -- если искать что-то
local.name = local.text;
local.short = split_part(local.name, '.', 1);
if local.short = local.name or position(' ' in local.short) > 0 or position(',' in local.short) > 0 then
local.short = null;
else
local.name = split_part(local.name, '.', 2);
end if;
local.name = ltrim(local.name, ' ');
end if;
if local.text is not null and local.parent is null then -- если искать что-то и родитель не задан
with _ as (
with _ as (
select * from gar_select_parent(local.parent::uuid, local.name, local.short, array['Город', 'Поселок', 'Поселение', 'Деревня', 'Населенный пункт', 'Село', 'Рабочий поселок', 'Поселок городского типа']::text, local.post, local.region)
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select * from _ offset local.offset limit local.limit
) select coalesce(json_agg((select json_agg(_) from (
select * from gar_select(_.id, local.parent::uuid)
) as _)), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict json;
else
local.type = translate(local.type, '[]','{}');
if local.full then -- если все результаты
with _ as (
with _ as (
select * from gar_select(local.parent::uuid, local.name, local.short, local.type, local.post, local.region)
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select * from _ offset local.offset limit local.limit
) select coalesce(json_agg((select json_agg(_) from (
select *, case when local.child then gar_child(_.id) end as child from gar_select(_.id::uuid)
) as _)), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict gar_select.json;
else -- иначе - не все результаты
with _ as (
with _ as (
select * from gar_select(local.parent::uuid, local.name, local.short, local.type, local.post, local.region)
) select count(1), gar_select.json as query, local.offset, local.limit, (
with _ as (
select *, case when local.child then gar_child(_.id) end as child from _ offset local.offset limit local.limit
) select coalesce(json_agg(_), '[]'::json) from _
) as data from _
) select to_json(_) from _ into strict gar_select.json;
end if;
end if;
end if;
end;$body$;
Комментарии (7)
Arashi5
25.10.2021 10:34if local.id is not null then -- если задан id local.id = translate(local.id, '[]','{}'); if local.id ilike '{%}' then -- если id - массив if local.full then -- если все результаты with _ as ( with _ as ( select * from gar_select(local.id::uuid[]) ) select count(1), gar_select.json as query, local.offset, local.limit, ( with _ as ( ... ... ... with _ as ( with _ as ( select * from gar_select(local.parent::uuid, local.name, local.short, local.type, local.post, local.region) ) select count(1), gar_select.json as query, local.offset, local.limit, ( with _ as ( select *, case when local.child then gar_child(_.id) end as child from _ offset local.offset limit local.limit ) select coalesce(json_agg(_), '[]'::json) from _ ) as data from _ ) select to_json(_) from _ into strict gar_select.json; end if; end if; end if; end;$body$;
И как вот это вот все поддерживать, масштабировать и передовать потомкам?
Arashi5
25.10.2021 10:37передАвать.
Простите, захлестнули эмоции.DeathSAAD
25.10.2021 19:35Да нее, такое только передОвать можно)
У самого на проекте такая жуть. Когда открываешь на правку, то хочется забиться в угол и заплакать как маленькая девочка.Arashi5
26.10.2021 08:36Как мне кажется, в каждом проекте есть то, за что стыдно. И это нормально)
Просто серия статей, плюс минус ок, хотя несколько и оторванна от современных реалий, но имеет место быть.
Но.
Жесть в реализации.
RekGRpth Автор
26.10.2021 10:57мне тоже очень не нравится, как эта функция выглядит
масштабировать
если одна база не будет справляться - можно поднять кучу реплик standby и пустить на них кучу nginx-ов, т.к. всё равно используются запросы только на чтение
Arashi5
26.10.2021 11:19Я, конечно, не знаю всех задач, инфраструткры, которая у вас есть. Но как мне кажется, выгоднее было бы завести редиску с дозаправкой или аналогичный ки-вэлью.
centralhardware
А это точно статья, а не выдержка рандомногом месте в методичке?