Для приготовления виджета Государственного Адресного Реестра сначала нужно его (ГАР) загрузить. При инициализации базы были созданы не только таблицы для загрузки в них ГАР, но также и таблица и функции для виджета. В этой статье остановимся на них подробнее.

Итак, для виджета будем использовать следующую иерархическую таблицу (и индексы), в которую поместим все актуальные данные по всем регионам:

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)


  1. centralhardware
    25.10.2021 08:16
    +5

    А это точно статья, а не выдержка рандомногом месте в методичке?


  1. Arashi5
    25.10.2021 10:34

    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 (
                         ...
                         ...
    										 ...
             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$;

    И как вот это вот все поддерживать, масштабировать и передовать потомкам?


    1. Arashi5
      25.10.2021 10:37

      передАвать.
      Простите, захлестнули эмоции.


      1. DeathSAAD
        25.10.2021 19:35

        Да нее, такое только передОвать можно)

        У самого на проекте такая жуть. Когда открываешь на правку, то хочется забиться в угол и заплакать как маленькая девочка.


        1. Arashi5
          26.10.2021 08:36

          Как мне кажется, в каждом проекте есть то, за что стыдно. И это нормально)
          Просто серия статей, плюс минус ок, хотя несколько и оторванна от современных реалий, но имеет место быть.
          Но.
          Жесть в реализации.


    1. RekGRpth Автор
      26.10.2021 10:57

      мне тоже очень не нравится, как эта функция выглядит

      масштабировать

      если одна база не будет справляться - можно поднять кучу реплик standby и пустить на них кучу nginx-ов, т.к. всё равно используются запросы только на чтение


      1. Arashi5
        26.10.2021 11:19

        Я, конечно, не знаю всех задач, инфраструткры, которая у вас есть. Но как мне кажется, выгоднее было бы завести редиску с дозаправкой или аналогичный ки-вэлью.