4,7K подписчиков

Реляционная база данных: Делаем список категорий

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

Код для этого выпуска лежит на github в ветке relation.

Итак, после завершения первичной разработки сайта у нас есть база данных MySQL и сайт на Apache + PHP, с помощью которого мы можем наполнять базу и читать из неё данные.

Хотя мы успешно пользуемся базой данных MySQL для хранения заметок, на самом деле мы используем её не по назначению, так как и MySQL, и другие базы на основе SQL – реляционные. Что это значит, мы сейчас рассмотрим.

Апгрейд заметок

Напомню, что наши данные – это заметки, которые имеют несколько полей: дату создания, заголовок и тело.

Предположим, мы решили в структуру заметки добавить ещё одно поле – категорию. Теперь каждая заметка может иметь свою категорию (автомобили, компьютеры, кино и т.п.)

Я в двух словах опишу, какие изменения надо сделать, но делать их по-настоящему мы не будем, так как это неправильное решение.

Очевидно, что для хранения категории нужно добавить в таблицу note ещё одно поле, назовём его category. Оно будет типа "строка".

Далее, мы меняем HTML-форму редактирования заметки, добавляя туда элемент <input type="text" name="category">. Теперь мы можем ввести категорию при редактировании и сохранить её.

И хотя эта схема вполне работоспособна, мы сразу сталкиваемся с некоторыми неприятностями.

1. Ошибки в названиях

Допустим, мы добавили заметку и указали для неё категорию "ретро-игры". Через некоторое время мы добавили ещё одну заметку и указали категорию "ретро игры".

Что произошло? Мы хотели в общем-то назначить одну и ту же категорию двум заметкам, но из-за невнимательности или забывчивости ввели две разные категории. Эти строки отличаются всего лишь одним знаком, но для компьютера это, конечно, совершенно разные значения.

Можно делать и другие ошибки: например, в одном месте пишем "программирование с нуля", а в другом "программирование для начинающих". Понятно, что мы имели в виду одно и то же, просто не придерживались системы в названиях.

2. Проблема переименования

Предположим, мы добавили 100 заметок с категорией "собаки", а потом поняли, что категория должна называться "собаководство". Значит, нам придётся открыть каждую из 100 заметок и исправить в ней категорию. Есть способ полегче: написать запрос к базе данных, который изменит категории всех записей, где указана категория "собаки", на "собаководство". Но мы же не для того строили сайт и делали пользовательские интерфейсы, чтобы руками писать запросы к базе?

3. Проблема хранения

Так как мы добавили текстовое поле в таблицу note, очевидно, что размер записи стал больше. В нашем случае это не существенно, но в целом надо представлять себе, что будет при масштабировании: несколько лишних байт умножить на миллион записей это уже несколько лишних мегабайт. Да, всё ещё несущественно, но важен принцип, плюс существуют такие проблемы оптимизации, которые мы ещё не проходили.

Реляция – это отношение

Для решения вышеописанных проблем мы поступаем так:

Создаём отдельную таблицу category. В ней будут следующие поля:

  • id – уникальный идентификатор категории, целое число с автоинкрементом.
  • title – наименование категории, строка

Далее пишем CRUD-контроллер, создаём представления и модель для категорий, то есть банально копируем всё то, что было сделано для заметок, поменяв названия файлов и полей в нескольких местах.

Отлично, теперь мы можем редактировать заметки, и также можем редактировать категории. Но как назначать категории заметкам?

Модифицируем таблицу note. Добавим в неё поле category_id, это целое число, которое соответствует id категории из таблицы category.

Чтобы назначить категорию заметке, мы записываем в category_id идентификатор категории.

Что это нам даёт:

  • Мы не можем ошибиться в наименовании категории
  • Нам не нужно каждый раз заново набирать наименование категории
  • Мы можем переименовать категорию, и при этом не нужно редактировать заметки
  • Мы сэкономили место в базе – вместо строки храним целое число

Мы создали реляцию, или отношение. Категории хранятся отдельно от заметок, но между категориями и заметками существует отношение, заданное с помощью category_id.

С точки зрения категории это отношение называется "один ко многим", то есть одна категория может относиться ко многим заметкам. А с точки зрения заметки это отношение "один к одному", так как одна заметка может иметь только одну категорию. Почему бы и не много? Обсудим потом.

Отношения есть везде

Реляционность используется очень широко. В программировании, например, когда вы свойству одного объекта присваиваете другой объект:

$a = new A();
$b = new B();
$b->child = $a;

Каждый объект имеет уникальный идентификатор: это его адрес в памяти. Вот этот адрес, или ссылку на объект, вы и присваиваете свойству child объекта $b. Тем самым устанавливая отношение между объектами $a и $b. Вы можете поменять какие-то данные в объекте $a, и чтобы их актуализировать в $b->child, там не нужно ничего менять – это просто ссылка, и она всегда показывает на актуальный объект $a.

