MySQL релация - Създаване на система с категории
13-11-2009
Тъй като много често има теми във форума за това как се правят категории реших да напиша урок.
В този урок ще ви покажа как да създадете таблица за категории и след това как се свързва с новини.
Ще използвам примерна система за новини, но може да се направи за всякакъв род сайтове, които се нуждаят от категории.
Например сайтове за филми, уроци, online магазини, игри и т.н.
Важно е да се схване принципа на връзката между две таблици в базата данни от типа "едно към много".

Отношение „едно към много“ между две таблици съществува тогава, когато един запис от първата таблица, наречена родителска, може да бъде свързан с много записи от втората таблица, наречена дъщерна, но запис от дъщерната таблица може да бъде свързан само с един запис от родителската таблица.



Както се вижда от картинката няколко новини могат да бъдат от една една и съща категория.
Връзката между 2-те таблици са полетата catid от таблицата на новините и полето id от таблицата
на категориите.
С една заявка вие може да определите кои новини към коя категория спадат.


Ето и стъпките за създаването на примерна система за новини с категории.

config.php

<?php
$host = "localhost";
$username = "root";
$password = "";
$database = "wt";
?>


I стъпка - създаване на таблиците
Първо ще създадем таблицата за категориите. Тя ще съдържа само две полета id и name:
CREATE TABLE IF NOT EXISTS `categories` (
`id` tinyint(3) unsigned NOT NULL auto_increment,
`name` varchar(15) NOT NULL,
PRIMARY KEY (`id`)
)


Тук няма какво толкова да се обяснява.
Създава се едно поле id tinyint unsigned. tinyint, защото едва ли ще имаме повече от 255 категории, а unsigned, защото категориите ни ще са само положителни числа.
Повече за mysql int типът можете да видите тук
Второто поле е varchar(15), защото в примера 15 е достатъчно дълго, за да побере имената на категориите в примера.

Сега да предположим, че имаме една таблица за новини. В примера новините няма да имат текст, а само заглавие и id:
CREATE TABLE IF NOT EXISTS `news` (
`id` int(10) unsigned NOT NULL auto_increment,
`title` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
)


II стъпка - свързване на двете таблици
Лесно е да се забележи, че тези две таблици не са свързани по никакъв начин. Точно затова трябва в таблицата news да създадем едно поле, в което ще записваме в коя категория е дадената новина. Това ще стане като добавим следния код:


ALTER TABLE news ADD catid tinyint unsigned NOT NULL


или ако сега създаваме таблицата с новини можем да я създадем директно с връзката към категориите:

CREATE TABLE IF NOT EXISTS `news` (
`id` int(10) unsigned NOT NULL auto_increment,
`title` varchar(50) NOT NULL,
`catid` tinyint(3) unsigned NOT NULL,
PRIMARY KEY (`id`)
)


III стъпка - напълваме с малко информация таблиците
Създаваме 7 категории:
INSERT INTO `categories` (`id`, `name`) VALUES
(1, 'Категория 1'),
(2, 'Категория 2'),
(3, 'Категория 3'),
(4, 'Категория 4'),
(5, 'Категория 5'),
(6, 'Категория 6'),
(7, 'Категория 7');


и вкарваме по няколко заглавия във всички без последната (за сега оставете поне една категория празна, за да видим една особенност в заявката за изкарването на новините - във видео урока).

INSERT INTO `news` (`id`, `title`, `catid`) VALUES
(1, 'Новина 1, категория 1', 1),
(2, 'Новина 2, категория 1', 1),
(3, 'Новина 3, категория 1', 1),
(4, 'Новина 1, категория 2', 2),
(5, 'Новина 2, категория 2', 2),
(6, 'Новина 1, категория 3', 3),
(7, 'Новина 1, категория 4', 4),
(8, 'Новина 2, категория 4', 4),
(9, 'Новина 3, категория 4', 4),
(10, 'Новина 4, категория 4', 4),
(11, 'Новина 1, категория 5', 5),
(12, 'Новина 2, категория 5', 5),
(13, 'Новина 3, категория 5', 5),
(14, 'Новина 4, категория 5', 5),
(15, 'Новина 5, категория 5', 5),
(16, 'Новина 1, категория 6', 6),
(17, 'Новина 2, категория 6', 6),
(18, 'Новина 3, категория 6', 6),
(19, 'Новина 4, категория 6', 6),
(20, 'Новина 5, категория 6', 6),
(21, 'Новина 6, категория 6', 6),
(22, 'Новина 7, категория 6', 6),
(23, 'Новина 8, категория 6', 6);


IV стъпка - Създаване на менюто с категориите
След като сме създали и напълнили таблиците вече сме свършили повече от половината работа. Остава единствено да направим кодовете, които да ни групират новините по категории (те вече са групирани, остава само да ги изкарваме правилно):

