MySQL create trigger for logging table - mysql

Hello I am new for MySQL. I have 2 table, a data table and a log/history table. I would like to make a trigger, that put the old data from Data to Data_log if any of the column change on the Data. I made a trigger but I don't know how to check if the value of any column changes. Lately I would like to create some procedure/view which can return one line data on a specific date. Like return all field from ID 1 on 2022-03-27
Data:
ID
name
data
price
1
thing1
desc of t1
100
2
thing2
desc of t2
300
Data_log:
log_id
data_id
column_name
old_data
date
1
1
data
desc t1
2022-03-28 06:49:14
2
2
price
600
2022-03-28 11:34:46
3
1
price
4400
2022-03-28 09:15:54
Trigger (only check price column):
DELIMITER //
CREATE TRIGGER `log_old_data` BEFORE UPDATE ON `data`
INSERT INTO data_log
(
data_id,
old_data
)
VALUES
(
OLD.id,
OLD.price <- I need here a Select I think
);
END//

Since you have few columns, it may be simpler to do it "by hand" for every columns
DELIMITER //
CREATE TRIGGER `log_old_data` BEFORE UPDATE ON `data`
IF NEW.name != OLD.name THEN
INSERT INTO data_log (data_id, old_data) VALUES (OLD.id, OLD.name);
END IF;
IF NEW.data != OLD.data THEN
INSERT INTO data_log (data_id, old_data) VALUES (OLD.id, OLD.data);
END IF;
IF NEW.price != OLD.price THEN
INSERT INTO data_log (data_id, old_data) VALUES (OLD.id, OLD.price);
END IF;
END //
DELIMITER ;
PS: I did not test it, but it should work. If it doesn't, leave your mysql version to allow me to test on your version
For the SELECT part, since yo record every change on a separate table you only have to do a query on it
SELECT * FROM log_old_data WHERE `log_id` = 1 AND DATE(`date`) = '2022-03-27';
PS: Careful, DATE() on a WHERE condition may not be the perfect choice, it will not use indexes. I use generated columns to add index on date for this kind of case.

Related

MySQL - Can't update table in stored function/trigger because it is already used by statement which invoked this stored function/trigger (select)

