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

При разработке TaCo я решил отойти от этого решения. И вот почему...

Impedance mismatch

Или точнее "Object-relational impedance mismatch" - это понятие, которое описывает тот факт, что объектно-ориентированное моделирование и реляционное моделирование построены на разных принципах, и когда их приходится совмещать, это не всегда происходит гладко. Я как-то не смог найти в сети внятного и точного технического описания проблемы, но самая вменяемая, на мой взгляд, статья есть на Wikipedia.

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

Наследование

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

Инкапсуляция данных

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

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

Избыточное обеспечение целостности

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

Но дальше появляется интересный момент: хорошая объектная программа заботится о корректном состоянии своих объектов сама, а не полагается на ошибки БД (через минуту к тому еще вернусь).

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

Разные способы поддержки корректности

Итак, почему бы объектной программе не полагаться на БД для проверки корректности данных.

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

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

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


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

В БД эти объекты лежат в виде двух табличек, и табличка Товаров ссылается на табличку Возвратов через внешний ключ - стандартное решение.

Есть разные правила сосуществования всех этих объектов и одно из них для примера: чтобы Возврат можно было перевести в состояние "Принят у клиента", у каждого Товара должно быть проставлено, на какой склад он принят.

Программа дает свободно редактировать и состояния Возврата и Товары пользователю, и дает кнопку, по которой сохраняет изменения в базу, просто вызывая сохранения двух табличек:

Procedure TReturn.Save;
Begin
  GoodsTable.Save;
  ReturnTable.Save;
End;

Эти две строчки по сути просто вызывают соответствующие SQLные UPDATE'ы для таблички Товаров и таблички Возвратов. Заметьте порядок сохранения: сначала сохраняются зависимая табличка, чтобы при сохранении таблички Возврата БД могла проверить, что у всех Товаров действительно проставлена ссылка на склад.

Все, вроде, просто? А теперь представим, что наш Возврат только что создан в программе, и в базе его еще нет. При попытке сохранить табличку зависимых Товаров БД тут же ругнется, что она не может вставлять зависимые записи Товаров, поскольку не существует главной для них записи Возврата - нарушается ссылочная целостность. И вот приходится писать совершенно дурацкую вилку, что "If запись новая Then сохранять в другом порядке". И следить уже за думя кусками кода, если понадобится добавить сохранение еще какой-нибудь таблички.


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

Да, все это, опять же, можно обойти. Например создавать на такие комплексные действия отдельные хранимые процедуры. Но это во-первых, еще большее дублирование кода приложения в базе, а во-вторых, во всех языках, что я видел, еще не научились работать с процедурами БД так же прозрачно, как с собственными процедурами языка, и все эти вызовы объектов БД сильно мусорят код.

Пути решения

Мне известно, по большому счету, два способа разрешать это "несовпадение импеданса".

Первый - это ORM - Object-Relational Mapping. К программе подключается некая библиотека или даже платформа, которая прозрачно для объектного программиста переводит его объекты в модель данных в базе. Проверяет типы, целостность и т.д. Таким образом она решает проблему дублирования: модель данных и доступ к ней определяются один раз в программе, а БД строится по ней автоматически и внимания не требует.

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

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

TaCo

В TaCo я выбрал как раз второй подход. Поскольку система простая и компактная, особенно много думать над сохранением объектов не приходится: они сохраняются в XML-файлы, с которыми в Python'е работать - одно удовольствие.

Немного о том, в какие плюсы и минусы это выливается по ходу разработки:

Плюсы:

