deleting nested categories from a table - php

I have a table called categories which looks like this
+-------------+--------------+---------------+
| id | catName | parentId |
+-------------+--------------+---------------+
| 1 | Category 1 | 0 |
| 2 | Category 2 | 0 |
| 3 | Sub Cat 1 | 1 |
| 4 | Sub Cat 2 | 1 |
| 5 | Sub sub cat 1| 4 |
| 6 | Sub sub cat 2| 4 |
| 7 | Category 2 | 0 |
+-------------+--------------+---------------+
It would be easy to delete a category and its direct child:
mysql_query("DELETE FROM `categories` WHERE `id`='$id'");
mysql_query("DELETE FROM `categories` WHERE `parentId`='$id'");
However I need to be able to delete ALL children of a category. For example if Category 1 was deleted Sub Cat 1, Sub Cat 2, Sub sub cat 1, Sub sub cat 2 will be deleted
Any help appreciated.

You could use foreign keys, a very nice feature of mysql. This is a kind of integrity check for referencing different relations. If you create a foreign key, mysql ensures that referenced entries exist, for example. During the creation of a foreign key, you can define what mysql should do if the "parent element" is deleted. You can specify "SET NULL", "UPDATE" or "DELETE CASCADE". This means, if you delete a category, every connected sub category is deleted as well. And because of the fact that every sub category is the parent category of a sub sub category, those are deleted as well.

Here is a simple recursive script for deleting your category and all the nested categories nested inside.
try {
$db = new PDO(DB_TYPE.':host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASS);
}catch( PDOException $e ) {
echo $e->getMessage();
}
class Category {
function delete_category($parrent){
global $db;
$stmt = $db->prepare("SELECT `id` FROM `categories` WHERE `parrentId`=:parrentId");
$stmt->execute(array('parrentId'=>$parrent));
while($category = $stmt->fetchObject('Category')){
$category->delete_category($category->id);
}
$stmt = $db->prepare("DELETE FROM `category` WHERE `id` = :id");
$stmt->execute(array('id'=>$parrent));
}
function __construct(array $data = NULL) {
if( empty($data) )
return;
foreach( $data as $field => $value ) {
$this->$field = $value;
}
}}
Where $db is your PDO object for connecting with the database. It may have some bugs, as i just modified mine to suit your problem. Hope this helps.
PS: for calling your function you just do: $category=new Category();
$category->delete_category($_GET['id'])

Quoted my original answer for clarity.
UPDATE 1: The problem with the table splitting is that it isn't clear that there is a fixed number of sub, sub, categories. For this to work, you would need to split into as many tables as there are subcategories, each with foreign keys back to their parent ids. Not practical if the number of sub (or sub sub) categories is dynamic.
This is a situation where I think you would benefit in changing your
table design. If you split the two tables into categories and
subcategories you could take advantage of the foreign key
functionality as described by strauberry. For example (these don't
look like tables, but hopefully this makes sense) Stuff in parentheses
is explanation:
categories
id (primary key)
catName
subcategories
sub_id (primary key)
id (foreign key referencing primary key of categories table)
subCatName
Then when you create the categories table, you can use the ON DELETE
CASCADE option. This will mean that whenever you delete a category
from the categories table, all related subcategories in the
subcategories table will be deleted automatically for you by MySQL.
Powerful, and reduces your code, and you don't have to make sure all
deletions happen in the right tables manually. Hope that clarifies a
little.
UPDATE 2: The problem with this option is, well, it won't work :) MySQL will not allow you to subquery on the same table that you are doing the DELETE on. So what appeared at first to be a simple problem... isn't.
If you can't change the table design then you could do the following
(substitute the id you actually want to delete for
category_id_to_delete):
DELETE FROM categories
WHERE id = category_id_to_delete OR id IN (
SELECT id
FROM categories
WHERE parentID = category_id_to_delete
)
So, I did some more checking, and came across this similar question on SO MySQL: How to find all IDs of children recursively?, and the highest rated answer points to this article Managing Hierarchical Data in MySQL. That article points out the type of queries with LEFT JOINS that you need to use to select data, but doesn't explicitly cover how to delete that data. The recommended solution would be to use the queries related to adjacent sets from the article to select all the id information you need, and then in your code (php I think?) loop over those ids and construct a new query to delete them.
For example, if you are trying to delete Category 1 (id=1), your goal by looping over the ids from the LEFT JOIN query from the article would be to create another query that looks like:
DELETE FROM categories
WHERE id IN (1,3,4,5,6);
The code in this forum post also may help you as a means of doing it recursively in php: Adjacency List Model recursive deletion
I know that isn't a neat and tidy answer, but perhaps this will point you in the right direction.

demonstration of the mysql "cascade on delete" feature. if you run this in your console, then you can just run all the queries at once, and the output will make sense. if you run it in some graphical tool, then i suppose you'll have to run the queries one by one.
CREATE DATABASE `recursivedemo`;
USE `recursivedemo`;
CREATE TABLE `categories` (
`id` INT AUTO_INCREMENT,
`catName` VARCHAR(50),
`parentId` INT,
PRIMARY KEY (`id`),
KEY `parentId` (`parentId`),
CONSTRAINT `categories_fk_self` FOREIGN KEY (`parentId`) REFERENCES `categories`(`id`) ON DELETE CASCADE
) Engine=InnoDB;
SELECT * FROM `categories`;
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Category 1', NULL);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Category 2', NULL);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Sub Cat 1.1', 1);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Sub Cat 1.2', 1);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Sub sub cat 1.2.1', 4);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Sub sub cat 1.2.2', 4);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Sub Cat 2.1', 2);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Sub Cat 2.2', 2);
INSERT INTO `categories`(`catName`, `parentId`) VALUES('Category 3', NULL);
SELECT * FROM `categories`;
DELETE FROM `categories` WHERE `id`=1;
SELECT * FROM `categories`;
DROP DATABASE `recursivedemo`;