I am new to MySQL and learning about trigger. I have 2 tables that I want : when a table (detail_transaction) has been inserted, a 'stock' field of another table (item) change.
'item' Table
id
name
price
stock
1
Item_A
15
900
2
Item_B
9
500
'detail_transaction' Table
id
id_item
count
total_price
1
1
5
75
If I insert new row in 'detail_transaction' table, I WANT my 'stock' field in 'item' table with the same 'id' to decrease and adjust to the 'count' of the 'detail_transaction'. For example :
I insert new row in 'detail_transaction' table :
id
id_item
count
total_price
2
1
10
150
I WANT the 'item' table updated to :
id
name
price
stock
1
Item_A
15
890
2
Item_B
9
500
I created a trigger to try achieve my purpose, but when I tried to insert new row in 'detail_transaction' I got this error : Can't update 'item' table in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
My trigger :
DELIMITER $$
CREATE TRIGGER update_stock
AFTER INSERT
ON detail_transaction
FOR EACH ROW
BEGIN
UPDATE item
JOIN detail_transaction ON detail_transaction.id_item = item.id
SET stock = stock - NEW.count
WHERE item.id = NEW.id_item;
END$$
DELIMITER ;
Then, I inserted row to detail_transaction table :
INSERT INTO detail_transaction (id, id_item, count, total_price)
VALUES (2, 1, 10, (SELECT price FROM item WHERE item.ID = 1) * 10);
But I got the error. What can I do to solve this? Is it because of the SELECT part when I try to INSERT? Thanks for your answer.
Firstly (and opinionated): triggers are hard to debug, test and maintain. Systems that include triggers are really hard to debug because they introduce side effects - "I did X on this table, and then Y happened on a different table". As a developer, you have to keep all the triggers in your head to understand what an individual statement might do.
If we take your example, for instance, you might have a trigger on the "stock" field in Item to create a purchase order record to replenish the stock if it falls below a threshold. The purchase order table might have an insert trigger to create a record in accounts payable, which might have an insert trigger to reject records if the total balance for a given vendor exceeds a threshold. That chain of triggers implements valid business logic, but results in really complex debugging process when suddenly an insert into detail_transaction is rejected because the product vendor exceeds their payment limit. (And yes, I have seen this kind of scenario!).
One of the challenges with triggers is that the database engine does not want an infinite loop to happen, or to have the value of the field you are SELECTing changing as a result of the trigger firing.
Also, you don't need that join - you can get the values from NEW.
DELIMITER $$
CREATE TRIGGER update_stock
AFTER INSERT
ON detail_transaction
FOR EACH ROW
BEGIN
UPDATE item
SET stock = stock - NEW.count
WHERE item.id = NEW.id_item;
END$$
DELIMITER ;
The way to do this is to use a variable:
SET #PRICE = ((SELECT price FROM item WHERE item.ID = 1) * 10);
INSERT INTO detail_transaction (id, id_item, count, total_price)
VALUES (2, 1, 10, #PRICE);
SELECT * from item;
See fiddle.
EDIT - some of the other answers show a simpler solution - calculating the total price in a trigger.
Reasonable people disagree about how to use triggers - but I would suggest that using triggers to calculate derived values - "total stock for a given item", or "total price of a transaction" - is often a bad idea. You're effectively duplicating data - the total stock level for an item is both the sum of transactions, and the attribute in a row. The total price is both "price * quantity", and an attribute in a row. What happens if someone executes an update statement for total_price or total_stock (either intentionally or as part of a bug)? Which value is correct?
You should not mix insert..values and insert..select I would rewrite the insert as
INSERT INTO detail_transaction (id, id_item, count, total_price)
select 2, 1, 10, price * 10
FROM item
WHERE item.ID = 1;
Although my choice would be a before insert trigger
DELIMITER $$
CREATE TRIGGER update_stock before INSERT ON detail_transaction
FOR EACH ROW
BEGIN
set new.total_price = (
select item.price * new.count
FROM item
WHERE item.ID = new.id
);
END$$
DELIMITER ;
with an insert
INSERT INTO detail_transaction (id, id_item, count, total_price)
VALUES (2, 1, 10, null);
The after insert publish by you fails because you use a multi table update invoking a table which fired the trigger, this is not allowed , the resolution of this issue appear in a previous answer.
CREATE TABLE item (
`id` INTEGER AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255),
`price` INTEGER,
`stock` INTEGER
);
INSERT INTO item VALUES
('1', 'Item_A', '15', '900'),
('2', 'Item_B', '9', '500');
SELECT * FROM item;
CREATE TABLE detail_transaction (
`id` INTEGER AUTO_INCREMENT PRIMARY KEY,
`id_item` INTEGER,
`count` INTEGER,
`total_price` INTEGER,
FOREIGN KEY (`id_item`) REFERENCES `item` (`id`)
);
INSERT INTO detail_transaction VALUES
('1', '1', '5', '75');
SELECT * FROM detail_transaction;
id
name
price
stock
1
Item_A
15
900
2
Item_B
9
500
id
id_item
count
total_price
1
1
5
75
-- trigger which calculates total_price value
CREATE TRIGGER tr_bi_get_total_price
BEFORE INSERT ON detail_transaction
FOR EACH ROW
SET NEW.total_price = (
SELECT NEW.`count` * item.price
FROM item
WHERE id = NEW.id_item
);
-- trigger which adjusts stock value
CREATE TRIGGER tr_ai_update_stock_in_item
AFTER INSERT ON detail_transaction
FOR EACH ROW
UPDATE item
SET stock = stock - NEW.count
WHERE item.id = NEW.id_item;
INSERT INTO detail_transaction (id_item, `count`) VALUES (1, 10);
SELECT * FROM detail_transaction;
SELECT * FROM item;
id
id_item
count
total_price
1
1
5
75
2
1
10
150
id
name
price
stock
1
Item_A
15
890
2
Item_B
9
500
fiddle
PS. Each trigger contains only one statement. So neither BEGIN-END noк DELIMITER command needed.

