MySQL innodb Procedure only active once(call serveral times) in a Transaction - mysql

Firstly,I made a MYSQL Procedure to increase a number[+1 every time](there is no TRANSACTION in it), and I called the Procedure n(n>1) times in a Spring Transaction and got a same number, and the number +1 finally(+n expected)
Secondly, I added TRANSACTION in Procedure and commit atfer +1, and got the same result as above;
Thirdly, I added #Transaction (rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) on the Method A(A calls Procedure), and call Method A serveral times in Method B, which is annotationed by #Transactional, then I got the same result as above;
Anyone help? can you give me a way to handle it?
Plus:
the table in MySQL
CREATE TABLE `SEQUENCE` (
`ID` bigint(10) NOT NULL ,
`COUNT` int(11) NOT NULL ,
`CUR_DATE` date NOT NULL ,
`READ_ME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`ID`)
);
the Procedure in MySQL
CREATE DEFINER="root"#"%" PROCEDURE "SEQUENCE_PROCEDURE"(IN _id bigint)
BEGIN
UPDATE `SEQUENCE` SET `COUNT`=-1,CUR_DATE=now() where `ID`=_id and TIMESTAMPDIFF(DAY,CUR_DATE,now())>0 and _id=1;
UPDATE `SEQUENCE` SET `COUNT`=-1,CUR_DATE=now() where `ID`=_id and TIMESTAMPDIFF(MONTH,CUR_DATE,now())>0 and _id=2;
UPDATE `SEQUENCE` SET `COUNT`=`COUNT`+1 where `ID`=_id;
SELECT * FROM `SEQUENCE` where `ID`=_id;
END
the SQL in mybatis
<select id="getSequence" parameterType="java.lang.Long" resultMap="baseResult" statementType="CALLABLE">
{call SEQUENCE_PROCEDURE(#{id,jdbcType=BIGINT,mode=IN})}
</select>
the Test in project
#Test
#Transactional
public void testSequence() {
System.out.println(sequenceService.getId(2L));
System.out.println(sequenceService.getId(2L));
System.out.println(sequenceService.getId(2L));
}
where
public String getId(Long id) {
Sequence sequence = sequenceMapper.getSequence(id);
String temp='000000000000'+sequence.getCount();
return temp.substring(temp.length-12);
}
the result of test
000000000000 000000000000 000000000000
expected result
000000000000 000000000001 000000000002
add START TRANSACTION and COMMIT in Procedure do not work!

In your procedure the lines 1 and 2 have specific queries to id 1 and 2 (why?) and double comparison to _id variable. Also the third query runs on another table COUNT, may be is an error.
I changed the procedure assuming the record already exists, and change the third query to SEQUENCE table:
CREATE PROCEDURE SEQUENCE_PROCEDURE(IN _id bigint)
BEGIN
UPDATE `SEQUENCE` SET `COUNT`=-1,CUR_DATE=now()
WHERE `ID`=_id and TIMESTAMPDIFF(DAY,CUR_DATE,now())>0;
UPDATE `SEQUENCE` SET `COUNT`=`COUNT`+1 where `ID`=_id;
SELECT * FROM `SEQUENCE` where `ID`=_id;
END

Related

problem with updating table in mysql procedures

I am creating a procedure that updates any invoice with unpaid status for more than 30 days to 'OVERDUE'. However with my current code whenever I am calling this procedure even the invoice which does not have UNPAID status gets its status updated to OVERDUE.
CREATE procedure sync_invoice()
begin
declare dDate date;
declare stat varchar(20);
declare d_finished int default 0;
declare d_array cursor for
select DATEISSUED, STATUS from invoice;
declare continue handler for not found set d_finished = 1;
open d_array;
repeat
fetch d_array into dDate, stat;
if (datediff(current_date(), dDate)> 30 )then
update invoice
set STATUS = 'OVERDUE'
where stat = 'UNPAID';
end if;
until d_finished
end repeat;
close d_array;
-- code
end
//
here is the invoice table
CREATE TABLE IF NOT EXISTS `invoice` (
`INVOICENO` INT(11) NOT NULL AUTO_INCREMENT,
`CAMPAIGN_NO` INT(11) NOT NULL,
`DATEISSUED` DATE NULL DEFAULT NULL,
`DATEPAID` DATE NULL DEFAULT NULL,
`BALANCEOWING` INT(11) NULL DEFAULT NULL,
`STATUS` VARCHAR(20) NULL DEFAULT NULL,
PRIMARY KEY (`INVOICENO`, `CAMPAIGN_NO`),
INDEX `FK_INVOICE_SENDS2_CAMPAIGN_idx` (`CAMPAIGN_NO` ASC),
CONSTRAINT `FK_INVOICE_SENDS2_CAMPAIGN`
FOREIGN KEY (`CAMPAIGN_NO`)
REFERENCES `campaign` (`CAMPAIGN_NO`)
ON DELETE RESTRICT
ON UPDATE RESTRICT)
AUTO_INCREMENT = 6;
Ok so there are a few wrong things here:
CREATE procedure sync_invoice()
begin
declare dDate date;
declare stat varchar(20);
declare d_finished int default 0;
declare d_array cursor for
select DATEISSUED, STATUS from invoice;
declare continue handler for not found set d_finished = 1;
open d_array;
repeat
-- The loop will be executed for each entry into invoice
fetch d_array into dDate, stat;
-- For each, if it's old enough, regardless of the status...
if (datediff(current_date(), dDate)> 30 )then
-- This SQL request, which is called over the whole invoice table is executed
update invoice
set STATUS = 'OVERDUE' -- Effectively setting the status to overdue
where stat = 'UNPAID'; -- for each entry, so far that the one inspected is UNPAID
end if;
until d_finished
end repeat;
close d_array;
-- code
end
So basically, with this code, you'll always set every invoice to OVERDUE so long that there is a single, 30+ days old UNPAID invoice in the table.
Some way around this include replacing the whole content of the procedure by the single appropriate UPDATE call:
Update invoice Set STATUS = 'OVERDUE' Where datediff(current_date(), DATEISSUED) > 30 And STATUS = 'UNPAID';
Edit: To clarify, you could 'fix' the whole thing by replacing 'stat' at line where stat = 'UNPAID'; by 'STATUS', but you would still be executing way too many instructions without any reason to, effectively iterating through your whole invoice table as many time as you have 30+ days old invoices.
But in practice, SQL is a quite powerful language in which you might not need to loop yourself for tasks working with a single entry in a table. Statements such as UPDATE ... WHERE ... do it for you. c:
Loops and cursor, then, become useful when you have to do process that need to consider multiple entry at once.

MySQL stored procedure UPDATE changing first row only

I have the following watching table
If I run this simple test query:
UPDATE
`watching`
SET
`lastPriceSeen` = 1.33
WHERE
`email` = 'blablanfff#gmail.com' AND
`productId` = 254857265
LIMIT 1
row ID 10 correctly updates to 1.33 in the lastPriceSeen column
If I convert this to a stored procedure:
DELIMITER ;;
CREATE PROCEDURE `updateLastSeenPrice`(
email VARCHAR(100),
productId INT(11),
price FLOAT
)
DETERMINISTIC
COMMENT 'Updates watcher last seen price'
BEGIN
UPDATE
`watching`
SET
`lastPriceSeen` = price
WHERE
`email` = email AND
`productId` = productId
LIMIT 1;
END;;
DELIMITER ;
and run CALL updateLastSeenPrice('blablanfff#gmail.com','254857265', 1.11)
Only the first row (ID 1) gets updated with the price, when I was expecting row ID 10 to update.
This makes no sense to me, the WHERE clause should prevent this from happening, and it's the same query! I've even added a SELECT at the end of the stored procedure to test it's receiving the IN parameters correctly and it is...
Any ideas as to why the first row gets updated? Why is it matching on that when the WHERE clause should prevent it from doing so?
The WHERE clause doesn't see the difference between the column name and parameter name. Therefore, any record will do and it will update the first one.
Change it into:
CREATE PROCEDURE `updateLastSeenPrice`(
p_email VARCHAR(100),
p_productId INT(11),
p_price FLOAT
)
DETERMINISTIC
COMMENT 'Updates watcher last seen price'
BEGIN
UPDATE
`watching`
SET
`lastPriceSeen` = p_price
WHERE
`email` = p_email AND
`productId` = p_productId
LIMIT 1;
END;;
DELIMITER ;

Check if a column is null or not before inserting another record in SQL

I need to check first if the EndTime column in my table is null or not before I can insert another record. If the Endtime column is not null than a new record can be inserted else an error must be thrown. I'm not sure how to create the error in SQL.
This is what I tried but it doesn't work
ALTER PROCEDURE [dbo].[AddDowntimeEventStartByDepartmentID]
(#DepartmentId int,
#CategoryId int,
#StartTime datetime,
#Comment varchar(100) = NULL)
AS
BEGIN TRY
PRINT N'Starting execution'
SET #StartTime = COALESCE(#StartTime, CURRENT_TIMESTAMP);
INSERT INTO DowntimeEvent(DepartmentId, CategoryId, StartTime, EndTime, Comment)
WHERE EndTime = NULL
OUTPUT
inserted.EventId, inserted.DepartmentId,
inserted.CategoryId, inserted.StartTime,
inserted.EndTime, inserted.Comment
VALUES(#DepartmentId, #CategoryId, #StartTime, NULL, #Comment)
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER(),ERROR_MESSAGE()
END CATCH
Here is my table:
CREATE TABLE [dbo].[DowntimeEvent](
[EventId] [int] IDENTITY(0,1) NOT NULL,
[DepartmentId] [int] NOT NULL,
[CategoryId] [int] NOT NULL,
[StartTime] [datetime] NOT NULL,
[EndTime] [datetime] NULL,
[Comment] [varchar](100) NULL,
)
You could use the INSERT...SELECT syntax instead of INSERT...VALUES to be able to use a WHERE clause (with a different condition to the one you tried to use, see below), then check the number of affected rows and raise an error if it is 0:
...
BEGIN TRY
...
INSERT INTO DowntimeEvent
...
SELECT #DepartmentId, #CategoryId, #StartTime, NULL, #Comment
WHERE NOT EXISTS (
SELECT *
FROM dbo.DowntimeEvent
WHERE DepartmentId = #DepartmentId
AND CategoryId = #CategoryId
AND EndTime IS NULL
);
IF ##ROWCOUNT = 0
RAISERROR ('A NULL row already exists!', 16, 1)
;
END TRY
BEGIN CATCH
...
END CATCH;
(Of course, you will need to omit your WHERE clause as invalid Transact-SQL.)
If you want a prevention mechanism at the database level rather than just in your stored procedure, so as to be able to prevent invalid additions from any caller, you may want to consider a trigger.
A FOR INSERT trigger like this would check if new rows violate the rule "Do not add rows newer than the existing NULL row" (as well as "Do not add older rows with empty EndTime") and roll back the transaction if they do:
CREATE TRIGGER DowntimeEvent_CheckNew
ON dbo.DowntimeEvent
FOR INSERT, UPDATE
-- do nothing if EndTime is not affected
IF NOT UPDATE(EndTime)
RETURN
;
-- raise an error if there is an inserted NULL row
-- older than another existing or inserted row
IF EXISTS (
SELECT *
FROM dbo.DowntimeEvent AS t
WHERE t.EndTime IS NULL
AND EXISTS (
SELECT *
FROM inserted AS i
WHERE i.DepartmentId = t.DepartmentId
AND i.CategoryId = t.CategoryId
AND i.StartTime >= t.StartTime
)
)
BEGIN
RAISERROR ("An attempt to insert an older NULL row!", 16, 1);
ROLLBACK TRANSACTION;
END;
-- raise an error if there is an inserted row newer
-- than the existing NULL row or an inserted NULL row
IF EXISTS (
SELECT *
FROM inserted AS i
WHERE i.EndTime IS NULL
AND EXISTS (
SELECT *
FROM dbo.DowntimeEvent AS t
WHERE t.DepartmentId = i.DepartmentId
AND t.CategoryId = i.CategoryId
AND t.StartTime >= i.StartTime
)
)
BEGIN
RAISERROR ("An older NULL row exists!", 16, 1);
ROLLBACK TRANSACTION;
END;
Note that merely issuing ROLLBACK TRANSACTION in a trigger already implies raising a level 16 error like this:
Msg 3609, Level 16, State 1, Line nnn
The transaction ended in the trigger. The batch has been aborted.
so, you may not need your own. There would be a difference in the meaning of Line nnn between the message above and the one brought by your own RAISERROR, however: the line number in the former would refer to the location of the triggering statement, whereas the line number in the latter would refer to a position in your trigger.

serialising rows in a table

I have a table which contains header information for transactions. The transactions belong to different projects.
In the header I have columns:
rhguid - uniqueidentifier
rhserial - int
rh_projectID - int
First I insert the row (there's more columns)
Then I calculate the serial number for that project:
update responseheader
set rhSerial = 1 + (select isnull(max(rhSerial), 0)
from responseheader
where (rhstatus = 0) AND (rh_projectID = 1234))
where
(rhGUID = <preassignedGUID>);
However when there are many transactions happening at the same time for a project I am finding duplicate rhserial values.
I'm doing this in classic ASP with SQL Server 2008.
Is there a better way?
From your example, it doesn't look like you're using a transaction. My guess is that the SELECT portion of the statement is running as READ UNCOMMITTED, otherwise you would not see duplicates. There are ways to start transactions with ADO, but I prefer using stored procedures instead.
Try implementing something like this:
CREATE PROC dbo.ResponseHeader_Insert
<more data to insert>,
#ProjectID INT,
#Status SMALLINT
as
insert responseheader (column names here)
select <param values here>, isnull(max(rhSerial), 0) + 1
from responseheader
where (rhstatus = #Status) AND (rh_projectID = #ProjectID))
If this doesn't work for ya, try creating sequence tables (one for each sequence).
create table <tablename> (
SeqID int identity(1,1) primary key,
SeqVal varchar(1)
)
Create a procedure to get the next identity:
create procedure GetNewSeqVal_<tablename>
as
begin
declare #NewSeqValue int
set NOCOUNT ON
insert into <tablename> (SeqVal) values ('a')
set #NewSeqValue = scope_identity()
delete from <tablename> WITH (READPAST)
return #NewSeqValue
end
If there are too many sequence tables that need to be created or you want to create sequences on the fly, try this approach:
Create table AllSequences (
SeqName nvarchar(255) primary key, -- name of the sequence
Seed int not null default(1), -- seed value
Incr int not null default(1), -- incremental
Currval int
)
Go
create procedure usp_CreateNewSeq
#SeqName nvarchar(255),
#seed int = 0,
#incr int = 1
as
begin
declare #currval int
if exists (
select 1 from AllSequences
where SeqName = #SeqName )
begin
print 'Sequence already exists.'
return 1
end
if #seed is null set #seed = 1
if #incr is null set #incr = 1
set #currval = #seed
insert into AllSequences (SeqName, Seed, Incr, CurrVal)
values (#SeqName, #Seed, #Incr, #CurrVal)
end
go
create procedure usp_GetNewSeqVal
#SeqName nvarchar(255)
as
begin
declare #NewSeqVal int
set NOCOUNT ON
update AllSequences
set #NewSeqVal = CurrVal = CurrVal+Incr
where SeqName = #SeqName
if ##rowcount = 0 begin
print 'Sequence does not exist'
return
end
return #NewSeqVal
end
go

Find node level in a tree

I have a tree (nested categories) stored as follows:
CREATE TABLE `category` (
`category_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`category_name` varchar(100) NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`category_id`),
UNIQUE KEY `category_name_UNIQUE` (`category_name`,`parent_id`),
KEY `fk_category_category1` (`parent_id`,`category_id`),
CONSTRAINT `fk_category_category1` FOREIGN KEY (`parent_id`) REFERENCES `category` (`category_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci
I need to feed my client-side language (PHP) with node information (child+parent) so it can build the tree in memory. I can tweak my PHP code but I think the operation would be way simpler if I could just retrieve the rows in such an order that all parents come before their children. I could do that if I knew the level for each node:
SELECT category_id, category_name, parent_id
FROM category
ORDER BY level -- No `level` column so far :(
Can you think of a way (view, stored routine or whatever...) to calculate the node level? I guess it's okay if it's not real-time and I need to recalculate it on node modification.
First update: progress so far
I've written these triggers based on feedback by Amarghosh:
DROP TRIGGER IF EXISTS `category_before_insert`;
DELIMITER //
CREATE TRIGGER `category_before_insert` BEFORE INSERT ON `category` FOR EACH ROW BEGIN
IF NEW.parent_id IS NULL THEN
SET #parent_level = 0;
ELSE
SELECT level INTO #parent_level
FROM category
WHERE category_id = NEW.parent_id;
END IF;
SET NEW.level = #parent_level+1;
END//
DELIMITER ;
DROP TRIGGER IF EXISTS `category_before_update`;
DELIMITER //
CREATE TRIGGER `category_before_update` BEFORE UPDATE ON `category` FOR EACH ROW BEGIN
IF NEW.parent_id IS NULL THEN
SET #parent_level = 0;
ELSE
SELECT level INTO #parent_level
FROM category
WHERE category_id = NEW.parent_id;
END IF;
SET NEW.level = #parent_level+1;
END//
DELIMITER ;
It seems to work fine for insertions and modifications. But it doesn't work for deletions: MySQL Server does not launch triggers when the rows are updated from ON UPDATE CASCADE foreign keys.
The first obvious idea is to write a new trigger for deletion; however, a trigger on table categories is not allowed to modify other rows on this same table:
DROP TRIGGER IF EXISTS `category_after_delete`;
DELIMITER //
CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
/*
* Raises an error, see below
*/
UPDATE category SET parent_id=NULL
WHERE parent_id = OLD.category_id;
END//
DELIMITER ;
Error:
Grid editing error: SQL Error (1442):
Can't update table 'category' in
stored function/trigger because it is
already used by statement which
invoked this stored function/trigger.
Second update: working solution (unless proved wrong)
My first attempt was pretty sensible but I found a problem I could not manage to solve: when you launch a series of operations from a trigger, MySQL will not allow to alter other lines from the same table. Since node deletions require to adjust the level of all descendants, I had hit a wall.
In the end, I changed the approach using code from here: rather than correcting individual levels when a node change, I have code to calculate all levels and I trigger it on every edit. Since it's a slow calculation and fetching data requires a very complex query, I cache it into a table. In my case, it's an acceptable solution since editions should be rare.
1. New table for cached levels:
CREATE TABLE `category_level` (
`category_id` int(10) NOT NULL,
`parent_id` int(10) DEFAULT NULL, -- Not really necesary
`level` int(10) NOT NULL,
PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci
2. Helper function to calculate levels
If I really got a grasp on how it works, it doesn't really return anything useful by itself. Instead, it stores stuff in session variables.
CREATE FUNCTION `category_connect_by_parent_eq_prior_id`(`value` INT) RETURNS int(10)
READS SQL DATA
BEGIN
DECLARE _id INT;
DECLARE _parent INT;
DECLARE _next INT;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET #category_id = NULL;
SET _parent = #category_id;
SET _id = -1;
IF #category_id IS NULL THEN
RETURN NULL;
END IF;
LOOP
SELECT MIN(category_id)
INTO #category_id
FROM category
WHERE COALESCE(parent_id, 0) = _parent
AND category_id > _id;
IF #category_id IS NOT NULL OR _parent = #start_with THEN
SET #level = #level + 1;
RETURN #category_id;
END IF;
SET #level := #level - 1;
SELECT category_id, COALESCE(parent_id, 0)
INTO _id, _parent
FROM category
WHERE category_id = _parent;
END LOOP;
END
3. Procedure to launch the recalculation process
It basically encapsulates the complex query that retrieves the levels aided by the helper function.
CREATE PROCEDURE `update_category_level`()
SQL SECURITY INVOKER
BEGIN
DELETE FROM category_level;
INSERT INTO category_level (category_id, parent_id, level)
SELECT hi.category_id, parent_id, level
FROM (
SELECT category_connect_by_parent_eq_prior_id(category_id) AS category_id, #level AS level
FROM (
SELECT #start_with := 0,
#category_id := #start_with,
#level := 0
) vars, category
WHERE #category_id IS NOT NULL
) ho
JOIN category hi ON hi.category_id = ho.category_id;
END
4. Triggers to keep cache table up-to-date
CREATE TRIGGER `category_after_insert` AFTER INSERT ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
CREATE TRIGGER `category_after_update` AFTER UPDATE ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
call update_category_level();
END
5. Known issues
It's pretty suboptimal if nodes are altered frequently.
MySQL does not allow transactions or table locking in triggers and procedures. You must take care of these details where you edit nodes.
There's an excellent series of articles here on Hierarchical Queries in MySQL that includes how to identify level, leaf nodes, loops in the hierarchy, etc.
If there won't be any cycles (if it'd always be a tree and not a graph), you can have a level field that is set to zero (top most) by default and a stored procedure that updates the level to (parent's level + 1) whenever you update the parent_id.
CREATE TRIGGER setLevelBeforeInsert BEFORE INSERT ON category
FOR EACH ROW
BEGIN
IF NEW.parent_id IS NOT NULL THEN
SELECT level INTO #pLevel FROM category WHERE id = NEW.parent_id;
SET NEW.level = #pLevel + 1;
ELSE
SET NEW.level = 0;
END IF;
END;
No level column so far :(
Hmm * shrug *
I'd just made this level field manually.
Say, like Materialized path, with just one update after insert and without all these fancy triggers.
A field which is going to be like 000000100000210000022 for the 3-rd level for example
so it can build the tree in memory.
if you going to get whole table into PHP, I see no problem here. A little recursive function can give you your nested arrays tree.
I can tweak my PHP code but I think the operation would be way simpler
well, well.
The code you've got so far doesn't seem to me "way simple" :)