You can use following Function in model for deleted nested categories:
public function delete_by_id($cat_id)
{
$this->db->delete('Categories', ['cat_id' => $cat_id]);
$q = $this->db->where('parent_id', $cat_id)->get('Categories');
foreach( $q->result() as $Child ) {
$this->delete_by_id($Child->cat_id);
}
}

Related

Mysql sub-cat and parent cat with posts [Database schema]

Okay so I have a simple category table and a separate posts table easy right but when the user posts a post I wast think should I store both the sub and parent cat in the posts table but would that not be a lot of data duplication so I instead just store the sub_cat then I use a few PHP functions to query the database for the primary cat and its name.
categories table
ID | cat_name | main_cat
1 | Dinner | 0
2 | Chicken | 1
posts table
ID | title | sub_cat | fields that are not related to Q
1 | test | 2 |
Get parent(main) category
$sub_cat = is from a selection query that gets posts and their sub_cats
function main_cat($sub_cat){
require("conn_posts.php");
$stmt = $conn_posts->prepare("SELECT `main_cat` FROM `cats` WHERE `ID` = ?");
$stmt->bind_param("s", $sub_cat);
$stmt->execute();
$stmt_results = $stmt->get_result(); // get result
while($row_get = $stmt_results->fetch_assoc()){
if($row_get['main_cat'] == 0){
return $sub_cat;
}elseif($row_get['main_cat'] !== ""){
return $row_get['main_cat'];
}
}
}
This function gets any category name as long as the id is valid
function cat_name($cat_number){
require("conn_posts.php");
$stmt = $conn_posts->prepare("SELECT `cat_name` FROM `cats` WHERE `ID` = ?");
$stmt->bind_param("s", $cat_number);
$stmt->execute();
$stmt_results = $stmt->get_result(); // get result
$row_get = $stmt_results->fetch_assoc();
if($stmt_results->num_rows <= 0){
return 0;
}elseif($stmt_results->num_rows == 1){
return $row_get['cat_name'];
}
}
My question is is this a good way to process my posts sub-category and parent category are there better ways of doing what I am currently doing? eg. is my database schema good(by good I mean is it better to just include the parent cat id in the posts table than to do the PHP server-side processing)?
Your database schema is good: it doesn't include any replication, I wouldn't change it. The way you're handling fetching the categories in PHP isn't really optimal though: you should almost always aim to minimize the number of queries as it (in general) will affect performance more than the complexity of a query.
If you're running MySQL 8+, a great way to do this is with a recursive CTE; it will allow you to fetch all parents with one query:
WITH RECURSIVE cte AS (
SELECT id, cat_name, main_cat, 0 as depth FROM categories WHERE ID=3
UNION ALL
SELECT categories.id, categories.cat_name, categories.main_cat, cte.depth+1 as depth
FROM cte inner join categories
ON cte.main_cat = categories.id
)
SELECT cat_name FROM cte order by depth ASC
The number '3' in that query can be replaced by the category you're trying to retrieve. You can check this DB fiddle for a live example. If I see your code, incorporating it into your PHP should be fairly trivial. If not, leave a comment and I'll try to expand.

