PHP / Мультимагазин. Статья для начинающих веб-программистов

в 6:28, , рубрики: интернет-магазин, интернет-магазины, метки: ,

Многие интересные технические решения, возникают в результате решения интересных проблем. А кто придумывает или создает такие проблемы инженерам? Ответ — конечно пользователи. Вот и эта статья как раз рассказывает об одной такой интересной проблеме и ее решении.
Итак. В общем, проблема, со слов пользователя, выглядит, как необходимость создать один Основной интернет магазин и несколько Дополнительных. Основной магазин имеет полную базу товаров. Товар Дополнительных магазинов формируется путем запроса списка товаров из Основного магазина. Основной магазин, получив при запросе от Дополнительного магазина его идентификатор, отдает нужный товар.
Теперь рассмотрим проблему глазами инженера, учитывая, что базы товаров Основного и Дополнительного магазина находятся на одном сервере. Количество Дополнительных магазинов нам не известно, но очевидно, что нагрузка на Основной магазин будет большой. Большое количество разного товара не дает нам возможности точно сформировать структуру таблиц с товаром. Свойства товара очень сильно разнятся в зависимости от его категории. Суммируя полученные данные, задача для проектировщика Основного магазина звучит как:Спроектировать базу данных Основного магазина так, чтобы скорость выборки товара была очень быстрая, а свойства товара, его атрибуты, были динамическими.
Разделяй и властвуй!

Из «дано», следует наличие двух сущностей. Это Основной магазин и Дополнительный. Давайте их так и назовем
Основной — PrimaryShop
Дополнительный — SecondaryShop
Очевидно также, что изнутри Дополнительного магазина, нужно будет обращаться как к внутренним данным, так и к внешним данным, к данным Основного магазина. Для удобства напишем следующие две функции:
/*
* Проверяет соединение с базой, устанавливает его и возвращает класс для работы с каталогом товара основного магазина.
* @shopId - идентификатор магазина
* @return false | object PrimaryGoods
*/
function PrimaryShop($shop = null) {
global $primaryshop, $shopId;

/*
* Для того, чтобы постоянно не передавать в запросах к основной базе
* идентификатор магазина, можно его установить глобально, нужно объявить
* глобальную переменную shopId и ей присвоить значение
* В случае, если передается параметр магазина в функцию, то использую его
*/
if (!isset($shop) && !isset($shopId)) {
die("Идентификатор магазина shopId не установлен");
} else {
$shopId = isset($shop) ? $shop : $shopId;
}
if (Database::Setup(Config::Get("primaryDbHost"), Config::Get("primaryDbName"), Config::Get("primaryDbUser"), Config::Get("primaryDbPass"))->Connect()) {
return isset($primaryshop) ? $primaryshop : new PrimaryGoods($shopId);
} else {
die("Не могу соединится с Основной базой данных");
}
}

/*
* Проверяет соединение с базой, устанавливает его и возвращает класс для работы с товаром дополнительного магазина
* @return false | object SecondaryGoods
*/
function SecondaryShop() {
if (Database::Setup(Config::Get("secondaryDbHost"), Config::Get("secondaryDbName"), Config::Get("secondaryDbUser"), Config::Get("secondaryDbPass"))->Connect()) {
return new SecondaryGoods();
} else {
die("Не могу соединится с Локальной базой данных");
}
}

Соответственно SecondaryGoods и PrimaryGoods классы, реализующие методы работы с товаром соответствующих магазинов.
И это все? Конечно же нет. Это функции — обвертки. Они нас ни как не приближают к конечной цели. Попробуем порассуждать далее. Как правило, интернет магазин демонстрирует товар пользователю в двух режимах. Режим просмотра списка и режим просмотра карточки товара. Частота получения данных для режима просмотра списка, очевидна очень высокая, на много выше чем частота получения данных для просмотра карточки товара. Отсюда следует простой вывод. В случае если в Основной базе, будет специальная таблица, таблица с подготовленными данными для отображения их в режиме просмотра списка товара, скорость получения данных будет больше, нежели чем формировать эти данные путем соединения нескольких таблиц в одну. Данные для просмотра товара в режиме карточка, формируются стандартным образом, путем соединения нескольких таблиц в одну.
Пример запроса к Основной базе, путем соединения нескольких таблиц в одну. Такой запрос априори будет работать медленней, чем простой запрос, без соединений.
-- Запрос с соединениями
SELECT *
FROM `data_goods` AS `a`
LEFT JOIN `data_goods_price` AS `b` ON `b`.`id` = `a`.`priceId`
LEFT JOIN `data_goods_images` AS `c` ON `c`.`id` = `a`.`imagesId`
LEFT JOIN `data_goods_attr` AS `d` ON `d`.`id` = `a`.`attrId`

