Mohammad Saberi
Mohammad Saberi

Reputation: 13166

update table column after insert new record using MySQL triggers

Imagine I have a MySQL table (tbl_test) with these fields: id, title, priority.
id will be incremented automatically. I need to fill priority field with a value as same as id field after inserting.
As I'm new in using MySQL triggers, please tell me what I have to write for it. I did something , but I think it is not true:

CREATE TRIGGER 'test' AFTER INSERT ON `tbl_test`
BEGIN
   SET new.priority = new.id;
END

Thanks for your assistance.

Upvotes: 15

Views: 40843

Answers (4)

Tyler
Tyler

Reputation: 500

Since:

  1. MySQL can't give you the auto increment ID for a new row inside BEFORE INSERT.
  2. MySQL will not allow you to update the row you're inserting inside AFTER INSERT.

A solution is to:

  1. Create a duplicate "staging" version of your table. You insert rows into this table and MySQL generates the IDs for you.
  2. Create a trigger on that table that automatically inserts rows (with the generated IDs) into the table you actually want to use.
CREATE TABLE `A` (
    `id` INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
    `foo` TEXT
);

CREATE TABLE `B` (
    `id` INTEGER PRIMARY KEY NOT NULL,
    `foo` TEXT,
    `bar` TEXT
);

DELIMITER //
CREATE TRIGGER `insert_row_into_b`
AFTER INSERT ON `A`
FOR EACH ROW
BEGIN
    INSERT INTO `B` (`id`, `foo`, `bar`)
    VALUES (
        `NEW`.`id`,
        `NEW`.`foo`,
        CONCAT('New Value - ', `NEW`.`foo`)
    );
END//
DELIMITER ;

INSERT INTO `A` (`foo`) VALUES ('test');

SELECT * FROM `B`;

Note: you can't create a trigger on B that updates A (going full circle), otherwise you'll run into the ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG error ("Can't update table '%s' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.").

Upvotes: 0

Richard A Quadling
Richard A Quadling

Reputation: 3988

My solution. I initially wanted the table's uniqueID in a navOrder column. But the issues of getting the uniqueID of the table in question whilst in a trigger for a bulk insert was too problematic. So I built a mechanism similar to MSSQL's ROWNUMBER feature where every row is numbered, regardless of the table or schema that the row is in. Whilst the solution I've built below doesn't generate a number for every table (it could if the triggers were added to every table), it solves my problem of needing a unique value for every row.

So, I have 2 tables, each with a BEFORE INSERT trigger that calls upon a User Defined Function (UDF) that will get the next unique sequential number.

I've bulk tested the functionality (inserting 1,000 rows in 1 query, running 1,000 queries, running all that 10 times in parallel) and we use this for a site that experiences around 2,000 real time users a minute and approximately 15,000 inserts a minute. Not a Facebook, but all we use this and it is working for us.

If you run the code below, you'll see that rolling back does NOT rollback the sequential number.

We get no deadlocks and no duplicate values (as the unique constraint on the navOrder columns does not allow duplicates).

For me this is a relatively easy to understand solution.

CREATE SCHEMA TestLastInsertId;
USE TestLastInsertId;