SQL result to multidimensional array (SQL to markdown file)

I am trying to transfer data from a MySQL database (with relational columns) to a markdown file (Pico CMS). For that I created the following SQL query to get the data:
SELECT DISTINCT
rqypj_mt_links.link_name,
rqypj_mt_links.link_desc,
rqypj_mt_links.address,
rqypj_mt_links.city,
rqypj_mt_links.state,
rqypj_mt_links.country,
rqypj_mt_links.postcode,
rqypj_mt_links.telephone,
rqypj_mt_links.fax,
rqypj_mt_links.email,
rqypj_mt_links.website,
rqypj_mt_links.price,
rqypj_mt_links.lat,
rqypj_mt_links.lng,
rqypj_mt_links.zoom,
rqypj_mt_cats.cat_name,
rqypj_mt_images.filename,
rqypj_mt_cfvalues.value,
rqypj_mt_customfields.caption
FROM rqypj_mt_links
LEFT JOIN rqypj_mt_cl
ON rqypj_mt_links.link_id = rqypj_mt_cl.link_id
LEFT JOIN rqypj_mt_cats
ON rqypj_mt_cl.cat_id = rqypj_mt_cats.cat_id
LEFT JOIN rqypj_mt_images
ON rqypj_mt_links.link_id = rqypj_mt_images.link_id
LEFT JOIN rqypj_mt_cfvalues
ON rqypj_mt_links.link_id = rqypj_mt_cfvalues.link_id
LEFT JOIN rqypj_mt_customfields
ON rqypj_mt_cfvalues.link_id = rqypj_mt_customfields.cf_id
ORDER BY rqypj_mt_links.link_id, rqypj_mt_cl.cat_id
LIMIT 100
This qives the following result
Title | desc | adress | cats | etc.
But when a title has multiple cats I get multiple rows with the same data only the cat row difference. Therefore I am looking for the best way to transfer the SQL data in to a PHP array. F.e.:
$result['title']
$result['desc']
$result['cats'][0]
$result['cats'][1]
Etc.
I guess that way it w'll be more easy to write the markdown file. Hopefully someone can get me some advise about the best approach and some PHP tips/scripts.
Thanks in advance!
Jelte
I think you want to do something similar to this question and similarly you can use GROUP_CONCAT.
Look at this SQL Fiddle. Maybe it will help you :-)
Schema:
CREATE TABLE link
(
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255),
PRIMARY KEY (id)
);
CREATE TABLE cat
(
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255),
PRIMARY KEY (id)
);
CREATE TABLE has_cat
(
`link_id` INT,
`cat_id` INT,
FOREIGN KEY (link_id) REFERENCES link(id),
FOREIGN KEY (cat_id) REFERENCES cat(id)
);
Values:
INSERT INTO link (`name`)
VALUES
('John'),
('Calvin')
;
INSERT INTO cat (`name`)
VALUES
('Garfield'),
('Nermal'),
('Hobbes')
;
INSERT INTO has_cat (link_id, cat_id)
VALUES
(1, 1),
(1, 2),
(2, 3)
;
Query:
SELECT link.name AS link, GROUP_CONCAT(cat.name) AS cats
FROM link, has_cat, cat
WHERE
link.id = has_cat.link_id AND
cat.id = has_cat.cat_id
GROUP BY link.name
Result:
| link | cats |
|--------|-----------------|
| Calvin | Hobbes |
| John | Garfield,Nermal |
Make a PHP array from it
From here, you can explode the comma separated string into an array:
var_dump( explode( ',', $result['cats'] ) );
That will give you:
array(2)
(
[0] => string(8) "Garfield"
[1] => string(6) "Nermal"
)

