How to use a transaction in a stored procedure? - mysql

I have an SQL script that I need to convert to a parameterized stored procedure. I've only written simple functions in the past, never a complex transactional query with parameters.
Any help is greatly appreciated - simplified queries below. This script could really be anything containing a transaction and some user inputs.
-- transaction ensures i can clean up a mess, if one happens
begin;
-- parameters for the script; currently set manually before execution
set #parent_id := 123;
set #identifier := 'someid';
-- insert some row with user-specified values
insert into users (field1, field2) values (#parent_id, #identifier);
-- get the new id
set #user_id := last_insert_id();
-- do another insert
insert into usersmeta (user_id, field1, field2) values (#user_id, 1, 2);
-- if no errors happened yet, commit transaction
commit;
-- "return value"; some select query (could be 1 or many rows)
select users.id userid, usersmeta metaid
from users
join usersmeta on usersmeta.user_id = users.id;
I started with but then I pretty much got stuck. I'm especially concerned with ensuring errors, in the event they occur, are somehow made visible to the calling code
delimiter ;;
CREATE PROCEDURE mytask(IN parent_id INT(11), IN identifier VARCHAR(200))
BEGIN
SET #query = ???
PREPARE q FROM #query;
EXECUTE q;
DEALLOCATE PREPARE q;
END;;
delimiter ;

It took a good amount of research, trial, and error, but I think I arrived at a pretty good solution.
DELIMITER //
CREATE PROCEDURE my_procedure (IN parent_id int, IN identifier varchar(255), OUT out_user_id int)
BEGIN
-- local variable, set later after user is created
DECLARE user_id int;
-- rollback transaction and bubble up errors if something bad happens
DECLARE exit handler FOR SQLEXCEPTION, SQLWARNING
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- insert some row with user-specified values
INSERT INTO users (field1, field2) values (parent_id, identifier);
-- get the new id
SET user_id = last_insert_id();
-- do another insert
INSERT INTO usersmeta (user_id, field1, field2) values (user_id, 1, 2);
-- if no errors happened yet, commit transaction
COMMIT;
-- return
SELECT user_id INTO out_user_id;
END //
DELIMITER ;
I can use it like this
-- run the procedure
CALL my_procedure(123, 'some_id', #user_id);
-- get the "return" value
SELECT #user_id as user_id;
This is definitely the most complex stored procedure I've written. If anyone sees an area for improvement, I'd be happy to learn how I can make it better.

Related

MySQL stored procedure - How to get last insert id

I have created one stored procedure which inserts a record into table and gets auto incremented ID of that record. Here I am getting an syntax error while setting LAST_INSERT_ID() into a variable.
ERROR 1064 (42000): You have an error in your SQL syntax; check the
manual that corresponds to your MySQL server version for the right
syntax to use near ');
SET _orderId = SELECT LAST_INSERT_ID(); END' at line 5
Please help me to solve this issue. Thanks in Advance.
My code is like below:
DELIMITER //
CREATE PROCEDURE placeOrder(IN _cartId INT, IN _createdBy INT)
BEGIN
DECLARE _orderId INT;
-- insert into order
INSERT INTO `TBL_ORDER`(`DealerId`, `OrderNo`, `CreatedBy`)
VALUES ((SELECT DealerId FROM TBL_SHOPPING_CART WHERE Id = _cartId), UNIX_TIMESTAMP(), _createdBy));
SET _orderId = SELECT LAST_INSERT_ID();
END//
DELIMITER ;
Try this.
delimiter //
CREATE PROCEDURE placeOrder(IN _cartId INT,IN _createdBy INT)
BEGIN
SET #orderId = '';
-- insert into order
INSERT INTO `TBL_ORDER`(`DealerId`, `OrderNo`, `CreatedBy`) VALUES ((SELECT DealerId FROM TBL_SHOPPING_CART WHERE Id = _cartId),UNIX_TIMESTAMP(),_createdBy));
SELECT LAST_INSERT_ID() INTO #orderId;
END//
delimiter ;
OR
delimiter //
CREATE PROCEDURE placeOrder(IN _cartId INT,IN _createdBy INT)
BEGIN
-- insert into order
INSERT INTO `TBL_ORDER`(`DealerId`, `OrderNo`, `CreatedBy`) VALUES ((SELECT DealerId FROM TBL_SHOPPING_CART WHERE Id = _cartId),UNIX_TIMESTAMP(),_createdBy);
SELECT LAST_INSERT_ID() AS '_orderId ';
END//
delimiter ;
You should make sure that your application is not having a global connection or shared connection. As last_insert_it() will return the last generated AI value it can be from any table. Especially if your host application is using async TASKs
Consider following scenario
Your application saves gps location every second => generating new AI value
You're trying above SP to insert a value => Generating new AI value
Between your insert and read last_insert_id, your application logs gps location again and created new AI value.
Now guess what happens? you get the last inserted id from the gps table not from your SP.
usually insert's are fast, but assume your SP had to wait for a table lock and got delayed. In such case, you will receive wrong ID.
Safest way can be to escapsulate your SP work within a transaction and get the max value of your AI column (assuming it's an unsigned AI column).
Try:
...
-- SET _orderId = SELECT LAST_INSERT_ID();
SET _orderId = LAST_INSERT_ID();
SELECT _orderId;
...
or
...
-- SET _orderId = SELECT LAST_INSERT_ID();
SET _orderId = (SELECT LAST_INSERT_ID());
SELECT _orderId;
...

MySQL stored procedure insert multiple rows from list

How can I write a MySQL stored procedure to insert values from a variable sized list? More specifically I need to insert data into one parent table, get the ID from the insert, and then insert a variable number of child records along with the new ID into another table in a one-to-many relationship. My schema looks something like this:
TableA:
table_a_id -- Auto Increment
counter
some_data...
TableB:
table_b_id -- Auto Increment
table_a_id -- Foreign Key Constraint
some_data_from_list...
My stored procedure so far looks like this:
DELIMITER ;;
CREATE PROCEDURE insert_group_alert(
IN _some_data_a VARCHAR(255),
IN _data_list_b TEXT,
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
END;
START TRANSACTION;
INSERT INTO TableA (
some_data,
counter
)
VALUES (
_some_data_a,
1
)
ON DUPLICATE KEY UPDATE
counter = counter + 1;
SELECT last_insert_id()
INTO #newId;
LIST INSERT ???:
INSERT INTO TableB (
table_a_id, some_data
) VALUES (
#newId,
list_item,
);
END LIST INSERT ???
COMMIT;
END ;;
DELIMITER ;
My thought was to pass in a list of items to insert into table B via a comma delimited string. The values are strings. I am not sure what to do in the LIST INSERT section. Do I need a loop of some sort? Is this stored procedure I have so far the correct way to do this? I don't want to do a batch as I could potentially have hundreds or even thousands of items in the list. Is there a better solution? I am using straight JDBC.
Yes, you need a loop, in which you can use substring_index() to get the values within the list. The solution is based on the answers from this SO topic:
DELIMITER ;;
CREATE PROCEDURE insert_group_alert(
IN _some_data_a VARCHAR(255),
IN _data_list_b TEXT,
)
BEGIN
DECLARE strLen INT DEFAULT 0;
DECLARE SubStrLen INT DEFAULT 0;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
END;
START TRANSACTION;
INSERT INTO TableA (
some_data,
counter
)
VALUES (
_some_data_a,
1
) -- you do not really need this, since you do not provide an id
ON DUPLICATE KEY UPDATE
counter = counter + 1;
SELECT last_insert_id()
INTO #newId;
do_this:
LOOP
SET strLen = CHAR_LENGTH(_data_list_b);
INSERT INTO TableB (table_a_id, some_data) VALUES(#newId,SUBSTRING_INDEX(_data_list_b, ',', 1));
SET SubStrLen = CHAR_LENGTH(SUBSTRING_INDEX(_data_list_b, ',', 1))+2;
SET _data_list_b = MID(_data_list_b, SubStrLen, strLen); --cut the 1st list item out
IF _data_list_b = '' THEN
LEAVE do_this;
END IF;
END LOOP do_this;
COMMIT;
END ;;
DELIMITER ;

How to write a stored procedure in MySQL that displays two messages when called

The goal here is to write a script attempts to insert a new category into categories table and if the insert was successful the message:
1 row was inserted
If the update is unsuccessful, the procedure should display this message:
Row was not inserted - duplicate entry
Whenever I run this script, I keep getting the second message no matter how many times I run it, when really it should display the first message, followed by the second. Here is my script. Can someone please help me spot what I'm overlooking? Thank you.
use my_guitar_shop;
/*drop procedure if exists add_category;*/
DELIMITER //
CREATE PROCEDURE add_category(
in categories VARCHAR(100)
)
begin
declare duplicate_entry_for_key TinyINT DEFAULT FALSE;
declare continue handler for 1062
SET duplicate_entry_for_key = TRUE;
insert into categories values (5, 'Electric');
select '1 row was inserted.' as message;
if duplicate_entry_for_key = true then
select 'Row was not inserted - duplicate entry.' as message;
end if;
end //
DELIMITER ;
/* call the stored procedure with 'Gibson' */
call add_category('Gibson');
call add_category('Gibson');
You should set duplicate_entry_for_key to True. If data insert is successful.
Do something like below after insert operation.
SET duplicate_entry_for_key = last_insert_id();
OR see the below example for bit check.
For example:
START TRANSACTION; -- Begin a transaction
INSERT INTO categories
VALUES
(
5
,'Electric'
);
IF ROW_COUNT() > 0 THEN -- ROW_COUNT() returns the number of rows updated/inserted/deleted
SET duplicate_entry_for_key = TRUE;
COMMIT; -- Finalize the transaction
ELSE
SET duplicate_entry_for_key = False;
--You can ROLLBACK the transaction also - Revert all changes made before the transaction began
END IF

Declare variable MySQL trigger

My question might be simple for you, if you're used to MySQL. I'm used to PostgreSQL SGBD and I'm trying to translate a PL/PgSQL script to MySQL.
Here is what I have :
delimiter //
CREATE TRIGGER pgl_new_user
AFTER INSERT ON users FOR EACH ROW
BEGIN
DECLARE m_user_team_id integer;
SELECT id INTO m_user_team_id FROM user_teams WHERE name = "pgl_reporters";
DECLARE m_projects_id integer;
DECLARE cur CURSOR FOR SELECT project_id FROM user_team_project_relationships WHERE user_team_id = m_user_team_id;
OPEN cur;
ins_loop: LOOP
FETCH cur INTO m_projects_id;
IF done THEN
LEAVE ins_loop;
END IF;
INSERT INTO users_projects (user_id, project_id, created_at, updated_at, project_access)
VALUES (NEW.id, m_projects_id, now(), now(), 20);
END LOOP;
CLOSE cur;
END//
But MySQL Workbench gives me an error on DECLARE m_projects_id. I don't really understand because I've the same instruction two lines above...
Any hints ?
EDIT: neubert solved this error. Thanks.
But yet, when I try to insert into users :
Error Code: 1329. No data - zero rows fetched, selected, or processed
Do you have any idea ? Or better, do you know how I can get a better error message ?
All DECLAREs need to be at the top. ie.
delimiter //
CREATE TRIGGER pgl_new_user
AFTER INSERT ON users FOR EACH ROW
BEGIN
DECLARE m_user_team_id integer;
DECLARE m_projects_id integer;
DECLARE cur CURSOR FOR SELECT project_id FROM user_team_project_relationships WHERE user_team_id = m_user_team_id;
SET #m_user_team_id := (SELECT id FROM user_teams WHERE name = "pgl_reporters");
OPEN cur;
ins_loop: LOOP
FETCH cur INTO m_projects_id;
IF done THEN
LEAVE ins_loop;
END IF;
INSERT INTO users_projects (user_id, project_id, created_at, updated_at, project_access)
VALUES (NEW.id, m_projects_id, now(), now(), 20);
END LOOP;
CLOSE cur;
END//
Agree with neubert about the DECLARE statements, this will fix syntax error. But I would suggest you to avoid using openning cursors, they may be slow.
For your task: use INSERT...SELECT statement which will help you to copy data from one table to another using only one query.
INSERT ... SELECT Syntax.

Trigger not working as expected

I trying execute an trigger on mysql database.
The command executes successfully, but the trigger not working.
DELIMITER #
CREATE TRIGGER generate_coupon AFTER INSERT ON order
FOR EACH ROW
BEGIN
DECLARE userid, couponvalue INT;
DECLARE couponcode VARCHAR;
SELECT idUser INTO userid FROM indication WHERE email = NEW.email;
SET couponvalue = 20;
SET couponcode = 'abc123';
INSERT INTO coupon(idUser,idOrder,couponvalue,couponcode) values(userid, NEW.id, couponvalue, couponcode);
END#
DELIMITER ;
I suspect your problem arises from the collisions between your variables couponvalue and couponcode with the same-named columns in your coupon table. As documented under Local Variable Scope and Resolution:
A local variable should not have the same name as a table column.
You could simplify your trigger to the following and, in so doing, avoid this problem entirely:
CREATE TRIGGER generate_coupon AFTER INSERT ON order FOR EACH ROW
INSERT INTO coupon
(idUser, idOrder, couponvalue, couponcode)
SELECT idUser, NEW.id, 20, 'abc123'
FROM indication
WHERE email = NEW.email
;