CREATE TABLE Table1 (
    `tempID`    INT(11)        NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `item`      VARCHAR(256)   NOT NULL,
    `navOrder`  INT(11) UNIQUE NOT NULL,
    `createdAt` TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    `updatedAt` TIMESTAMP(6)   NULL     DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(6)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci AUTO_INCREMENT = 1;

CREATE TABLE Table2 (
    `tempID`    INT(11)        NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `item`      VARCHAR(256)   NOT NULL,
    `navOrder`  INT(11) UNIQUE NOT NULL,
    `createdAt` TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    `updatedAt` TIMESTAMP(6)   NULL     DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(6)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COLLATE = utf8_unicode_ci AUTO_INCREMENT = 1;

CREATE TABLE IF NOT EXISTS `nav_order_sequence` (
    `navOrderSequence` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

DELIMITER ;;

CREATE FUNCTION getNextNavOrder()
  RETURNS INT(11) LANGUAGE SQL NOT DETERMINISTIC MODIFIES SQL DATA SQL SECURITY INVOKER
  BEGIN
    INSERT INTO nav_order_sequence() VALUES();
    SET @navOrder = LAST_INSERT_ID();
    DELETE FROM nav_order_sequence WHERE navOrderSequence = @navOrder;
    RETURN @navOrder;
  END;;

CREATE TRIGGER Table1_BEFORE_INSERT BEFORE INSERT ON Table1 FOR EACH ROW
BEGIN
SET NEW.navOrder = getNextNavOrder();
END;;

CREATE TRIGGER Table2_BEFORE_INSERT BEFORE INSERT ON Table2 FOR EACH ROW
BEGIN
SET NEW.navOrder = getNextNavOrder();
END;;

DELIMITER ;

INSERT INTO Table1(item) VALUES('Item1'),('Item2'),('Item3');
INSERT INTO Table2(item) VALUES('Item4'),('Item5'),('Item6');
SELECT * FROM Table1; -- Result 1
SELECT * FROM Table2; -- Result 2
BEGIN;
INSERT INTO Table1(item) VALUES('Item7'),('Item8'),('Item9');
INSERT INTO Table2(item) VALUES('Item10'),('Item11'),('Item12');
SELECT * FROM Table1; -- Result 3
SELECT * FROM Table2; -- Result 4
ROLLBACK;
INSERT INTO Table1(item) VALUES('Item13'),('Item14'),('Item15');
INSERT INTO Table2(item) VALUES('Item16'),('Item17'),('Item18');
SELECT * FROM Table1; -- Result 5
SELECT * FROM Table2; -- Result 6

DROP SCHEMA TestLastInsertId;
Result 1 - Add 3 rows to Table 1 - navOrders 1, 2, and 3.
1   Item1   1   2019-11-02 18:58:28.657690  
2   Item2   2   2019-11-02 18:58:28.657690  
3   Item3   3   2019-11-02 18:58:28.657690  
Result 2 - Add 3 rows to Table 2 - navOrders 4, 5, and 6.
1   Item4   4   2019-11-02 18:58:28.669873  
2   Item5   5   2019-11-02 18:58:28.669873  
3   Item6   6   2019-11-02 18:58:28.669873  
Result 3 - Add 3 more rows to Table 1 - navOrders 7, 8, and 9.
1   Item1   1   2019-11-02 18:58:28.657690  
2   Item2   2   2019-11-02 18:58:28.657690  
3   Item3   3   2019-11-02 18:58:28.657690  
4   Item7   7   2019-11-02 18:58:28.704766  
5   Item8   8   2019-11-02 18:58:28.704766  
6   Item9   9   2019-11-02 18:58:28.704766  
Result 4 - Add 3 more rows to Table 2 - navOrders 10, 11, and 12.
1   Item4   4   2019-11-02 18:58:28.669873  
2   Item5   5   2019-11-02 18:58:28.669873  
3   Item6   6   2019-11-02 18:58:28.669873  
4   Item10  10  2019-11-02 18:58:28.706930  
5   Item11  11  2019-11-02 18:58:28.706930  
6   Item12  12  2019-11-02 18:58:28.706930  

A rollback happened here, so rows 4, 5, and 6 of both tables are removed.

Result 5 - Add 3 more rows to Table 1 after a rollback - navOrders 13, 14, and 15.
1   Item1   1   2019-11-02 18:58:28.657690  
2   Item2   2   2019-11-02 18:58:28.657690  
3   Item3   3   2019-11-02 18:58:28.657690  
7   Item13  13  2019-11-02 18:58:28.727303  
8   Item14  14  2019-11-02 18:58:28.727303  
9   Item15  15  2019-11-02 18:58:28.727303  
Result 6 - Add 3 more rows to Table 1 after a rollback - navOrders 16, 17, and 18.
1   Item4   4   2019-11-02 18:58:28.669873  
2   Item5   5   2019-11-02 18:58:28.669873  
3   Item6   6   2019-11-02 18:58:28.669873  
7   Item16  16  2019-11-02 18:58:28.730307  
8   Item17  17  2019-11-02 18:58:28.730307  
9   Item18  18  2019-11-02 18:58:28.730307  

If you were to remove the UNIQUE constraint on navOrder and replace the function called in the triggers to be LAST_INSERT_ID(), you'll see the duplicate values.

Upvotes: 0

Ravinder Reddy
Ravinder Reddy

Reputation: 24002

The way you are trying to set value to a column is an update. Because you are doing it after insert operation is completed.

You actually need a before trigger.

And to assign the same new auto incremented value of primary key column of same table, you better get it from information_schema.tables.

Example:

delimiter //
drop trigger if exists bi_table_name //

create trigger bi_table_name before insert on table_name
for each row begin
  set @auto_id := ( SELECT AUTO_INCREMENT 
                    FROM INFORMATION_SCHEMA.TABLES
                    WHERE TABLE_NAME='table_name'
                      AND TABLE_SCHEMA=DATABASE() ); 
  set new.priority= @auto_id;
end;
//

delimiter ;

Note: Make sure that you don't have any pre-defined trigger with the same name and/or action. If have some, then drop them before creating the new.

Observations:
As per mysql documentation on last_insert_id(),

"if you insert multiple rows using a single INSERT statement, LAST_INSERT_ID() returns the value generated for the first inserted row only."

hence, depending on last_insert_id() and auto_increment field values in batch inserts seems not reliable.

Upvotes: 20

Abdul Manaf
Abdul Manaf

Reputation: 4888

I don't think you can do that. An AFTER INSERT trigger cannot modify the same table, neither by issuing an UPDATE nor by something like this:

DROP TRIGGER new_tbl_test;

DELIMITER $$

CREATE TRIGGER new_tbl_test 
AFTER INSERT ON tbl_test for each row
begin
UPDATE tbl_test SET priority = new.id WHERE id = new.id;
END $$

DELIMITER ;

It gives error like

ERROR 1442 (HY000): Can't update table 'tbl_test' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.

What you can do, is use a transaction:

Example : Table structure is like below

mysql> show create table tbl_test\G
*************************** 1. row ***************************
       Table: tbl_test
Create Table: CREATE TABLE `tbl_test` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `title` char(30) DEFAULT NULL,
  `priority` int(11) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

Transaction

START TRANSACTION ;
  INSERT INTO tbl_test (title)
    VALUES ('Dr');
  UPDATE tbl_test
    SET `priority` = id
    WHERE id = LAST_INSERT_ID();
COMMIT ;

Check data

mysql> SELECT * FROM tbl_test;
+----+-------+----------+
| ID | title | priority |
+----+-------+----------+
|  1 | Dr    |        1 |
+----+-------+----------+
1 row in set (0.00 sec)

Upvotes: 3

Related Questions