Mysql triggers - capture each column change - mysql

I am trying to create trigger, that capture changes in database after update.
Table my_table I am watching:
Table my_table_log where I am writing changes to log them
And here is trigger so far:
CREATE TRIGGER `log_update`
AFTER UPDATE ON `my_table`
FOR EACH ROW
BEGIN
INSERT INTO
`my_table_log`
(
`id`,
`action`,
`column_name`,
`value_before`,
`value_after`,
`who`,
`ts`
)
VALUES
(
NEW.id,
'u',
'name',
OLD.name,
NEW.name,
user(),
NOW()
);
END
Question: How to log each change of column ?
Problem: I am curently watching only if column name changed in my_table. And I have another trigger for column age. How to set trigger for each row and each column that was changed?
Thank you for your suggestions/code/inspirations

You might use ifs for every column you'd like to watch in your trigger:
create trigger `log_update`
after update on `my_table`
for each row
begin
if (old.name <> new.name) then
insert into `my_table_log`
(
`id`,
`action`,
`column_name`,
`value_before`,
`value_after`,
`who`,
`ts`
)
values
(
new.id,
'u',
'name',
old.name,
new.name,
user(),
now()
);
end if;
if (old.age <> new.age) then
insert into `my_table_log`
(
`id`,
`action`,
`column_name`,
`value_before`,
`value_after`,
`who`,
`ts`
)
values
(
new.id,
'u',
'age',
old.age,
old.age,
user(),
now()
);
end if;
end
But better make the insert a stored procedure to avoid redudancy:
create procedure `log_insert`
(
id int(11),
`action` char,
column_name varchar(255),
value_before varchar(255),
value_after varchar(255)
)
begin
insert into `my_table_log`
(
`id`,
`action`,
`column_name`,
`value_before`,
`value_after`,
`who`,
`ts`
)
values
(
id,
`action`,
column_name,
value_before,
value_after,
user(),
now()
);
end
And call it in your trigger:
create trigger `log_update`
after update on `my_table`
for each row
begin
if (old.name <> new.name) then
call log_insert
(
new.id,
'u',
'name',
old.name,
new.name
);
end if;
if (old.age <> new.age) then
call log_insert
(
new.id,
'u',
'age',
old.age,
new.age
);
end if;
end
You can re-use the stored procedure to log events in your insert and delete triggers.
Make shure to use a composite primary key in your my_table_log to allow updates over several columns. I'd use at least:
primary key(id,column_name,who,ts).
Or use dedicated single column primary key to avoid varchars in your primary key for better performance.

One alternative is to just log the new values together with user() and now():
create table my_table_log
( id ...
, name ...
, age ...
, action ...
, who ...
, ts ... )
To determine what was changed, compare with the previous row.
It is however rather expensive to determine what a row looked like at a certain point in time, you will have to find the last version before that point in time. Another model that makes this a lot easier is to keep track of begin_ts and end_ts for each row:
create table my_table_log
( id ...
, name ...
, age ...
, action ...
, who ...
, begin_ts ...
, end_ts ...)
The insert trigger adds a copy of the row with begin_ts = now() and end_ts = null. The update trigger updates end_ts = now() where end_ts is null and inserts a row like the insert trigger. The delete trigger updates end_ts and might add a copy together with who deleted the row. Determining what a row looked like at ts t is just a matter of where t between start_ts and end_ts

Related

Can anyone help me out how to perform MySQL trigger logic from existing MS SQL trigger Logic