Когда вы читаете текст закона и в нём встречаете ссылку на другой закон – это тоже отношение, где уникальным id является номер закона и статьи. В общем, вокруг нас существует большой и удивительный мир отношений.

Реализуем форму с выбором категории

Достаточно лирики. У нас осталась одна нерешённая проблема. Мы наладили отношения между таблицами, но как конкретно мы будем присваивать заметке category_id? Вот, допустим, форма редактирования заметки:

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

У неё есть поле "Категория", которое можно заполнить. Но заполнять его надо идентификатором, то есть числом, так как это id категории.

Что, естественно, чудовищно неудобно. Мы должны знать, у какой категории какой id. Допустим, мы хотим присвоить заметке категорию "автомобили". Мы идём в базу, смотрим, какой id у категории "автомобили" (например, 15), и в поле категории вводим число 15. Ну так, конечно, нельзя поступать с людьми.

Решение в том, чтобы дать пользователю готовый список категорий. Тогда ему не надо ничего вводить руками, а только выбрать категорию из списка.

Делаем выпадающий список

Для того, чтобы в HTML отобразить выпадающий список, мы используем элемент <select>:

<select name="category_id">...</select>

Каждая позиция в списке задаётся элементом <option>:

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

Выглядеть это будет так:

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

Ну собственно вы знаете, как выглядит выпадающий список. Работает же он так: всякий раз в выпадающем списке выбран один из элементов <option>. У каждого <option> есть атрибут value. В него мы помещаем id категории. То есть внешне на странице мы видим названия категорий, а по факту выбираем не название, а id. И именно выбранный id будет отправлен на сервер, когда мы нажмём кнопку "Сохранить".

Формирование списка

Сейчас список категорий задан вручную. Но нам нужно брать категории из базы и формировать список динамически.

Для этого, когда мы передаём модель заметки в представление формы, нужно дополнительно получить из базы список категорий и передать его тоже. А уже в представлении просто перебрать его циклом и сгенерировать элементы <option>.

Поэтому в контроллере заметок (controllers/note.php) дописываем функцию для получения списка категорий:

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

И далее, в функции update(), где мы вызываем render(), нужно передать в render() список категорий, чтобы представление могло им воспользоваться... Но не тут-то было. Для передачи в render() доступен только один параметр ($data), и он уже занят – это модель заметки:

function render($container, $view, $data)

Что делать?

Мы можем просто расширить сигнатуру функции render(), чтобы передавать туда ещё один параметр:

function render($container, $view, $data, $data2)

Но мы уже должны видеть будущие проблемы. А что, если потом параметров потребуется не два, а больше? А что, если количество параметров в разных вызовах будет вообще разное?

Мы можем решить этот вопрос, немного изменив логику представления. Получив параметр $data, оно будет считать его не единичной моделью, а массивом, в котором хранятся все необходимые данные. Очевидно, что в массив мы можем запихать сколько угодно параметров, например так:

['model' => $model, 'categories' => $categories]

Представление будет обращаться не просто к $data, а к $data['model'], если ему нужна модель, или к $data['categories'], если нужны категории.

И это замечательно, только такое обращение чисто стилистически громоздко.

Экстракция данных

В PHP есть интересная функция extract(). Если в неё передать массив из пар "ключ – значение", то она создаст переменные с именами как у ключей, и со значениями как у... да, значений.

Например, вот такой массив:

$data = ['a' => 1, 'b' => 2];

Если передать его в extract():

extract($data);

То в программе возникнут переменные $a и $b, которые равны 1 и 2. После чего мы можем ими пользоваться:

$c = $a + $b;

Это то, что нам надо, чтобы представление обращалось не к громоздкому массиву, а к простым именам переменных.

Мы передадим в render() в качестве переменной $data такой массив:

['model' => html_convert($model), 'categories' => $categories]

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

A render() сразу же сделает extract($data), и переменные $model и $categories станут доступны представлению:

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

Меняем код представления (views/note/form.php): вместо переменной $data теперь используем переменную $model. Ну а добравшись до генерации списка категорий, используем переменную $categories:

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

Всё, теперь мы можем видеть список категорий, когда редактируем заметку. Конечно, чтобы его увидеть, нужно сначала в базу добавить несколько категорий.

Остались ещё две проблемы: повторное редактирование и просмотр. А также: что будет, если удалить категорию? Их мы будем решать в следущем выпуске.

Я привел здесь фрагменты кода контроллера и представлений. Полный код вы найдёте на гитхабе. Я также добавил в проект файл cat.sql, в котором содержатся актуальные команды для создания таблиц.

Читайте дальше: