How to implement cascading deletion of hierarchical data in MySQL? - php

I'm working on a project that has categories/subcategories. The database table for this is unique, with the following structure:
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(11) NOT NULL auto_increment,
`publish` tinyint(1) NOT NULL default '0',
`parent_id` int(11) NOT NULL default '0',
`description` text NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
So, in case the category is a "base" one, the parent_id is zero, and if the category has a parent, it herds the parent id. What I want to know is this: I need to delete everything above and related with a category when choosing that option, a cascade-like deletion, but I only have this table (no foreign keys). How do I do that? (Without a large amount of queries.)

You can write a trigger to do it.
DELIMITER //
CREATE TRIGGER CatDelete AFTER DELETE ON categories
FOR EACH ROW BEGIN
DELETE FROM categories WHERE parent_id = old.id;
END//
DELIMITER ;
You can ALTER your MyISAM tables to InnoDB, and then define foreign key constraints with the ON DELETE CASCADE option.
ALTER TABLE categories ENGINE=InnoDB;
ALTER TABLE categories ADD CONSTRAINT
FOREIGN KEY (parent_id) REFERENCES categories (id) ON DELETE CASCADE;
Re your comment, the first thing I'd check is if you have some orphan categories, that is with parent_id pointing to a non-existant row. That would prevent you from creating the constraint.
SELECT c1.*
FROM categories c1
LEFT OUTER JOIN categories c2
ON (c1.parent_id = c2.id)
WHERE c2.id IS NULL;

Just my $0.02 - this not so trivial solution should require MVC to handle the cascade deletion.

Related

Database Announcements Table - Add Excutable Code Within

I have a database containing over 1,000 item information and I am now developing a system that will have this check the API source via a regular Cron Job adding new entries as they come. Usually, but not always the case, when a new item is released, it will have limited information, eg; Image and name only, more information like description can sometimes be initially withheld.
With this system, I am creating a bulletin to let everyone know new items have been released, so like most announcements, they get submitted to a database, however instead of submitting static content to the database for the bulletin, is it possible to submit something which will be executed upon the person loading that page and that bulletin data is firstly obtained then the secondary code within run?
, For example, within the database could read something like the following
<p>Today new items were released!</p>
<?php $item_ids = "545, 546, 547, 548"; ?>
And then on the page, it will fetch the latest known information from the other database table for items "545, 546, 547, 548"
Therefore, there would be no need to go back and edit any past entries, this page would stay somewhat up-to-date dynamically.
Typically you would do something like have a date field on your items, so you can show which items were released on a given date. Or if you need to have the items associated with some sort of announcement record, create a lookup table that joins your items and announcements. Do not insert executable code in the DB and then pull it out and execute it.
CREATE TABLE `announcements` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`publish_date` DATETIME NOT NULL,
`content` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE `items` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` VARCHAR(128) NOT NULL,
`description` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE `announcement_item_lkp` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`announcement_id` int(11) unsigned NOT NULL,
`item_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `announcement_item_lkp_uk1` (`announcement_id`,`item_id`),
KEY `announcement_item_lkp_fk_1` (`announcement_id`),
KEY `announcement_item_lkp_fk_2` (`item_id`),
CONSTRAINT `announcement_item_lkp_fk_1` FOREIGN KEY (`announcement_id`) REFERENCES `announcements` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `announcement_item_lkp_fk_2` FOREIGN KEY (`item_id`) REFERENCES `items` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
With the announcement_item_lkp table, you can associate as many items to your announcement as you like. And since you have cascading deletes, if an item gets deletes, its lookup records are deleted as well, so you don't have to worry about orphaned references in your announcements, like you would it you just stuff a string of IDs somewhere.
You're already using a relational database, let it do its job.

Can various users access the same table in database and have different values for everyone?

