Oracle-триггеры для создания строки истории при обновлении
во-первых, в настоящее время у нас есть желаемое поведение, но это не тривиально поддерживать, когда необходимы какие-либо изменения в базе данных. Я ищу что-то проще, эффективнее, и проще в обслуживании (все, что делает любой из этих 3 будет самым желанным). Когда мы выполняем обновление, создается строка истории, которая является копией настоящее row, а затем обновляются значения текущей строки. В результате у нас есть запись истории о том, как строка была до того, как он был обновлен.
рассуждения: мы должны соответствовать ряду федеральных правил, и пошел этот маршрут, чтобы иметь полную историю аудита всего, а также мы можем посмотреть на базу данных в любой момент времени и посмотреть, как все выглядело (будущее требование). по аналогичным причинам я не могу изменить способ записи истории...любое решение должно привести к тем же данным,что и текущие триггеры.
вот как выглядят текущие триггеры для Contact
стол:
(лишен бесполезных полей для краткости, количество полей не имеет значения)
перед обновлением (каждая строка):
DECLARE
indexnb number;
BEGIN
:new.date_modified := '31-DEC-9999';
indexnb := STATE_PKG.newCONTACTRows.count + 1;
:new.date_start := sysdate;
:new.version := :old.version + 1;
state_pkg.newCONTACTRows(indexnb).ID := :old.ID;
state_pkg.newCONTACTRows(indexnb).PREFIX := :old.PREFIX;
state_pkg.newCONTACTRows(indexnb).FIRST_NAME := :old.FIRST_NAME;
state_pkg.newCONTACTRows(indexnb).MIDDLE_NAME := :old.MIDDLE_NAME;
state_pkg.newCONTACTRows(indexnb).LAST_NAME := :old.LAST_NAME;
--Audit columns after this
state_pkg.newCONTACTRows(indexnb).OWNER := :old.OWNER;
state_pkg.newCONTACTRows(indexnb).LAST_USER := :old.LAST_USER;
state_pkg.newCONTACTRows(indexnb).DATE_CREATED := :old.DATE_CREATED;
state_pkg.newCONTACTRows(indexnb).DATE_MODIFIED := sysdate;
state_pkg.newCONTACTRows(indexnb).VERSION := :old.VERSION;
state_pkg.newCONTACTRows(indexnb).ENTITY_ID := :old.id;
state_pkg.newCONTACTRows(indexnb).RECORD_STATUS := :old.RECORD_STATUS;
state_pkg.newCONTACTRows(indexnb).DATE_START := :old.DATE_START;
END;
перед обновлением (один раз для всех строк):
BEGIN
state_pkg.newCONTACTRows := state_pkg.eCONTACTRows;
END;
после обновления (один раз для всех строк):
DECLARE
BEGIN
for i in 1 .. STATE_PKG.newCONTACTRows.COUNT loop
INSERT INTO "CONTACT" (
ID,
PREFIX,
FIRST_NAME,
MIDDLE_NAME,
LAST_NAME,
OWNER,
LAST_USER,
DATE_CREATED,
DATE_MODIFIED,
VERSION,
ENTITY_ID,
RECORD_STATUS,
DATE_START)
VALUES (
CONTACT_SEQ.NEXTVAL,
state_pkg.newCONTACTRows(i).PREFIX,
state_pkg.newCONTACTRows(i).FIRST_NAME,
state_pkg.newCONTACTRows(i).MIDDLE_NAME,
state_pkg.newCONTACTRows(i).LAST_NAME,
state_pkg.newCONTACTRows(i).OWNER,
state_pkg.newCONTACTRows(i).LAST_USER,
state_pkg.newCONTACTRows(i).DATE_CREATED,
state_pkg.newCONTACTRows(i).DATE_MODIFIED,
state_pkg.newCONTACTRows(i).VERSION,
state_pkg.newCONTACTRows(i).ENTITY_ID,
state_pkg.newCONTACTRows(i).RECORD_STATUS,
state_pkg.newCONTACTRows(i).DATE_START
);
end loop;
END;
пакет, определенный как (обрезанная, полная версия - это просто копия этого в таблице):
PACKAGE STATE_PKG IS
TYPE CONTACTArray IS TABLE OF CONTACT%ROWTYPE INDEX BY BINARY_INTEGER;
newCONTACTRows CONTACTArray;
eCONTACTRows CONTACTArray;
END;
нынешний результат
вот результирующая история пример:
ID First Last Ver Entity_ID Date_Start Date_Modified
1196 John Smith 5 0 12/11/2009 10:20:11 PM 12/31/9999 12:00:00 AM
1201 John Smith 0 1196 12/11/2009 09:35:20 PM 12/11/2009 10:16:49 PM
1203 John Smith 1 1196 12/11/2009 10:16:49 PM 12/11/2009 10:17:07 PM
1205 John Smith 2 1196 12/11/2009 10:17:07 PM 12/11/2009 10:17:19 PM
1207 John Smith 3 1196 12/11/2009 10:17:19 PM 12/11/2009 10:20:00 PM
1209 John Smith 4 1196 12/11/2009 10:20:00 PM 12/11/2009 10:20:11 PM
каждая запись истории имеет Entity_ID, который является идентификатором текущей строки, Date_Start в новой записи соответствует Date_Modified последней строки истории. Это позволяет нам делать такие запросы, как Where Entity_ID = :id Or ID = :id And :myDate < Date_Modified And :myDate >= Date_Start
. История может быть извлечена Entity_ID = :current_id
.
есть ли лучший подход, надеюсь, более ремонтопригодный / гибкий для этого? концепция проста, при обновлении строки скопируйте ее в ту же таблицу через вставку со старыми значениями, затем обновите текущую строку...но на самом деле, мне еще предстоит найти более простой способ. Я надеялся, что кто-то намного хитрее/мудрее в Oracle имеет лучший подход к этому. Скорость не имеет большого значения, мы 99% Читаем 1% пишет, как и большинство веб-приложений, и все массовые операции являются вставками, а не обновлениями, которые не создали бы никакой истории.
если у кого-нибудь есть идеи упростить обслуживание на этом, я был бы очень благодарен, спасибо!
7 ответов
хорошо, это переписать. Что я пропустил, когда я впервые ответил, Это то, что приложение хранит свою историю в главной таблице. Теперь я понимаю, почему @NickCraver так извиняется за код.
первое, что нужно сделать, это выследить исполнителей этого проекта и убедиться, что они никогда не сделают этого снова. Хранение такой истории не масштабируется, делает обычные (неисторические) запросы более сложными и саботирует реляционную целостность. Очевидно, что есть сценарии, где ничто из этого не имеет значения, и, возможно, ваш сайт является одним из них, но в целом это очень плохая реализация.
лучший способ сделать это -Oracle 11g общий отзыв. Это элегантное решение, с полностью невидимой и эффективной реализацией, и - по стандартам других платных дополнений Oracle-по вполне разумной цене.
но если о полном отзыве не может быть и речи, и вы действительно должны это сделать, не позволяют обновления. Изменение существующей записи контакта должно быть вставкой. Чтобы сделать эту работу, вам может потребоваться создать представление с триггером вместо триггера. Это все еще гадко, но не так гадко, как то, что у вас есть сейчас.
начиная с Oracle 11.2.0.4 Total Recall был ребрендирован Flashback Archive и включен как часть корпоративной лицензии (хотя сокращен сжатых таблиц журнала, если мы не покупаем опцию Advanced Compress).
эта щедрость от Oracle должна сделать FDA обычным способом хранения истории: она эффективна, она перформативна, это встроенный Oracle со стандартным синтаксисом для поддержки исторических запросов. Увы, я ожидаю увидеть наполовину приготовленные реализации с триггерами spatchcocked, сломанными первичными ключами и ужасной производительностью еще много лет. Потому что журналирование кажется одним из тех отвлечений, которые радуют разработчиков, несмотря на то, что это низкоуровневая сантехника, которая во многом не имеет отношения к 99.99% всех бизнес-операций.
к сожалению, нет способа избежать ссылки на все имена столбцов (: OLD.это,: старый.это и т. д.) в триггерах. Однако, что вы можете сделать, это написать программу создать код триггера из определения таблицы (в USER_TAB_COLS). Затем, когда таблица изменяется, вы можете сгенерировать и скомпилировать новую копию триггеров.
посмотреть этот AskTom нити как это сделать.
в случае, если у кого-то есть тот же узкоспециализированный случай, который мы делаем (Linq access делает историю одной таблицы намного чище/проще, это то, что я сделал, чтобы упростить то, что у нас есть, приветствуем любые улучшения....это всего лишь скрипт, который будет запускаться при каждом изменении базы данных, восстанавливая триггеры аудита, основным изменением является PRAGMA AUTONOMOUS_TRANSACTION;
размещение истории, генерирующей автономную транзакцию, и не заботясь о мутации (что не имеет значения для того, как мы аудит):
Declare
cur_trig varchar(4000);
has_ver number;
Begin
For seq in (Select table_name, sequence_name
From user_tables ut, user_sequences us
Where sequence_name = replace(table_name, '_','') || '_SEQ'
And table_name Not Like '%$%'
And Exists (Select 1
From User_Tab_Columns utc
Where Column_Name = 'ID' And ut.table_name = utc.table_name)
And Exists (Select 1
From User_Tab_Columns utc
Where Column_Name = 'DATE_START' And ut.table_name = utc.table_name)
And Exists (Select 1
From User_Tab_Columns utc
Where Column_Name = 'DATE_MODIFIED' And ut.table_name = utc.table_name))
Loop
--ID Insert Triggers (Autonumber for oracle!)
cur_trig := 'CREATE OR REPLACE TRIGGER ' || seq.table_name || 'CR' || chr(10)
|| 'BEFORE INSERT ON ' || seq.table_name || chr(10)
|| 'FOR EACH ROW' || chr(10)
|| 'BEGIN' || chr(10)
|| ' SELECT ' || seq.sequence_name || '.NEXTVAL INTO :new.ID FROM DUAL;' || chr(10)
|| ' IF(:NEW.ENTITY_ID = 0) THEN' || chr(10)
|| ' SELECT sysdate, sysdate, ''31-DEC-9999'' INTO :NEW.DATE_CREATED, :NEW.DATE_START, :NEW.DATE_MODIFIED FROM DUAL;' || chr(10)
|| ' END IF;' || chr(10)
|| 'END;' || chr(10);
Execute Immediate cur_trig;
--History on update Triggers
cur_trig := 'CREATE OR REPLACE TRIGGER ' || seq.table_name || '_HIST' || chr(10)
|| ' BEFORE UPDATE ON ' || seq.table_name || ' FOR EACH ROW' || chr(10)
|| 'DECLARE' || chr(10)
|| ' PRAGMA AUTONOMOUS_TRANSACTION;' || chr(10)
|| 'BEGIN' || chr(10)
|| ' INSERT INTO ' || seq.table_name || ' (' || chr(10)
|| ' DATE_MODIFIED ' || chr(10)
|| ' ,ENTITY_ID ' || chr(10);
For col in (Select column_name
From user_tab_columns ut
Where table_name = seq.table_name
And column_name NOT In ('ID','DATE_MODIFIED','ENTITY_ID')
Order By column_name)
Loop
cur_trig := cur_trig || ' ,' || col.column_name || chr(10);
End Loop;
cur_trig := cur_trig || ') VALUES ( --ID is Automatic via another trigger' || chr(10)
|| ' SYSDATE --DateModified Set' || chr(10)
|| ' ,:old.ID --EntityID Set' || chr(10);
has_ver := 0;
For col in (Select column_name
From user_tab_columns ut
Where table_name = seq.table_name
And column_name NOT In ('ID','DATE_MODIFIED','ENTITY_ID')
Order By column_name)
Loop
cur_trig := cur_trig || ' ,:old.' || col.column_name || chr(10);
If Upper(col.column_name) = 'VERSION' Then
has_ver := 1;
End If;
End Loop;
cur_trig := cur_trig || ');' || chr(10)
|| ':new.DATE_MODIFIED := ''31-DEC-9999'';' || chr(10)
|| ':new.DATE_START := SYSDATE;' || chr(10);
If has_ver = 1 Then
cur_trig := cur_trig || ':new.version := :old.version + 1;' || chr(10);
End If;
cur_trig := cur_trig || 'COMMIT;' || chr(10)
|| 'END;' || chr(10);
Execute Immediate cur_trig;
End Loop;
End;
/
Если вы можете улучшить, не стесняйтесь...Я написал только несколько скриптов PL/SQL, необходимость не возникает часто...вероятно, там еще многое оставалось желать лучшего.
ответ кредит APC за то, что заставил меня посмотреть на это немного сложнее. Я не рекомендую этот макет истории, если это не остальная часть вашей модели/приложения / стека очень хорошо. Для этого приложения мы постоянно показываем смесь истории и текущей, а также фильтрации намного проще, чем комбинировать, когда дело доходит до доступа к стилю Linq-to-SQL. Спасибо за все ответы, ребята, за все хорошие предложения...и когда у меня будет больше времени, а график выпуска не будет давить на меня, я вернусь к этому вопросу, чтобы посмотреть, можно ли его улучшить.
Я понимаю, что ваши требования к приложению specifc должны иметь историю и текущие значения в одной таблице, но, возможно, это можно было бы обработать, спустившись по более обычному маршруту наличия отдельной таблицы аудита, но построив ее как псевдо-материализованное представление для представления объединенного представления для приложения.
для меня это имеет преимущество иметь простое "текущее" представление и отдельное, но полностью автоматизированное" аудиторское " представление (которое в этом случае также имеет текущее вид.)
что-то типа:
create sequence seq_contact start with 1000 increment by 1 nocache nocycle;
create table contact (
contact_id integer,
first_name varchar2(120 char),
last_name varchar2(120 char),
last_update_date date
);
alter table contact add constraint pk_contact primary key (contact_id);
create table a$contact (
version_id integer,
contact_id integer,
first_name varchar2(120 char),
last_name varchar2(120 char),
last_update_date date
);
alter table a$contact add constraint pk_a$contact primary key
(contact_id, version_id);
create or replace trigger trg_contact
before insert or delete or update on contact
for each row
declare
v_row contact%rowtype;
v_audit a$contact%rowtype;
begin
select seq_contact.nextval into v_audit.version_id from dual;
if not deleting then
:new.last_update_date := sysdate;
end if;
if inserting or updating then
v_audit.contact_id := :new.contact_id;
v_audit.first_name := :new.first_name;
v_audit.last_name := :new.last_name;
v_audit.last_update_date := :new.last_update_date;
elsif deleting then
v_audit.contact_id := :old.contact_id;
v_audit.first_name := :old.first_name;
v_audit.last_name := :old.last_name;
v_audit.last_update_date := sysdate;
end if;
insert into a$contact values v_audit;
end trg_contact;
/
insert into contact (contact_id, first_name, last_name) values
(1,'Nick','Pierpoint');
insert into contact (contact_id, first_name, last_name) values
(2, 'John', 'Coltrane');
insert into contact (contact_id, first_name, last_name) values
(3, 'Sonny', 'Rollins');
insert into contact (contact_id, first_name, last_name) values
(4, 'Kenny', 'Wheeler');
update contact set last_name = 'Cage' where contact_id = 1;
delete from contact where contact_id = 1;
update contact set first_name = 'Zowie' where contact_id in (2,3);
select * from a$contact order by contact_id, version_id;
VERSION_ID CONTACT_ID FIRST_NAME LAST_NAME LAST_UPDATE_DATE
1000 1 Nick Pierpoint 11/02/2010 14:53:49
1004 1 Nick Cage 11/02/2010 14:54:00
1005 1 Nick Cage 11/02/2010 14:54:06
1001 2 John Coltrane 11/02/2010 14:53:50
1006 2 Zowie Coltrane 11/02/2010 14:54:42
1002 3 Sonny Rollins 11/02/2010 14:53:51
1007 3 Zowie Rollins 11/02/2010 14:54:42
1003 4 Kenny Wheeler 11/02/2010 14:53:53
Если вы хотите разработать общее решение, вы можете взглянуть на пакет DBMS_SQL. С его помощью вы можете разработать пакет/процедуру, которая принимает имя таблицы в качестве входных данных и строит обновления на основе этого, изучая структуру таблицы в словаре и создавая обновления на лету. Это будет нетривиальная предварительная разработка, но намного меньше обслуживания в будущем, так как если структура таблицы изменится, код почувствует это и адаптируется. Этот метод будет работать для любой стол, с которым вы захотите его использовать.
в зависимости от сложности вашей базы данных (количество таблиц, размер, глубина отношений PK/FK, другая логика в триггерах), вы можете посмотреть на Oracle Workspace Management. Вызов API для размещения таблицы в разделе Управление рабочей областью приводит к замене Oracle таблицы на обновляемое представление и другие соответствующие объекты, которые поддерживают историю всех версий строк.
Я использовал это, и хотя есть недостатки, один преимущество аудита заключается в том, что все объекты кода генерируются Oracle и их правильность обычно предполагается.
единственный раз, когда я мог бы рекомендовать, чтобы исторические записи хранились в той же таблице, что и "текущие" записи, - это когда ссылки FK на записи должны или могут потребоваться. Например, одно приложение, которое я видел, имело некоторые ссылки FK, которые будут ссылаться на запись с "момента времени", то есть, если запись была обновлена, FK все равно будет ссылаться на историческую запись - это была важная часть дизайна, и разделение записей истории на вторую таблицу сделало бы это более громоздкий.
кроме того, я бы предпочел, чтобы бизнес-требование для отслеживания всех изменений было решено с помощью отдельной таблицы "история" для каждой таблицы. Конечно, это означает больше DDL, но это значительно упрощает код приложения, и вы также выиграете от лучшей производительности и масштабируемости.