Минусы:

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

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

  1. Dima

    В порядке эксперимента это конечно интересно, но вообще в БД есть такое понятие как транзакции ;)
    Правда я как раз недавно жалел, что в MySQL 3.23 это не поддерживается.

  2. Иван Сагалаев

    А как транзакции отменяют все, что написано в статье? :-)

    Хотя я понимаю, откуда может быть путаница. Это, видимо, о той части, где базу нужно временно вывести из корректного состояния... Но это не то. Транзакция обеспечивает целостность логической операции. А вот ссылочная целостность поддерживается базой всегда, не дожидаясь commit'а текущей транзакции, после любого оператора.

    Транзакции я, на самом деле, намеренно не трогал, чтобы еще больше не усложнять :-)

  3. Давид Мзареулян

    База — это не столько механизм хранения, сколько механизм построения выборок. Делать же более-менее нетривиальные выборки из файлового хранилища сложно. Особенно если на этапе создания программист не озаботился реализацией (вручную!) нужных индексов, а через год эксплуатации они вдруг понадобились.

  4. Иван Сагалаев

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

    Угу, я как раз о том и пишу.

    Делать же более-менее нетривиальные выборки из файлового хранилища сложно.

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

    Особенно если на этапе создания программист не озаботился реализацией (вручную!) нужных индексов, а через год эксплуатации они вдруг понадобились.

    Все не так сложно, как звучит. Если код сам по себе не является кашей, то добавить индекс - это очень просто. Это просто отсортированный массив по какому-то признаку. И его также легко сдампить на диск.

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

  5. Dima

    ... и в конечном итоге получить базу данных, написанную с нуля :)

  6. Иван Сагалаев

    Хочу еще один раз разъяснить ключевое непонимание, которое, судя по всему, есть у многих :-).

    Смысл в том, что не писать базу данных. Ни писать, ни имитировать, ни страдать от ее отсутствия.

    Смысл в том, чтобы устранить impedance mismatch за счет устранения базы данных. Объектная модель обладает достаточными средствами защиты данных сама по себе, что делает в данном конкретном случае базу данных не нужной.

    Идея-то, кстати, не новая. Если интересно поискать источники - Google is your friend.

  7. Алексей Захлестин

    может быть стоило посмотреть в сторону нереляционных БД? что-то вроде BDB-XML или eXist

    http://www.rpbourret.com/xml/XMLDatabaseProds.htm#native

  8. Corwin

    Вопрос скорее представляет теоритический интерес, чем практический :) Ты сам себе противоречешь и скатываешся на изобретение велосипеда. В реалии же проще и быстрее устранить impedance mismatch и следовательно быстрее получить конечный результат, при этом вполне качественный. Хотя не сомневаюсь что из спортивного интереса ты все сделаешь хорошо и правильно :) И не понятно чем тебя так Phyton зацепил, взял бы яву с ее уже встроенной сериализацией. Все имхо..

  9. Макс

    Тоже проводил такие эксперименты. в т.ч. на Python/XML - результат так себе. Причем чем более долгосрочный проект, тем больше проблем вылазит. Хотя, тут все конечно от задачи зависит.

    Ну а извечный спор RDBMS vs. OODBMS по-моему не имеет решения. :)

  10. Alena

    Интересный подход, понравится тем, кто не может/не хочет работать с базой данных, например: удаленная разработка + дешевый хостинг без базы данных/с еле шевелящейся базой данных. С год назад на valuehost'е я мучалась с базой, которая не отвечала постоянно, не знаю как у них сейчас дела обстоят.
    В плане переносимости TaCo тоже проще получается.

    Я знаю об удачном примере отказа от базы данных по причине того что было сложно организовывать неплоские структуры данных. На последней КРИ человек из Нивала рассказывал об их ресурсной системе (запись и слайды, если кому интересно). Но они и от XML отказались и в итоге пришли к своему формату.

  11. Иван Сагалаев

    Кстати, насчет извечных споров... Где-то я читал (потерял ссылку) довольно большую статью про то, что священные войны таки имеют решение: история расставляет все по своим местам. Так случилось с войной "call или goto", так случилось с войной "процедуры или объекты" и т.д.

    Так что, поживем увидим :-).

  12. Родион

    А вот ссылочная целостность поддерживается базой всегда, не дожидаясь commit’а текущей транзакции, после любого оператора.

    Oracle позволяет определить правила целостности таким образом, чтобы они проверялись только после commit.

  13. Макс

    Хороший пост по поводу XML abuse ;)

  14. Ragnar

    Мне кажется проблема что называется "притянута за уши". Я сам разрабатываю под веб почти исключительно на ООП и никогда не сталкивался с проблемой реализации взаимодействия между БД и обьектной моделью.

    Сплошь и рядом в объектной программе можно иметь контейнеры разных наследников одного базового объекта, но хранить в одной таблице записи с разным количеством полей нельзя.

    Мне кажется что автор не понимает разницу между обьектами и данными (а БД это просто хранилище данных + описание структуры данных). У данных и не должно быть ни какого наследования, ближе всего в терминах ооп для данных и таблиц БД подходит static или final, от которых нельзя наследовать они просто есть.

    Объект должен иметь все данные, с которыми работает, что называется 'при себе'. Причем, независимо от их структуры. Наример, если в программе как-то представляется ветка форума, то все ее сообщения будут храниться в ней.

    Сообщения могут быть также представлены отдельными обьектами и ветка форума может о них ничего не знать. Так что инкапсуляция тут не к месту даже если ветка ето класс - контейнер для сообщений (которые тоже обьекты) так как "in capsula" подразумевает всего лиш что свойства и методы конкретного обьекта должны хранится в нем.

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

    Ну да если писать как в ПХП нюке где сотня запросов для генерации простой странички. Нормальные веб приложения по минимуму дергают БД так как "пустяковые" проверки можно сделать и в самом коде.

    Объект же внутренние данные скрывает

    Обьект данные не скрывает а инкапсулирует путать эти понятия нельзя!

    Объектная программа может позволить себе временную некорректность данных по ходу исполнения кода метода, потому что она знает, что в этом методе будет в итоге сделано все, что надо.

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

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

    Срочно читаем про транзакции и не позоримся.

    У меня никогда не будет болеть голова про SQL injection

    Если правильно программировать она и так не будет болеть по этому поводу, а при неумении бороться с хаками их применят при любом подходе к реализации программы.

  15. Sigura

    Нашёл условно-полезный документ "НадРеляционный Манифест", как мне кажеться в тему

  16. U-gene

    Мда, как видно у всех одно и то же на уме :) Зашли бы Вы, что ли, на sql.ru в форм "Сравнение БД".... Там есть несколько тем, в которых эти вопросы активно обсуждаются и рассмтариваются. Их только надо поискать:
    "Сравнение объектно-ориентированных, реляционных и объектно-реляционных СУБД" - это классика :) .
    "СУБД CACHE'" - что еще есть.
    "Объектные СУБД от Versant Corporation (FastObjects .NET, FastObjects t7, Developer Suite)" - объекты, это не только полезно...
    "Cerebrum : Сетевая объектно-ориентированная система управления базой знаний (1,2,3,4,5,6,7,8,все)" - спешка нужна только при ловле блох.
    "НРМ" - все совсем не так, как оно есть на самом деле...
    "РМД пора на пенсию?" - да ну?.

    Что касается поднятой темы, то ИМХО типичный для програмиста подход. В данном случаае, когда речь идет о "...проектировании веб-систем более-менее неигрушечного размера..." он ИМХО может быть и оправдан, просто не стоит забывать, что в целом термины "БД" и "СУБД" гораздо более наполнены, чем просто хранение данных для какой-либо программы, и, тем более, для какой-либо конкретной программы.

  17. Yura Ivanov

    С одной стороны:
    1. Не все хранится в базе. (профиль пользователя - куки, ini, реестр, внешние xml)
    2. Разные СУБД имеют свои возможности и недостатки, сконвертировать при необходимости будет не так просто. У меня есть пара проектов, которые неплохо было бы распространить и на веб (на Yaffil'е и Sybase'е), реинжиниринг и прочие xDBC не предлагать.
    3. Дэдлоки сакс.

    С другой стороны:
    1. Транзакции
    2. Распределенность
    3. Хранимые процедуры, не перегружающие сервер приложений (хотя для веба я так понимаю не сильно актуально)
    4. мощь select.
    5. все уже написано. парсер написать проще, чем ээфективную модель хранения данных. в итоге можно упереться в производительность, ибо прогнозу новое решение сложно поддается. Как такой вид хранения будет ворочить 20-ю гигами инфы? Другой вопрос будут ли они эти 20 гигов...

    Мое мнение, если есть желание и/или возможность реализовывать такой вид хранения, то надо делать. Ждем результатов.

  18. Лобанов Леонид

    и являющийся большим его поклонником, частенько на это самое “несовпадение” наталкиваюсь.

    Может, пишешь слишком сложный код? Будь проще :). Мне даже обьекты не всегда нужны, хотя не сказал бы, что очень простые вещи пишу. Вообще, обьектная модель имеет нехилые минусы. Она очень удобна при разработке GUI (в delphi|VC++). А в текстовом мире веба иногда черезмерно всё усложняет.

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

    Перекрёстные запросы в критичных местах - это просто ошибки при разработке структуры базы. Полдня над размышлениями над структурой надо провести хотя бы. (до написания первой строчки кода) А можно и день.

    У меня никогда не будет болеть голова про SQL injection.

    применительно к PHP - magic_quotes_gpc on + всё и всегда заключать в кавычки (даже если поля - числа). Можно добавить $temp=(int)$temp для целночисленных значений.
    И можно про SQL-иньекции забыть в принципе.

    Иван Сагалаев> Вот как раз в том и смысл, что выборок делать не нужно.

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

    Alena> удаленная разработка + дешевый хостинг без базы данных/с еле шевелящейся базой данных.

    А время программиста бесплатное (пусть даже оно ваше)? Средненький программист за день получает НАМНОГО больше, чем стоит навороченный хостинг с быстрой базой данных за месяц. Так что выгоды не видно.

    Иван Сагалаев> так случилось с войной “процедуры или объекты”
    А что с ней случилось? :). Если goto умер (jmp-ы в асме не считаем), но процедуры - нет и ещё раз нет! :)
    __
    Фильмы на CD

  19. Лобанов Леонид

    применительно к PHP - magicquotesgpc on + всё и всегда заключать в кавычки (даже если поля - числа). Можно добавить $temp=(int)$temp для целночисленных значений.
    И можно про SQL-иньекции забыть в принципе.

    Тут я конечно, погорячился. Совсем забыл про один неприятный вариант - смешивание из ввода пользователя (где слеши есть после magicquotes) и данных из базы (где их нет). Если такое имеет место - лучше слеши добавлять непосредственно перед запросом к БД. Или вырезать все кавычки из массива $_POST.
    __
    Фильмы

  20. Андрей Федосеев

    Вставлю свои пять копеек. Обратите внимание на Zope. Это и объектная база данных и веб-сервер и фреймворк. Все на Python.

  21. Александр Соловьёв

    Ха, хорошо всё написано. Некоторые комментарии смешно читать. ;) Но просто хотелось дать эту ссылку. Зачем нужен XML? :)

  22. Простите, но я этот спор видел уже много раз. Все аргументы в итоге сводятся к тому, что сторонники "логики в СУБД" воспринимают её как надёжную структуру, а приложение — не воспринимают. Вроде как триггер как-то обязательно отработает, а какой-то непонятный application-код по определению будет глючить. Но это только субъективное восприятие.

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