prevent soft delete if row is referenced in another table - php

I have two tables, tableA and tableB. They are connected by a foreign key on their IDs (ATableID, BTableID).
TableB has a stored procedure that allows rows to be archived, It has 2 columns in it named Available_Days and Available_Night. In TableA
There is also columns Available_Days and Available_Night, which are joined to tableB by a left join. If there is data in those columns in TableA, you must not be able to archive the row in TableB.
This is the stored procedure currently, I need help implementing the criteria explained above.
#BTableID VARCHAR(500) = '',
#UserArchived INT = 0
AS
DECLARE #Local BTableID VARCHAR(500)
DECLARE #LocalUserArchived INT
DECLARE #LocalSql NVARCHAR(500)
set #Local BTableID = #BTableID;
set #LocalUserArchived = #UserArchived;
set #LocalSql = 'UPDATE tableB
SET DateArchived = GETDATE(),
UserArchived = '+CAST(#UserArchived as NVARCHAR)+'
WHERE BTableID IN ('+# BTableID +')'
I am also open for any suggestions if the current direction isn't wise.
For full context, this stored procedure will be used in php Laravel framework where a user will be able to delete a row(TableB), unless that rows information is referenced elsewhere(TableA), in which case, they will be prompted to update the other table before attempting to delete again.
Once again, the desired result is to prevent archiving/soft deleting (preventing setting DateArchived = GETDATE()) a row if its information is referenced in another table.
UPDATE:
I've made two possible adjustments (currently unable to test them as I'm not able to access the db at the moment.)
set #LocalSql = 'UPDATE BTable
SET DateArchived = GETDATE(),
UserArchived = '+CAST(#UserArchived as NVARCHAR)+'
WHERE BTableID IN ('+#BTableID+')
WHERE NOT EXISTS (
SELECT FROM ATable at
WHERE tp.ATable_FK = tp.BTableID)'
and
set #LocalSql = 'UPDATE tableB
SET DateArchived = GETDATE(),
UserArchived = '+CAST(#UserArchived as NVARCHAR)+'
WHERE tableBID IN ('+#tableBID+')
LEFT JOIN tableA cm ON cm.tableAID = tp.tableBID
WHERE cm.tableAID IS NULL’
I'm not able to test them at this time but will update this post to let you know if either did the job. If you have any suggestions on improvements please leave a comment or answer. :)

