Данная статья основана на реальных событиях,
и все проблемы в ней не вымышленные. (С)
В начале хотелось бы отметить, что статья не призвана показать изобретение велосипеда, потому как многие приёмы уже давно существуют в культуре разработки баз данных. Однако обобщить, проанализировать проблемы, которые они могут решить и показать, как с ними можно работать. А проблем хватает несмотря на то, что нормативно-справочная информация (НСИ) не относится к бизнес-логике, а скорее находится в обслуживании у неё. Стандартный процесс по рисованию очередной таблички для хранения справочника очень скоро начинает обрастать костылями или трудоёмкими переделками.
Вот и в моём случае оказалась та же картина — система стоит на продуктиве более десяти лет, строилась по тому же принципу, если что нужно, рисуем и включаем в оборот. Таким образом были созданы несколько таблиц для хранения разного рода оборудования. Но вот пришёл час Х, когда стало необходимо добавить ещё пару таблиц для нового оборудования и при этом все (включая старые) должны входить в определённую группу. Это значит, что ссылки на разные таблицы должны быть включены в кросс-таблицу между группой и всеми пятью видами оборудования, то есть для каждого своё поля с констреинтом на соответствующую таблицу. А если ещё одно добавится, менять структуру. И обработку нужно делать в зависимости от того, какие поля заполнены. Вот и возникает первая проблема, как разные таблицы обобщить, что бы с ними одинаково можно было работать и не менять структуру, если добавляется ещё одна. Замечательная мысль, создаём отдельную табличку, которая призвана хранить абстрактное понятие оборудование с указанием типа, а тогда остальные таблички ссылаются по внешнему ключу на своего родителя. На этой радостной волне мы заливаем в созданную табличку записи из одной и пытаемся тоже сделать для другой. Но что-то пошло не так, сработало ограничение первичного ключа, к чему бы это? А к тому, что на заре бурной молодости системы для каждой табличке б��ли свои сиквенсы. Конечно, со временем это безобразие поправили, но старые ключи всё равно остались. Более того, они корнями проросли по внешним ключам с другими таблицам. Фиксируем вторую проблему, связанную со сквозной нумерацией всех справочников.
На этом мучения с таблицами оборудования не закончились. Потому как по последним требованиям оборудование имеет различные характеристики, более того их число переменно, а одна характеристика может иметь несколько значений. А значит появляется третья проблема, а именно иметь возможность хранить переменное число характеристик какой-то записи.
Вроде как с этим справились, но заказчик не дремлет, у него всегда есть наготове что-нибудь новенькое. И вот приходит требование — все справочники историчные (например, название продукта было одним, а потом его переименовали, и по документам на разные даты нужно показывать актуальное название). Само по себе требование нормальное, ничего не скажешь. А если ещё в отделе разработки есть кто-то, кто проходит испытательный срок, так вообще всё в шоколаде, можно и не заметить, что это проблема. Однако проходит всё, как обычно — с полным авралом, а тут ещё этим нужно заниматься. Создаём таблички, дублирующие таблицы соответствующих справочников для того, чтобы там хранить хронологию изменений справочника. Но, создавая эти таблицы, мы заодно создаём себе четвёртую проблему, теперь в коде нужно в зависимости от даты обращаться то ли к основной таблице, то ли к исторической.
Ну мы же молодцы, мы и это победили))) Теперь, попивая чай из своей кружки, начинаешь дискутировать с другими коллегами на тему, что им приходилось решать, и понимаешь, что список проблем пополняется. В обсуждении стоит вопрос как хранить версии одной и той же записи. Хочу оговорится, что версия, это не то, что укладывается в таблицу историчности. В историчности понятно, до такого-то числа было одно название, а начиная с этой даты актуальным становится другое. А в версионности подразумевается, что запись была сначала сохранена с ошибкой, а через несколько часов это поняли и её изменили, и нужно знать все состояния этой записи. Во-первых, здесь должно быть дробление на время, не только сутки. А во-вторых, такие следы нужны в случае разборок. Например, заполняли прайс, ошиблись, успели товар продать по такой цене, а потом поправили, но в конце дня случился дебаланс. Однако решение для таких ситуаций меня лично напрягло, предлагалась все такие изменения хранить в самой таблице. Не буду устраивать холивар на сколько так правильно, но для меня точно обозначилась пятая проблема, а именно хранение изменений записей.
Итак, обобщая вышесказанное мы видим перед собой пять увесистых грабель. Теперь наша задача определить стратегию, позволяющую обойти и не наступить на них.
Начиная проектировать систему с нуля, никто не может предугадать путь её развития, а значит не сможет сказать на каком уровне придётся обобщать, как в описанном примере с оборудованием. Поэтому имеет смысл сразу задать абстрактную сущность, распространяемую на все таблицы НСИ. Таким образом все справочники будут иметь прообраз в едином справочнике с разделением на типы.
Таблица nsi_type системная, заполняется по мере добавления новых справочников. Таблица nsi хранит ключи и системные поля. Заодно создаём собственный сиквенс и тем самым закрываем вторую проблему.
Так же создадим пакет, содержащий основную функциональ��ость по работе со справочниками и будем его постепенно заполнять.
Здесь пока представлены вспомогательные функции для обеспечения необходимой инфраструктуры.
Итак стоит задача создать справочник организаций, куда же без него, любое предприятие контактирует со сторонними организациями — это и поставщики, и клиенты, и партнёры. Сразу добавим соответствующий тип в таблицу nsi_type и определим таблицу nsi_organization.
Теперь, пока не поздно, нужно вспомнить про грабли с номером «пять». Если начнём добавлять записи в созданную таблицу организаций, то это событие нужно где-то фиксировать.
А так же в пакет добавлена функция логирования.
Таким образом разрешена пятая проблема, теперь для любой записи НСИ можно посмотреть, что с ней происходило.
Пытаемся добавить туда организацию.
Конечно мы нарвёмся на констраинт nsi_organization_nsi_fk. Поэтому все справочные таблицы должны быть снабжены необходимой доработкой триггеров.
А теперь добавление записи пройдёт без проблем (ключ уже указывать не надо). Заодно в таблице nsi появится первая запись, а так же в таблице логирования будет зафиксировано это событие.
Но пока можно заметить только дополнительные расходы на создание таблицы какого-то справочника, а никак не преимущество единого подхода. Тогда вспомним про четвёртую проблему — нам необходимо хранить историчность данных в таблицах справочника, а так же извлекать актуальное состояние на заданную дату.
В пакет pkg_nsi добавим функцию сохранения записи в историческую таблицу. Хранить запись будем в формате json, поэтому в пакете так же появится возможность получить json для переданного запроса.
Таким образом любой справочник может воспользоваться этой функцией, чтобы увести в историю текущее состояние. Уже хорошо, хоть что-то полезное появилось от такого обобщения))) Для извлечения актуального состояния справочника добавим в пакет соответствующую pipeline-функцию. Записи справочника будут возвращаться в тип, расширенный системными полями.
Применим к нашей таблице nsi_organization.
Функция nsi_organization_table очень полезна, потому как удовлетворяет нашим требованиям и окончательно уводит проблему номер четыре в прошлое.
Идём дальше. Раз у нас появилось такое преимущество с введением единого подхода для работы со всеми справочниками, то воспользуемся им и для хранения дополнительной информации, которой может быть наделена любая запись из любого справочника. Такое механизм уже давно существует, называется EAV-pattern, его и реализуем.
Очень часто в контексте документов имена собственные необходимо использовать в каком-то падеже, поэтому создадим новую таблицу с физическими лицами и по аналогии с организациями добавим обработку триггеров и тип для выборки.
Осталось дополнить пакет pkg_nsi обработкой этой таблицы.
И добавим кого-нибудь в эту таблицу.
Создадим атрибуты для самого востребованного родительного падежа.
В пакете pkg_nsi добавим функции для работы с атрибутами справочников.
Теперь присвоим соответствующие атрибуты.
Таким образом мы победим третью проблему.
Кроме таблиц справочников в системе НСИ также важны отношение между ними. Так, например крупные организации включают в себя различные подразделения, филиалы, отделы и т.п., которые можно выстроить в древовидную структуру. Для начала заведём в нашей системе ещё несколько организаций, которые будут в подчинении у уже существующей «Рога и копыта».
Теперь нужно показать в каком отношении эти организации находятся между собой. Для этого необходима таблица с древовидной структурой и указанием периода действия, потому как всё подвержено изменением во времени и нужно это учитывать.
Конечно, следует расширить возможности пакета pkg_nsi, чтобы можно было настраивать структуру для различных таблиц.
После появления такого инструмента можно смело выстраивать отношения между организациями.
Так как справочники отделены от структуры, то каждый раз обращаться к организациям с учётом их отношений становится грамозко, поэтому немного упростим себе жизнь.
То, что мы строим дерево это замечательно, но все узлы этого дерева относятся к одной сущности, а наша задача реализовать построение отношения между разными сущностями. Это тоже не проблема, потому как структура не завязывается на какой-то определённый справочник, а работает в целом на всей системе НСИ. Для примера построим классификатор для должностей государственной гражданской службы и классификатор для должностей муниципалитета.
Осталось только заполнить и собрать необходимые классификаторы.
Ой, как это не читабельно!
Следует не забывать, что кроме отношения включения (в том числе и древовидного), существует отношение пересечения, то есть кросс-таблиц. Здесь добавляется дополнительное условие проверки пересечения по времени.
Всё, теперь мы с уверенностью можем сказать, что закрыли первую проблему.
Конечно можно много чего пытаться прикрутить к этой системе, но я думаю, что поставленную задачу в начале статьи я выполнила, а остальное уже можно рассмотреть в процессе дискуссии.
Материал подготавливался на версии Oracle 18c, хотя нативное поддержание формата json уже присутствует в версии 12. Здесь ссылка с архивом скриптов.
и все проблемы в ней не вымышленные. (С)
В начале хотелось бы отметить, что статья не призвана показать изобретение велосипеда, потому как многие приёмы уже давно существуют в культуре разработки баз данных. Однако обобщить, проанализировать проблемы, которые они могут решить и показать, как с ними можно работать. А проблем хватает несмотря на то, что нормативно-справочная информация (НСИ) не относится к бизнес-логике, а скорее находится в обслуживании у неё. Стандартный процесс по рисованию очередной таблички для хранения справочника очень скоро начинает обрастать костылями или трудоёмкими переделками.
Вот и в моём случае оказалась та же картина — система стоит на продуктиве более десяти лет, строилась по тому же принципу, если что нужно, рисуем и включаем в оборот. Таким образом были созданы несколько таблиц для хранения разного рода оборудования. Но вот пришёл час Х, когда стало необходимо добавить ещё пару таблиц для нового оборудования и при этом все (включая старые) должны входить в определённую группу. Это значит, что ссылки на разные таблицы должны быть включены в кросс-таблицу между группой и всеми пятью видами оборудования, то есть для каждого своё поля с констреинтом на соответствующую таблицу. А если ещё одно добавится, менять структуру. И обработку нужно делать в зависимости от того, какие поля заполнены. Вот и возникает первая проблема, как разные таблицы обобщить, что бы с ними одинаково можно было работать и не менять структуру, если добавляется ещё одна. Замечательная мысль, создаём отдельную табличку, которая призвана хранить абстрактное понятие оборудование с указанием типа, а тогда остальные таблички ссылаются по внешнему ключу на своего родителя. На этой радостной волне мы заливаем в созданную табличку записи из одной и пытаемся тоже сделать для другой. Но что-то пошло не так, сработало ограничение первичного ключа, к чему бы это? А к тому, что на заре бурной молодости системы для каждой табличке б��ли свои сиквенсы. Конечно, со временем это безобразие поправили, но старые ключи всё равно остались. Более того, они корнями проросли по внешним ключам с другими таблицам. Фиксируем вторую проблему, связанную со сквозной нумерацией всех справочников.
На этом мучения с таблицами оборудования не закончились. Потому как по последним требованиям оборудование имеет различные характеристики, более того их число переменно, а одна характеристика может иметь несколько значений. А значит появляется третья проблема, а именно иметь возможность хранить переменное число характеристик какой-то записи.
Вроде как с этим справились, но заказчик не дремлет, у него всегда есть наготове что-нибудь новенькое. И вот приходит требование — все справочники историчные (например, название продукта было одним, а потом его переименовали, и по документам на разные даты нужно показывать актуальное название). Само по себе требование нормальное, ничего не скажешь. А если ещё в отделе разработки есть кто-то, кто проходит испытательный срок, так вообще всё в шоколаде, можно и не заметить, что это проблема. Однако проходит всё, как обычно — с полным авралом, а тут ещё этим нужно заниматься. Создаём таблички, дублирующие таблицы соответствующих справочников для того, чтобы там хранить хронологию изменений справочника. Но, создавая эти таблицы, мы заодно создаём себе четвёртую проблему, теперь в коде нужно в зависимости от даты обращаться то ли к основной таблице, то ли к исторической.
Ну мы же молодцы, мы и это победили))) Теперь, попивая чай из своей кружки, начинаешь дискутировать с другими коллегами на тему, что им приходилось решать, и понимаешь, что список проблем пополняется. В обсуждении стоит вопрос как хранить версии одной и той же записи. Хочу оговорится, что версия, это не то, что укладывается в таблицу историчности. В историчности понятно, до такого-то числа было одно название, а начиная с этой даты актуальным становится другое. А в версионности подразумевается, что запись была сначала сохранена с ошибкой, а через несколько часов это поняли и её изменили, и нужно знать все состояния этой записи. Во-первых, здесь должно быть дробление на время, не только сутки. А во-вторых, такие следы нужны в случае разборок. Например, заполняли прайс, ошиблись, успели товар продать по такой цене, а потом поправили, но в конце дня случился дебаланс. Однако решение для таких ситуаций меня лично напрягло, предлагалась все такие изменения хранить в самой таблице. Не буду устраивать холивар на сколько так правильно, но для меня точно обозначилась пятая проблема, а именно хранение изменений записей.
Итак, обобщая вышесказанное мы видим перед собой пять увесистых грабель. Теперь наша задача определить стратегию, позволяющую обойти и не наступить на них.
Сколько можно наступать на одни и те же грабли, давайте скинимся и купим новые
Начиная проектировать систему с нуля, никто не может предугадать путь её развития, а значит не сможет сказать на каком уровне придётся обобщать, как в описанном примере с оборудованием. Поэтому имеет смысл сразу задать абстрактную сущность, распространяемую на все таблицы НСИ. Таким образом все справочники будут иметь прообраз в едином справочнике с разделением на типы.
CREATE TABLE nsi_type (
nsi_type_id NUMBER(10) NOT NULL,
name VARCHAR2(50) NOT NULL,
descr VARCHAR2(100),
table_name VARCHAR2(50) NOT NULL,
CONSTRAINT nsi_type_pk PRIMARY KEY (nsi_type_id)
);
CREATE TABLE nsi (
nsi_id NUMBER(10) NOT NULL,
nsi_type_id NUMBER(10) NOT NULL,
descr VARCHAR2(100),
create_date DATE NOT NULL,
modif_date DATE NOT NULL,
begin_date DATE,
CONSTRAINT nsi_nsi_type_fk FOREIGN KEY (nsi_type_id) REFERENCES nsi_type (nsi_type_id),
CONSTRAINT nsi_uk UNIQUE(nsi_type_id, nsi_id)
);
CREATE SEQUENCE nsi_seq
MINVALUE 1
START WITH 1
INCREMENT BY 1
CACHE 20;
Таблица nsi_type системная, заполняется по мере добавления новых справочников. Таблица nsi хранит ключи и системные поля. Заодно создаём собственный сиквенс и тем самым закрываем вторую проблему.
Так же создадим пакет, содержащий основную функциональ��ость по работе со справочниками и будем его постепенно заполнять.
create or replace NONEDITIONABLE PACKAGE BODY pkg_nsi
IS
/* По названию таблицы возвращается тип НСИ
* @param p_table_name VARCHAR2 - название таблицы
* @return nsi.nsi_type_id%TYPE - тип из таблицы nsi_type
*/
FUNCTION get_type_id(p_table_name IN VARCHAR2)
RETURN nsi_type.nsi_type_id%TYPE
AS
v_type_id nsi_type.nsi_type_id%TYPE;
BEGIN
SELECT nsi_type_id INTO v_type_id
FROM nsi_type
WHERE TRIM(LOWER(table_name)) = TRIM(LOWER(p_table_name));
RETURN v_type_id;
END get_type_id;
/* Возвращает следующий id из nsi_seq
* @return nsi.nsi_id%TYPE - id из nsi_seq
*/
FUNCTION get_nsi_id
RETURN nsi.nsi_id%TYPE
AS
v_id nsi.nsi_id%TYPE;
BEGIN
SELECT nsi_seq.NEXTVAL INTO v_id FROM DUAL;
RETURN v_id;
END get_nsi_id;
/* По типу справочника возвращает наименование таблицы
* @param p_nsi_type_id nsi_type.nsi_type_id%TYPE - тип из таблицы nsi_type
* @return nsi_type.table_name%TYPE - название таблицы
*/
FUNCTION get_table_name(p_nsi_type_id IN nsi_type.nsi_type_id%TYPE)
RETURN nsi_type.table_name%TYPE
AS
v_table_name nsi_type.table_name%TYPE;
BEGIN
SELECT table_name INTO v_table_name
FROM nsi_type
WHERE nsi_type_id = p_nsi_type_id;
RETURN v_table_name;
END get_table_name;
/* Для определённого справчоника возвращает описание из таблицы nsi
* @param p_nsi_id nsi.nsi_id%TYPE - ключ справочника
* @param p_nsi_type_id nsi_type.nsi_type_id%TYPE - тип справочника
* @return nsi.descr%TYPE - описание
*/
FUNCTION get_nsi_descr (
p_nsi_id IN nsi.nsi_id%TYPE,
p_nsi_type_id IN nsi.nsi_type_id%TYPE)
RETURN nsi.descr%TYPE
AS
v_nsi_descr nsi.descr%TYPE;
BEGIN
SELECT descr
INTO v_nsi_descr
FROM nsi
WHERE nsi_id = p_nsi_id
AND nsi_type_id = p_nsi_type_id;
RETURN v_nsi_descr;
END get_nsi_descr;
...
END pkg_nsi;
Здесь пока представлены вспомогательные функции для обеспечения необходимой инфраструктуры.
Итак стоит задача создать справочник организаций, куда же без него, любое предприятие контактирует со сторонними организациями — это и поставщики, и клиенты, и партнёры. Сразу добавим соответствующий тип в таблицу nsi_type и определим таблицу nsi_organization.
CREATE TABLE nsi_organization (
nsi_id NUMBER(10) NOT NULL,
name VARCHAR2(50) NOT NULL,
full_name VARCHAR2(100) NOT NULL,
inn VARCHAR2(12) NOT NULL,
CONSTRAINT nsi_organization_pk PRIMARY KEY (nsi_id),
CONSTRAINT nsi_organization_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id)
);
INSERT INTO nsi_type (nsi_type_id, name, descr, table_name)
VALUES (11, 'Организация', 'Акционерное общество, компания, филиал, предприятие', 'nsi_organization');
Теперь, пока не поздно, нужно вспомнить про грабли с номером «пять». Если начнём добавлять записи в созданную таблицу организаций, то это событие нужно где-то фиксировать.
CREATE TABLE nsi_log (
nsi_log_id NUMBER(10) NOT NULL,
nsi_id NUMBER(10) NOT NULL,
table_name VARCHAR2(100),
oper_num NUMBER,
descr CLOB,
create_date DATE,
CONSTRAINT nsi_log_pk PRIMARY KEY (nsi_log_id),
CONSTRAINT nsi_log_oper_num_ch CHECK (oper_num IN (1, 2, 3, 4, 5, 6, 7))
);
COMMENT ON TABLE nsi_log IS 'НСИ. Логирование операций';
COMMENT ON COLUMN nsi_log.nsi_log_id IS 'Ключ';
COMMENT ON COLUMN nsi_log.nsi_id IS 'Справочник';
COMMENT ON COLUMN nsi_log.table_name IS 'Наименование таблицы';
COMMENT ON COLUMN nsi_log.oper_num IS 'Номер операции (1 - создание записи, 2 - изменение записи, 3 - удаление записи, 4 - добавление атрибута, 5 - изменение атрибута, 6 - удаление атрибута, 7 - создание версии истории).';
COMMENT ON COLUMN nsi_log.descr IS 'Описание';
COMMENT ON COLUMN nsi_log.create_date IS 'Дата создания';
А так же в пакет добавлена функция логирования.
-- Ограничение CHECK nsi_log_oper_num_ch
NSI_LOG_OPERNUM_INSERT NUMBER := 1;
NSI_LOG_OPERNUM_UPDATE NUMBER := 2;
NSI_LOG_OPERNUM_DELETE NUMBER := 3;
NSI_LOG_OPERNUM_ATTR_INSERT NUMBER := 4;
NSI_LOG_OPERNUM_ATTR_UPDATE NUMBER := 5;
NSI_LOG_OPERNUM_ATTR_DELETE NUMBER := 6;
NSI_LOG_OPERNUM_HISTORY_PUSH NUMBER := 7;
/* Добавление записи логирования операций.
* @param p_nsi_id nsi.nsi_id%TYPE - справочник
* @param p_nsi_type_id nsi_type.nsi_type_id%TYPE - тип справочника
* @param p_oper_num NUMBER - номер операции
* @param p_descr VARCHAR2 - описание
*/
PROCEDURE log_oper (
p_nsi_id IN nsi.nsi_id%TYPE,
p_nsi_type_id IN nsi_type.nsi_type_id%TYPE,
p_oper_num IN NUMBER,
p_descr IN VARCHAR2)
AS
BEGIN
INSERT INTO nsi_log
(nsi_log_id, nsi_id, table_name, oper_num, descr, create_date)
VALUES
(get_nsi_id(), p_nsi_id, get_table_name(p_nsi_type_id), p_oper_num, p_descr, Sysdate);
END;
Таким образом разрешена пятая проблема, теперь для любой записи НСИ можно посмотреть, что с ней происходило.
Пытаемся добавить туда организацию.
INSERT INTO nsi_organization (nsi_id, name, full_name, inn)
VALUES (1, 'АО "Рога и копыта"', 'Акционерное общество "Рога и копыта"', '11223344');
Конечно мы нарвёмся на констраинт nsi_organization_nsi_fk. Поэтому все справочные таблицы должны быть снабжены необходимой доработкой триггеров.
CREATE OR REPLACE TRIGGER nsi_organization_trg_insert
BEFORE INSERT ON nsi_organization FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_organization');
:NEW.nsi_id := pkg_nsi.get_nsi_id();
INSERT INTO nsi (nsi_id, nsi_type_id, descr, create_date, modif_date, begin_date)
VALUES (:NEW.nsi_id, v_type_id, :NEW.name, Trunc(Sysdate), Trunc(Sysdate), Trunc(Sysdate));
v_log_descr := 'name = ''' || :NEW.name || ''', full_name = ''' || :NEW.full_name || ''', inn = ''' || :NEW.inn || ''' ';
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_INSERT, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_organization_trg_update
BEFORE UPDATE ON nsi_organization FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_organization');
UPDATE nsi
SET modif_date = Trunc(Sysdate)
WHERE nsi_id = :NEW.nsi_id
AND nsi_type_id = v_type_id;
v_log_descr := 'name = ''' || :NEW.name || ''', full_name = ''' || :NEW.full_name || ''', inn = ''' || :NEW.inn || ''' ';
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_organization_trg_delete
AFTER DELETE ON nsi_organization FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_organization');
DELETE FROM nsi
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
v_log_descr := 'name = ''' || :OLD.name || ''', full_name = ''' || :OLD.full_name || ''', inn = ''' || :OLD.inn || ''' ';
pkg_nsi.log_oper (:OLD.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END;
А теперь добавление записи пройдёт без проблем (ключ уже указывать не надо). Заодно в таблице nsi появится первая запись, а так же в таблице логирования будет зафиксировано это событие.
INSERT INTO nsi_organization (name, full_name, inn)
VALUES ('АО "Рога и копыта"', 'Акционерное общество "Рога и копыта"', '11223344');
Но пока можно заметить только дополнительные расходы на создание таблицы какого-то справочника, а никак не преимущество единого подхода. Тогда вспомним про четвёртую проблему — нам необходимо хранить историчность данных в таблицах справочника, а так же извлекать актуальное состояние на заданную дату.
CREATE TABLE nsi_history (
nsi_history_id NUMBER(10) NOT NULL,
nsi_id NUMBER(10) NOT NULL,
nsi_type_id NUMBER(10) NOT NULL,
version NUMBER(10) NOT NULL,
content CLOB NOT NULL,
note VARCHAR2(100),
begin_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT nsi_history_pk PRIMARY KEY (nsi_history_id),
CONSTRAINT nsi_history_nsi_type_fk FOREIGN KEY (nsi_type_id) REFERENCES nsi_type (nsi_type_id),
CONSTRAINT nsi_history_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id),
CONSTRAINT nsi_history_content_json_chk CHECK (content IS JSON)
);
COMMENT ON TABLE nsi_history IS 'Историчность справочника';
COMMENT ON COLUMN nsi_history.nsi_history_id IS 'Ключ';
COMMENT ON COLUMN nsi_history.nsi_id IS 'Справочник';
COMMENT ON COLUMN nsi_history.nsi_type_id IS 'Тип справочника';
COMMENT ON COLUMN nsi_history.version IS 'Версия';
COMMENT ON COLUMN nsi_history.content IS 'Содержимое справочника';
COMMENT ON COLUMN nsi_history.note IS 'Пояснение';
COMMENT ON COLUMN nsi_history.begin_date IS 'Дата начала действия';
COMMENT ON COLUMN nsi_history.end_date IS 'Дата окончания действия';
В пакет pkg_nsi добавим функцию сохранения записи в историческую таблицу. Хранить запись будем в формате json, поэтому в пакете так же появится возможность получить json для переданного запроса.
/* Для переданного запроса возвращается json
* @param p_query VARCHAR2 - запрос
* @return CLOB - нотация json
*/
FUNCTION get_json(p_query IN VARCHAR2)
RETURN CLOB
AS
v_theCursor integer default dbms_sql.open_cursor;
v_columnValue varchar2(4000);
v_status integer;
v_descTbl dbms_sql.desc_tab;
v_colCnt number;
v_res clob;
BEGIN
dbms_sql.parse(v_theCursor, p_query, dbms_sql.native);
dbms_sql.describe_columns( v_theCursor, v_colCnt, v_descTbl);
FOR i IN 1 .. v_colCnt LOOP
dbms_sql.define_column(v_theCursor, i, v_columnValue, 4000);
END LOOP;
v_status := dbms_sql.execute(v_theCursor);
WHILE ( dbms_sql.fetch_rows(v_theCursor) > 0 ) LOOP
FOR i IN 1 .. v_colCnt LOOP
dbms_sql.column_value( v_theCursor, i, v_columnValue );
IF i > 1 THEN
v_res := v_res || ', ';
END IF;
v_res := v_res || '"' || v_descTbl(i).col_name || '" : "' || replace(v_columnValue, '"', '\"') || '"';
END LOOP;
-- Пока что подразумеваем, что возвращается только одна запись, поэтому берём её
-- в случае необходимости изменим логику
EXIT;
END LOOP;
RETURN '{' || v_res || '}';
exception
when others then dbms_sql.close_cursor( v_theCursor ); RAISE;
END get_json;
/* Сохранение текущего состояния справочника в таблицу историчности.
* @param p_nsi_id nsi.nsi_id%TYPE - справочник
* @param p_nsi_type_id nsi_type.nsi_type_id%TYPE - тип справочника
* @param p_end_date nsi_history.end_date%TYPE - дата окончания текущего содержимого справочника
* @param p_note nsi_history.note%TYPE - пояснение причины сохранения в истории
*/
PROCEDURE nsi_history_push (
p_nsi_id IN nsi.nsi_id%TYPE,
p_nsi_type_id IN nsi_type.nsi_type_id%TYPE,
p_end_date IN nsi_history.end_date%TYPE,
p_note IN nsi_history.note%TYPE)
AS
v_table_name VARCHAR2(50);
v_content CLOB;
v_max_ver NUMBER;
v_begin_date DATE;
BEGIN
IF (p_end_date IS NULL) THEN
RAISE_APPLICATION_ERROR (NSI_ERROR_CODE,
'[nsi_history_push] Дата окончания должна быть определена.');
END IF;
IF (Trunc(p_end_date) > Trunc(Sysdate) ) THEN
RAISE_APPLICATION_ERROR (NSI_ERROR_CODE,
'[nsi_history_push] Дата окончания не должна превышать текущую дату.');
END IF;
SELECT begin_date INTO v_begin_date
FROM nsi
WHERE nsi_id = p_nsi_id
AND nsi_type_id = p_nsi_type_id;
IF (Trunc(p_end_date) < Trunc(v_begin_date) ) THEN
RAISE_APPLICATION_ERROR (NSI_ERROR_CODE,
'[nsi_history_push] Дата окончания не должна быть меньше даты начала действия текущей версии записи.');
END IF;
v_table_name := get_table_name(p_nsi_type_id);
v_content := get_json ('select * from ' || v_table_name || ' where nsi_id=' || p_nsi_id);
SELECT MAX(version) INTO v_max_ver
FROM nsi_history
WHERE nsi_id = p_nsi_id
AND nsi_type_id = p_nsi_type_id;
IF (v_max_ver IS NULL) THEN
v_max_ver := 0;
END IF;
v_max_ver := v_max_ver + 1;
UPDATE nsi
SET begin_date = Trunc(p_end_date) + 1
WHERE nsi_id = p_nsi_id
AND nsi_type_id = p_nsi_type_id;
INSERT INTO nsi_history
(nsi_history_id, nsi_id, nsi_type_id, version, content, note, begin_date, end_date)
VALUES (get_nsi_id, p_nsi_id, p_nsi_type_id, v_max_ver, v_content, p_note, v_begin_date, Trunc(p_end_date));
log_oper(p_nsi_id, p_nsi_type_id, NSI_LOG_OPERNUM_HISTORY_PUSH, v_content);
END nsi_history_push;
Таким образом любой справочник может воспользоваться этой функцией, чтобы увести в историю текущее состояние. Уже хорошо, хоть что-то полезное появилось от такого обобщения))) Для извлечения актуального состояния справочника добавим в пакет соответствующую pipeline-функцию. Записи справочника будут возвращаться в тип, расширенный системными полями.
-- Запись содержит поля таблицы nsi_organization и дополнены служебными полями nsi
TYPE nsi_organization_rec IS RECORD(
nsi_id nsi_organization.nsi_id%TYPE,
name nsi_organization.name%TYPE,
full_name nsi_organization.full_name%TYPE,
inn nsi_organization.inn%TYPE,
nsi_type_id nsi.nsi_type_id%TYPE,
create_date nsi.create_date%TYPE,
modif_date nsi.create_date%TYPE,
version nsi_history.version%TYPE,
begin_date nsi.begin_date%TYPE,
end_date nsi_history.end_date%TYPE
);
TYPE nsi_organization_list IS TABLE OF nsi_organization_rec;
/* Возвращает список, актуальный на указанную дату.
* Если дата не задана, актуальной считается текущая дата.
* @param p_date DATE - дата, на которую необходимо получить состояние справочника
* @return nsi_organization_table - таблица с записями nsi_organization_rec
*/
FUNCTION nsi_organization_table(p_date IN DATE := null)
RETURN nsi_organization_list PIPELINED
AS
v_date date;
BEGIN
v_date := Trunc(Sysdate);
IF p_date IS NOT NULL THEN
v_date := Trunc(p_date);
END IF;
FOR rec IN (
SELECT
o.nsi_id, o.name, o.full_name, o.inn,
n.nsi_type_id, n.create_date, n.modif_date,
0 AS version, n.begin_date, to_date(null) AS end_date
FROM
nsi_organization o INNER JOIN nsi n
ON (o.nsi_id = n.nsi_id)
WHERE
n.begin_date <= v_date
UNION ALL
SELECT
n.nsi_id,
json_value(h.content, '$.NAME') AS name,
json_value(h.content, '$.FULL_NAME') AS full_name,
json_value(h.content, '$.INN') AS inn,
n.nsi_type_id, n.create_date, n.modif_date,
h.version, h.begin_date, h.end_date
FROM
nsi_history h INNER JOIN nsi n
ON (h.nsi_id = n.nsi_id AND h.nsi_type_id = n.nsi_type_id)
WHERE
h.begin_date <= v_date
AND h.end_date >= v_date
) LOOP
PIPE ROW (rec);
END LOOP;
END nsi_organization_table;
Применим к нашей таблице nsi_organization.
select * from nsi where nsi_id=1;
---------------------------------------------------------------------------------------
"NSI_ID" "NSI_TYPE_ID" "DESCR" "CREATE_DATE" "MODIF_DATE" "BEGIN_DATE"
1 1 "АО ""Рога и копыта""" 11.03.20 11.03.20 11.03.20
---------------------------------------------------------------------------------------
begin
-- конечно это нереальная ситуация по смене инн, но для тестового примера вполне подойдёт
pkg_nsi.nsi_history_push(202, 1, sysdate, 'смена инн');
end;
select * from nsi_history;
---------------------------------------------------------------------------------------
"NSI_HISTORY_ID" "NSI_ID" "NSI_TYPE_ID" "VERSION" "CONTENT" "NOTE" "BEGIN_DATE" "END_DATE"
205 1 1 1 "{""NSI_ID"" : ""1"", ""NAME"" : ""АО \""Рога и копыта\"""", ""FULL_NAME"" : ""Акционерное общество \""Рога и копыта\"""", ""INN"" : ""11223344""}" "смена инн" 11.03.20 11.03.20
---------------------------------------------------------------------------------------
-- следует обратить внимание на дату начала
-- так как был вызов сохранения в историю, то новая версия начала быть актуальной на следующий день
select * from nsi where nsi_id=1;
---------------------------------------------------------------------------------------
"NSI_ID" "NSI_TYPE_ID" "DESCR" "CREATE_DATE" "MODIF_DATE" "BEGIN_DATE"
1 1 "АО ""Рога и копыта""" 11.03.20 11.03.20 12.03.20
---------------------------------------------------------------------------------------
-- обновим инн и посмотрим выборку на различные даты
-- различия присутствуют в полях inn, version, begin_date, end_date
-- текущая запись в таблице имеет версию 0
update nsi_organization set inn='99887766' where nsi_id=1;
select * from table(pkg_nsi.nsi_organization_table(sysdate));
---------------------------------------------------------------------------------------
"NSI_ID" "NAME" "FULL_NAME" "INN" "NSI_TYPE_ID" "CREATE_DATE" "MODIF_DATE" "VERSION" "BEGIN_DATE" "END_DATE"
1 "АО ""Рога и копыта""" "Акционерное общество ""Рога и копыта""" "11223344" 1 11.03.20 11.03.20 1 11.03.20 11.03.20
---------------------------------------------------------------------------------------
select * from table(pkg_nsi.nsi_organization_table(sysdate+1));
---------------------------------------------------------------------------------------
"NSI_ID" "NAME" "FULL_NAME" "INN" "NSI_TYPE_ID" "CREATE_DATE" "MODIF_DATE" "VERSION" "BEGIN_DATE" "END_DATE"
1 "АО ""Рога и копыта""" "Акционерное общество ""Рога и копыта""" "99887766" 1 11.03.20 11.03.20 0 12.03.20
---------------------------------------------------------------------------------------
Функция nsi_organization_table очень полезна, потому как удовлетворяет нашим требованиям и окончательно уводит проблему номер четыре в прошлое.
Идём дальше. Раз у нас появилось такое преимущество с введением единого подхода для работы со всеми справочниками, то воспользуемся им и для хранения дополнительной информации, которой может быть наделена любая запись из любого справочника. Такое механизм уже давно существует, называется EAV-pattern, его и реализуем.
-- Ограничение CHECK nsi_attribute_type_ch
NSI_ATTRIBUTE_TYPE_STRING NUMBER := 1;
NSI_ATTRIBUTE_TYPE_INT NUMBER := 2;
NSI_ATTRIBUTE_TYPE_DOUBLE NUMBER := 3;
NSI_ATTRIBUTE_TYPE_DATE NUMBER := 4;
CREATE TABLE nsi_attribute_type (
nsi_attribute_type_id NUMBER(10) NOT NULL,
value_type NUMBER NOT NULL,
descr VARCHAR2(100) NOT NULL,
CONSTRAINT nsi_attribute_type_pk PRIMARY KEY (nsi_attribute_type_id),
CONSTRAINT nsi_attribute_type_ch CHECK (value_type IN (1, 2, 3, 4)),
CONSTRAINT nsi_attribute_type_fk FOREIGN KEY (nsi_attribute_type_id) REFERENCES nsi (nsi_id)
);
COMMENT ON TABLE nsi_attribute_type IS 'НСИ. Тип атрибута';
COMMENT ON COLUMN nsi_attribute_type.nsi_attribute_type_id IS 'Ключ';
COMMENT ON COLUMN nsi_attribute_type.value_type IS 'Тип значения (1 - строка, 2 - целое, 3 - дробное, 4 - дата)';
COMMENT ON COLUMN nsi_attribute_type.descr IS 'Описание';
CREATE TABLE nsi_attribute (
nsi_attribute_id NUMBER(10) NOT NULL,
nsi_attribute_type_id NUMBER(10) NOT NULL,
nsi_id NUMBER(10) NOT NULL,
nsi_type_id NUMBER(10) NOT NULL,
value_1 VARCHAR2(100),
value_2_3 NUMBER,
value_4 DATE,
begin_date DATE,
end_date DATE,
CONSTRAINT nsi_attribute_pk PRIMARY KEY (nsi_attribute_id),
CONSTRAINT nsi_attribute_type_fk FOREIGN KEY (nsi_attribute_type_id) REFERENCES nsi_attribute_type (nsi_attribute_type_id),
CONSTRAINT nsi_attribute_nsi_fk FOREIGN KEY (nsi_id, nsi_type_id) REFERENCES nsi (nsi_id, nsi_type_id)
);
COMMENT ON TABLE nsi_attribute IS 'НСИ. Тип атрибута';
COMMENT ON COLUMN nsi_attribute.nsi_attribute_id IS 'Ключ';
COMMENT ON COLUMN nsi_attribute.nsi_attribute_type_id IS 'Тип атрибута';
COMMENT ON COLUMN nsi_attribute.nsi_id IS 'Справочник';
COMMENT ON COLUMN nsi_attribute.nsi_type_id is 'Тип справочника';
COMMENT ON COLUMN nsi_attribute.value_1 IS 'Значение типа строка';
COMMENT ON COLUMN nsi_attribute.value_2_3 IS 'Значение типа целое или дробное';
COMMENT ON COLUMN nsi_attribute.value_4 IS 'Значение типа дата';
COMMENT ON COLUMN nsi_attribute.begin_date IS 'Дата начала действия атрибута';
COMMENT ON COLUMN nsi_attribute.end_date IS 'Дата окончания действия атрибута';
Очень часто в контексте документов имена собственные необходимо использовать в каком-то падеже, поэтому создадим новую таблицу с физическими лицами и по аналогии с организациями добавим обработку триггеров и тип для выборки.
CREATE TABLE nsi_person (
nsi_id NUMBER(10) NOT NULL,
surname VARCHAR2(30) NOT NULL,
name VARCHAR2(30) NOT NULL,
patronymic VARCHAR2(30) NOT NULL,
birthday DATE,
CONSTRAINT nsi_person_pk PRIMARY KEY (nsi_id),
CONSTRAINT nsi_person_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id)
);
COMMENT ON TABLE nsi_person IS 'НСИ. Физическое лицо';
COMMENT ON COLUMN nsi_person.nsi_id IS 'Ключ';
COMMENT ON COLUMN nsi_person.surname IS 'Фамилия';
COMMENT ON COLUMN nsi_person.name IS 'Имя';
COMMENT ON COLUMN nsi_person.patronymic IS 'Отчество';
COMMENT ON COLUMN nsi_person.birthday IS 'Дата рождения';
CREATE OR REPLACE TRIGGER nsi_person_trg_insert
BEFORE INSERT ON nsi_person FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_person');
:NEW.nsi_id := pkg_nsi.get_nsi_id();
INSERT INTO nsi (nsi_id, nsi_type_id, descr, create_date, modif_date, begin_date)
VALUES (:NEW.nsi_id, v_type_id, :NEW.name, Trunc(Sysdate), Trunc(Sysdate), Trunc(Sysdate));
v_log_query := 'SELECT ''' || :NEW.surname || ''' AS surname, ''' || :NEW.name || ''' AS name, ''' || :NEW.patronymic || ''' AS patronymic, to_date(''' || :NEW.birthday || ''', ''dd.mm.yy'') AS birthday FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_INSERT, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_person_trg_update
BEFORE UPDATE ON nsi_person FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_person');
UPDATE nsi
SET modif_date = Trunc(Sysdate)
WHERE nsi_id = :NEW.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :NEW.surname || ''' AS surname, ''' || :NEW.name || ''' AS name, ''' || :NEW.patronymic || ''' AS patronymic, to_date(''' || :NEW.birthday || ''', ''dd.mm.yy'') AS birthday FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_person_trg_delete
AFTER DELETE ON nsi_person FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_person');
DELETE FROM nsi_history
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi_attribute
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :OLD.surname || ''' AS surname, ''' || :OLD.name || ''' AS name, ''' || :OLD.patronymic || ''' AS patronymic, to_date(''' || :OLD.birthday || ''', ''dd.mm.yy'') AS birthday FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:OLD.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END;
Осталось дополнить пакет pkg_nsi обработкой этой таблицы.
-- Запись содержит поля таблицы nsi_person и дополнены служебными полями nsi
TYPE nsi_person_rec IS RECORD(
nsi_id nsi_person.nsi_id%TYPE,
surname nsi_person.surname%TYPE,
name nsi_person.name%TYPE,
patronymic nsi_person.patronymic%TYPE,
birthday nsi_person.birthday%TYPE,
nsi_type_id nsi.nsi_type_id%TYPE,
create_date nsi.create_date%TYPE,
modif_date nsi.create_date%TYPE,
version nsi_history.version%TYPE,
begin_date nsi.begin_date%TYPE,
end_date nsi_history.end_date%TYPE
);
TYPE nsi_person_list IS TABLE OF nsi_person_rec;
/* Возвращает список, актуальный на указанную дату.
* Если дата не задана, актуальной считается текущая дата.
* @param p_date DATE - дата, на которую необходимо получить состояние справочника
* @return nsi_person_table - таблица с записями nsi_person_rec
*/
FUNCTION nsi_person_table(p_date IN DATE := null)
RETURN nsi_person_list PIPELINED
AS
v_date date;
BEGIN
v_date := Trunc(Sysdate);
IF p_date IS NOT NULL THEN
v_date := Trunc(p_date);
END IF;
FOR rec IN (
SELECT
p.nsi_id, p.surname, p.name, p.patronymic, p.birthday,
n.nsi_type_id, n.create_date, n.modif_date,
0 AS version, n.begin_date, to_date(null) AS end_date
FROM
nsi_person p INNER JOIN nsi n
ON (p.nsi_id = n.nsi_id)
WHERE
n.begin_date <= v_date
UNION ALL
SELECT
n.nsi_id,
json_value(h.content, '$.SURNAME') AS surname,
json_value(h.content, '$.NAME') AS name,
json_value(h.content, '$.PATRONYMIC') AS patronymic,
to_date(json_value(h.content, '$.BIRTHDAY')) AS birthday,
n.nsi_type_id, n.create_date, n.modif_date,
h.version, h.begin_date, h.end_date
FROM
nsi_history h INNER JOIN nsi n
ON (h.nsi_id = n.nsi_id AND h.nsi_type_id = n.nsi_type_id)
WHERE
h.begin_date <= v_date
AND h.end_date >= v_date
) LOOP
PIPE ROW (rec);
END LOOP;
END nsi_person_table;
И добавим кого-нибудь в эту таблицу.
INSERT INTO nsi_person
(surname, name, patronymic, birthday)
VALUES ('Деревянный', 'Буратино', 'Карлович', to_date('22.12.70', 'dd.mm.yy'));
Создадим атрибуты для самого востребованного родительного падежа.
INSERT INTO nsi_attribute_type (nsi_attribute_type_id, value_type, descr)
VALUES (1, 1, 'Фамилия в род. падеже');
INSERT INTO nsi_attribute_type (nsi_attribute_type_id, value_type, descr)
VALUES (2, 1, 'Имя в род. падеже');
INSERT INTO nsi_attribute_type (nsi_attribute_type_id, value_type, descr)
VALUES (3, 1, 'Отчество в род. падеже');
В пакете pkg_nsi добавим функции для работы с атрибутами справочников.
/* Для переданного id возвращает тип значения и описание атрибута.
* @param p_nsi_attribute_type_id nsi_attribute_type.nsi_attribute_type_id%TYPE - Тип атрибута
* @param p_value_type nsi_attribute_type.value_type%TYPE - Тип знаения
* @param p_descr nsi_attribute_type.descr%TYPE - Описание атрибута
*/
PROCEDURE get_attribute_type (
p_nsi_attribute_type_id IN nsi_attribute_type.nsi_attribute_type_id%TYPE,
p_value_type OUT nsi_attribute_type.value_type%TYPE,
p_descr OUT nsi_attribute_type.descr%TYPE)
AS
BEGIN
SELECT value_type, descr
INTO p_value_type, p_descr
FROM nsi_attribute_type
WHERE nsi_attribute_type_id = p_nsi_attribute_type_id;
END;
/* Реализует вставку записи.
* @param p_nsi_attribute_type_id nsi_attribute.nsi_attribute_type_id%TYPE - Тип атрибута
* @param p_nsi_id nsi_attribute.nsi_id%TYPE - Справочник
* @param p_nsi_type_id nsi_attribute.nsi_type_id%TYPE - Тип справочника
* @param p_value_1 nsi_attribute.value_1%TYPE - Значение строкового типа
* @param p_value_2_3 nsi_attribute.value_2_3%TYPE - Значение числового типа
* @param p_value_4 nsi_attribute.value_4%TYPE - Значение типа даты
* @param p_begin_date nsi_attribute.begin_date%TYPE - Дата начала действия атрибута
* @param p_end_date nsi_attribute.end_date%TYPE - Дата окончания действия атрибута
*/
PROCEDURE nsi_attribute_insert (
p_nsi_attribute_type_id IN nsi_attribute.nsi_attribute_type_id%TYPE,
p_nsi_id IN nsi_attribute.nsi_id%TYPE,
p_nsi_type_id IN nsi_attribute.nsi_type_id%TYPE,
p_value_1 IN nsi_attribute.value_1%TYPE,
p_value_2_3 IN nsi_attribute.value_2_3%TYPE,
p_value_4 IN nsi_attribute.value_4%TYPE,
p_begin_date IN nsi_attribute.begin_date%TYPE,
p_end_date IN nsi_attribute.end_date%TYPE)
AS
v_id NUMBER;
v_log_descr nsi_log.descr%TYPE;
v_value_type nsi_attribute_type.value_type%TYPE;
v_descr nsi_attribute_type.descr%TYPE;
BEGIN
v_id := get_nsi_id;
get_attribute_type(p_nsi_attribute_type_id, v_value_type, v_descr);
IF (v_value_type = NSI_ATTRIBUTE_TYPE_STRING) THEN
INSERT INTO nsi_attribute
(nsi_attribute_id, nsi_attribute_type_id, nsi_id, nsi_type_id,
value_1, value_2_3, value_4, begin_date, end_date)
VALUES (v_id, p_nsi_attribute_type_id, p_nsi_id, p_nsi_type_id,
p_value_1, null, null, p_begin_date, p_end_date);
v_log_descr := p_value_1;
ELSIF (v_value_type IN (NSI_ATTRIBUTE_TYPE_INT, NSI_ATTRIBUTE_TYPE_DOUBLE)) THEN
INSERT INTO nsi_attribute
(nsi_attribute_id, nsi_attribute_type_id, nsi_id, nsi_type_id,
value_1, value_2_3, value_4, begin_date, end_date)
VALUES (v_id, p_nsi_attribute_type_id, p_nsi_id, p_nsi_type_id,
null, p_value_2_3, null, p_begin_date, p_end_date);
v_log_descr := p_value_2_3;
ELSE
INSERT INTO nsi_attribute
(nsi_attribute_id, nsi_attribute_type_id, nsi_id, nsi_type_id,
value_1, value_2_3, value_4, begin_date, end_date)
VALUES (v_id, p_nsi_attribute_type_id, p_nsi_id, p_nsi_type_id,
null, null, p_value_4, p_begin_date, p_end_date);
v_log_descr := p_value_4;
END IF;
v_log_descr := '[' || get_nsi_descr(p_nsi_id, p_nsi_type_id) || '] ' ||
' Атрибут: ' || v_descr ||
' Значение: ' || v_log_descr ||
' Период: ' || p_begin_date || ' - ' || p_end_date;
log_oper(p_nsi_id, p_nsi_type_id, NSI_LOG_OPERNUM_ATTR_INSERT, v_log_descr);
END;
/* Реализует обновление типа и значения атрибута.
* @param p_nsi_attribute_id nsi_attribute.nsi_attribute_id%TYPE - Ключ атрибута
* @param p_value_1 nsi_attribute.value_1%TYPE - Значение строкового типа
* @param p_value_2_3 nsi_attribute.value_2_3%TYPE - Значение числового типа
* @param p_value_4 nsi_attribute.value_4%TYPE - Значение типа даты
*/
PROCEDURE nsi_attribute_value (
p_nsi_attribute_id IN nsi_attribute.nsi_attribute_id%TYPE,
p_value_1 IN nsi_attribute.value_1%TYPE,
p_value_2_3 IN nsi_attribute.value_2_3%TYPE,
p_value_4 IN nsi_attribute.value_4%TYPE)
AS
v_nsi_id nsi.nsi_id%TYPE;
v_nsi_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_value_type nsi_attribute_type.value_type%TYPE;
v_descr nsi_attribute_type.descr%TYPE;
v_nsi_attribute_type_id nsi_attribute.nsi_attribute_type_id%TYPE;
BEGIN
SELECT nsi_attribute_type_id, nsi_id, nsi_type_id
INTO v_nsi_attribute_type_id, v_nsi_id, v_nsi_type_id
FROM nsi_attribute
WHERE nsi_attribute_id = p_nsi_attribute_id;
get_attribute_type(v_nsi_attribute_type_id, v_value_type, v_descr);
IF (v_value_type = NSI_ATTRIBUTE_TYPE_STRING) THEN
UPDATE nsi_attribute
SET value_1 = p_value_1,
value_2_3 = null,
value_4 = null
WHERE nsi_attribute_id = p_nsi_attribute_id;
v_log_descr := p_value_1;
ELSIF (v_value_type IN (NSI_ATTRIBUTE_TYPE_INT, NSI_ATTRIBUTE_TYPE_DOUBLE)) THEN
UPDATE nsi_attribute
SET value_1 = null,
value_2_3 = p_value_2_3,
value_4 = null
WHERE nsi_attribute_id = p_nsi_attribute_id;
v_log_descr := p_value_2_3;
ELSE
UPDATE nsi_attribute
SET value_1 = null,
value_2_3 = null,
value_4 = p_value_4
WHERE nsi_attribute_id = p_nsi_attribute_id;
v_log_descr := p_value_4;
END IF;
v_log_descr := '[' || get_nsi_descr(v_nsi_id, v_nsi_type_id) || '] ' ||
' Атрибут: ' || v_descr ||
' Изменение значения: ' || v_log_descr;
log_oper(v_nsi_id, v_nsi_type_id, NSI_LOG_OPERNUM_ATTR_UPDATE, v_log_descr);
END;
/* Реализует обновление периода действия атрибута.
* @param p_nsi_attribute_id nsi_attribute.nsi_attribute_id%TYPE - Ключ атрибута
* @param p_begin_date nsi_attribute.begin_date%TYPE - Дата начала действия атрибута
* @param p_end_date nsi_attribute.end_date%TYPE - Дата окончания действия атрибута
*/
PROCEDURE nsi_attribute_period (
p_nsi_attribute_id IN nsi_attribute.nsi_attribute_id%TYPE,
p_begin_date IN nsi_attribute.begin_date%TYPE,
p_end_date IN nsi_attribute.end_date%TYPE)
AS
v_nsi_id nsi.nsi_id%TYPE;
v_nsi_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_value_type nsi_attribute_type.value_type%TYPE;
v_descr nsi_attribute_type.descr%TYPE;
v_nsi_attribute_type_id nsi_attribute.nsi_attribute_type_id%TYPE;
BEGIN
SELECT nsi_id, nsi_type_id, nsi_attribute_type_id
INTO v_nsi_id, v_nsi_type_id, v_nsi_attribute_type_id
FROM nsi_attribute
WHERE nsi_attribute_id = p_nsi_attribute_id;
get_attribute_type(v_nsi_attribute_type_id, v_value_type, v_descr);
UPDATE nsi_attribute
SET begin_date = p_begin_date,
end_date = p_end_date
WHERE nsi_attribute_id = p_nsi_attribute_id;
v_log_descr := '[' || get_nsi_descr(v_nsi_id, v_nsi_type_id) || '] ' ||
' Атрибут: ' || v_descr ||
' Изменение периода: ' || p_begin_date || ' - ' || p_end_date;
log_oper(v_nsi_id, v_nsi_type_id, NSI_LOG_OPERNUM_ATTR_UPDATE, v_log_descr);
END;
/* Реализует удаление записи.
* @param p_nsi_attribute_id nsi_person.nsi_attribute_id%TYPE - id записи nsi_attribute
*/
PROCEDURE nsi_attribute_delete (p_nsi_attribute_id nsi_attribute.nsi_attribute_id%TYPE)
AS
v_nsi_id nsi.nsi_id%TYPE;
v_nsi_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_value_type nsi_attribute_type.value_type%TYPE;
v_descr nsi_attribute_type.descr%TYPE;
v_nsi_attribute_type_id nsi_attribute.nsi_attribute_type_id%TYPE;
BEGIN
SELECT nsi_id, nsi_type_id, nsi_attribute_type_id
INTO v_nsi_id, v_nsi_type_id, v_nsi_attribute_type_id
FROM nsi_attribute
WHERE nsi_attribute_id = p_nsi_attribute_id;
get_attribute_type(v_nsi_attribute_type_id, v_value_type, v_descr);
DELETE FROM nsi_attribute
WHERE nsi_attribute_id = p_nsi_attribute_id;
v_log_descr := '[' || get_nsi_descr(v_nsi_id, v_nsi_type_id) || '] ' ||
' Атрибут: ' || v_descr;
log_oper(v_nsi_id, v_nsi_type_id, NSI_LOG_OPERNUM_ATTR_DELETE, v_log_descr);
END;
Теперь присвоим соответствующие атрибуты.
begin
pkg_nsi.nsi_attribute_insert(1, 225, 6, 'деревянного', null, null, sysdate, null);
pkg_nsi.nsi_attribute_insert(2, 225, 6, 'Буратино', null, null, sysdate, null);
pkg_nsi.nsi_attribute_insert(3, 225, 6, 'Карловича', null, null, sysdate, null);
end;
-- но когда просматриваем результат видим ошибку, фамилия Буратино написана с маленькой буквы, нужно исправить
--------------------------------------------------------------------------------------------
"NSI_ATTRIBUTE_ID" "NSI_ATTRIBUTE_TYPE_ID" "NSI_ID" "NSI_TYPE_ID" "VALUE_1" "VALUE_2_3" "VALUE_4" "BEGIN_DATE" "END_DATE"
230 1 225 6 "деревянного" 11.03.20
232 2 225 6 "Буратино" 11.03.20
234 3 225 6 "Карловича" 11.03.20
--------------------------------------------------------------------------------------------
begin
pkg_nsi.nsi_attribute_value(230, 'Деревянного', null, null);
end;
--------------------------------------------------------------------------------------------
"NSI_ATTRIBUTE_ID" "NSI_ATTRIBUTE_TYPE_ID" "NSI_ID" "NSI_TYPE_ID" "VALUE_1" "VALUE_2_3" "VALUE_4" "BEGIN_DATE" "END_DATE"
230 1 225 6 "Деревянного" 11.03.20
232 2 225 6 "Буратино" 11.03.20
234 3 225 6 "Карловича" 11.03.20
--------------------------------------------------------------------------------------------
-- перенесём начало действия атрибута на день раньше
begin
pkg_nsi.nsi_attribute_period(230, sysdate-1, null);
pkg_nsi.nsi_attribute_period(232, sysdate-1, null);
pkg_nsi.nsi_attribute_period(234, sysdate-1, null);
end;
--------------------------------------------------------------------------------------------
"NSI_ATTRIBUTE_ID" "NSI_ATTRIBUTE_TYPE_ID" "NSI_ID" "NSI_TYPE_ID" "VALUE_1" "VALUE_2_3" "VALUE_4" "BEGIN_DATE" "END_DATE"
230 1 225 6 "Деревянного" 10.03.20
232 2 225 6 "Буратино" 10.03.20
234 3 225 6 "Карловича" 10.03.20
--------------------------------------------------------------------------------------------
Таким образом мы победим третью проблему.
Кроме таблиц справочников в системе НСИ также важны отношение между ними. Так, например крупные организации включают в себя различные подразделения, филиалы, отделы и т.п., которые можно выстроить в древовидную структуру. Для начала заведём в нашей системе ещё несколько организаций, которые будут в подчинении у уже существующей «Рога и копыта».
INSERT INTO nsi_organization (name, full_name, inn)
VALUES ('Подразделение по обслуживанию рогов', 'Подразделение по обслуживанию рогов', '1111111111');
INSERT INTO nsi_organization (name, full_name, inn)
VALUES ('Подразделение по обслуживанию копыт', 'Подразделение по обслуживанию копыт', '2222222222');
INSERT INTO nsi_organization (name, full_name, inn)
VALUES ('Отдел по изготовлению подков', 'Отдел по изготовлению подков', '3333333333');
----------------------------------------------------------------------------
281 1 Подразделение по обслуживанию рогов 13.03.20 13.03.20 13.03.20
283 1 Подразделение по обслуживанию копыт 13.03.20 13.03.20 13.03.20
285 1 Отдел по изготовлению подков 13.03.20 13.03.20 13.03.20
1 1 АО "Рога и копыта" 11.03.20 13.03.20 12.03.20
----------------------------------------------------------------------------
Теперь нужно показать в каком отношении эти организации находятся между собой. Для этого необходима таблица с древовидной структурой и указанием периода действия, потому как всё подвержено изменением во времени и нужно это учитывать.
CREATE TABLE nsi_structure (
nsi_structure_id NUMBER(10) NOT NULL,
nsi_parent_structure_id NUMBER(10),
nsi_id NUMBER(10) NOT NULL,
nsi_type_id NUMBER(10) NOT NULL,
ordnum NUMBER,
begin_date DATE NOT NULL,
end_date DATE,
CONSTRAINT nsi_structure_pk PRIMARY KEY (nsi_structure_id),
CONSTRAINT nsi_parent_struct_fk FOREIGN KEY (nsi_parent_structure_id) REFERENCES nsi_structure (nsi_structure_id),
CONSTRAINT nsi_struct_nsi_fk FOREIGN KEY (nsi_id, nsi_type_id) REFERENCES nsi (nsi_id, nsi_type_id)
);
COMMENT ON TABLE nsi_structure IS 'НСИ. Таблица структуры справочников';
COMMENT ON COLUMN nsi_structure.nsi_structure_id IS 'Ключ';
COMMENT ON COLUMN nsi_structure.nsi_parent_structure_id IS 'Ключ родителя';
COMMENT ON COLUMN nsi_structure.nsi_id IS 'Справочник';
COMMENT ON COLUMN nsi_structure.nsi_type_id IS 'Тип справочника';
COMMENT ON COLUMN nsi_structure.ordnum IS 'Порядковый номер';
COMMENT ON COLUMN nsi_structure.begin_date IS 'Дата начала действия';
COMMENT ON COLUMN nsi_structure.end_date IS 'Дата окончания действия';
Конечно, следует расширить возможности пакета pkg_nsi, чтобы можно было настраивать структуру для различных таблиц.
/* Реализует вставку записи.
* @param p_nsi_parent_structure_id nsi_structure.nsi_parent_structure_id%TYPE - запись родителя
* @param p_nsi_id nsi_structure.nsi_id%TYPE - справочник
* @param p_nsi_type_id nsi_structure.nsi_type_id%TYPE - тип справочника
* @param p_ordnum nsi_structure.ordnum%TYPE - порядковый номер
* @param p_begin_date nsi_structure.begin_date%TYPE - дата начала действия записи
* @param p_end_date nsi_structure.end_date%TYPE - дата окончания действия записи
*/
FUNCTION nsi_structure_insert (
p_nsi_parent_structure_id IN nsi_structure.nsi_parent_structure_id%TYPE,
p_nsi_id IN nsi_structure.nsi_id%TYPE,
p_nsi_type_id IN nsi_structure.nsi_type_id%TYPE,
p_ordnum IN nsi_structure.ordnum%TYPE,
p_begin_date IN nsi_structure.begin_date%TYPE,
p_end_date IN nsi_structure.end_date%TYPE)
RETURN nsi_structure.nsi_structure_id%TYPE
AS
v_id NUMBER;
v_log_descr nsi_log.descr%TYPE;
v_type_id nsi.nsi_type_id%TYPE;
BEGIN
v_id := get_nsi_id;
v_type_id := get_type_id('nsi_structure');
INSERT INTO nsi_structure (
nsi_structure_id, nsi_parent_structure_id,
nsi_id, nsi_type_id, ordnum, begin_date, end_date)
VALUES (
v_id, p_nsi_parent_structure_id,
p_nsi_id, p_nsi_type_id, p_ordnum, Trunc(p_begin_date), Trunc(p_end_date));
v_log_descr := '[' || get_nsi_descr(p_nsi_id, p_nsi_type_id) || '] ';
v_log_descr := v_log_descr || 'Период ' || p_begin_date || ' - ' || p_end_date;
log_oper (v_id, v_type_id, NSI_LOG_OPERNUM_INSERT, v_log_descr);
RETURN v_id;
END nsi_structure_insert;
/* Реализует обновление порядкового номера записи.
* @param p_nsi_structure_id nsi_structure.nsi_structure_id%TYPE - ключ nsi_structure
* @param p_ordnum nsi_structure.ordnum%TYPE - порядковый номер
*/
PROCEDURE nsi_structure_ordnum (
p_nsi_structure_id IN nsi_structure.nsi_structure_id%TYPE,
p_ordnum IN nsi_structure.ordnum%TYPE)
AS
v_nsi_id nsi_structure.nsi_id%TYPE;
v_nsi_type_id nsi_structure.nsi_type_id%TYPE;
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := get_type_id('nsi_structure');
SELECT nsi_id, nsi_type_id
INTO v_nsi_id, v_nsi_type_id
FROM nsi_structure
WHERE nsi_structure_id = p_nsi_structure_id;
UPDATE nsi_structure
SET ordnum = p_ordnum
WHERE nsi_structure_id = p_nsi_structure_id;
v_log_descr := '[' || get_nsi_descr(v_nsi_id, v_nsi_type_id) || '] ';
v_log_descr := v_log_descr || 'Номер ' || p_ordnum;
log_oper (p_nsi_structure_id, v_type_id, NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
/* Реализует обновление периода действия записи.
* @param p_nsi_structure_id nsi_structure.nsi_structure_id%TYPE - ключ nsi_structure
* @param p_begin_date nsi_structure.begin_date%TYPE - дата начала действия
* @param p_end_date nsi_structure.end_date%TYPE - дата окончания действия
*/
PROCEDURE nsi_structure_period (
p_nsi_structure_id IN nsi_structure.nsi_structure_id%TYPE,
p_begin_date IN nsi_structure.begin_date%TYPE,
p_end_date IN nsi_structure.end_date%TYPE)
AS
v_nsi_id nsi_structure.nsi_id%TYPE;
v_nsi_type_id nsi_structure.nsi_type_id%TYPE;
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := get_type_id('nsi_structure');
SELECT nsi_id, nsi_type_id
INTO v_nsi_id, v_nsi_type_id
FROM nsi_structure
WHERE nsi_structure_id = p_nsi_structure_id;
UPDATE nsi_structure
SET begin_date = Trunc(p_begin_date),
end_date = Trunc(p_end_date)
WHERE nsi_structure_id = p_nsi_structure_id;
v_log_descr := '[' || get_nsi_descr(v_nsi_id, v_nsi_type_id) || '] ';
v_log_descr := v_log_descr || 'Период ' || p_begin_date || ' - ' || p_end_date;
log_oper (p_nsi_structure_id, v_type_id, NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
/* Реализует удаление записи.
* @param p_nsi_structure_id nsi_structure.nsi_structure_id%TYPE - ключ nsi_structure
*/
PROCEDURE nsi_structure_delete (p_nsi_structure_id IN nsi_structure.nsi_structure_id%TYPE)
AS
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_structure');
FOR rec IN (
SELECT nsi_structure_id, nsi_parent_structure_id,
nsi_id, nsi_type_id, ordnum, begin_date, end_date
FROM nsi_structure
START WITH nsi_structure_id = p_nsi_structure_id
CONNECT BY PRIOR nsi_structure_id = nsi_parent_structure_id
)
LOOP
v_log_descr := '[' || pkg_nsi.get_nsi_descr(rec.nsi_id, rec.nsi_type_id) || '] ';
v_log_descr := v_log_descr || 'Период ' || rec.begin_date || ' - ' || rec.end_date;
pkg_nsi.log_oper (rec.nsi_structure_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END LOOP;
DELETE FROM nsi_structure
WHERE nsi_structure_id = p_nsi_structure_id;
END;
После появления такого инструмента можно смело выстраивать отношения между организациями.
declare
id number;
root_id number;
begin
root_id := pkg_nsi.nsi_structure_insert(null, 1, 1, null, to_date('13.02.20', 'dd.mm.yy'), null);
id := pkg_nsi.nsi_structure_insert(root_id, 281, 1, null, to_date('13.02.20', 'dd.mm.yy'), to_date('15.02.20', 'dd.mm.yy'));
id := pkg_nsi.nsi_structure_insert(root_id, 283, 1, null, to_date('13.02.20', 'dd.mm.yy'), null);
id := pkg_nsi.nsi_structure_insert(id, 285, 1, null, to_date('13.02.20', 'dd.mm.yy'), null);
end;
SELECT *
FROM nsi_structure
START WITH (nsi_parent_structure_id IS NULL)
CONNECT BY (nsi_parent_structure_id = PRIOR nsi_structure_id);
-----------------------------------------------------------------------------------
316 1 1 13.02.20
318 316 281 1 13.02.20 15.02.20
320 316 283 1 13.02.20
322 320 285 1 13.02.20
-----------------------------------------------------------------------------------
-- а если необходимо задать определённую сортировку
begin
pkg_nsi.nsi_structure_ordnum(320, 1);
pkg_nsi.nsi_structure_ordnum(318, 2);
end;
SELECT *
FROM nsi_structure
START WITH (nsi_parent_structure_id IS NULL)
CONNECT BY (nsi_parent_structure_id = PRIOR nsi_structure_id)
ORDER SIBLINGS BY ordnum;
-----------------------------------------------------------------------------------
316 1 1 13.02.20
320 316 283 1 1 13.02.20
322 320 285 1 13.02.20
318 316 281 1 2 13.02.20 15.02.20
-----------------------------------------------------------------------------------
-- изменим периоды действия подразделения
begin
pkg_nsi.nsi_structure_period(320, to_date('14.02.20', 'dd.mm.yy'), to_date('14.02.20', 'dd.mm.yy'));
end;
SELECT *
FROM nsi_structure
START WITH (nsi_parent_structure_id IS NULL)
CONNECT BY (nsi_parent_structure_id = PRIOR nsi_structure_id)
ORDER SIBLINGS BY ordnum;
-----------------------------------------------------------------------------------
316 1 1 13.02.20
320 316 283 1 1 14.02.20 14.02.20
322 320 285 1 13.02.20
318 316 281 1 2 13.02.20 15.02.20
-----------------------------------------------------------------------------------
Так как справочники отделены от структуры, то каждый раз обращаться к организациям с учётом их отношений становится грамозко, поэтому немного упростим себе жизнь.
CREATE OR REPLACE VIEW V_NSI_ORGANIZATION AS
SELECT
s.nsi_structure_id, s.nsi_parent_structure_id,
s.ordnum, s.begin_date, s.end_date,
s.nsi_id, s.nsi_type_id, o.name, o.full_name, o.inn
FROM nsi_structure s INNER JOIN nsi_organization o
ON (s.nsi_id = o.nsi_id)
START WITH (nsi_parent_structure_id IS NULL)
CONNECT BY (nsi_parent_structure_id = PRIOR nsi_structure_id)
ORDER SIBLINGS BY ordnum;
SELECT * FROM v_nsi_organization;
-----------------------------------------------------------------------------------
316 13.02.20 1 1 АО "Рога и копыта" Акционерное общество "Рога и копыта" 99887766
320 316 1 14.02.20 14.02.20 283 1 Подразделение по обслуживанию копыт Подразделение по обслуживанию копыт 2222222222
322 320 13.02.20 285 1 Отдел по изготовлению подков Отдел по изготовлению подков 3333333333
318 316 2 13.02.20 15.02.20 281 1 Подразделение по обслуживанию рогов Подразделение по обслуживанию рогов 1111111111
-----------------------------------------------------------------------------------
То, что мы строим дерево это замечательно, но все узлы этого дерева относятся к одной сущности, а наша задача реализовать построение отношения между разными сущностями. Это тоже не проблема, потому как структура не завязывается на какой-то определённый справочник, а работает в целом на всей системе НСИ. Для примера построим классификатор для должностей государственной гражданской службы и классификатор для должностей муниципалитета.
CREATE TABLE nsi_classifier (
nsi_id NUMBER(10) NOT NULL,
code VARCHAR2(10),
name VARCHAR2(200) NOT NULL,
CONSTRAINT nsi_classifier_pk PRIMARY KEY (nsi_id),
CONSTRAINT nsi_classifier_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id)
);
COMMENT ON TABLE nsi_classifier IS 'НСИ. Классификатор';
COMMENT ON COLUMN nsi_classifier.nsi_id IS 'Ключ';
COMMENT ON COLUMN nsi_classifier.code IS 'Код';
COMMENT ON COLUMN nsi_classifier.name IS 'Наименование';
CREATE OR REPLACE TRIGGER nsi_classifier_trg_insert
BEFORE INSERT ON nsi_classifier FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_classifier');
:NEW.nsi_id := pkg_nsi.get_nsi_id();
INSERT INTO nsi (nsi_id, nsi_type_id, descr, create_date, modif_date, begin_date)
VALUES (:NEW.nsi_id, v_type_id, :NEW.name, Trunc(Sysdate), Trunc(Sysdate), Trunc(Sysdate));
v_log_query := 'SELECT ''' || :NEW.name || ''' AS name, ''' || :NEW.code || ''' AS code FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_INSERT, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_classifier_trg_update
BEFORE UPDATE ON nsi_classifier FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_classifier');
UPDATE nsi
SET modif_date = Trunc(Sysdate)
WHERE nsi_id = :NEW.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :NEW.name || ''' AS name, ''' || :NEW.code || ''' AS code FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_classifier_trg_delete
AFTER DELETE ON nsi_classifier FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_classifier');
DELETE FROM nsi_history
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi_attribute
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :OLD.name || ''' AS name, ''' || :OLD.code || ''' AS code FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:OLD.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END;
CREATE TABLE nsi_post_group (
nsi_id NUMBER(10) NOT NULL,
name VARCHAR2(50) NOT NULL,
CONSTRAINT nsi_post_group_pk PRIMARY KEY (nsi_id),
CONSTRAINT nsi_post_group_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id)
);
COMMENT ON TABLE nsi_post_group is 'НСИ. Группа должности';
COMMENT ON COLUMN nsi_post_group.nsi_id is 'Ключ';
COMMENT ON COLUMN nsi_post_group.name is 'Наименование';
CREATE OR REPLACE TRIGGER nsi_post_group_trg_insert
BEFORE INSERT ON nsi_post_group FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post_group');
:NEW.nsi_id := pkg_nsi.get_nsi_id();
INSERT INTO nsi (nsi_id, nsi_type_id, descr, create_date, modif_date, begin_date)
VALUES (:NEW.nsi_id, v_type_id, :NEW.name, Trunc(Sysdate), Trunc(Sysdate), Trunc(Sysdate));
v_log_query := 'SELECT ''' || :NEW.name || ''' AS name FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_INSERT, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_post_group_trg_update
BEFORE UPDATE ON nsi_post_group FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post_group');
UPDATE nsi
SET modif_date = Trunc(Sysdate)
WHERE nsi_id = :NEW.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :NEW.name || ''' AS name FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_post_group_trg_delete
AFTER DELETE ON nsi_post_group FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post_group');
DELETE FROM nsi_history
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi_attribute
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :OLD.name || ''' AS name FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:OLD.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END;
CREATE TABLE nsi_post_category (
nsi_id NUMBER(10) NOT NULL,
name VARCHAR2(50) NOT NULL,
CONSTRAINT nsi_post_category_pk PRIMARY KEY (nsi_id),
CONSTRAINT nsi_post_category_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id)
);
COMMENT ON TABLE nsi_post_category is 'НСИ. Категория должности';
COMMENT ON COLUMN nsi_post_category.nsi_id is 'Ключ';
COMMENT ON COLUMN nsi_post_category.name is 'Наименование';
CREATE OR REPLACE TRIGGER nsi_post_category_trg_insert
BEFORE INSERT ON nsi_post_category FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post_category');
:NEW.nsi_id := pkg_nsi.get_nsi_id();
INSERT INTO nsi (nsi_id, nsi_type_id, descr, create_date, modif_date, begin_date)
VALUES (:NEW.nsi_id, v_type_id, :NEW.name, Trunc(Sysdate), Trunc(Sysdate), Trunc(Sysdate));
v_log_query := 'SELECT ''' || :NEW.name || ''' AS name FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_INSERT, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_post_category_trg_update
BEFORE UPDATE ON nsi_post_category FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post_category');
UPDATE nsi
SET modif_date = Trunc(Sysdate)
WHERE nsi_id = :NEW.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :NEW.name || ''' AS name FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_post_category_trg_delete
AFTER DELETE ON nsi_post_category FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post_category');
DELETE FROM nsi_history
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi_attribute
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :OLD.name || ''' AS name FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:OLD.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END;
CREATE TABLE nsi_post (
nsi_id NUMBER(10) NOT NULL,
code_OKPDTR VARCHAR2(10),
name VARCHAR2(50) NOT NULL,
CONSTRAINT nsi_post_pk PRIMARY KEY (nsi_id),
CONSTRAINT nsi_post_nsi_fk FOREIGN KEY (nsi_id) REFERENCES nsi (nsi_id)
);
COMMENT ON TABLE nsi_post IS 'НСИ. Должность';
COMMENT ON COLUMN nsi_post.nsi_id IS 'Ключ';
COMMENT ON COLUMN nsi_post.code_OKPDTR IS 'Код ОКПДТР';
COMMENT ON COLUMN nsi_post.name IS 'Наименование';
CREATE OR REPLACE TRIGGER nsi_post_trg_insert
BEFORE INSERT ON nsi_post FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post');
:NEW.nsi_id := pkg_nsi.get_nsi_id();
INSERT INTO nsi (nsi_id, nsi_type_id, descr, create_date, modif_date, begin_date)
VALUES (:NEW.nsi_id, v_type_id, :NEW.name, Trunc(Sysdate), Trunc(Sysdate), Trunc(Sysdate));
v_log_query := 'SELECT ''' || :OLD.name || ''' AS name, ''' || :OLD.code_OKPDTR || ''' AS code_OKPDTR FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_INSERT, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_post_trg_update
BEFORE UPDATE ON nsi_post FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post');
UPDATE nsi
SET modif_date = Trunc(Sysdate)
WHERE nsi_id = :NEW.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :OLD.name || ''' AS name, ''' || :OLD.code_OKPDTR || ''' AS code_OKPDTR FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:NEW.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
CREATE OR REPLACE TRIGGER nsi_post_trg_delete
AFTER DELETE ON nsi_post FOR EACH ROW
DECLARE
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
v_log_query VARCHAR(4000);
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_post');
DELETE FROM nsi_history
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi_attribute
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
DELETE FROM nsi
WHERE nsi_id = :OLD.nsi_id
AND nsi_type_id = v_type_id;
v_log_query := 'SELECT ''' || :OLD.name || ''' AS name, ''' || :OLD.code_OKPDTR || ''' AS code_OKPDTR FROM DUAL';
v_log_descr := pkg_nsi.get_json(v_log_query);
pkg_nsi.log_oper (:OLD.nsi_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END;
Осталось только заполнить и собрать необходимые классификаторы.
INSERT INTO nsi_classifier (name) VALUES ('Классификатор должностей ГГС');
INSERT INTO nsi_classifier (name) VALUES ('Классификатор должностей муниципалитета');
INSERT INTO nsi_post_group (name) VALUES ('Высшие');
INSERT INTO nsi_post_group (name) VALUES ('Главные');
INSERT INTO nsi_post_group (name) VALUES ('Ведущие');
INSERT INTO nsi_post_group (name) VALUES ('Старшие');
INSERT INTO nsi_post_group (name) VALUES ('Младшие');
INSERT INTO nsi_post_category (name) VALUES ('Руководители');
INSERT INTO nsi_post_category (name) VALUES ('Помощники (советники)');
INSERT INTO nsi_post_category (name) VALUES ('Специалисты');
INSERT INTO nsi_post_category (name) VALUES ('Обеспечивающие специалист');
INSERT INTO nsi_post (code_OKPDTR, name)
VALUES ('24742', 'Начальник отдела');
INSERT INTO nsi_post (code_OKPDTR, name)
VALUES ('26480', 'Советник');
INSERT INTO nsi_post (code_OKPDTR, name)
VALUES ('23509', 'Консультант');
INSERT INTO nsi_post (code_OKPDTR, name)
VALUES ('20419', 'Ведущий специалист');
INSERT INTO nsi_post (code_OKPDTR, name)
VALUES ('26541', 'Специалист');
INSERT INTO nsi_post (code_OKPDTR, name)
VALUES ('26544', 'Специалист 2 разряда');
commit;
declare
post_id number;
classif_id number;
categ_id number;
group_id number;
begin
-- Классификатор должностей ГГС
classif_id := pkg_nsi.nsi_structure_insert(null, 331, 5, null, to_date('13.02.20', 'dd.mm.yy'), null);
-- Руководители
categ_id := pkg_nsi.nsi_structure_insert(classif_id, 347, 4, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Высшие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 355, 3, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Главные
group_id := pkg_nsi.nsi_structure_insert(categ_id, 357, 3, 2, to_date('13.02.20', 'dd.mm.yy'), null);
-- Начальник отдела
post_id := pkg_nsi.nsi_structure_insert(group_id, 335, 2, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Ведущие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 359, 3, 3, to_date('13.02.20', 'dd.mm.yy'), null);
-- Помощники (советники)
categ_id := pkg_nsi.nsi_structure_insert(classif_id, 349, 4, 2, to_date('13.02.20', 'dd.mm.yy'), null);
-- Высшие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 355, 3, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Главные
group_id := pkg_nsi.nsi_structure_insert(categ_id, 357, 3, 2, to_date('13.02.20', 'dd.mm.yy'), null);
-- Ведущие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 359, 3, 3, to_date('13.02.20', 'dd.mm.yy'), null);
-- Советник
post_id := pkg_nsi.nsi_structure_insert(group_id, 337, 2, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Специалисты
categ_id := pkg_nsi.nsi_structure_insert(classif_id, 351, 4, 3, to_date('13.02.20', 'dd.mm.yy'), null);
-- Высшие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 355, 3, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Главные
group_id := pkg_nsi.nsi_structure_insert(categ_id, 357, 3, 2, to_date('13.02.20', 'dd.mm.yy'), null);
-- Ведущие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 359, 3, 3, to_date('13.02.20', 'dd.mm.yy'), null);
-- Ведущий специалист
post_id := pkg_nsi.nsi_structure_insert(group_id, 341, 2, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Старшие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 361, 3, 4, to_date('13.02.20', 'dd.mm.yy'), null);
-- Обеспечивающие специалист
categ_id := pkg_nsi.nsi_structure_insert(classif_id, 353, 4, 4, to_date('13.02.20', 'dd.mm.yy'), null);
-- Главные
group_id := pkg_nsi.nsi_structure_insert(categ_id, 357, 3, 1, to_date('13.02.20', 'dd.mm.yy'), null);
-- Ведущие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 359, 3, 2, to_date('13.02.20', 'dd.mm.yy'), null);
-- Старшие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 361, 3, 3, to_date('13.02.20', 'dd.mm.yy'), null);
-- Младшие
group_id := pkg_nsi.nsi_structure_insert(categ_id, 363, 3, 4, to_date('13.02.20', 'dd.mm.yy'), null);
-- Специалист 2 разряда
post_id := pkg_nsi.nsi_structure_insert(group_id, 345, 2, 1, to_date('13.02.20', 'dd.mm.yy'), null);
commit;
end;
SELECT *
FROM nsi_structure s
START WITH (nsi_id = 331)
CONNECT BY (nsi_parent_structure_id = PRIOR nsi_structure_id)
ORDER SIBLINGS BY ordnum;
----------------------------------------------------------------------------------
"NSI_STRUCTURE_ID" "NSI_PARENT_STRUCTURE_ID" "NSI_ID" "NSI_TYPE_ID" "ORDNUM" "BEGIN_DATE" "END_DATE"
385 331 5 13.02.20
387 385 347 4 1 13.02.20
389 387 355 3 1 13.02.20
391 387 357 3 2 13.02.20
393 391 335 2 1 13.02.20
395 387 359 3 3 13.02.20
397 385 349 4 2 13.02.20
399 397 355 3 1 13.02.20
401 397 357 3 2 13.02.20
403 397 359 3 3 13.02.20
405 403 337 2 1 13.02.20
407 385 351 4 3 13.02.20
409 407 355 3 1 13.02.20
411 407 357 3 2 13.02.20
413 407 359 3 3 13.02.20
415 413 341 2 1 13.02.20
417 407 361 3 4 13.02.20
419 385 353 4 4 13.02.20
421 419 357 3 1 13.02.20
423 419 359 3 2 13.02.20
425 419 361 3 3 13.02.20
427 419 363 3 4 13.02.20
429 427 345 2 1 13.02.20
----------------------------------------------------------------------------------
Ой, как это не читабельно!
CREATE OR REPLACE VIEW V_NSI_CLASSIFIRE_GGS AS
SELECT
s.nsi_structure_id, s.nsi_parent_structure_id,
s.ordnum, s.begin_date, s.end_date,
n.nsi_id, n.nsi_type_id, n.descr
FROM nsi_structure s INNER JOIN nsi n
ON (s.nsi_id = n.nsi_id)
START WITH (s.nsi_id = 331)
CONNECT BY (nsi_parent_structure_id = PRIOR nsi_structure_id)
ORDER SIBLINGS BY ordnum;
SELECT * FROM V_NSI_CLASSIFIRE_GGS ;
----------------------------------------------------------------------------------
"NSI_STRUCTURE_ID" "NSI_PARENT_STRUCTURE_ID" "NSI_ID" "NSI_TYPE_ID" "ORDNUM" "BEGIN_DATE" "END_DATE"
385 13.02.20 331 5 Классификатор должностей ГГС
387 385 1 13.02.20 347 4 Руководители
389 387 1 13.02.20 355 3 Высшие
391 387 2 13.02.20 357 3 Главные
393 391 1 13.02.20 335 2 Начальник отдела
395 387 3 13.02.20 359 3 Ведущие
397 385 2 13.02.20 349 4 Помощники (советники)
399 397 1 13.02.20 355 3 Высшие
401 397 2 13.02.20 357 3 Главные
403 397 3 13.02.20 359 3 Ведущие
405 403 1 13.02.20 337 2 Советник
407 385 3 13.02.20 351 4 Специалисты
409 407 1 13.02.20 355 3 Высшие
411 407 2 13.02.20 357 3 Главные
413 407 3 13.02.20 359 3 Ведущие
415 413 1 13.02.20 341 2 Ведущий специалист
417 407 4 13.02.20 361 3 Старшие
419 385 4 13.02.20 353 4 Обеспечивающие специалист
421 419 1 13.02.20 357 3 Главные
423 419 2 13.02.20 359 3 Ведущие
425 419 3 13.02.20 361 3 Старшие
427 419 4 13.02.20 363 3 Младшие
429 427 1 13.02.20 345 2 Специалист 2 разряда
----------------------------------------------------------------------------------
Следует не забывать, что кроме отношения включения (в том числе и древовидного), существует отношение пересечения, то есть кросс-таблиц. Здесь добавляется дополнительное условие проверки пересечения по времени.
CREATE TABLE nsi_cross (
nsi_cross_id NUMBER(10) NOT NULL,
nsi_main_id NUMBER(10) NOT NULL,
nsi_main_type_id NUMBER(10) NOT NULL,
nsi_detail_id NUMBER(10) NOT NULL,
nsi_detail_type_id NUMBER(10) NOT NULL,
begin_date DATE NOT NULL,
end_date DATE,
CONSTRAINT nsi_cross_pk PRIMARY KEY (nsi_cross_id),
CONSTRAINT nsi_cross_main_nsi_fk FOREIGN KEY (nsi_main_type_id, nsi_main_id) REFERENCES nsi (nsi_type_id, nsi_id),
CONSTRAINT nsi_cross_detail_nsi_fk FOREIGN KEY (nsi_detail_type_id, nsi_detail_id) REFERENCES nsi (nsi_type_id, nsi_id)
);
COMMENT ON TABLE nsi_cross IS 'НСИ. Кросс-таблица справочников';
COMMENT ON COLUMN nsi_cross.nsi_cross_id IS 'Ключ';
COMMENT ON COLUMN nsi_cross.nsi_main_id IS 'Ключ основной таблицы';
COMMENT ON COLUMN nsi_cross.nsi_main_type_id IS 'Тип справочника основной таблицы';
COMMENT ON COLUMN nsi_cross.nsi_detail_id IS 'Ключ таблици детализации';
COMMENT ON COLUMN nsi_cross.nsi_detail_type_id IS 'Тип справочника таблици детализации';
COMMENT ON COLUMN nsi_cross.begin_date IS 'Дата начала действия';
COMMENT ON COLUMN nsi_cross.end_date IS 'Дата окончания действия';
/* Реализует проверку на пересечения периода действия записи кросс-таблицы.
* @param p_nsi_main_id nsi_cross.nsi_main_id%TYPE - справочник основной записи
* @param p_nsi_main_type_id nsi_cross.nsi_main_type_id%TYPE - тип справочника основной записи
* @param p_nsi_detail_id nsi_cross.nsi_detail_id%TYPE - справочник записи детализации
* @param p_nsi_detail_type_id nsi_cross.nsi_detail_type_id%TYPE - ттип справочника записи детализации
* @param p_begin_date DATE - дата начала действия записи
* @param p_end_date DATE - дата окончания действия записи
*/
PROCEDURE nsi_cross_check_period (
p_nsi_cross_id IN nsi_cross.nsi_cross_id%TYPE,
p_begin_date IN nsi_cross.begin_date%TYPE,
p_end_date IN nsi_cross.end_date%TYPE)
AS
v_cnt NUMBER;
v_nsi_main_id nsi_cross.nsi_main_id%TYPE;
v_nsi_main_type_id nsi_cross.nsi_main_type_id%TYPE;
v_nsi_detail_id nsi_cross.nsi_detail_id%TYPE;
v_nsi_detail_type_id nsi_cross.nsi_detail_type_id%TYPE;
BEGIN
IF (p_end_date IS NOT NULL) AND (Trunc(p_begin_date) > Trunc(p_end_date)) THEN
RAISE_APPLICATION_ERROR (NSI_ERROR_CODE,
'[nsi_cross_check_period] Дата начала не может быть больше даты окончания ' || Trunc(p_begin_date) || ' - ' || Trunc(p_end_date));
END IF;
SELECT MIN(nsi_main_id), MIN(nsi_main_type_id),
MIN(nsi_detail_id), MIN(nsi_detail_type_id)
INTO v_nsi_main_id, v_nsi_main_type_id,
v_nsi_detail_id, v_nsi_detail_type_id
FROM nsi_cross
WHERE nsi_cross_id = p_nsi_cross_id;
v_cnt := 0;
IF (v_nsi_main_id IS NOT NULL) THEN
IF (p_end_date IS NOT NULL) THEN
SELECT COUNT(*)
INTO v_cnt
FROM nsi_cross
WHERE nsi_main_id = v_nsi_main_id
AND nsi_main_type_id = v_nsi_main_type_id
AND nsi_detail_id = v_nsi_detail_id
AND nsi_detail_type_id = v_nsi_detail_type_id
AND nsi_cross_id <> p_nsi_cross_id
AND begin_date <= Trunc(p_end_date)
AND ((end_date IS NULL) OR (end_date >= Trunc(p_end_date)));
ELSE
SELECT COUNT(*)
INTO v_cnt
FROM nsi_cross
WHERE nsi_main_id = v_nsi_main_id
AND nsi_main_type_id = v_nsi_main_type_id
AND nsi_detail_id = v_nsi_detail_id
AND nsi_detail_type_id = v_nsi_detail_type_id
AND nsi_cross_id <> p_nsi_cross_id
AND ((
(end_date IS NOT NULL) AND (end_date >= Trunc(p_begin_date))
) OR (end_date IS NULL)
);
END IF;
END IF;
IF (v_cnt > 0) THEN
RAISE_APPLICATION_ERROR (NSI_ERROR_CODE,
'[nsi_cross_check_period] Присутствует пересечение с периодом ' || p_begin_date || ' - ' || p_end_date);
END IF;
END;
/* Реализует вставку записи.
* @param p_nsi_main_id nsi_cross.nsi_main_id%TYPE - справочник основной записи
* @param p_nsi_main_type_id nsi_cross.nsi_main_type_id%TYPE - тип справочника основной записи
* @param p_nsi_detail_id nsi_cross.nsi_detail_id%TYPE - справочник записи детализации
* @param p_nsi_detail_type_id nsi_cross.nsi_detail_type_id%TYPE - ттип справочника записи детализации
* @param p_begin_date DATE - дата начала действия записи
* @param p_end_date DATE - дата окончания действия записи
*/
PROCEDURE nsi_cross_insert (
p_nsi_main_id IN nsi_cross.nsi_main_id%TYPE,
p_nsi_main_type_id IN nsi_cross.nsi_main_type_id%TYPE,
p_nsi_detail_id IN nsi_cross.nsi_detail_id%TYPE,
p_nsi_detail_type_id IN nsi_cross.nsi_detail_type_id%TYPE,
p_begin_date IN nsi_cross.begin_date%TYPE,
p_end_date IN nsi_cross.end_date%TYPE)
AS
v_id NUMBER;
v_log_descr nsi_log.descr%TYPE;
v_type_id nsi.nsi_type_id%TYPE;
BEGIN
v_id := get_nsi_id;
v_type_id := get_type_id('nsi_cross');
INSERT INTO nsi_cross (
nsi_cross_id, nsi_main_id, nsi_main_type_id,
nsi_detail_id, nsi_detail_type_id,
begin_date, end_date)
VALUES (
v_id, p_nsi_main_id, p_nsi_main_type_id,
p_nsi_detail_id, p_nsi_detail_type_id,
Trunc(p_begin_date), Trunc(p_end_date));
nsi_cross_check_period (v_id, p_begin_date, p_end_date);
v_log_descr := '[' || get_nsi_descr(p_nsi_main_id, p_nsi_main_type_id) || ' <=> ' || get_nsi_descr(p_nsi_detail_id, p_nsi_detail_type_id) || '] ';
v_log_descr := v_log_descr || 'Период ' || p_begin_date || ' - ' || p_end_date;
log_oper (v_id, v_type_id, NSI_LOG_OPERNUM_INSERT, v_log_descr);
END nsi_cross_insert;
/* Реализует обновление периода действия записи.
* @param p_nsi_cross_id nsi_cross.nsi_cross_id%TYPE - ключ nsi_cross
* @param p_begin_date nsi_cross.begin_date%TYPE - дата начала действия
* @param p_end_date nsi_cross.end_date%TYPE - дата окончания действия
*/
PROCEDURE nsi_cross_period (
p_nsi_cross_id IN nsi_cross.nsi_cross_id%TYPE,
p_begin_date IN nsi_cross.begin_date%TYPE,
p_end_date IN nsi_cross.end_date%TYPE)
AS
v_main_id nsi_cross.nsi_main_id%TYPE;
v_main_type_id nsi_cross.nsi_main_type_id%TYPE;
v_detail_id nsi_cross.nsi_detail_id%TYPE;
v_detail_type_id nsi_cross.nsi_detail_type_id%TYPE;
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := get_type_id('nsi_cross');
SELECT nsi_main_id, nsi_main_type_id,
nsi_detail_id, nsi_detail_type_id
INTO v_main_id, v_main_type_id,
v_detail_id, v_detail_type_id
FROM nsi_cross
WHERE nsi_cross_id = p_nsi_cross_id;
nsi_cross_check_period (p_nsi_cross_id, p_begin_date, p_end_date);
UPDATE nsi_cross
SET begin_date = Trunc(p_begin_date),
end_date = Trunc(p_end_date)
WHERE nsi_cross_id = p_nsi_cross_id;
v_log_descr := '[' || get_nsi_descr(v_main_id, v_main_type_id) || ' <=> ' || get_nsi_descr(v_detail_id, v_detail_type_id) || '] ';
v_log_descr := v_log_descr || 'Период ' || p_begin_date || ' - ' || p_end_date;
log_oper (p_nsi_cross_id, v_type_id, NSI_LOG_OPERNUM_UPDATE, v_log_descr);
END;
/* Реализует удаление записи.
* @param p_nsi_cross_id nsi_cross.nsi_cross_id%TYPE - ключ nsi_cross
*/
PROCEDURE nsi_cross_delete (p_nsi_cross_id IN nsi_cross.nsi_cross_id%TYPE)
AS
v_type_id nsi.nsi_type_id%TYPE;
v_log_descr nsi_log.descr%TYPE;
BEGIN
v_type_id := pkg_nsi.get_type_id('nsi_cross');
FOR rec IN (
SELECT nsi_cross_id, nsi_main_id, nsi_main_type_id,
nsi_detail_id, nsi_detail_type_id,
begin_date, end_date
FROM nsi_cross
WHERE nsi_cross_id = p_nsi_cross_id
)
LOOP
v_log_descr := '[' || pkg_nsi.get_nsi_descr(rec.nsi_main_id, rec.nsi_main_type_id) || ' <=> ' || pkg_nsi.get_nsi_descr(rec.nsi_detail_id, rec.nsi_detail_type_id) || '] ';
v_log_descr := v_log_descr || 'Период ' || rec.begin_date || ' - ' || rec.end_date;
pkg_nsi.log_oper (rec.nsi_cross_id, v_type_id, pkg_nsi.NSI_LOG_OPERNUM_DELETE, v_log_descr);
END LOOP;
DELETE FROM nsi_cross
WHERE nsi_cross_id = p_nsi_cross_id;
END;
Всё, теперь мы с уверенностью можем сказать, что закрыли первую проблему.
Конечно можно много чего пытаться прикрутить к этой системе, но я думаю, что поставленную задачу в начале статьи я выполнила, а остальное уже можно рассмотреть в процессе дискуссии.
Материал подготавливался на версии Oracle 18c, хотя нативное поддержание формата json уже присутствует в версии 12. Здесь ссылка с архивом скриптов.