mysql like not wildcard

I have this table:
+---------+----------+
+ Items + Person +
+---------+----------+
+ 2,99,75 + Jack +
+ 4,9,63 + Rose +
+---------+----------+
Now I do a simple
LIKE :items
and binding it using
$stmt->bindParam(':items',$item,PDO::PARAM_STR);
where
$item = "%9%".
The result contains both Jack and Rose, which is wrong because I expected to have Rose only as my result. It seems that LIKE sees both 99 and 9. How can I restrict my LIKE to have only 9 because that was the value of $items?
The other answers are good to do. However, I pose this alternative based on the fact that Items appears to be ID's.
If you need to query off comma separated values I would recommend a separate table. Using LIKE to query in a single field will never truly be fool-proof and could be a security concern. Try this.
Table 1: Person
+---------+----------+
+ ID + Person +
+---------+----------+
+ <int> + <string> +
+---------+----------+
Table 2: Item
+---------+----------+
+ PersonID+ ItemID +
+---------+----------+
+ <int> + <int> +
+---------+----------+
Then use joins to query both tables as needed.
SELECT * FROM Person INNER JOIN Items ON Items.PersonID = Person.ID
WHERE Items.ItemID = '9';
This should provide you with every record in Person that has ItemID "9" associated with them.
Perhaps this might help: http://www.codinghorror.com/blog/2007/10/a-visual-explanation-of-sql-joins.html
Its because % represent one or more character (anything). So 99 will match the "%9%"
If you want only 9, you can try with
"%,9,%"
I believe the main issue on this is what #interrobang said, the way you are representing the data.
If this table X that you show is the list itens for each person, you should have a column with the person id and another column with the item id, and multiple lines to represent multiple itens for each person. Doing like this your search would be much faster and easier to use and mantain in the future.
SQL Fiddle
MySQL 5.5.30 Schema Setup:
CREATE TABLE person (
id int auto_increment primary key,
name varchar(20)
);
CREATE TABLE item (
id int auto_increment primary key,
name varchar(20)
);
CREATE TABLE person_item (
id int auto_increment primary key,
person_id int,
item_id int
);
ALTER TABLE person_item ADD UNIQUE (person_id,item_id);
INSERT INTO person(id,name) VALUES
(1, 'John'),
(2, 'Mary'),
(3, 'Oliver');
INSERT INTO item (id,name) VALUES
(1,'Pen'),
(2,'Pencil'),
(3,'Book');
INSERT INTO person_item (person_id,item_id) VALUES
(1,1),
(1,3),
(2,2),
(3,1);
Query 1:
select p.name from person_item pi, person p, item i
where pi.person_id = p.id
and pi.item_id = i.id
and i.name LIKE 'Book%'
Results:
| NAME |
--------
| John |

need help in search [closed]