Okay, I didn't know how to put this in a sentence. I'm planning to build a web application that lets the users have a track of what books they have read. These books are in a table in MySQL database, along with a boolean column 'is_complete' that is set false by default. When the user clicks 'completed', the value will be set to true in the column.
My question is: Is this possible with a single table of books with the boolean column? Or do I have to create a table for each user with the boolean column and with foreign key(root.books)? What is the best way to get this done? I'm still learning.
P.S. I'm using Apache server, PHP and MySQL
Some quickly put together example sql of how you might structure a database for this purpose - trying to normalise as far as possible ( we could take normalisation a stage further but that would require another table and probably not worth it for the example )
You could run this in your gui so long as you don't already have a database called bookworms just to observe the structure for yourself.
drop database if exists `bookworms`;
create database if not exists `bookworms`;
use `bookworms`;
drop table if exists `publishers`;
create table if not exists `publishers` (
`pid` smallint(5) unsigned not null auto_increment,
`publisher` varchar(50) not null,
primary key (`pid`)
) engine=innodb default charset=utf8;
drop table if exists `books`;
create table if not exists `books` (
`bid` int(10) unsigned not null auto_increment,
`pid` smallint(5) unsigned not null default 1,
`title` varchar(50) not null default '0',
primary key (`bid`),
key `pid` (`pid`),
constraint `fk_pid` foreign key (`pid`) references `publishers` (`pid`) on delete cascade on update cascade
) engine=innodb default charset=utf8;
drop table if exists `users`;
create table if not exists `users` (
`uid` int(10) unsigned not null auto_increment,
`username` varchar(50) not null default '0',
primary key (`uid`)
) engine=innodb default charset=utf8;
drop table if exists `library`;
create table if not exists `library` (
`id` int(10) unsigned not null auto_increment,
`uid` int(10) unsigned not null default '0',
`bid` int(10) unsigned not null default '0',
`status` tinyint(3) unsigned not null default '0' comment 'indicates if the book has been read',
primary key (`id`),
key `uid` (`uid`),
key `bid` (`bid`),
constraint `fk_bid` foreign key (`bid`) references `books` (`bid`) on delete cascade on update cascade,
constraint `fk_uid` foreign key (`uid`) references `users` (`uid`) on delete cascade on update cascade
) engine=innodb default charset=utf8;
insert into `publishers` (`pid`, `publisher`) values
(1, 'unknown'),
(2, 'penguin'),
(3, 'faber cassell'),
(4, 'domino'),
(5, 'unknown');
insert into `books` (`bid`, `pid`, `title`) values
(1, 1, 'miss piggy got caught shoplifting'),
(2, 2, 'my life on crack by kermit the frog');
insert into `users` (`uid`, `username`) values
(1, 'joe bloggs'),
(2, 'fred smith'),
(3, 'john doe');
insert into `library` (`id`, `uid`, `bid`, `status`) values
(1, 1, 1, 1),
(2, 2, 2, 1);
Then, when you need to query for a particular book, user or publisher a derivation of the following:
$sql="select * from library l
left outer join users u on u.uid=l.uid
left outer join books b on b.bid=l.bid
left outer join publishers p on p.pid=b.pid
where u.username='joe bloggs';";
you need books table and users table and table books_users with id's of book and id of user who clicked is_completed (and with that you don't need boolean is_completed)
Impossible. The database looks the same for any user.
You don't have to create a table for each user either! That would be a really stupid solution.
Just create a table with two columns: user ID and book ID with proper links/joins/etc. If the user have already read the book, add a row inside this table with the corresponding user and book IDs. To check if the user have already read the book, just look for a row in this new table with the corresponding user and book IDs. If such a row is in the table - the user have already completed the book. If not - he had not.
As the row can't create itself, the default value is that he have not read the book yet. By removing the row you may also "change the user's history" - that will mark the book as incompleted by that user.
Yes this is possible. Set it up so when each user registers each book is set to false as you were stating.
Here is an example:
As each user is added, the book_id column is set to false for that user.
As others stated, of course you will need the Users table, but if you wanted, you could just transfer the user_id and setup a table like pictured above with each book (book_id into multiple columns) set to false. It would be easier to manager and understand.

Doctrine, how to left join an entity which wasn't generated?