Can't update value in mySQL database

So I'm trying to make a simple scheme of order-> payment-> shipping. Here is the structure of the main tables:
cust_order (order_id, customer_id, catalog_id, items_ordered, order_date, payment_status(Paid/Unpaid), shipping_status (Preparing/Shipped))
payment (payment_id, order_id, pay_bank, pay_accnum, pay_date)
shipping (shipping_id, order_id, shipping_date)
And the plot is:
An order was made. The default value of payment_status would be Unpaid and shipping_status = Preparing
(Trigger 1) If an order was made, then a row will be inserted on payment table, with the value of pay_bank, pay_accnum, pay_date is NULL
Assume that the customer has paid, the value of pay_bank, pay_accnum, pay_date has been filled. Then (Trigger 2), the payment_status on the order table will be changed to 'Paid'
(Trigger 3) if the payment_status on the order table is changed to 'Paid', then a row will be inserted on shipping table (so the shipping admin can see the order that need to be sent) with the shipping_date value is NULL
Assume that the shipping admin has send the product, he edited the value of shipping_date, so it's no longer NULL. Then (Trigger 4), the shipping_status in order table is changed to 'Shipped'
I've created all the 4 triggers. Here is the code:
Trigger 1
create trigger order_to_payment
after insert on cust_order
for each row
begin
insert into payment (order_id,catalog_id)
values (new.order_id, new.catalog_id);
end;
//
Trigger 2
create trigger verifPay
after update on payment
for each row
begin
if new.pay_bank is NOT NULL and new.pay_accnum is NOT NULL and new.pay_date is NOT NULL then
update cust_order
set payment_status = 'Paid'
where order_id=new.order_id;
end if;
end;//
Trigger 3
create trigger paid_to_shipping
after update on cust_order
for each row
begin
if new.payment_status = 'Paid' then
insert into shipping(order_id,customer_id)
values (new.order_id, new.customer_id);
end if;
end;
//
Trigger 4
create trigger ChangeShippedStatus
after update on shipping
for each row
begin
if new.shipping_date is NOT NULL then
update cust_order
set shipping_status='Shipped'
where order_id=new.order_id;
end if;
end;//
And here I found a trouble when I'm trying to edit the pay_date value on the shipping table, it says #1442 - Can't update table 'shipping' in stored function/trigger because it is already used by statement which invoked this stored function/trigger
I've made a similar scheme before and it worked, so I'm trying to figure out where the problem is in this case

Create a Trigger in MYSQL

I want to create a trigger on one of the Table, lets say AssetDataTable In which the values gets populated via a Windows service, so basically I want to do is
SELECT * FROM AssetDataTable Where AssetID = 105 ORDER by 1 DESC;
I get this one row back,
Now if any of the column value is zero, it should update in another StatusTable
AssetID Column1 Column2 Column3 Column4 Column5
105 18.8 19.9 13.0 18.7 0
Now In My StatusTable, the row should become
AssetID Status
105 0
I really don't have any clue how to do, any ideas?
Try with this example this is the guidelines for your desired result i hope this may helps you
DELIMITER //
CREATE TRIGGER contacts_after_update
AFTER UPDATE
ON contacts FOR EACH ROW
BEGIN
DECLARE vUser varchar(50);
-- Find username of person performing the INSERT into table
SELECT USER() INTO vUser;
-- Insert record into audit table
INSERT INTO contacts_audit
( contact_id,
updated_date,
updated_by)
VALUES
( NEW.contact_id,
SYSDATE(),
vUser );
END; //
DELIMITER ;