It's difficult to tell what is being asked here. This question is ambiguous, vague, incomplete, overly broad, or rhetorical and cannot be reasonably answered in its current form. For help clarifying this question so that it can be reopened, visit the help center.
Closed 9 years ago.
As per my system i need to search brand based on category and subcategory.
Brand table like this
id brandname categoryid subcategoryid
1 xys 1,2,3 1,5,6
Now when i search i select category then as per category all subcategory come now i select subcategory now need to show all the brand based on that category and subcategory.
My brand table look like this because same brand have multiple category and subcategory.Please help me to solve this problem
Given your database design, you can do that like this:
SELECT * FROM mytable WHERE
FIND_IN_SET('5', categoryid) > 0 AND FIND_IN_SET('3', subcategoryid) > 0;
This will find all items from category 5 and subcategory 3.
SELECT * FROM mytable WHERE
FIND_IN_SET('5', categoryid) > 0 AND (
FIND_IN_SET('3', subcategoryid) > 0
OR
FIND_IN_SET('9', subcategoryid) > 0
);
The above will find items in category 5, subcategories 3 and 9. Of course you can also restrict to items that are in both categories, by using AND instead of OR.
But all this is needlessly expensive. You would do better by having a table for brand names, and other tables for category and subcategory IDs, and links, like this:
// This is an article. Many-to-one relation with brands.
CREATE TABLE articles
(
id integer primary key not null auto_increment,
name varchar(...),
brand_id integer,
//, other data
);
CREATE TABLE brands
(
id integer primary key not null auto_increment,
name varchar(...)
//, other data
);
// Categories. Many-to-Many relationship with articles.
CREATE TABLE categories
(
id integer primary key not null auto_increment,
name varchar(...)
//, other data
);
// Subcategories. These are independent from categories, which
// may be right or wrong, depending. Being independent, we do not
// store here parent_category_id.
CREATE TABLE subcategories
(
id integer primary key not null auto_increment,
name varchar(...)
//, other data
);
// Many to many relationship between articles and categories
CREATE TABLE mtm_article_in_category
(
article_id integer not null,
category_id integer not null
);
CREATE TABLE mtm_article_in_subcategory
(
article_id integer not null,
subcategory_id integer not null
);
// Add article 5 to categories 25, 37 and 119:
INSERT IGNORE INTO mtm_article_in_category VALUES ( 5, 25 ), ( 5, 37 ), ( 5, 119 );
// Remove article 18 from subcategory 92
DELETE FROM mtm_article_in_category WHERE article_id = 18 AND subcategory_id = 92;
This way you can run much faster queries, and not have problems such as the inability to assign an article to more than "so many" categories (e.g. 50); nor the headaches if you wanted to move an article from a category to another, that with your current design would be next to impossible.
My search like this at first i chose category the all the subcategory
come based on category.Then when i chose subcategory all the brand
based on that category and subcategory come.Now i add one brand name
one time with multiple category and multiple subcategory.
I have to say, "Oh my God". To be able to "select all the subcategories" /now/, you would have to transform this
category subcategory
4,5 1,7,9,19
5 7,9,11
in
5 1
5 7
5 9
5 19
5 7
5 9
5 11
then run a DISTINCT, and finally use the subcategories as an INNER JOIN based on FIND_IN_SET.
The first step ("explode" a CSV row) can be done like this: http://www.marcogoncalves.com/2011/03/mysql-split-column-string-into-rows/ ... and as you can see it is all but trivial.
I expect that currently you are doing the splitting in PHP.
After doing that, the INNER JOIN is awfully expensive.
We are throwing good money after bad. Your current database design does not allow to do what you want, easily. The simplest way would be:
// My search like this at first i chose category the all the subcategory
// come based on category.
$query = "SELECT subcategoryid FROM mytable WHERE FIND_IN_SET(:mycategory, categoryid) > 0;";
// and run the query.
$subcategories = array();
while($tuple = sql_fetch_tuple($exec))
{
// Explode "1,2,3" into array {1, 2, 3}. Merge into subcategories removing
// duplicates. Rinse. Repeat.
$subcategories = array_unique(array_merge($subcategories, explode(',', $tuple['subcategoryid'])));
}
sql_free($exec);
// Now we have an array of subcategories.
// Then when i chose subcategory all the brand
// based on that category and subcategory come.
$subcat_query = array();
foreach($subcategories as $subcategory)
$subcat_query[] = "FIND_IN_SET('$subcategory', subcategoryid)";
$subcat_query_sql = implode(' OR ', $subcat_query);
$query = "SELECT DISTINCT brand FROM mytable WHERE FIND_IN_SET(:cat, categoryid) AND ( $subcat_query_sql );";
// And here we get all brands. It is wise to save $subcat_query_sql in _SESSION.
// Next search will be:
// >Now i add one brand name
// > one time with multiple category and multiple subcategory.
// Note that you've subtly moved the target once more, now the 'category' has become "multiple".
$brands_arr[] = array();
foreach($brands as $brand)
$brands_arr[] = "'" . sql_escape($brand) . "'";
$brands_sql = implode(',', $brands_arr);
// The cost of this $query is estimated as a significant percentage of U.S. gross internal product, so it ought to be cleared with the FED.
$query = "SELECT * FROM mytable WHERE brand IN ($brands_sql) AND FIND_IN_SET(:cat, categoryid) AND ( $subcat_query_sql );";
It is also possible that the above query will return nothing at all. Suppose that you looked for subcategory 5 and category 12. By your request, getting "all subcategories" and "all categories" might return also brand 6 and subcategory 9. Then these two rows come out,
Marlboro 5 12
Lucky 6 9
and the user selects "Marlboro 6 12". He won't get anything - no rows will match that query.
I am afraid that the user interface and workflow/use case needs looking into, too.