I have two tables one is Master table where the Daily data keeps loading. Other one is Audit Table which tracks whenever there is an update (single/multiple columns updates) happened on Master Table. Below is the logic used for MS SQL
/*** Mastertable creation **/
CREATE TABLE [dbo].[Master](
[ID] [uniqueidentifier] primary key NOT NULL DEFAULT (newid()),
[Name] [varchar](100) ,
[Status] [varchar](10),
[Dept] [varchar](10) )
/** Inserting Data to Master table ***/
Insert into [dbo].[Master] ([Name],[Status],[Dept]) Values
('AAAA', 'Open','EC'),
('BBBB', 'Closed','CS')
/** Audit Table creation ***/
CREATE TABLE [dbo].[Orders_Audit](
[Id] [uniqueidentifier] NOT NULL,
[ColName] [varchar](50),
[OldValue] [varchar](200),
[NewValue] [varchar](200),
[ModifiedAt] [datetime],
[ModifiedBy] [varchar](50) )
Go
/*** Trigger used based on columns for any updates in Master Table***/
Create TRIGGER [dbo].[Tr_Master]
ON [dbo].[Master]
FOR UPDATE
AS
BEGIN
Declare #action varchar(50)
IF UPDATE([Status])
BEGIN
SET #Action = 'Status'
Insert into [dbo].[Orders_Audit] (Id,ColName,OldValue,NewValue,ModifiedAt,ModifiedBy)
select i.Id,#action ,d.[Status] ,i.[Status] ,getdate() ,SUSER_SNAME()
from inserted i,
deleted d where i.Id = d.Id
END
IF UPDATE([Dept])
BEGIN
SET #Action = 'Dept'
Insert into [dbo].[Orders_Audit] (Id,ColName,OldValue,NewValue,ModifiedAt,ModifiedBy)
select i.Id,#action ,d.[Dept] ,i.[Dept] ,getdate() ,SUSER_SNAME()
from inserted i,
deleted d where i.Id = d.Id
END
End
GO
Need to implement same logic in MySQL
Thanks in Advance!!
CREATE TRIGGER trigger_name
AFTER UPDATE ON Master
FOR EACH ROW
BEGIN
IF NOT (OLD.Status <=> NEW.Status) THEN
INSERT INTO Orders_Audit (Id, ColName, OldValue, NewValue, ModifiedAt, ModifiedBy)
SELECT NEW.Id, 'Status', OLD.Status, NEW.Status, NOW(), CURRENT_USER();
END IF;
IF NOT (OLD.Dept <=> NEW.Dept) THEN
INSERT INTO Orders_Audit (Id, ColName, OldValue, NewValue, ModifiedAt, ModifiedBy)
SELECT NEW.Id, 'Dept', OLD.Dept, NEW.Dept, NOW(), CURRENT_USER();
END IF;
END
IF NOT (OLD.column <=> NEW.column) checks that the column value was changed. This expression is NULL-safe (rather than IF OLD.column <> NEW.column) - i.e. it correctly checks the result when one of the values or both of them are NULL.
PS. CURRENT_USER() returns the account name which was used for authentication. If you need the username provided by the client during authentication then use USER() function instead. Example: user may provide 'john'#'1.2.3.4' (and USER() returns this) but the account used may be 'john'#'%' (and CURRENT_USER() returns this). For stored objects/views CURRENT_USER() may return not invoker but definer account (depends on SECURITY attribute).

Increment a number field via a trigger INSERT in MySQL

I'm building a data versioning system, and I need to increment a version number each time a new row is added to the version table, but it increments once and then stops:
DELIMITER |
CREATE TRIGGER trigger2 AFTER UPDATE ON something
FOR EACH ROW
BEGIN
IF NEW.updated_at <> OLD.updated_at THEN
INSERT INTO versions_something (
`id`,
`some_id`,
`version`,
`title`,
`description`,
`created_at`,
`updated_at`
) VALUES (
null,
NEW.id,
1,
NEW.title,
NEW.description,
NOW(),
NOW()
);
END IF;
UPDATE
versions_something
SET
version = (SELECT MAX(version)) + 1
WHERE versions_something.id = LAST_INSERT_ID();
END;
|
DELIMITER ;
I've tried putting the UPDATE into a separate trigger (AFTER INSERT ON versions_something ...), but MySQL complains that it's clashing with the trigger before it.
I've tried the UPDATE on its own, using the last ID in the table and it works each time, so I have no idea what's happening.

