| |
| ||||||
MySQL Hacks: Preventing deletion of specific rowsThis is a discussion on MySQL Hacks: Preventing deletion of specific rows within the DWH Tip Feeds forums, part of the Data Warehousing Tips and Techniques category; Recently, someone emailed me: I have a requirement in MYSQL as follows: we have a table EMP and we have to restrict the users not delete employees with DEPT_ID = ... |
![]() |
| | LinkBack | Thread Tools | Search this Thread | Display Modes |
| | #1 |
| News Bot Join Date: Nov 2007
Posts: 15,067
![]() | Recently, someone emailed me: I have a requirement in MYSQL as follows:Frankly, I usually refer people that write me these things to a public forum, but this time I felt like giving it a go. I figured it would be nice to share my solution and I'm also curious if others found other solutions still. (Oh, I should point out that I haven't asked what the underlying reasons are for this somewhat extraordinary requirement. I normally would do that if I would be confronted with sucha a requirement in a professional setting. In this case I'm only interested in finding a crazy hack) My first suggestion was: Raising the error won't help you achieve your goal: as soon as you raise the error, the statement will either abort (in case of a non-transactional table) or rollback all row changes made up to raising the error (in case of a transactional table)Alas, I should've actually tried it myself before replying, because it doesn't work. If you do try it, a DELETEresults in this runtime error:Can't update table 'emp' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.This is also known as "the mutating table problem". As it turns out, there is a workaround that meets all of the original demands. The workaround relies on the FEDERATED storage engine, which we can use to trick MySQL into thinking we're manipulating a different table than the one that fires the trigger. My first attempt went something like this: CREATE TABLE t ( id INT AUTO_INCREMENT PRIMARY KEY, dept_id INT, INDEX(dept_id) ); CREATE TABLE federated_t ( id INT AUTO_INCREMENT PRIMARY KEY, dept_id INT, INDEX(dept_id) ) ENGINE FEDERATED CONNECTION = 'mysql://root@localhost:3306/test/t'; DELIMITER // CREATE TRIGGER adr_t AFTER DELETE ON t FOR EACH ROW IF old.dept_id = 10 THEN INSERT INTO t_federated VALUES (old.id, old.dept_id); END IF; // DELIMITER ; So the idea is to let the trigger re-insert the deleted rows back into the federated table, which in turn points to the original table that fired the trigger to fool MySQL into thinking it isn't touching the mutating table. Although this does prevent one from deleting any rows that satisfy the DEPT_ID = 10 condition, it does not work as intended: mysql> INSERT INTO t VALUES (1,10), (2,20), (3,30); Query OK, 3 rows affected (0.11 sec) mysql> DELETE FROM t; ERROR 1159 (08S01): Got timeout reading communication packets mysql> SELECT * FROM t; +----+---------+ | id | dept_id | +----+---------+ | 1 | 10 | | 2 | 20 | | 3 | 30 | +----+---------+ 3 rows in set (0.00 sec) At this point I can only make an educated guess about the actual underlying reason for this failure. It could be that the deletion is locking the rows or even the table, thereby blocking the insert into the federated table until we get a timeout. Or maybe MySQL enters into an infinite loop of deletions and insertions until we hit a timeout. I didn't investigate, so I don't know, but it seems clear this naive solution doesn't solve he problem. It turns out that we can solve it with a FEDERATED table by turning the problem around: Instead of manipulating the original table, we can INSERT and DELETE from the FEDERATED table, and have an AFTER DELETE trigger on the FEDERATED table re-insert the deleted rows back into the original table: DROP TRIGGER adr_t; DELIMITER // CREATE TRIGGER adr_federated_t AFTER DELETE ON federated_t FOR EACH ROW IF old.dept_id = 10 THEN INSERT INTO t VALUES (old.id, old.dept_id); END IF; // DELIMITER ; Now, the DELETE does work as intended: mysql> DELETE FROM federated_t; Query OK, 3 rows affected (0.14 sec) mysql> SELECT * FROM federated_t; +----+---------+ | id | dept_id | +----+---------+ | 1 | 10 | +----+---------+ 1 row in set (0.00 sec)Of course, to actually use this solution, one would grant applications access only to the federated table, and "hide" the underlying table so they can't bypass the trigger by deleting rows directly from the underlying table. Now, even though this solution does seem to fit the original requirements, I would not recommend it for several reasons:
So how would we go about implementing a solution for this changed requirement? We certainly could try the approach that was suggested in the original request: create a trigger that raises an exception whenever we find the row should not be deleted. However, this would still rely on a trigger (which is slow). And if you're not on MySQL 5.5 (or higher), you would have to use one of the ugly hacks to raise an exception. As it turns out, there is a very simple solution that does not rely on triggers. We can create a "guard table" that references the table we want to protect using a foreign key constraint: mysql> CREATE TABLE t_guard ( -> dept_id INT PRIMARY KEY, -> FOREIGN KEY (dept_id) -> REFERENCES t(dept_id) -> ); Query OK, 0 rows affected (0.11 sec) mysql> INSERT INTO t_guard values (10); Query OK, 1 row affected (0.08 sec) mysql> DELETE FROM t; ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`test`.`t_guard`, CONSTRAINT `t_guard_ibfk_1` FOREIGN KEY (`dept_id`) REFERENCES `t` (`dept_id`)) mysql> DELETE FROM t WHERE dept_id != 10; Query OK, 2 rows affected (0.05 sec) (Like in the prior example with the federated table, the guard table would not be accessible to the application, and the "guard rows" would have to be inserted by a privileged user) You might have noticed that there's something quite peculiar about the foreign key constraint: typically, foreign key constraints serve to relate "child" rows to their respective "parent" row. To do that, the foreign key would typically point to a column (or set of columns) that make up either the primary key or a unique constraint in the parent table. But in this case, the referenced column dept_id in the t table is contained only in an index which is not unique. Strange as it may seem, this is allowed by MySQL (or rather, InnoDB). In this particular case, this flexibility (or is it a bug?) serves us quite well, and it allows us to guard many rows in the t table with dept_id = 10 with just one single row in the guard table. More from Roland Bouman's Blog ... |
| | |
![]() |
| Bookmarks |
| Thread Tools | Search this Thread |
| Display Modes | |
| |
Similar Threads | ||||
| Thread | Thread Starter | Forum | Replies | Last Post |
| SpeCond: a method to detect condition-specific gene expression. | admin | Analytic News Feeds | 0 | 20th October 2011 11:13 PM |
| Ranking and Numbering rows ? and subsets of rows ? in T-SQL | James Beresford | BI Monkey | 0 | 19th May 2011 10:43 PM |
| Visualizing deletion discussions on Wikipedia | admin | Analytic News Feeds | 0 | 12th January 2011 07:08 AM |
| DoubleClick Ad Planner adds country specific top 100 most visited website rankings in | admin | Presentation News Feeds | 0 | 3rd December 2010 04:23 PM |
| Preventing SQL Agent Jobs calling SSIS reporting failure | James Beresford | BI Monkey | 0 | 31st August 2009 06:50 PM |
| | |
| | |