How do I get the count of all the products in a category and its sub categories (and sub-sub categories)?

The category table looks like somewhat as below:
id -- name -- parent_id
1 -- Men -- 0
2 -- Women -- 0
3 -- Shirts -- 1
4 -- Half-sleeve -- 3
5 -- Full-sleeve -- 3
Relationship table:
Product_id -- Category Id
1 -- 2
2 -- 2
3 -- 4 ....
I can retrieve the number of products in any one category and its immediate sub categories with ease with ease. But if there are more than 2 levels things get messy.
So my question is How do I get the number of all of the products in Men and its sub categories. Or Shirts and its subcategories?
Any ideas, Thanks.
UPDATE:
I know there is Nested Set Model but I am not in position to change the structure to that now.
If it is possible I would check out Managing Hierarchical Data in MySQL.
It is hard to get your head around at first, but it makes tasks like this much easier.
If you can't do this, you will have to do a recursive function, e.g.:
$prods = 0;
function getProdsInCat($cat)
{
global $prods;
$prods += mysql_result(mysql_query(SELECT COUNT(`Product_id`) FROM `prod_to_cat` WHERE `Category Id` = '".$cat."'),0);
$moreCats = mysql_query("SELECT `cat_id` FROM `cats` WHERE `parent_id` = '".$cat."'");
while($cats = mysql_fetch_assoc($moreCats)
{
getProdsInCat($cats['cat_id']);
}
}
Assuming you can add an extra column to the categories table.
Said column will have the path to the category.
id -- name -- parent_id path
1 -- Men -- 0 0/
2 -- Women -- 0 0/
3 -- Shirts -- 1 0/1
4 -- Half-sleeve -- 3 0/1/3
5 -- Full-sleeve -- 3 0/1/3
That way finding all the subcategories becomes one query:
SELECT id as CatId FROM categories WHERE path LIKE '0/1/%';
And to get the count of all the products within a category and its childrens is pretty easy too:
SELECT count(p.id) as Total
FROM products as p
JOIN categories as c ON p.category_id = c.id
WHERE c.path like '0/1/%';
Pretty efficient query.
This article provides more information: More Trees & Hierarchies in SQL

Categories