-- Простой запрос, без соединений
SELECT *
FROM `data_goods_short`

Подытожим. Обращение к определенному магазину осуществляем через функции обвертки. Для построения страницы со списком товара создаем специальную таблицу, специально для этого предназначенную.
Многосвойственность

Как сделать так, чтобы у товара А были следующие свойстваМаленькая картинка

Большая картинка

Размеры

Материал

Цена оптовая

Цена розничная

А у товара Б, были следующие свойстваСредняя картинка

Большая картинка

Цена розничная

Цена розничная, специальная

Нет ни какой возможности заранее заложить все возможные свойства товара. Ну в самом деле, ни делать же таблицу с пустыми колонками: Col1, Col2,… ColN.
Первое что приходит на ум, это сделать дополнительную таблицу, справочник. В этой таблице будет храниться набор возможных атрибутов товара, в зависимости от группы товара. Сам процесс получения карточки товара получается двух этапный. Этап первый: получаем список возможных атрибутов. Этап второй: в соответствии с этим списком, обращаемся к нужным таблицам и собираем информацию. Сами понимаете, процесс не быстрый. Годится он? Ответ — нет.
Если подробней посмотреть на содержимое карточки товара, то можно сгруппировать свойства товара в двух таблицах. Пусть таблица один, data_goods_images будет содержать список всех изображений товара, а таблица data_goods_attr будет содержать дополнительные атрибуты.
CREATE TABLE IF NOT EXISTS `data_goods_full` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goodsId` int(11) NOT NULL,
`categoryId` int(11) NOT NULL,
`shopId` int(11) NOT NULL,
`article` varchar(150) NOT NULL,
`intro` varchar(255) NOT NULL,
`name` varchar(150) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`),
KEY `categoryId` (`categoryId`),
KEY `goodsId` (`goodsId`),
KEY `shopId` (`shopId`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=501 ;

CREATE TABLE IF NOT EXISTS `data_goods_images` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goodsId` int(11) NOT NULL,
`shopId` int(11) NOT NULL,
`type` enum('Маленькая','Большая','Превью') NOT NULL,
`value` varchar(150) NOT NULL,
PRIMARY KEY (`id`),
KEY `goodsId` (`goodsId`),
KEY `shopId` (`shopId`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `data_goods_attr` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goodsId` int(11) NOT NULL,
`type` enum('Размер','Цвет','Позиция','Материал') DEFAULT NULL,
`value` varchar(150) NOT NULL,
PRIMARY KEY (`id`),
KEY `goodsId` (`goodsId`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Обратите внимание на поле type, в таблица data_goods_images и data_goods_attr. Это поле типа ENUM. Данный тип может принимать значение из списка. Теперь сделав запрос и соединив в нем эти три таблицы, мы получим все характеристики товара
SELECT *
FROM `data_goods_full` AS `a`
LEFT JOIN `data_goods_images` AS `b` ON `b`.`goodsId` = `a`.`goodsId`
LEFT JOIN `data_goods_attr` AS `c` ON `c`.`goodsId` = `a`.`goodsId`

Мы можем, также, получить любые определенные свойства, отдельно, не производя соединения. Например, я хочу получить только Маленькие картинки, товара B. Идентификатор товара, пусть будет равен 100
SELECT *
FROM `data_goods_images`
WHERE `type` = 'Маленькая' AND `goodsId` = '100'

Теперь давайте вернемся к вопросу, который был озвучен в начале главы. Как сделать так, чтобы у товара А были одни свойства, а у товара Б — другие. В нашем случае, нужно добавить в таблицу data_goods_attr дополнительные типы свойств товара, такие как: цена розничная, цена оптовая и цена розничная, специальная. Сделать это можно следующей командой:
ALTER TABLE `data_goods_attr` CHANGE `type` `type` ENUM('Размер','Цвет','Позиция','Материал','Цена розничная','Цена оптовая','Цена розничная, специальная')

Теперь добавим в базу данных товар А и товар Б (goodsId 100 и goodsId 101)
-- Заносим основные свойства товара
INSERT INTO `data_goods_full` (`id`, `goodsId`, `categoryId`, `shopId`, `article`, `intro`, `name`, `description`)
VALUES (null, '100', '1', '1', 'Арт-100', 'Это вступление', 'Товар А', 'Описание');

-- Заносим изображения
INSERT INTO `data_goods_images` (`id`, `goodsId`, `shopId`, `type`, `value`)
VALUES
(null, '100', '1', '1', 'Маленькая', 'small_a.png'),
(null, '100', '1', '1', 'Большая', 'big_a.png');

-- Заносим атрибуты
INSERT INTO `data_goods_attr` (`id`, `goodsId`, `shopId`, `type`, `value`)
VALUES
(null, '100', '1', '1', 'Размеры', 'XL'),
(null, '100', '1', '1', 'Материал', 'Резина'),
(null, '100', '1', '1', 'Цена оптовая', '100 руб.'),
(null, '100', '1', '1', 'Цена розничная', '125 руб.');

-- Теперь делаем все тоже самое, только для товара Б
-- Заносим основные свойства товара
INSERT INTO `data_goods_full` (`id`, `goodsId`, `categoryId`, `shopId`, `article`, `intro`, `name`, `description`)
VALUES (null, '101', '1', '1', 'Арт-101', 'Это вступление', 'Товар Б', 'Описание');

-- Заносим изображения
INSERT INTO `data_goods_images` (`id`, `goodsId`, `shopId`, `type`, `value`)
VALUES
(null, '101', '1', '1', 'Средняя', 'midle_b.png'),
(null, '101', '1', '1', 'Большая', 'big_b.png');

-- Заносим атрибуты
INSERT INTO `data_goods_attr` (`id`, `goodsId`, `shopId`, `type`, `value`)
VALUES
(null, '101', '1', '1', 'Цена розничная', '125 руб.'),
(null, '101', '1', '1', 'Цена розничная, специальная', '120 руб.');

Ну и теперь, давайте прочитаем, все что мы занесли в базу. После небольших
манипуляций с результатом запроса, получим следующее:
Array
(
[property] => stdClass Object
(
[id] => 1
[categoryId] => 1
[article] => Арт-100
[intro] => Вступление
[name] => Товар А
[text] =>
)

[images] => Array
(
[0] => stdClass Object
(
[imageType] => Маленькая
[imageValue] => small_a.png
)

[1] => stdClass Object
(
[imageType] => Большая
[imageValue] => big_a.png
)

)

[attributes] => Array
(
[0] => stdClass Object
(
[attrType] => Размер
[attrValue] => XL
)

[1] => stdClass Object
(
[attrType] => Материал
[attrValue] => Резина
)

[2] => stdClass Object
(
[attrType] => Цена оптовая
[attrValue] => 100 руб.
)

[3] => stdClass Object
(
[attrType] => Цена розничная
[attrValue] => 125 руб.
)

)

)

Array
(
[property] => stdClass Object
(
[id] => 2
[categoryId] => 1
[article] => Арт-101
[intro] => Вступление
[name] => Товар Б
[text] =>
)

[images] => Array
(
[0] => stdClass Object
(
[imageType] => Средняя
[imageValue] => midle_b.png
)

[1] => stdClass Object
(
[imageType] => Большая
[imageValue] => big_b.png
)

)

[attributes] => Array
(
[0] => stdClass Object
(
[attrType] => Цена розничная
[attrValue] => 125 руб.
)

[1] => stdClass Object
(
[attrType] => Цена розничная, специальная
[attrValue] => 120 руб.
)

)

)

Заключение

Для того, чтобы работать с полями типа enum, удалять и добавлять свойства, нужно уметь получать список значений этих полей. Получить такой список можно с помощью SQL команды DESCRIBE. Ниже, я приведу пример такой функции:
/*
* Функция возращает список справочников, список возможных значений
* поля type в таблице images
*/
function GetImagesSet() {
$query = "n DESCRIBE `data_goods_images` `type`";
if ($result = Database::Exec($query)->Read(0, "Type")) {
$result = str_replace(array("enum", "(", ")", "'"), array("", "", "", ""), $result);
return $result ? explode(",", $result) : $result;
}
}

/*
* Этот метод добавляет новое свойство для изображений.
* Маленькая, Большая.. и т.д.
*/
function AddImagesSet($type = null) {
if (isset($type)) {
$result = $this->GetImagesSet();
if (!empty($result)) {
foreach ($result as $item) {
$enum[] = "'".$item."'";
}
$enum = implode(",", $enum).",";
} else {
$enum = "";
}
$enum = $enum.str_replace(" ", "", $type);
$query = "n ALTER TABLE `d ata_goods_images` CHANGE `type` `type` ENUM(".$enum.")";
return Database::Exec($query)->Read();
} else {
return false;
}
}
/*
* Удаляем свойства из справочника.
* @example: 'Большая', 'Маленькая', 'Толстая' DelImagesSet('Толстая') => 'Большая', 'Маленькая'
* @return true | flase
*/
function DelImagesSet($type = null) {
if (isset($type)) {
foreach(self::GetImagesSet() as $value) {
if ($type != $value) {
$enum[] = "'".$value."'";
}
}
$query = "n ALTER TABLE `data_goods_images` CHANGE `type` `type` ENUM(".implode(",", $enum).")";
return Database::Exec($query)->Read();
} else {
return false;
}
}

Можно также скачать все одним файлом

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js