I have rights:
CREATE TABLE `rights` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE INDEX `U_name` (`name`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
and profiles:
CREATE TABLE `profile` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE INDEX `U_name` (`name`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
I want to connect profiles to rights and also profiles to profiles:
CREATE TABLE `profile_profile` (
`profile_id1` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`profile_id2` INT(10) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`profile_id1`, `profile_id2`),
INDEX `I_profile_id2` (`profile_id2`),
CONSTRAINT `FK_profile_profile-profile-1` FOREIGN KEY (`profile_id1`) REFERENCES `profile` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `FK_profile_profile-profile-2` FOREIGN KEY (`profile_id2`) REFERENCES `profile` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
CREATE TABLE `profile_right` (
`profile_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
`right_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`profile_id`, `right_id`),
INDEX `I_right_id` (`right_id`),
CONSTRAINT `FK_profile_right-profile` FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `FK_profile_right-rights` FOREIGN KEY (`right_id`) REFERENCES `rights` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
a better overview:
so I generate entities:
php apps/doctrine.php dev orm:generate-entities libs/ --no-backup
--extend="\Doctrine\Entity\BaseEntity"
here come the problems. The Profile and Rights entities gets created, while Profile_rights and Profile_profile not. How to use them then?
In doctrine, join tables are not represented by an Entity.
You can find a #ORM\ManyToMany in your entities, with a #ORM\JoinTable and all informations about your associations.
This is the representation of your join table(s), use getters and setters like said by #Richard to access them.
Get more informations in the Associations mapping (to see all kind of associations) and Working with associations (to learn how work with them) chapters of the documentation.
Hope you have a good experience with doctrine.
EDIT
After look more at your UML, at least one of your associations doesn't need a many-to-many (As said by the first answer), but if they really have join tables in SQL, and you imported them by reverse engineering, they will surely be exactly as they are in SQL (a many-to-many with join table).
If you want to access the joining entity on a ManyToMany relationship you need to break it down to a OneToMany, ManyToOne.
E.g.
Profile - OneToMany < ProfileRight > ManyToOne - Profile.
Whether you should is another question. You only need to do this if you want to store extra data on the join table.
With what you have there it's trivial to get rights for a profile. For any profile you have loaded you simply call
$profile->getRights()
and doctrine will (assuming your generated entity mappings are correct) transparently fetch all the associated Rights entities for you based on the join table.
Similarly if you add a Right to a profile:
$right = new Right();
$profile->addRight($right);
Doctrine will transparently add the join table entry for you.

Transactions and Order Safety with MySQL (InnoDB)

Scenario
Say I have a list of voucher codes that I am giving away, I need to ensure that if two persons place an order at the exact same time, that they do not get the same voucher.
Tables
CREATE TABLE IF NOT EXISTS `order` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`voucher_id` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `voucher_id` (`voucher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
ALTER TABLE `order` ADD CONSTRAINT `order_fk` FOREIGN KEY (`voucher_id`) REFERENCES `voucher` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE IF NOT EXISTS `voucher` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`code` varchar(10) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Sample data
INSERT INTO `voucher` (`code`) VALUES ('A'), ('B'), ('C');
Sample Query
SELECT #voucher_id := v.id FROM `voucher` v LEFT JOIN `order` o ON o.voucher_id = v.id WHERE o.id IS NULL;
INSERT INTO `order` (`voucher_id`) VALUES (#voucher_id);
Question
I believe the UNIQUE KEY on voucher_id in the order table will prevent two orders having the same voucher_id, giving an error / throwing an exception if the same voucher id is inserted twice. This would require a while loop to retry upon failure.
The alternative is read locking the vouchers table before the SELECT and releasing that lock after the INSERT, ensuring the same voucher isn't picked twice.
My question is therefore:
Which is faster?
A while loop in PHP code.
Read locking the vouchers table.
Is there another way?
Edits
ALTER TABLEorderCHANGEvoucher_idvoucher_idBIGINT(20) UNSIGNED NOT NULL
will cause the INSERT to fail if #voucher_id is null (as desired, as there would be no vouchers left).
The "correct" and by that I mean best way to do what you're looking to do is to generate the voucher at the time you place the order. Look at the documentation for the sha1() function in php. You can seed it with unique information to prevent duplicates and use that for your voucher along with an auto_increment field for the unique ID.
When the order is placed, PHP generates a new voucher, saves it to the database, and sends it to the user. This way you're only storing valid vouchers and you're also preventing duplicates from being created.
You can use START TRANSACTION, COMMIT, and ROLLBACK to prevent race conditions in your SQL. http://dev.mysql.com/doc/refman/4.1/en/commit.html
In your case, I would just put your transactions into a critical area bounded by these tokens.

delete main row and all children mysql and php

I have inherited a PHP project and the client is wanting to add some functionality to their CMS, basically the CMS allows them to create some news, all the news starts with the same content, and that is saved in one table, the actually news headline and articles are saved in another table, and the images for the news are saved in another, basically if the base row for the news is deleted I need all the related rows to be deleted, the database is not setup to work with foreign keys so I cannot use cascade deletion, so how can I delete the all the content I need to, when I only what the ID of the base news row is?
Any help would be very helpful I am sorry I cannot give you much more help, here is this the original SQL of tables scheme if that helps?
--
-- Table structure for table `mailers`
--
CREATE TABLE IF NOT EXISTS `mailers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mailer_title` varchar(150) NOT NULL,
`mailer_header` varchar(60) NOT NULL,
`mailer_type` enum('single','multi') NOT NULL,
`introduction` varchar(80) NOT NULL,
`status` enum('live','dead','draft') NOT NULL,
`flag` enum('sent','unsent') NOT NULL,
`date_mailer_created` int(11) NOT NULL,
`date_mailer_updated` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=13 ;
-- --------------------------------------------------------
--
-- Table structure for table `mailer_content`
--
CREATE TABLE IF NOT EXISTS `mailer_content` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`headline` varchar(60) NOT NULL,
`content` text NOT NULL,
`mailer_id` int(11) NOT NULL,
`position` enum('left','right','centre') DEFAULT NULL,
`created_at` int(10) NOT NULL,
`updated_at` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=18 ;
-- --------------------------------------------------------
--
-- Table structure for table `mailer_images`
--
CREATE TABLE IF NOT EXISTS `mailer_images` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(150) NOT NULL,
`filename` varchar(150) NOT NULL,
`mailer_id` int(11) NOT NULL,
`content_id` int(11) DEFAULT NULL,
`date_created` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=15 ;
It is worth noting that the schema cannot be changed nor can I change to the DB to MYISAM so that I can use foreign keys.
Add foreign key to table mailer_content
FOREIGN KEY (mailer_id)
REFERENCES mailers(id)
ON DELETE CASCADE
Add foreign key to table mailer_images
FOREIGN KEY (content_id)
REFERENCES mailer_content(id)
ON DELETE CASCADE
http://dev.mysql.com/doc/refman/5.1/en/innodb-foreign-key-constraints.html
It is worth noting that the schema cannot be changed nor can I change to the DB to MYISAM so that I can use foreign keys.
Why can't the schema be changed? You designed the app, didn't you? Even if you didn't, adding the proper keys is just a matter of adding the right indexes and then altering the right columns. #Michael Pakhantosv's answer has what looks to be the right bits of SQL.
Further, it's InnoDB that does foreign keys, not MyISAM. You're fine there already.
If you could change the schema, making the appropriate IDs actual, real Foreign Keys and using ON DELETE CASCADE would work. Or maybe triggers. But that's just asking for it.
Now, for some reason, ON DELETE CASCADE isn't liked very much around here. I disagree with other people's reasons for not liking it, but I don't disagree with their sentiment. Unless your application was designed to grok ON DELETE CASCADE, you're in for a world of trouble.
But, given your requirement...
basically if the base row for the news is deleted I need all the related rows to be deleted
... that's asking for ON DELETE CASCADE.
So, this might come as a shock, but if you can't modify the database, you'll just have to do your work in the code. I'd imagine that deleting a news article happens in only one place in your code, right? If not, it'd better. Fix that first. Then just make sure you delete all the proper rows in an appropriate order. And then document it!
If you can not change the schema then triggers are not an option.
InnoDB supports transactions, so deleting from two tables should not be an issue, what exactly is your problem?
P.S. It would be worth noting which version of the server are you using.

Categories