<?php
include("config.php"); //Инклудваме config файла, който съдържа хоста, потребителското име, паролата и базата данни

$connect = mysql_connect($host, $username, $password)
or die("Не може да свърже с базата данни"); //Правим връзка със сървъра и при неуспех изкарваме грешка
mysql_select_db($database, $connect)
or die("Не може да селектира базата данни"); //Селектираме базата данни и при неуспех отново изкарваме грешка
mysql_query("SET CHARACTER SET cp1251"); //Това съм го сложил защото при създаването на базата данни използвах колация cp1251_bulgarian_ci
$result = mysql_query("SELECT c.id, c.name, count(n.id) as count FROM categories as c LEFT OUTER JOIN news as n ON c.id = n.catid GROUP BY c.id")
or die("Не може да изпълни заявката"); //Правим заявката, към базата данни. Нея ще я обясня под кода
while($row = mysql_fetch_array($result)) {
echo '<a href="news.php?category='.$row['id'].'">'.$row['name'].' ('.$row['count'].')</a><br />'; //изкарваме линк към страницата с новини от категориите
}
?>


SELECT c.id, c.name, count(n.id) as count - взимаме само нужните полета, а не цялото съдържание на таблицата. Нужни са ни id на категорията, името на категорията и броя на новините в дадена категория. Префиксите c. и n. се използват за по-кратко записване. Ако го нямаше categories as c и news as n, трябваше да се изписват с цялото име на таблицата, което е твърде много писане.
FROM categories as c LEFT OUTER JOIN news as n ON c.id = n.catid - Повече за LEFT OUTER JOIN. Налага се да използваме LEFT OUTER JOIN, защото искаме да изкараме броя на новините. Благодарение на LEFT OUTER JOIN тук изкарваме всички записи от лявата таблица (categories) и смятаме броя на новините в категориите. ON c.id = n.catid - ето заради това ни трябваше да свържем двете таблици по някакъв начин.
Но заявката до тук не е достатъчна (пробвайте я без GROUP BY), защото без GROUP BY ще излезе само първия резултат като се сметне, че всички новини принадлежат на него и в моя промер ще изкара: Категория 1 (23)
GROUP BY c.id - Повече за GROUP BY. Накратко тук GROUP BY играе ключова роля за това да можем да изкараме правилния резултат. GROUP BY се използва със събирателните функции като sum, count и т.н, за да може да разграничи сумирането или броенете (както е в нашия случай) по даден критерии. В примера се групират по id на категорията, но може и по име. Така за всяка отделна категория се смята по отделно колко новини има в нея.

Ако не искаме да изкарваме броя на новините в дадената категория си спестяваме главоболията по разбирането на горната заявка и кодът може да стане чисто и проста така:

<?php
include("config.php");

$connect = mysql_connect($host, $username, $password)
or die("Не може да свърже с базата данни");
mysql_select_db($database, $connect)
or die("Не може да селектира базата данни");
mysql_query("SET CHARACTER SET cp1251");
$result = mysql_query("SELECT id, name FROM categories")
or die("Не може да изпълни заявката");
while($row = mysql_fetch_array($result)) {
echo '<a href="news.php?category='.$row['id'].'">'.$row['name'].'</a><br />';
}
?>


V стъпка - преглеждане на новините само от избраната категория
Както виждате сега линковете от менюто водят към news.php?category=число. Това е така, за да можем да преглеждаме заглавията на новините от всяка категория поотделно:


<?php
include("config.php"); //Инклудваме config файла, който съдържа хоста, потребителското име, паролата и базата данни
$connect = mysql_connect($host, $username, $password)
or die("Не може да свърже с базата данни"); //Правим връзка със сървъра и при неуспех изкарваме грешка
mysql_select_db($database, $connect)
or die("Не може да селектира базата данни"); //Селектираме базата данни и при неуспех отново изкарваме грешка
mysql_query("SET CHARACTER SET cp1251"); //Това съм го сложил защото при създаването на базата данни използвах колация cp1251_bulgarian_ci
$category = 1; //Присвояваме на $category 1, така винаги $category ще има някаква валидна стойност
if(isset($_GET['category']) and is_numeric($_GET['category'])) { // Правим проверка дали имаме подадена $_GET променлива category и дали тя е число
$category = intval($_GET['category']); //Ако проверката е върнала true заменяме стойността на $category с числената стойност на $_GET['category']. Тук се използва intval, за да превърне всяка стойност, минала проверката в цяло число. Махнете intval() и пробвайте да извикате скрипта с news.php?category=4.5, за да видите за какво се използва.
}
$result = mysql_query("SELECT c.name, n.title FROM news as n LEFT OUTER JOIN categories as c ON n.catid = c.id WHERE n.catid = '".$category."'")
or die("Не може да изпълни заявката"); //тук пак използваме LEFT OUTER JOIN, защото изкарваме категорията на новината до нейното заглавие.
if(mysql_num_rows($result) > 0) { //Ако има повече от 0 резултати
while($row = mysql_fetch_array($result)) {
echo $row['title']. " в категория: ". $row['name']."<br />"; //изкарваме името на новината и името на категоията, в която се намира дадената новина
}
} else { //Ако е 0
echo "Няма резултати в тази категория."; //изкарваме, че няма резултати в тази категория
}
?>