SOLUTION:
I figured out a very simple solution to solve my issue. I forgot all about the archive/soft delete stored procedures and created two stored procedures for table A and table B which selects one record from the table. ie :
#TableAIB int
AS
SELECT
TableBID,
TableBDescription,
FROM TableB
WHERE TableBID = #TableBID
RETURN ##ERRO
and
GO
#TableAID int
AS
SELECT
TableAID,
TabelADescritption
FROM TableA
WHERE TableAID = #TableAID
RETURN ##ERROR
GO
these two two tables are connected via a foreign key(on tableAID and tableBID).
in the controller I then did this:
public function archive(int $id)
{
$tableA = tableA::find($id);
$tableB = tableB::find($id);
if ($tableA->TableAID == $tableB->TableBID){
return redirect()->route(‘myPage.index')
->with('warning', ’This can not be archived, it is being used in another row. ');
}
}
This is essentially saying that if the two stored procedure's ID's match, redirect to the index page with a warning.

Related

PHP, script for moving db2 data from one database to another

I need to port some data from tables on a development database into identical tables on the production database, but the production already has records with primary keys that match the dev database so I can't dump the data in with primary keysbundleRenderer.renderToStream
In this case, item_id is the primary key in the parent record, which is used to relate child records to it. Doing the insert of parent records will create a new primary key, so I need child inserts to also have the newly created primary key so that the relationship is maintained on the production databasebundleRenderer.renderToStream
my script so far:
<?php
$DB2connPROD = odbc_connect("schema","user", "pass");
$DB2connDEV = odbc_connect("schema","user", "pass");
//Get itemt records from dev
$getDevitems = "
select item_id,item_typet_id,item_identifier,expiration_timestamp
from development.itemt where item_typet_id in (2,3)
";
//$getDevitems will get records that have a primary key item_id which is used to get the records in the following select queries
foreach($getDevitems as $items){
//Get all comments
$getComments = "
select tc.item_id, tc.comment, tc.comment_type_id from development.item_commentt tc
inner join development.itemt t on tc.item_id = t.item_id
where t.item_id = {item_id_from_getDevitems}
";
$insertitem = "INSERT into production (item_identifier,expiration_timestamp)
values (item_identifier,expiration_timestamp)";
$insertComment = "INSERT into productionComment (item_id, comment, comment_type_id)
values (item_id, comment, comment_type_id)";
}
?>
So if $getDevitems returns
item_id | item_typet_id | item_identifier | expiration_timestamp
----------------------------------------------------------------------------
123 1 544 '2020-03-01 12:00:00'
I would want it to now run the comment select with 123 as the ID in the where clause:
select tc.item_id, tc.comment, tc.comment_type_id from development.item_commentt tc
inner join development.itemt t on tc.item_id = t.item_id
where t.item_id = 123
Now for my legacy parent record I have all of the parent data and all of the relational child data. so I want to insert the new parent record into the database, creating the new ID, and inserting the child record with the newly created primary key/ID. So for the new parent record I would do:
$insertitem = "INSERT into production (item_identifier,expiration_timestamp)
values (544,'2020-03-01 12:00:00')";
Let's say that creates the new record with item_id = 43409. I want my comment insert to be:
$insertComment = "INSERT into productionComment (item_id, comment, comment_type_id)
values (43409, comment, comment_type_id)";
Bottom LIne: I need to take relational data (all based on item_id) from a development database, and insert these into a new database which creates a new primary key but I need to keep the relationship.
How can I properly finish this to do what I need and make sure I maintain the full relationship for each originally selected item?
Given that your inserts are:
$insertitem = "INSERT into production (item_identifier,expiration_timestamp)
values (item_identifier,expiration_timestamp)";
$insertComment = "INSERT into productionComment (item_id, comment, comment_type_id)
values (item_id, comment, comment_type_id)";
It looks like you are using an identity column for item_id. You can retrieve the most recent generated identity value using the IDENTITY_VAL_LOCAL() function so the second insert should be:
$insertComment = "INSERT into productionComment (item_id, comment, comment_type_id)
values (IDENTITY_VAL_LOCAL(), comment, comment_type_id)";
I cannot help with PHP but with DB2 for IBMi you have different solutions :
If i understand it correctly item_id is a GENERATED ALWAYS as IDENTITY column
You can get newly created item_id using
select item_id from final table (
INSERT into production (item_identifier,expiration_timestamp)
values (544,'2020-03-01 12:00:00')
)
Or you can force value of item_id with dev value or your own increment
INSERT into production (idtem_id, item_identifier,expiration_timestamp)
values (<your value>, 544,'2020-03-01 12:00:00')
OVERRIDING SYSTEM VALUE
In this case you will have to set next value for item_id by issueing
alter table production alter column item_id restart with <restart value>

PHP after Delete recording with foreign key, check int the table

I have two tables
Product :
- id : int;
- name : varchar(255);
- content : varchar(255);
- categories_id : int (foreign key)
Categories :
- id : int;
- title : varchar(255);
when I want to delete a product, I want to check is that the category is already exist.
I do not want to delete a category already used in the product table
$sql = $db->query("SELECT p.*, c.* FROM product as c INNER JOIN categories as c ON c.id = p.categories_id ");
$res = $sql->fetch(PDO::FETCH_ASSOC);
$p = $res['id'];
$m_id = $_POST['id '];
$sql = $db->query("DELETE FROM categories WHERE id = $m_id AND categories_id = '$c' )");
if($sql){echo 'success';
}else{
echo 'Impossible to delete this categories becauss already exist in the table product, must be removed product in the table prodcut, ';
}
You have multiple ways to be sure, that a parent row will not being deleted when it is referenced.
Enforce the constraint using foreign keys
You can define a foreign key constraint on the tables with the ON DELETE NO ACTION option to prevent such action on the parent table.
This is the most reliable option, but it has some prerequirements in MySQL, such as:
InnoDB storage engine (both tables)
There must be at least a UNIQUE constraint on the referenced column in the parent table
MySQL will report an error when you'll try to delete a row from the categories table and there is a referencing record in the products table.
You can check in the delete query if there is a child row
DELETE FROM
categories
WHERE
id = :id -- < Parameter
NOT EXISTS (SELECT * FROM products WHERE products.categories_id = categories.id)
This will succeed in any cases, but won't delete the record when there is a referencing record.
You can check the rows affected right after the query, or you can issue a SELECT to check if the category is still there.
The delete in this form is transaction safe and handles race conditions properly.
SELECT and DELETE from code
You can check if there is a record in the child table and issue a delete when this condition is false, however THIS IS NOT TRANSACTION SAFE, NOR HANDLES RACE CONDITION on it's own.
You either have to start a transaction manually and commit/rollback it, or you can use the SELECT .. FOR UPDATE syntax to lock the records, but this won't stop other threads to INSERT into the products table.
Summary
Define the foreign key constraint in the database, this is the most reliable solution to prevent accidental deletion and to enforce data integrity.
Read more about the foreign key options in the MySQL manual: http://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html
You can read about locking reads (SELECT .. FOR UPDATE) on the following manual page: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
There many problems with your current code:
1. You don't need every column value when you perform SELECT query.
2. fetch(PDO::FETCH_ASSOC); this will return multiple rows not single id.
3. You haven't added WHERE clause in first query for product ID.
4. In DELETE query you don't need to specify category ID.
So you can update your code as follow.
<?php
$m_id = $_POST['id '];
$sql = $db->query("SELECT c.id FROM categories as c INNER JOIN product as p ON c.id = p.categories_id WHERE p.id=".$m_id."");
$res = $sql->fetch(PDO::FETCH_ASSOC);
if(sizeof($res)>0) { // Making sure there is no category match in categories table.
echo 'Impossible to delete this product, must be removed categories in the table categories';
} else { // No matching row found in categories table, remove product safely.
$sql = $db->query("DELETE FROM product WHERE id = $m_id");
if ($sql) {
echo 'success';
}
}
I've edited your question to meet your requirements. General practice is to use foreign key constraints and upon delete MySQL automatically throws exception if parent row exists in reference table. Complete manual on foreign keys: http://dev.mysql.com/doc/refman/5.7/en/create-table-foreign-keys.html

pdo update multiple rows in one query [duplicate]

I know that you can insert multiple rows at once, is there a way to update multiple rows at once (as in, in one query) in MySQL?
Edit:
For example I have the following
Name id Col1 Col2
Row1 1 6 1
Row2 2 2 3
Row3 3 9 5
Row4 4 16 8
I want to combine all the following Updates into one query
UPDATE table SET Col1 = 1 WHERE id = 1;
UPDATE table SET Col1 = 2 WHERE id = 2;
UPDATE table SET Col2 = 3 WHERE id = 3;
UPDATE table SET Col1 = 10 WHERE id = 4;
UPDATE table SET Col2 = 12 WHERE id = 4;
Yes, that's possible - you can use INSERT ... ON DUPLICATE KEY UPDATE.
Using your example:
INSERT INTO table (id,Col1,Col2) VALUES (1,1,1),(2,2,3),(3,9,3),(4,10,12)
ON DUPLICATE KEY UPDATE Col1=VALUES(Col1),Col2=VALUES(Col2);
Since you have dynamic values, you need to use an IF or CASE for the columns to be updated. It gets kinda ugly, but it should work.
Using your example, you could do it like:
UPDATE table SET Col1 = CASE id
WHEN 1 THEN 1
WHEN 2 THEN 2
WHEN 4 THEN 10
ELSE Col1
END,
Col2 = CASE id
WHEN 3 THEN 3
WHEN 4 THEN 12
ELSE Col2
END
WHERE id IN (1, 2, 3, 4);
The question is old, yet I'd like to extend the topic with another answer.
My point is, the easiest way to achieve it is just to wrap multiple queries with a transaction. The accepted answer INSERT ... ON DUPLICATE KEY UPDATE is a nice hack, but one should be aware of its drawbacks and limitations:
As being said, if you happen to launch the query with rows whose primary keys don't exist in the table, the query inserts new "half-baked" records. Probably it's not what you want
If you have a table with a not null field without default value and don't want to touch this field in the query, you'll get "Field 'fieldname' doesn't have a default value" MySQL warning even if you don't insert a single row at all. It will get you into trouble, if you decide to be strict and turn mysql warnings into runtime exceptions in your app.
I made some performance tests for three of suggested variants, including the INSERT ... ON DUPLICATE KEY UPDATE variant, a variant with "case / when / then" clause and a naive approach with transaction. You may get the python code and results here. The overall conclusion is that the variant with case statement turns out to be twice as fast as two other variants, but it's quite hard to write correct and injection-safe code for it, so I personally stick to the simplest approach: using transactions.
Edit: Findings of Dakusan prove that my performance estimations are not quite valid. Please see this answer for another, more elaborate research.
Not sure why another useful option is not yet mentioned:
UPDATE my_table m
JOIN (
SELECT 1 as id, 10 as _col1, 20 as _col2
UNION ALL
SELECT 2, 5, 10
UNION ALL
SELECT 3, 15, 30
) vals ON m.id = vals.id
SET col1 = _col1, col2 = _col2;
All of the following applies to InnoDB.
I feel knowing the speeds of the 3 different methods is important.
There are 3 methods:
INSERT: INSERT with ON DUPLICATE KEY UPDATE
TRANSACTION: Where you do an update for each record within a transaction
CASE: In which you a case/when for each different record within an UPDATE
I just tested this, and the INSERT method was 6.7x faster for me than the TRANSACTION method. I tried on a set of both 3,000 and 30,000 rows.
The TRANSACTION method still has to run each individually query, which takes time, though it batches the results in memory, or something, while executing. The TRANSACTION method is also pretty expensive in both replication and query logs.
Even worse, the CASE method was 41.1x slower than the INSERT method w/ 30,000 records (6.1x slower than TRANSACTION). And 75x slower in MyISAM. INSERT and CASE methods broke even at ~1,000 records. Even at 100 records, the CASE method is BARELY faster.
So in general, I feel the INSERT method is both best and easiest to use. The queries are smaller and easier to read and only take up 1 query of action. This applies to both InnoDB and MyISAM.
Bonus stuff:
The solution for the INSERT non-default-field problem is to temporarily turn off the relevant SQL modes: SET SESSION sql_mode=REPLACE(REPLACE(##SESSION.sql_mode,"STRICT_TRANS_TABLES",""),"STRICT_ALL_TABLES",""). Make sure to save the sql_mode first if you plan on reverting it.
As for other comments I've seen that say the auto_increment goes up using the INSERT method, this does seem to be the case in InnoDB, but not MyISAM.
Code to run the tests is as follows. It also outputs .SQL files to remove php interpreter overhead
<?php
//Variables
$NumRows=30000;
//These 2 functions need to be filled in
function InitSQL()
{
}
function RunSQLQuery($Q)
{
}
//Run the 3 tests
InitSQL();
for($i=0;$i<3;$i++)
RunTest($i, $NumRows);
function RunTest($TestNum, $NumRows)
{
$TheQueries=Array();
$DoQuery=function($Query) use (&$TheQueries)
{
RunSQLQuery($Query);
$TheQueries[]=$Query;
};
$TableName='Test';
$DoQuery('DROP TABLE IF EXISTS '.$TableName);
$DoQuery('CREATE TABLE '.$TableName.' (i1 int NOT NULL AUTO_INCREMENT, i2 int NOT NULL, primary key (i1)) ENGINE=InnoDB');
$DoQuery('INSERT INTO '.$TableName.' (i2) VALUES ('.implode('), (', range(2, $NumRows+1)).')');
if($TestNum==0)
{
$TestName='Transaction';
$Start=microtime(true);
$DoQuery('START TRANSACTION');
for($i=1;$i<=$NumRows;$i++)
$DoQuery('UPDATE '.$TableName.' SET i2='.(($i+5)*1000).' WHERE i1='.$i);
$DoQuery('COMMIT');
}
if($TestNum==1)
{
$TestName='Insert';
$Query=Array();
for($i=1;$i<=$NumRows;$i++)
$Query[]=sprintf("(%d,%d)", $i, (($i+5)*1000));
$Start=microtime(true);
$DoQuery('INSERT INTO '.$TableName.' VALUES '.implode(', ', $Query).' ON DUPLICATE KEY UPDATE i2=VALUES(i2)');
}
if($TestNum==2)
{
$TestName='Case';
$Query=Array();
for($i=1;$i<=$NumRows;$i++)
$Query[]=sprintf('WHEN %d THEN %d', $i, (($i+5)*1000));
$Start=microtime(true);
$DoQuery("UPDATE $TableName SET i2=CASE i1\n".implode("\n", $Query)."\nEND\nWHERE i1 IN (".implode(',', range(1, $NumRows)).')');
}
print "$TestName: ".(microtime(true)-$Start)."<br>\n";
file_put_contents("./$TestName.sql", implode(";\n", $TheQueries).';');
}
UPDATE table1, table2 SET table1.col1='value', table2.col1='value' WHERE table1.col3='567' AND table2.col6='567'
This should work for ya.
There is a reference in the MySQL manual for multiple tables.
Use a temporary table
// Reorder items
function update_items_tempdb(&$items)
{
shuffle($items);
$table_name = uniqid('tmp_test_');
$sql = "CREATE TEMPORARY TABLE `$table_name` ("
." `id` int(10) unsigned NOT NULL AUTO_INCREMENT"
.", `position` int(10) unsigned NOT NULL"
.", PRIMARY KEY (`id`)"
.") ENGINE = MEMORY";
query($sql);
$i = 0;
$sql = '';
foreach ($items as &$item)
{
$item->position = $i++;
$sql .= ($sql ? ', ' : '')."({$item->id}, {$item->position})";
}
if ($sql)
{
query("INSERT INTO `$table_name` (id, position) VALUES $sql");
$sql = "UPDATE `test`, `$table_name` SET `test`.position = `$table_name`.position"
." WHERE `$table_name`.id = `test`.id";
query($sql);
}
query("DROP TABLE `$table_name`");
}
Why does no one mention multiple statements in one query?
In php, you use multi_query method of mysqli instance.
From the php manual
MySQL optionally allows having multiple statements in one statement string. Sending multiple statements at once reduces client-server round trips but requires special handling.
Here is the result comparing to other 3 methods in update 30,000 raw. Code can be found here which is based on answer from #Dakusan
Transaction: 5.5194580554962
Insert: 0.20669293403625
Case: 16.474853992462
Multi: 0.0412278175354
As you can see, multiple statements query is more efficient than the highest answer.
If you get error message like this:
PHP Warning: Error while sending SET_OPTION packet
You may need to increase the max_allowed_packet in mysql config file which in my machine is /etc/mysql/my.cnf and then restart mysqld.
There is a setting you can alter called 'multi statement' that disables MySQL's 'safety mechanism' implemented to prevent (more than one) injection command. Typical to MySQL's 'brilliant' implementation, it also prevents user from doing efficient queries.
Here (http://dev.mysql.com/doc/refman/5.1/en/mysql-set-server-option.html) is some info on the C implementation of the setting.
If you're using PHP, you can use mysqli to do multi statements (I think php has shipped with mysqli for a while now)
$con = new mysqli('localhost','user1','password','my_database');
$query = "Update MyTable SET col1='some value' WHERE id=1 LIMIT 1;";
$query .= "UPDATE MyTable SET col1='other value' WHERE id=2 LIMIT 1;";
//etc
$con->multi_query($query);
$con->close();
Hope that helps.
You can alias the same table to give you the id's you want to insert by (if you are doing a row-by-row update:
UPDATE table1 tab1, table1 tab2 -- alias references the same table
SET
col1 = 1
,col2 = 2
. . .
WHERE
tab1.id = tab2.id;
Additionally, It should seem obvious that you can also update from other tables as well. In this case, the update doubles as a "SELECT" statement, giving you the data from the table you are specifying. You are explicitly stating in your query the update values so, the second table is unaffected.
You may also be interested in using joins on updates, which is possible as well.
Update someTable Set someValue = 4 From someTable s Inner Join anotherTable a on s.id = a.id Where a.id = 4
-- Only updates someValue in someTable who has a foreign key on anotherTable with a value of 4.
Edit: If the values you are updating aren't coming from somewhere else in the database, you'll need to issue multiple update queries.
No-one has yet mentioned what for me would be a much easier way to do this - Use a SQL editor that allows you to execute multiple individual queries. This screenshot is from Sequel Ace, I'd assume that Sequel Pro and probably other editors have similar functionality. (This of course assumes you only need to run this as a one-off thing rather than as an integrated part of your app/site).
And now the easy way
update my_table m, -- let create a temp table with populated values
(select 1 as id, 20 as value union -- this part will be generated
select 2 as id, 30 as value union -- using a backend code
-- for loop
select N as id, X as value
) t
set m.value = t.value where t.id=m.id -- now update by join - quick
Yes ..it is possible using INSERT ON DUPLICATE KEY UPDATE sql statement..
syntax:
INSERT INTO table_name (a,b,c) VALUES (1,2,3),(4,5,6)
ON DUPLICATE KEY UPDATE a=VALUES(a),b=VALUES(b),c=VALUES(c)
use
REPLACE INTO`table` VALUES (`id`,`col1`,`col2`) VALUES
(1,6,1),(2,2,3),(3,9,5),(4,16,8);
Please note:
id has to be a primary unique key
if you use foreign keys to
reference the table, REPLACE deletes then inserts, so this might
cause an error
I took the answer from #newtover and extended it using the new json_table function in MySql 8. This allows you to create a stored procedure to handle the workload rather than building your own SQL text in code:
drop table if exists `test`;
create table `test` (
`Id` int,
`Number` int,
PRIMARY KEY (`Id`)
);
insert into test (Id, Number) values (1, 1), (2, 2);
DROP procedure IF EXISTS `Test`;
DELIMITER $$
CREATE PROCEDURE `Test`(
p_json json
)
BEGIN
update test s
join json_table(p_json, '$[*]' columns(`id` int path '$.id', `number` int path '$.number')) v
on s.Id=v.id set s.Number=v.number;
END$$
DELIMITER ;
call `Test`('[{"id": 1, "number": 10}, {"id": 2, "number": 20}]');
select * from test;
drop table if exists `test`;
It's a few ms slower than pure SQL but I'm happy to take the hit rather than generate the sql text in code. Not sure how performant it is with huge recordsets (the JSON object has a max size of 1Gb) but I use it all the time when updating 10k rows at a time.
The following will update all rows in one table
Update Table Set
Column1 = 'New Value'
The next one will update all rows where the value of Column2 is more than 5
Update Table Set
Column1 = 'New Value'
Where
Column2 > 5
There is all Unkwntech's example of updating more than one table
UPDATE table1, table2 SET
table1.col1 = 'value',
table2.col1 = 'value'
WHERE
table1.col3 = '567'
AND table2.col6='567'
UPDATE tableName SET col1='000' WHERE id='3' OR id='5'
This should achieve what you'r looking for. Just add more id's. I have tested it.
UPDATE `your_table` SET
`something` = IF(`id`="1","new_value1",`something`), `smth2` = IF(`id`="1", "nv1",`smth2`),
`something` = IF(`id`="2","new_value2",`something`), `smth2` = IF(`id`="2", "nv2",`smth2`),
`something` = IF(`id`="4","new_value3",`something`), `smth2` = IF(`id`="4", "nv3",`smth2`),
`something` = IF(`id`="6","new_value4",`something`), `smth2` = IF(`id`="6", "nv4",`smth2`),
`something` = IF(`id`="3","new_value5",`something`), `smth2` = IF(`id`="3", "nv5",`smth2`),
`something` = IF(`id`="5","new_value6",`something`), `smth2` = IF(`id`="5", "nv6",`smth2`)
// You just building it in php like
$q = 'UPDATE `your_table` SET ';
foreach($data as $dat){
$q .= '
`something` = IF(`id`="'.$dat->id.'","'.$dat->value.'",`something`),
`smth2` = IF(`id`="'.$dat->id.'", "'.$dat->value2.'",`smth2`),';
}
$q = substr($q,0,-1);
So you can update hole table with one query

InnoDB only insert record if referenced id exists (without FOREIGN KEYS)

Foreign keys may be the best approach for this problem. However, I'm trying to learn about table locking/transactions, and so I'm hoping that we can ignore them for the moment.
Let's pretend that I have two tables in an InnoDB database: categories and jokes; and that I'm using PHP/MySQLi to do the work. The tables look like so:
CATEGORIES
id (int, primary, auto_inc) | category_name (varchar[64])
============================================================
1 knock, knock
JOKES
id (int, primary, auto_inc) | category_id (int) | joke_text (varchar[255])
=============================================================================
empty
Here are two functions, each of which is being called by a different connection, at the same time. The calls are: delete_category(1) and add_joke(1,"Interrupting cow. Interrup-MOOOOOOOO!")
function delete_category($category_id) {
// only delete the category if there are no jokes in it
$query = "SELECT id FROM jokes WHERE category_id = '$category_id'";
$result = $conn->query($query);
if ( !$result->num_rows ) {
$query = "DELETE FROM categories WHERE id = '$category_id'";
$result = $conn->query($query);
if ( $conn->affected_rows ) {
return true;
}
}
return false;
}
function add_joke($category_id,$joke_text) {
$new_id = -1;
// only add the joke if the category exists
$query = "SELECT id FROM categories WHERE id = '$category_id'";
$result = $conn->query($query);
if ( $result->num_rows ) {
$query = "INSERT INTO jokes (joke_text) VALUES ('$joke_text')";
$result = $conn->query($query);
if ( $conn->affected_rows ) {
$new_id = $conn->insert_id;
return $new_id;
}
}
return $new_id;
}
Now, if the SELECT statements from both functions execute at the same time, and proceed from there, delete_category will think it's okay to delete the category, and add_joke will think it's okay to add the joke to the existing category, so I'll get an empty categories table and an entry in the joke table that references a non-existent category_id.
Without using foreign keys, how would you solve this problem?
My best thought so far would be to do the following:
1) "LOCK TABLES categories WRITE, jokes WRITE" at the start of delete_category. However, since I'm using InnoDB, I'm quite keen to avoid locking entire tables (especially main ones that will be used often).
2) Making add_joke a transaction and then doing "SELECT id FROM categories WHERE id = '$category_id'" after inserting the record as well. If it doesn't exist at that point, rollback the transaction. However, since the two SELECT statements in add_joke might return different results, I believe I need to look into transaction isolation levels, which I'm not familiar with.
It seems to me that if I did both of those things, it should work as expected. Nevertheless, I'm keen to hear more informed opinions. Thanks.
You can DELETE a category only if is no matching joke:
DELETE c FROM categories AS c
LEFT OUTER JOIN jokes AS j ON c.id=j.category_id
WHERE c.id = $category_id AND j.category_id IS NULL;
If there are any jokes for the category, the join will find them, and therefore the outer join will return a non-null result. The condition in the WHERE clause eliminates non-null results, so the overall delete will match zero rows.
Likewise, you can INSERT a joke to a category only if the category exists:
INSERT INTO jokes (category_id, joke_text)
SELECT c.id, '$joke_text'
FROM categories AS c WHERE c.id = $category_id;
If there is no such category, the SELECT returns zero rows, and the INSERT is a no-op.
Both of these cases create a shared lock (S-lock) on the categories table.
Demonstration of an S-lock:
In one session I run:
mysql> INSERT INTO bar (i) SELECT SLEEP(600) FROM foo;
In second session I run:
mysql> SHOW ENGINE INNODB STATUS\G
. . .
---TRANSACTION 3849, ACTIVE 1 sec
mysql tables in use 2, locked 2
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 18, OS thread handle 0x7faefe7d1700, query id 203 192.168.56.1 root User sleep
insert into bar (i) select sleep(600) from foo
TABLE LOCK table `test`.`foo` trx id 3849 lock mode IS
RECORD LOCKS space id 22 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`foo` trx id 3849 lock mode S
You can see that this creates an IS-lock on the table foo, and an S-lock on one row of foo, the table I'm reading from.
The same thing happens for any hybrid read/write operations such as SELECT...FOR UPDATE, INSERT...SELECT, CREATE TABLE...SELECT, to block the rows being read from being modified while they are needed as a source for the write operation.
The IS-lock is a table-level lock that prevents DDL operations on the table, so no one issues DROP TABLE or ALTER TABLE while this transaction is depending on some content in the table.

MySQL update from select and further select and updates

Currently trying to find a way to do the following inside some form of loop (preferably without a performance hit on database).
I have 3 tables user_hours, user_calendar and hours_statistics. I need to first do:
SELECT user_calendar.date_start,
user_calendar.opportunity_id,
user_hours.user_id,
user_hours.agreed_hours,
user_hours.completed_hours,
user_hours.hours_committed
FROM user_calendar
JOIN user_hours
ON user_calendar.user_calendar_id = user_hours.user_calendar_id
WHERE user_calendar.date_start = CURRENT_DATE()
AND user_hours.completed_hours IS NULL
AND user_hours.hours_committed = 'accepted'
This query could return like the following:
http://i.imgur.com/5cJ5v.png
So for each opportunity_id and user_id returned i'd like to then do:
UPDATE user_hours
SET completed_hours = agreed_hours,
hours_committed = 'completed'
WHERE opportunity_id = {opportunity_id}
AND user_id = {user_id}
AND hours_committed = 'accepted'
AND completed_hours IS NULL
Note that {opportunity_id} and {user_id} would need to be looped at this point (see screenshot) because we need to go through each user on each opportunity.
Then for each updated record i'd need to then get the total hours like:
// Get hours they have done to send to statistics data table
SELECT sum(completed_hours) FROM user_hours WHERE user_id = {user_id} AND opportunity_id = {opportunity_id} 
// Get the completed hours total somehow as a variable
$completed_hours = (from result above)
// Commit stats
UPDATE hours_statistics SET completed_hours = (completed_hours+$completed_hours)
WHERE user_id = {user_id} AND opportunity_id =  {opportunity_id} 
Could anyone help write this as a procedure or a trigger of some kind or help me in the right direction to get a starting point for looping over this stuff? Manually the querying works, just need to be looped / automatic for a stats update to run.
You can create a trigger to update hours_statistics whenever user_hours is updated (you may also want to add similar triggers for INSERT and DELETE operations, depending on your application logic).
Assuming that a UNIQUE key has been defined on hours_statistics.(user_id, opportunity_id) one can use INSERT ... ON DUPLICATE KEY UPDATE within the trigger:
CREATE TRIGGER foo AFTER UPDATE ON user_hours FOR EACH ROW
INSERT INTO hours_statistics (user_id, opportunity_id, completed_hours) VALUES
(OLD.user_id, OLD.opportunity_id, -OLD.completed_hours),
(NEW.user_id, NEW.opportunity_id, +NEW.completed_hours)
ON DUPLICATE KEY UPDATE
completed_hours = completed_hours + VALUES(completed_hours);
Then you can use a single UPDATE statement (using the multiple-table syntax to join user_hours with user_calendar) to perform all of the updates on user_hours in one go, which will cause the above trigger to update hours_statistics as desired:
UPDATE user_hours JOIN user_calendar USING (user_calendar_id, opportunity_id)
SET user_hours.completed_hours = agreed_hours,
user_hours.hours_committed = 'completed'
WHERE user_hours.hours_committed = 'accepted'
AND user_hours.completed_hours IS NULL
AND user_calendar.date_start = CURRENT_DATE();

Categories