и все проблемы в ней не вымышленные. (С)
В начале хотелось бы отметить, что статья не призвана показать изобретение велосипеда, потому как многие приёмы уже давно существуют в культуре разработки баз данных. Однако обобщить, проанализировать проблемы, которые они могут решить и показать, как с ними можно работать. А проблем хватает несмотря на то, что нормативно-справочная информация (НСИ) не относится к бизнес-логике, а скорее находится в обслуживании у неё. Стандартный процесс по рисованию очередной таблички для хранения справочника очень скоро начинает обрастать костылями или трудоёмкими переделками.
Вот и в моём случае оказалась та же картина — система стоит на продуктиве более десяти лет, строилась по тому же принципу, если что нужно, рисуем и включаем в оборот. Таким образом были созданы несколько таблиц для хранения разного рода оборудования. Но вот пришёл час Х, когда стало необходимо добавить ещё пару таблиц для нового оборудования и при этом все (включая старые) должны входить в определённую группу. Это значит, что ссылки на разные таблицы должны быть включены в кросс-таблицу между группой и всеми пятью видами оборудования, то есть для каждого своё поля с констреинтом на соответствующую таблицу. А если ещё одно добавится, менять структуру. И обработку нужно делать в зависимости от того, какие поля заполнены. Вот и возникает первая проблема, как разные таблицы обобщить, что бы с ними одинаково можно было работать и не менять структуру, если добавляется ещё одна. Замечательная мысль, создаём отдельную табличку, которая призвана хранить абстрактное понятие оборудование с указанием типа, а тогда остальные таблички ссылаются по внешнему ключу на своего родителя. На этой радостной волне мы заливаем в созданную табличку записи из одной и пытаемся тоже сделать для другой. Но что-то пошло не так, сработало ограничение первичного ключа, к чему бы это? А к тому, что на заре бурной молодости системы для каждой табличке были свои сиквенсы. Конечно, со временем это безобразие поправили, но старые ключи всё равно остались. Более того, они корнями проросли по внешним ключам с другими таблицам. Фиксируем вторую проблему, связанную со сквозной нумерацией всех справочников.
На этом мучения с таблицами оборудования не закончились. Потому как по последним требованиям оборудование имеет различные характеристики, более того их число переменно, а одна характеристика может иметь несколько значений. А значит появляется третья проблема, а именно иметь возможность хранить переменное число характеристик какой-то записи.
Вроде как с этим справились, но заказчик не дремлет, у него всегда есть наготове что-нибудь новенькое. И вот приходит требование — все справочники историчные (например, название продукта было одним, а потом его переименовали, и по документам на разные даты нужно показывать актуальное название). Само по себе требование нормальное, ничего не скажешь. А если ещё в отделе разработки есть кто-то, кто проходит испытательный срок, так вообще всё в шоколаде, можно и не заметить, что это проблема. Однако проходит всё, как обычно — с полным авралом, а тут ещё этим нужно заниматься. Создаём таблички, дублирующие таблицы соответствующих справочников для того, чтобы там хранить хронологию изменений справочника. Но, создавая эти таблицы, мы заодно создаём себе четвёртую проблему, теперь в коде нужно в зависимости от даты обращаться то ли к основной таблице, то ли к исторической.
Ну мы же молодцы, мы и это победили))) Теперь, попивая чай из своей кружки, начинаешь дискутировать с другими коллегами на тему, что им приходилось решать, и понимаешь, что список проблем пополняется. В обсуждении стоит вопрос как хранить версии одной и той же записи. Хочу оговорится, что версия, это не то, что укладывается в таблицу историчности. В историчности понятно, до такого-то числа было одно название, а начиная с этой даты актуальным становится другое. А в версионности подразумевается, что запись была сначала сохранена с ошибкой, а через несколько часов это поняли и её изменили, и нужно знать все состояния этой записи. Во-первых, здесь должно быть дробление на время, не только сутки. А во-вторых, такие следы нужны в случае разборок. Например, заполняли прайс, ошиблись, успели товар продать по такой цене, а потом поправили, но в конце дня случился дебаланс. Однако решение для таких ситуаций меня лично напрягло, предлагалась все такие изменения хранить в самой таблице. Не буду устраивать холивар на сколько так правильно, но для меня точно обозначилась пятая проблема, а именно хранение изменений записей.
Итак, обобщая вышесказанное мы видим перед собой пять увесистых грабель. Теперь наша задача определить стратегию, позволяющую обойти и не наступить на них.
Сколько можно наступать на одни и те же грабли, давайте скинимся и купим новые
Начиная проектировать систему с нуля, никто не может предугадать путь её развития, а значит не сможет сказать на каком уровне придётся обобщать, как в описанном примере с оборудованием. Поэтому имеет смысл сразу задать абстрактную сущность, распространяемую на все таблицы НСИ. Таким образом все справочники будут иметь прообраз в едином справочнике с разделением на типы.
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. Здесь ссылка с архивом скриптов.
aborouhin
Я заранее извиняюсь, если чего-то не понимаю в Ораклах и прочих больших системах за многоденег, — но зачем эту задачу решать именно так? Тащить бизнес-логику в базу данных? Писать портянки кода на процедурном языке с кучей вложенных конструкций? Использовать паттерн EAV, который только ленивый ещё не пнул? Это же антипаттерн на антипаттерне.
Почему не сделать какой-нибудь NSIService со своим storage? Раз нужна версионность и история изменений — просто напрашивается Event Sourcing и CQRS...
LadyShack Автор
Вы ошибаетесь, предложенная система вообще не зависит от бизнес-логике, а содержит голую структуру. И чем она будет наполнена зависит уже от разрабатываемой системы.
Что касается EAV, то я тоже не сторонник пихать его везде, где только можно, поэтому использую его для поддержания очень незначительной функциональности. При этом я не утверждаю, что такое решение самое-самое, это одно из возможных, кто-то может предложить другое исходя из потребности системы, которую он ведёт.
Хранилище, это замечательная идея, а если к ней добавить еще Event Sourcing и CQRS, то простынки кода на поддержание этой инфраструктуры будут никак не меньше, того, что здесь описано. И выгоды по критерию «многоденег» вряд ли удастся получить.
Любая система применима к определённым условиям, и то, что вы говорите, не есть альтернатива, а просто другая потребность. Когда есть необходимость создать хранилище, тогда стоит говорить каким образом это делать.
aborouhin
Где начинается бизнес-логика — вопрос философский… как мне кажется, если хранимая процедура/функция что-то знает о сущности предметной области (и пишется именно для organization, а не какого-нибудь для абстрактного nsi_item) — это уже в полный рост бизнес-логика. Впрочем, спорить не буду.
Что касается большего количества кода для написания отдельного сервиса с Event Sourcing / CQRS — безусловно, его будет больше. Но он будет лучше читаем, отлаживаем, тестируем, масштабируем, переносим, при необходимости может быть использован другими системами, которые не работают с этой БД, и т.д. и т.п.
При этом я догадываюсь, что вся эта предложенная мной красота может оказаться не подходящей в данном случае из-за каких-то критичных требований по производительности. О чём и был вопрос…
LadyShack Автор
А в чём проблема относится к таблице nsi_organization, как к nsi_item? Я показала для примера, и совершенно не утверждаю, что такоё подход нужен исключительно только для систем, где необходимо работа с организациями, подставьте на это место другой справочник, структура от этого не поменяется.
И, кстати, у вас заказчик сильно идёт на внедрение новых технологий (я имею ввиду хранилище данных)? Если да, то вам повезло))))
aborouhin
Мне вообще легко — я с некоторых пор сам себе заказчик. И с похожими проблемами сталкиваюсь для себя. Причём тот же НСИ в части организаций — это весь ЕГРЮЛ на 2 с лишним миллиона записей, с перспективой подключения зарубежных реестров. Поэтому искренне любопытно, что даст (в отсутствие внешних ограничений и вводных на выбор технологий) именно такое решение и не надо ли мне начинать копить деньги на Оракл :)
А проблема, как мне кажется, в том, что Ваше решение и так ужe гвоздями приколочено к Ораклу, а при увеличении количества таблиц для разных типов НСИ эта зависимость будет только ухудшаться. В итоге при замене Оракла на другую БД надо переписывать не точечно один коннектор, а очень много чего в других местах. В общем, не layered architecture совсем получается. А ситуации разные бывают: деньги на Оракл закончились, под санкции попали и Оракл сам отказал в лицензии, принудительно вынудили импортозаместиться, да, в конце концов, понадобилось применить наработки в другом месте, где Ораклом и не пахнет…
Ну и да, писать процедурный код со вложенными циклами, когда вокруг только и говорят, что о красоте и эффективности функционального программирования, просто не доставляет удовлетворения :)
LadyShack Автор
Ну если вы сами себе заказчик, тогда действительно легко))))
Начать копить на Оракле, чтобы доказать автору статьи, что его модель для вашего случая не подходит — сомнительная цель. Может быть начать с того, что определится чего вы хотите получить, а потом уже пытаться это решать. Я в статье обозначила это, а ваших комментариях я этого не вижу.
aborouhin
Помилуйте, я ничего не хочу никому доказать. Если бы я хотел поделиться своим опытом — я бы написал свою статью :) А комментарий я написал, чтобы получить знания, которых у меня нет.
И поскольку (а) в Вашей статье не содержалось обоснования, почему выбран именно Оракл и почему предложенный способ создания системы НСИ был реализован именно на хранимых функциях в нём, а (б) я не имею ни малейшего опыта работы с данной системой и (в) предложенный механизм показался мне сопряжённым со значительными ограничениями, — я и постарался уточнить причину такого выбора.
Если эта причина «Оракл уже был и корпоративная политика делать именно на нём» — ОК, это не мой случай. А если причина «так оно летает в 10 раз быстрее, чем если сделать на оперсорсных продуктах, сравнивали с такими-то и такими-то вариантами» — то про это действительно интересно узнать подробнее.
LadyShack Автор
Думаю, вы сами себе ответили на вопрос. Если бы я хотела дать в статье сравнительную оценку быстродействия для различных серверов БД, то наверное бы так и поступила. Да и заголовок статья был бы тоже другой. Поэтому похоже, что вы заблудились в поисках недостающих знаний.
Shurikh
1. Никогда, ни за что, ни в коем случае не используйтё EAV для создания системы управления НСИ. Сразу вспоминается пара лет, убитых на то, чтобы уйти с такой модели данных, когда упёрлись в полное отсутствие масштабируемости такого решения, а оно было растиражировано в дюжину систем у множества заказчиков, уже работавших в проде.
2. Что за стремление использовать при проектировании системы, нагрузка на чтение в которой на 2-3 порядка выше, чем нагрузка на запись (а почти любая система управления НСИ такова), использовать структуры данных, оптимизированные для быстрой записи, но создающие абсолютно неадекватную нагрузку при чтении?
3. Использование конструкций вида
для динамического SQL в Oracle это прям кровь из глаз и верный способ завалить собеседование и обратить на себя гнев DBA. Не надо так. Use bind variables, Luke!
LadyShack Автор
1. Не обобщайте. EAV был предложен исключительно для атрибутов и не более.
2. А чем так сильно увеличилась нагрузка для выборки на чтение из таблицы организации (select * from nsi_organization)?
Shurikh
Модель данных Adjacency List для представления иерархии именно что оптимизирована на запись в ущерб чтению — простая и интуитивно понятная модификация иерархии, но построение иерархии из таблиц в такой модели данных требует рекурсивных запросов, которые изрядно грузят CPU.
LadyShack Автор
Работа с иерархической структурой такая же необходимая вещь, как и с линейным списком. Практические все системы имеют такие справочники. И если вы создадите таблицу организаций, в которой укажите поле родителя, то вам тоже придётся для построения всех зависимостей обратиться к таблице деревянным запросом. И если для организаций глубина вложенности не будет особо большой (от силы порядка четырёх), то для построение структуры больших агрегатов, в которые включены более мелкие комплектующие, там глубина может исчисляться десятками.
Существуют различные способы упрощения жизни при работе с иерархией, например создать поле PATH, где будет храниться путь от корня и тогда выстроить запрос в линейный, использую оператор LIKE «1.23.%». Но это уже отдельная задача оптимизации работы с деревьями.
Shurikh
вы точно изучили теорию перед проектированием структуры данных? Nested sets и subsets не требуют рекурсивных запросов при построении, но да, сложнее в управлении и кодинге, чем наивное представление дерева через список смежности и уж точно эффективнее построения чего угодно с table access full и фильтрацией по строковому шаблону (тоже не самая дешёвая операция, между прочим).
Вообще статья вызвала ностальгию, вспомнилось хождение по граблям со справочниками в 2002-2008 годах. Пока что вы идёте ровно по тому же маршруту.
LadyShack Автор
Вы только что подтвердили то, что написала во втором абзаце: «Существуют различные способы упрощения жизни при работе с иерархией...». Nested sets — один из таких способов.
Anton_Zh
Ну CQRS тут точно ни к селу, ни к городу. А код да, слабочитабельный. Legacy система, что тут скажешь. Автор крутится, как может, ищет выход из положения.