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

Дело в том, что корзина и заказ очень похожи по сути: и то, и другое хранит описание того, что клиент хочет купить в магазине. То есть, по идее, заказ - это та же корзина, но уже с подтверждением от клиента, что он закончил ее набирать и хочет расплатиться.

Сталкиваясь с таким сходством, нормальный программист, который не хочет плодить сущности, скорее всего захочет описывать корзину и заказ в одной и той же структуре, просто добавив признак типа "не оформлен/оформлен". И действительно, мне приходилось с такой моделью и работать, и в своем текущем проекте я тоже думал сделать именно так.

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

Лишние поля

Cостояние оформленности, на практике, оказывается совсем не единственным отличием корзины и заказы. Корзине нужны атрибуты, имеющие смысл только до оформления, связанные, например, с отображением ее на сайте, какая-нибудь текущая сортировка, признак открытости/закрытости и связь с клиентской сессией. Заказ тоже после оформления обрастает своими деталями типа времени оформления, привязки к конкретному зарегистрированному клиенту и системой статусов.

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

Валидация

Легко может быть, что поля заказа до оформления допустимо держать незаполенными, а после оформления они должны быть обязательно. Если это поля одной таблицы, то такой constraint не поставишь: поле должно быть либо NULL, либо NOT NULL.

Такую валидацию можно сделать триггером, но это ухудшает поддерживаемость кода, так как валидация становится чем-то менее предсказуемым и размазывается по разным неочевидным местам.

И вообще, все (оба!) вменяемые базоданщики, с которыми мне приходилось работать, отзывались о триггерах агрессивно в духе "это зло!" :-)

Усложнение запросов

При работе с табличкой "заказов-корзин" вам всегда придется включать в SQL-запросы условие, показывающее, с какой частью таблицы вы хотите работать (and 'оформлен'=1). Ситуацию же, когда заказы и корзины нужны одновременно, мне представить трудно.

Большая табличка

Корзина - временное образование, она существует или пока не превратиться в заказ, или пока не протухнет. Заказы же, напротив, предполагается хранить вечно. Это означает, что табличка это будет все время расти. А значит, в тех случаях, когда вы будете обращаться к этой табличке за корзинами, вы будете заставлять базу перемалывать большой объем данных, который точно не понадобится.

Конечно, на признак оформленности можно поставить индекс, чтобы выборка по нему происходила очень быстро. Но это, насколько я понимаю, не спасает в случаях, когда выборка идет с соединением табличек. Тогда, по идее, ваша распространенная простая и быстрая СУБД сначала объединит все данные в большо-о-ое декартово произведение, а потом начнет выбирать и отсекать. Что медленнее, чем если бы этих данных просто не было.

Впрочем, врать не буду, последнее - просто умозрительный вывод, на практике я это не проверял. Базоданщики, поправьте меня, если я дурь написал :-).

Удачного проектирования!

Комментарии: 6

  1. Slach

    гхм =)
    IMHO корзина, это всего лишь "табличная часть" заказа
    а заказ на нее просто ссылается

    IMHO корзины нужны ВСЕ =)
    как еще вы будете строить внутреннюю статистику по популярности и отслеживать конверсию "положил в корзину" -> "купил товар"?
    а если надо за произвольный промежуток времени, а если надо фильтровать по товарным группам?

    вообще =) там где у меня шла работа с ЗАКАЗОМ... у меня просто брался
    а этап выборки SELECT * FROM basket WHERE session_id=? AND status=0
    заменялся на
    if ($basket->fields['status']>0) throw new Exeption();
    и соответвующим сообщением об ошибке вроде
    "извините, вы уже оформляли этот заказ, пожалуйста пройдите в витрину"

    вот как то так =)

    хотя конечно можно сущности и наплодить, если завязываться на уникальный ID какой нибудь

  2. Slach

    тьфу...
    опечатки

    вообще =) там где у меня шла работа с ЗАКАЗОМ… у меня просто было ДВА быстрых запроса
    один выборка данных заказа
    второй выборка данных корзины
    оба быстрые
    оба по уникальному ключу ...

  3. Сергей Осипчук

    Соблазно обобщения велик.
    Вообще рассмотренный пример и аргументы, в частности валидация, не серьёзны. В нормальных проектах правила валидации очень жуткие - кто-то имеет право что-то оставлять пустым, кто-то нет, ещё у каждого объекта есть жизненный цикл, и поля которые могут быть пустыми меняются постоянно - не заводить же табличку на каждое состояние объекта.
    У того же заказа это может быть - создан, наполнен, заказан, оплачен, собран на складе (если не собран - заказ разбивается на два - на то что отправляется сейчас, и то что будет отправлено позже), отправлен по почте (тут дописывается tracking id), получено подтверждение о доставку.
    Надеяться на БД в таких случая нет смысла и нужно сделать максимально удобную с точки зрения объектов форму представления информации.

    У меня был проект с 100+ таблицами, и это с учётом обобщений - если бы там сделать по несколько табличек на объекты в разном жизненном состоянии то там вообще надо было бы стреляться.

    Программист программирующий бизнес логику вообще не должен особо беспокоиться как именно хранятся данные - он должен использовать GetOrder когда ему нужны ордера и GetCart если ему нужен cart. А там пусть всё хранится в одной таблице вместе с юзерами, товарами и статистикой посещений - это проблемы слоя DAL.

    Кстати совсем недавно в статье кто-то призывал отказываться от DB потому что они плоские и сериализовать объекты в текстовых файлах, а здесь вот такой шаг в противоположную сторону - наоборот "уплощать" объекты.

  4. Иван Сагалаев
        <p>Универсализация (обобщение) и специализация - это не религия, это шкала. В проектировании всегда приходится делать выбор, что именно обобщить, а что специализировать. Сделать все универсальным обычно так же не нужно, как и все -  специальным. Поэтому</p>
    

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

    ... это не то, что я написал, а то, что вы, Сергей, прочитали :-). По-моему, есть большая разница между "отделить корзину от заказа" и "сделать таблицу на каждое состояние". Не надо доводить до абсурда обоснованное предложение.

    Дальше. Я не меряю "нормальность" или "крутость" проекта сложностью. Если проект получается сложным, то это ошибка проектировщика. Потому что его дело - не плодить сущности, призванные сделать все универсальным, а делать систему адекватной задаче. Например, если корзина и заказы свалены в одну сущность, то для ее валидации какая-нибудь функция ValidateOrder будет содержать логику:

    если заказ не формлен то
      проверяется одно
    иначе
      проверяется другое

    Вот это - и есть усложнение. Потому что, как я и написал, на практике почти никогда не бывает нужно работать с общим списком и корзин, и заказов. Обычно это разные вещи с точки зрения бизнес-операций. И нет никакого смысла запихивать обработку разных сущностей в один код.

    Дальше.

    Программист программирующий бизнес логику вообще не должен особо беспокоиться как именно хранятся данные

    Полная абстракция хранилища данных - это красивая легенда, которая не работает с текущими технологиями. Не получается, как бы ни хотелось, не думать о том, как хранятся объекты по одной просто причине: производительность. Если бизнес-логике надо посчитать количество заказов в каком-то состоянии, и она для этого вместо "select count where state=..." 100000 раз вызовет вынимающий объекты GetObject, чтобы проверить его поле и увеличить счетчик, такая система очень быстро сдохнет.

    Вот еще по теме: [закон дырявых абстракций](http://russian.joelonsoftware.com/Articles/LeakyAbstractions.html).

    Дальше.

    Кстати совсем недавно в статье кто-то призывал отказываться от DB потому что они плоские и сериализовать объекты в текстовых файлах, а здесь вот такой шаг в противоположную сторону - наоборот “уплощать” объекты.

    Видать, я опять непонятно написал :-). Ну что за беда!

    1. Дело в том, что в этой статье я как раз предлагаю не "уплощать". То, что я предложил - это, по сути, тоже шаг от плоского табличного представления к более инкапсулированному, где тут "уплощение"?. Другое дело, что тут я не предлагаю отказываться от БД, потому что:

    2. В статьях про отказ от БД и в комментариях к ним я несколько раз обрисовал и подчеркнул область применения этой технологии. Интернет-магазин с полутысячей заказов в день в нее не входит. (И, кстати, про текстовые файлы я тоже не писал)

  5. Денис Зайцев

    Поправляю :-)

    по идее, ваша распространенная простая и быстрая СУБД сначала объединит все данные в большо-о-ое декартово произведение, а потом начнет выбирать и отсекать.

    Есть конечно и такой метод соединения, sort-merge join называется, но он относится к наиболее неэффективному способу, и оптимизатор его выбирает только если ему совсем нечего делать (нет индексов, статистики и т.д.).
    Однако индекс (по крайней мере обычный b-tree) по признаку оформленности - тоже плохая идея, поскольку его селективность будет крайне низка и придется все равно перелопачивать гору данных (сначала индекс, потом таблицу). Пожалуй единственный способ решения этой проблемы (если рассматривать хранение всех яиц в одной корзине) - архивация выполненных заказов, причем это нужно только для back offic'а, в котором необходимо получить список заказов, а на этом этапе корзина, как правило, и не нужна, след-но ненужно и соединение. Полный просмотр же плоских таблиц современные dbms решают довольно эффективно, попросту говоря наиболее часто опрашиваемые таблицы (к которым относятся заказы), или их подмножество будут находиться в кеше.
    Обычно же, там где идет непосредственная работа с заказом (оформление, обработка) доступ все равно будет производиться по ключу (номер заказа), а это наиболее быстрый способ :-)

    У меня вообще сложилось впечатление что то, о чем ты написал, описывает банальный процесс нормализации таблиц ;-) В этом отношении действительно следует разделить атрибуты, относящиеся к процессу оформления заказа, атрибуты оформленного заказа, а также атрибуты, например, связанные с доставкой. Причем для систем класса "оперативной обработки транзакций" (OLTP), к которым относится обработка заказов, это необходимое требование. Однако, в базах данных есть и обратный процесс - денормализация, которую проводят в целях оптимизации скорости доступа к объектам, поскольку соединение множества таблиц - по прежнему тяжелая задача.

  6. Макс Лапшин

    Как вариант, для этого можно пользоваться механизмом Single Table Inheritance. В одной таблице будут разные объекты.

Добавить комментарий