Create a trigger to insert the old data to a new table

Here is the table I created.
USE my_guitar_shop;
DROP TABLE IF EXISTS Products_Audit;
CREATE TABLE Products_Audit (
audit_id INT PRIMARY KEY,
category_id INT REFERENCES categories(category_id),
product_code VARCHAR ( 10 ) NOT NULL UNIQUE ,
product_name VARCHAR ( 255 ) NOT NULL,
list_price INT NOT NULL,
discount_percent INT NOT NULL DEFAULT 0.00 ,
date_updated DATETIME NULL);
Create a trigger named products_after_update. This trigger should insert the old data about the product into the Products_Audit table after the row is updated. Then, test this trigger with an appropriate UPDATE statement.
Here is the trigger I created but the data is not showing up in the Products_Audit table it is showing all null.
USE my_guitar_shop;
DROP TRIGGER IF EXISTS products_after_update;
DELIMITER $$
CREATE TRIGGER products_after_update
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
INSERT INTO products_audit (audit_id, product_id, category_id, product_code,
product_name, list_price, discount_percent, date_updated)
SELECT audit_id, products.product_id, products.category_id, products.product_code,
products.product_name,products.list_price, products.discount_percent, date_updated
FROM products JOIN products_audit
ON products_audit.audit_id = (SELECT audit_id FROM inserted);
END $$
DELIMITER ;
EDIT with the INSERT INTO
USE my_guitar_shop;
DROP TRIGGER IF EXISTS products_after_update;
DELIMITER $$
CREATE TRIGGER products_after_update
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
INSERT INTO products_audit (audit_id, product_id, category_id,product_code,
product_name, list_price, discount_percent, date_updated)
VALUES (OLD.audit_id, OLD.product_id, OLD.category_id, OLD.product_code,
OLD.product_name, OLD.list_price, OLD.discount_percent, OLD.date_updated)
DELIMITER ;
You are overcomplicating the insert. As mysql documentation on triggers says:
In an UPDATE trigger, you can use OLD.col_name to refer to the columns
of a row before it is updated and NEW.col_name to refer to the columns
of the row after it is updated.
Therfore, use the OLD.column_name format in the insert. Also, I would set the audit_id field to auto increment and leave it out of the insert:
INSERT INTO products_audit (product_id, category_id, product_code,
product_name, list_price, discount_percent, date_updated)
VALUES (OLD.product_id, OLD.category_id, OLD.product_code,
OLD.product_name, OLD.list_price, OLD.discount_percent, OLD.date_updated)
Here's an example of how I do it:
CREATE OR REPLACE EDITIONABLE TRIGGER E_TABLE_TRG
before insert or update or delete on e_table
for each row
declare
l_seq number;
begin
-- Get a unique sequence value to use as the primary key
select s_seq.nextval
into l_seq
from dual;
if inserting then
:new.date_opened := sysdate;
:new.last_txn_date := null;
:new.status := 'A';
end if;
if inserting then
insert into e_table_history
(
t_seq,
user_id,
date_opened,
last_txn_date,
status,
insert_update_delete,
insert_update_delete_date
)
values
(
l_seq,
:new.user_id,
:new.date_opened,
:new.last_txn_date,
:new.status,
'I',
sysdate
);
elsif updating then
insert into e_table_history
(
t_seq,
date_opened,
last_txn_date,
status,
insert_update_delete,
insert_update_delete_date
)
values
(
l_seq,
:new.date_opened,
:new.last_txn_date,
:new.status,
'U',
sysdate
);
else
insert into e_table_history
(
t_seq,
date_opened,
last_txn_date,
status,
insert_update_delete,
insert_update_delete_date
)
values
(
l_seq,
:old.date_opened,
:old.last_txn_date,
:old.status,
'D',
sysdate
);
end if;
end;
/
ALTER TRIGGER E_TABLE_TRG ENABLE;
/