Тук пак се налага да използваме LEFT OUTER JOIN, защото искаме да изкараме името на категорията. В реални ситуации това едва ли би било така, затова можем да преправим скрита така:


<?php
include("config.php");
$connect = mysql_connect($host, $username, $password)
or die("Не може да свърже с базата данни");
mysql_select_db($database, $connect)
or die("Не може да селектира базата данни");
mysql_query("SET CHARACTER SET cp1251");
$category = 1;
if(isset($_GET['category']) and is_numeric($_GET['category'])) {
$category = intval($_GET['category']);
}
$result = mysql_query("SELECT title FROM news WHERE catid = '".$category."'")
or die("Не може да изпълни заявката");
if(mysql_num_rows($result) > 0) {
while($row = mysql_fetch_array($result)) {
echo $row['title']."<br />";
}
} else {
echo "Няма резултати в тази категория.";
}
?>


Примерен код видео урока има във файловете за сваляне.

Файлове за сваляне
Файлове за сваляне - линк 1
Файлове за сваляне - линк 2
Файлове за сваляне - линк 3




/ Трябва да сте регистриран за да напишете коментар /
От: stoqnski
19:32 13-11-2009
Направи едно продължение на урока ти примерно за добавяне на новина от страница , защото много въпроси ще възникнат от потребителите :) А и да е по-обширно и завършено . Ако нямаш време мога да го направя вместо теб :) Иначе урока е 6 . Всичко е добре обяснено , навсякъде има коментари , показал си начините за вкарване на SQL в БД-то , видеото е с добро качество . Браво !
От: GaTio2
10:44 14-11-2009
мерси помогна ми до някъде :)
От: djman
20:15 14-11-2009
Мен ми е интересно как ще стане това:
Категория 1
- Новина 1
- Новина 2
Категория 2
- Новина 1

И т.н., а категории в които няма новини - да не се показват...
От: adrian
23:06 14-11-2009
djman,
news.php е точно това, само че съм разменил и категория 1 се показва в края.
От: dhtodorov
16:30 15-11-2009
@adrian :
Ако приемаш предложиения ще е хубаво да направиш v2 на този урок. Идеата е, че са ще се почнат въпросите за дървоовидната структура как се прави и как е най-лесно да се извежда и т.н.. Хубаво е ако ти се занимава да моднеш урока така, че в таблица "Категорий" да имаш "parent_id" за да създадеш дървоовидната структура и да направиш една рекурсивна функция за извеждането и. Мисля, че ще има добър ефект за хората тук. Иначе добре си се справил в урока, хареса ми! Поздравления!
От: dhtodorov
16:32 15-11-2009
Извинявам се за грешката "дървовидната структура", а не "дървоовидна структура" :-)
От: xlebabarov
17:16 22-11-2009
Това за дървовидната структора означава ли, че ще има N на брой под категории?
От: k0c3to0o
9:35 04-12-2009
Моля един урок и с подкатегории на новините
От: Rapon
10:34 05-12-2009
Може да добавиш един мета таг в config.php за да стане всичко на бб
От: rivo
18:55 27-01-2011
Благодаря и на мен ми помогна до някъде :)
От: qazxsw
5:06 05-06-2011
Ми колкото и да го чета просто не го разбирам. Като се стигне до момента със самият Left Join и ме губиш с обясненията си, и особено така както си го съкратил c. .n n бла бла бла, тотално ме объркваш. За хора които копират целия код дирекно и не си правят труда да го разберат или хора които знаят как да си го направят сами става урока, но за хора като мен които не знаят как става и искат да РАЗБЕРАТ какво всъщност става, просто мен ме губиш :\
От: teroristd
15:41 04-07-2011
И аз с тия съкращения се обърквам въпреки че постигнах желания ефект. Принципно за начинаещите, тоест като мене :) е по добре да е написано цялото нищо че е много писане. Иначе 6 за урока много е полезен.
От: teroristd
16:04 04-07-2011
А също и в изкарването имам малка забележка, прекалено много кавички и точки има. В урока е така

echo ''.$row['name'].' ('.$row['count'].')';

а аз го пиша така и пак си работи

echo " $row[name] ($row[count])";

Всеки си има стил на писане но лично мене ме объркват прекалено многото кавички и точки.
1