mysql insert with value equal to primary key + 1

I have an auto increment column ID, and for some situation I wanted the other column to be equal to the primary key + 1 value
ID | other
1 | 2
2 | 3
3 | 4
4 | 123 (some situation, it is not always plus 1)
How can I achieve this?
Here's what I have tried
INSERT INTO table (`ID`,`other`) VALUES ('',(SELECT MAX(ID)+1 FROM table))
But that returns an error
You can't specify target table 'table' for update in FROM clause
Try Below query:
ALTER TABLE dbo.table ADD
Column AS ([ID]+1)
GO
It will definitely work
Using a normal AUTO_INCREMENT column as id, I cannot think of a way to do this in MySQL. Triggers, which otherwise would have been an option, don't work well with AUTO_INCREMENT columns.
The only way I see is to do two commands for an INSERT;
INSERT INTO bop (value) VALUES ('These values should be 1 and 2');
UPDATE bop SET other = id+1 WHERE id = LAST_INSERT_ID();
An SQLfiddle to test with.
The closest I'm getting to what you're looking for is to generate sequences separately from AUTO_INCREMENT using a function, and use that instead to generate the table id;
DELIMITER //
CREATE TABLE bop (
id INT UNIQUE,
other INT,
value VARCHAR(64)
)//
CREATE TABLE bop_seq ( seq INT ) // -- Sequence table
INSERT INTO bop_seq VALUES (1) // -- Start value
CREATE FUNCTION bop_nextval() RETURNS int
BEGIN
SET #tmp = (SELECT seq FROM bop_seq FOR UPDATE);
UPDATE bop_seq SET seq = seq + 1;
RETURN #tmp;
END//
CREATE TRIGGER bop_auto BEFORE INSERT ON bop
FOR EACH ROW
SET NEW.id = bop_nextval(), NEW.other=NEW.id + 1;
//
That'd let you do inserts and have it autonumber like you want. The FOR UPDATE should keep the sequence transaction safe, but I've not load tested so you may want to do that.
Another SQLfiddle.
I solved this by updating 2 times the DB..
I wanted to do +1 from 19 till ..
UPDATE `table` SET `id`=`id`+101 WHERE id <= 19
UPDATE `table` SET `id`=`id`-100 WHERE id <= 119 AND id >= 101

mysql triggers simulating assertions

Let’s consider table
Video(
IDvideo(PK),
Date,
Description,
User
)
with mysql I have no way of writing assertions.
Is it possible to simulate the following assertion using one or more triggers ?
create assertion asser1
check (0 =
( select count(*)
from Video
where Date >= DATE_SUB(current_date(),INTERVAL 1 YEAR )
&& Date<=current_date()
group by User
having count(*) > 200
)
)
how should I write that trigger?
Well, the problem is that MySQL doesn't have an equivalent of a STOP ACTION command. So basically, the work arounds are quite dirty:
One way is that you can violate a constraint inside the trigger to bubble an error and cancel the insert:
CREATE TABLE stop_action (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(35),
UNIQUE KEY (id, name)
);
INSERT INTO stop_action (1, 'Assert Failure');
Then, in the trigger, just try to:
INSERT INTO stop_action (1, 'Assert Failure');
The benefit of that, is that the error that's returned will be a duplicate key error, and the text will include "Assert Failure".
So then your trigger would become:
delimiter |
CREATE TRIGGER asser1_before BEFORE INSERT ON test1
FOR EACH ROW BEGIN
SELECT count(*) INTO test FROM (select count(*)
from Video
where Date >= DATE_SUB(current_date(),INTERVAL 1 YEAR )
&& Date<=current_date()
group by User
having count(*) > 200);
IF test != 0 THEN
INSERT INTO stop_action (1, 'Assert Failure');
END IF;
END;
|
delimiter ;
Now, you'd need to do this before UPDATE as well, otherwise you could update the date into an invalid state. But otherwise, that should at least get you started...