Get column name and values for MySQL trigger

I'm beginner with MySQL and I'm trying to create a log trigger that fills a log table like this:
DELIMITER $$
CREATE TRIGGER ai_user AFTER UPDATE ON user
FOR EACH ROW
BEGIN
INSERT INTO user_log (action,id,timestamp,column_name,old_value, new_value)
VALUES('update',NEW.id,NOW(),COLUMN.NAME,OLD.column_value, NEW.column_value);
END$$
DELIMITER ;
But I'm with problems to get the changed column name and it's old and new value.
Any help would be appreciated. Thanks.
You have to do this the painful way, one at a time:
if not (old.col1 = new.col1 or old.col1 is null and new.col1 is null)
INSERT INTO user_log(action, id, timestamp, column_name, old_value, new_value)
VALUES('update', NEW.id, NOW(), 'col1', OLD.col1, NEW.col1);
end if;
if not (old.col2 = new.col2 or old.col2 is null and new.col2 is null)
INSERT INTO user_log(action, id, timestamp, column_name, old_value, new_value)
VALUES('update', NEW.id, NOW(), 'col2', OLD.col2, NEW.col2);
end if;
. . .
Note that these do not have else clauses. You want to check for every value.
By the way, when you do updates this way, you need to be careful about types if the columns you are comparing have different types.

difficult constraint for a mysql-table

I need a constraint for a mysql-table. The table has the fields 'id', 'ref', 'from' and 'to'. The constraint schould guarantee that there are no datasets with the same 'ref' and a time overlapping (fields 'from' and 'to').
In sql: The following statement should always return '0'.
select count(*)
from `TABLE` d1 inner join `TABLE` d2 on
d1.`ref` = d2.`ref` and d1.`id` <> d2.`id` and
d1.`to` >= d2.`from` and d1.`from`<=d2.`to`
Is there a way to handle this with constrains?
Now I have the following triggers. Thanks for your help!
DELIMITER $$
USE `devtestplandb`$$
CREATE
TRIGGER `db`.`trig1`
BEFORE INSERT ON `db`.`TABLE`
FOR EACH ROW
BEGIN
SET #CNT = (
select count(*)
from `TABLE` d
where d.`ref` = NEW.`ref` and
d.`to` >= NEW.`from` and
d.`from` <= NEW.`to`);
IF #CNT != 0 THEN
CALL error_001();
END IF;
END$$
CREATE
TRIGGER `db`.`trig2`
BEFORE UPDATE ON `db`.`TABLE`
FOR EACH ROW
BEGIN
SET #CNT = (
select count(*)
from `TABLE` d
where d.`ref` = NEW.`ref` and
d.`ID` <> NEW.`ID` and
d.`to` >= NEW.`from` and
d.`from` <= NEW.`to`);
IF #CNT != 0 THEN
CALL error_002();
END IF;
END$$
"Is there a way to handle this with constrains?"
Yes, SQL Standard 2011 supports this kind of scenarios in readable declarative manner:
unique constraint definition
<without overlap specification> ::=
<application time period name> WITHOUT OVERLAPS
In your example:
CREATE TABLE tab (
id INT AUTO_INCREMENT PRIMARY KEY,
ref VARCHAR(100),
from_date DATE,
end_date DATE,
PERIOD FOR ref_period(from_date, end_date),
UNIQUE (ref, ref_period WITHOUT OVERLAPS)
);
And sample inserts:
INSERT INTO tab(ref, from_date, end_date) VALUES ('a', '2020-01-01','2020-03-01');
-- OK
INSERT INTO tab(ref, from_date, end_date) VALUES ('a', '2020-03-01','2020-05-01');
-- OK
INSERT INTO tab(ref, from_date, end_date) VALUES ('a', '2020-04-01','2020-07-01')
-- Duplicate entry 'a-2020-07-01-2020-04-01' for key 'ref
SELECT * FROM tab;
db<>fiddle demo - Maria DB 10.5