Мне не доводилось профессионально заниматься вопросами объектно-реляционного отображения, и я всегда довольно скептически относился к попыткам наладить «бесшовные» переходники между программами на объектно-ориентированных языках и SQL-ориентированными СУБД. По сути дела, решением проблемы «потери соответствия» (impedance mismatch) сообщество баз данных занимается больше 20 лет. На этом пути возникли объектно-ориентированные базы данных, появились объектные расширения SQL, возник «Третий манифест» Дейта и Дарвена. В каждом из этих подходов предлагается свое решение проблемы, но, как видно, ни одно из решений не удовлетворяет разработчиков приложений с использованием объектно-ориентированных языков программирования. Мне трудно судить, с чем это связано. Может быть, причиной является историческое взаимное непонимание разработчиков технологии баз данных и разработчиков приложений: первые считают, что они делают все возможное для вторых, а вторые полагают, что первые работают для собственного удовольствия. Возможно, причина состоит в отсутствии единой для всех объектно-ориентированных языков объектной модели.
Во всяком случае, в этой краткой вводной заметке я не берусь провести какой-либо содержательный анализ этих причин. Тем не менее, технология объектно-ориентированного отображения жива и развивается. И в этом отношении статья Теда Ньюворда, пересказ которой вам предлагается, кажется мне очень полезной. Я согласен с автором, что при применении объектно-реляционного отображения нельзя рассчитывать на наличие единого чудотворного решения. Автор приводит примеры проблем, которые трудно решаются по одиночке и, скорее всего, в принципе не поддаются совместному решению.
Хочу также отметить, что этот материал не является законченной научной или технической статьей. Автор написал его для своего блога и сам называет это эссе. Поэтому прошу снисходительно отнестись к некоторой нестрогости материала, неточностям в терминологии и т.д. Я не стремился к переработке авторского материала, а лишь изложил его по-русски. Надеюсь, что это принесет пользу.
Сергей Кузнецов
(Два года тому назад на конференции TechEd компании Microsoft в Сан-Диего на мероприятии, посвященном закрытию конференции, я втянулся в беседу с Гарри Паерсоном (Harry Pierson) и Клеменсом Вастерсом (Clemens Vasters). Как обычно, когда мы собираемся втроем, обсуждались, прежде всего, архитектурные темы. Вокруг нас собралась толпа, и беседа превратилась в импровизированную сессию «люди одного полета» (birds-of-a-feather). Разговор зашел об объектно-реляционном отображении, и именно там и тогда я впервые произнес фразу «Объектно-ориентированное отображение – это Вьетнам компьютерной науки». Впоследствии ко мне поступили многочисленные просьбы расширить обсуждение этого утверждения. Недавнее объявление компании Microsoft относительно «поддержки сущностей» и появление Java Persistence API как замены и EJB Entity Beans, и JDO убедили меня в том, что действительно пришло время сделать это.)
Ни один из вооруженных конфликтов не мучил американских военных так, как Вьетнам. В создании наиболее решающей поворотной точки в современной американской истории объединилось так много разнородных элементов, что непрофессионалу просто невозможно во всех них разобраться. И все же история Вьетнамской войны, по существу, характеризуется следующим: Соединенные Штаты начали военный проект при наличии неясных и противоречивых целей и быстро увязли в трясине, которая не только погубила два правительства (одно законным образом, другое – под давлением армии), но также оставила глубокий след в американской военной доктрине на следующие два десятилетия (как минимум).
Хотя это звучит банально, но объектно-реляционное отображение является Вьетнамом компьютерной науки. Это болото, в которое легко войти, но чем дальше, тем труднее продвигаться, и вскоре путники бредут, не видя никаких вешек, не зная своих шансов на успех и не имея никакой стратегии для выхода из болота.
История
Хорошее краткое описание Вьетнамской войны находится на сайте Государственной службы радиовещания, но для тех, кто более заинтересован в компьютерной науке, а не в истории политики или военных столкновений, предлагается следующий сокращенный вариант.
Южный Индокитай, на территории которого теперь находятся государства Вьетнам, Таиланд, Лаос и Камбоджа, имеет длинную историю борьбы за автономию. До колонизации Францией (которая началась в середине 1800-х гг.) Южный Индокитай боролся за региональную независимость от Китая. В течение Второй мировой войны этот район подчинила себе Япония, а позже она была «освобождена» Союзниками, что позволило Франции возобновить свое колониальное господство (как поступила и Британия со своими колониями в Азии и Индии). Однако после Второй мировой войны народы Южного Индокитая, избавившись от одного угнетателя, расширили свою освободительную борьбу теперь уже против Франции. В результате в 1954 г. Франция капитулировала, подписав Женевские мирные соглашения, в которых Вьетнаму предоставлялась независимость. К сожалению, под давлением других стран эти достижения были несколько извращены, и вместо постоянного мирного договора было принято временное решение, в соответствии с которым страна была поделена по семнадцатой параллели, и были образованы два государства, хотя ранее такое разделение отсутствовало. В 1956 г. должны были состояться выборы с целью воссоединения страны, но США опасались, что на этих выборах слишком большую силу получит коммунистическая партия Вьетнама, и стали поддерживать антикоммунистический режим к югу от 17-й параллели, а также образовали вокруг него ряд многосторонних соглашений, таких как SEATO. Так родилось новое государство Южный Вьетнам, и его первым (сомнительно) избранным лидером стал Нго Динь Дьем, стойкий антикоммунист, который почти незамедлительно объявил, что его страну атакуют коммунисты. Администрация Эйзензауера поддерживала правительство Дьема, но у народа с самого начала практически отсутствовало к нему доверие.
К тому времени, когда в Белый дом пришел лидер Демократической партии США Джон Кеннеди, обстановка в Южном Вьетнаме стала взрывоопасной. Кеннеди послал во Вьетнам группу специалистов для изучения обстановки с тем, чтобы впоследствии помочь сформулировать стратегию администрации США по этому поводу. В отчете этой группы, который теперь принято называть «December 1961 White Paper», приводились доводы в пользу оказания Южному Вьетнаму военной, технической и экономической помощи, а также направления многочисленных американских «советников» для помощи в стабилизации правительства Дьема и ликвидации Национально-освободительного фронта, окрещенного в США Вьетконгом. Однако не всем известно, что ряд советников Кеннеди выступал против такого развития событий, называя Вьетнам «тупиком».
Поставленный перед выбором одного из двух диаметрально противоположных путей, Кеннеди, как было свойственно его администрации, выбрал средний путь: вместо того чтобы взять на себя трудные обязательства или полностью от них отказаться, Кеннеди решил прибегнуть к ограниченной форме поддержки, оказывая помощь, но не направляя в Южный Вьетнам крупные вооруженные формирования, и этот подход был почти обречен с самого начала. После ряда грубых стратегических ошибок, включая насильственное переселение сельских жителей (акция, называвшаяся Стратегической деревенской программой, Strategic Hamlet Program), режим Дьема оказался настолько глубоко подорван, что Кеннеди нерешительно и с рядом колебаний поддержал государственный переворот, во время которого Дьем был убит. Три недели спустя был убит и сам Кеннеди, что привело к смятению на внутренней политической сцене США. Как ни странно, конфликт, начатый Кеннеди, позднее стало более тесно ассоциироваться с его заменой на посту президента.
Война Джонсона
К моменту убийства Кеннеди во Вьетнаме находилось 16000 американских советников, большая часть которых не участвовала в повседневных боевых операциях. Однако вице-президент в администрации Кеннеди и его приемник на посту президента Линдон Бейнс Джонсон не был убежден, что такой путь приведет к успеху; он решил, что требуются более решительные действия. Под предлогом сомнительного инцидента, в котором вьетнамские патрульные катера атаковали американские эсминцы в Тонкинском заливе, Джонсон воспользовался воинственными настроениями в Конгрессе, чтобы провести резолюцию, дающую ему полномочия провести военную операцию без явного объявления войны. (Более поздний анализ событий руководителями, среди которых был тогдашний министр обороны Роберт Макнамара, показал, что половины этого нападения вообще не было.) Попросту говоря, Джонсон хотел, чтобы эта война велась «хладнокровно». Это означало, что Америка собиралась выполнить во Вьетнаме точную хирургическую операцию, которая не должна была оказать существенное воздействие на внутреннюю культуру страны. Для ограниченной войны требовалась ограниченная мобилизация ресурсов, материалов и людей, и это не должно было сильно подействовать на повседневную жизнь в Америке. По существу, последствия этой войны должны были ощущаться только вьетнамцами – жизнь в Америке должна была продолжаться без каких-либо упоминаний о событиях во Вьетнаме, оставляя Джонсону возможность следовать его мечте о «великом обществе», в котором были бы излечены многие болезни американского общества, в частности, бедность. (Вероятно, наибольшая ирония этой войны состоит в том, человек, выбранный судьбой руководить Америкой во время ее крупнейших внешних затруднений, являлся лидером, который больше всего стремился к позитивным действиям в пределах своих родных берегов. Если бы обстоятельства сложились иным образом, то хиппи, певшие «Hey, hey LBJ, how many boys did you kill today» напротив Овального кабинета, могли бы стать вернейшими сторонниками Джонсона.) Конечно, история знает, что получилось на самом деле, и – возможно, безжалостно – называет вьетнамский конфликт «войной Джонсона».
Прежде всего, необходимо заметить, что восприятие вьетнамской войны как катастрофы появилось позже; как показывают результаты опросов общественного мнения, еще в 1967 г. американцы были убеждены, что эта война является правильной, что коммунизм необходимо остановить, и что за Вьетнамом, если позволить ему пасть, последует ряд других государств, уступивших коммунистической подрывной деятельности. Эта «теория домино» была стандартным рефреном американской политики во второй половине 20-го века. Подобные беспокойства досаждали американской внешней политике еще с того времени, когда коммунисты успешно или почти успешно низвергли несколько правительств в Европе во второй половине 1940-х гг., а затем китайское правительство в 1950-е гг. (Необходимо заметить, что творцы этой теории Эйзенхауэр и Джон Фостер Даллес никогда не включали Вьетнам в свой ряд косточек домино, которые нужно было оберегать от падения, и в действительности Эйзенхауэр был поразительно безразличен к ситуации во Вьетнаме во время своих встреч с Кеннеди, когда покидал Белый дом.) Однако в 1968 г. восприятие вьетнамской войны существенно изменилось, поскольку северные вьетнамцы и вьетконговцы начали операцию Тэт, кампанию, которая показала лживость всех заверений американского правительства о том, что оно выигрывает войну во Вьетнаме. Парадоксальным является тот факт, что, как часто случалось в этой войне, северовьетнамцы и вьетконговцы понесли существенные потери, намного большие, чем их американские противники, но, тем не менее, историки считают именно операцию Тэт переломной точкой в отношении американцев к войне во Вьетнаме. После этого общественное мнение во всем обвинило Джонсона, и на драматической пресс-конференции он объявил, что не будет стремиться к переизбранию на пост президента. Более того, он объявил, что будет пытаться урегулировать конфликт в процессе переговоров с вьетнамцами.
Обещание Никсона
К сожалению, положение американцев на переговорах было серьезно ослаблено, прежде всего, той же самой волной протестов, которая привела американское правительство за стол переговоров; лидеры Северного Вьетнама и Вьетконга понимали, что, несмотря на ошеломляющие военные потери, которые привели почти к полному уничтожению северовьетнамских и вьетконговских войск (несколько раз), они могут просто продолжать делать то же самое, что и прежде, и добиваться уступок от американцев, ничего не предлагая им взамен. Преемник Джонсона республиканец Ричард Никсон пытался применить несколько тактик для оказания давления на войска Серверного Вьетнама и Вьетконга, включая расширение боевых операций в воздухе (таких как «рождественская бомбардировка» Ханоя и Хайфона и секретная операция под кодовым названием Operation Menu, в ходе которой бомбардировкам подверглась территория Комбоджи) и регулярное нарушение границ соседних Лаоса и Комбоджи с целью ликвидации цепочки поставок из Северного Вьетнама в ячейки Вьетконга в Южном Вьетнаме. Однако ничто из этого не сработало, и в 1973 г. администрация Никсона подписала Парижский мирный договор, завершивший американское участие в этом конфликте. Через два года Южный Вьетнам был осажден, и 30 апреля 1975 г. коммунистические войска захватили Сайгон, столицу Южного Вьетнама, вынудив эвакуироваться американское посольство, и самым незабвенным образом этой войны является поток бегущих людей, которые надеялись найти себе место на борту вертолета, севшего на крышу посольства.
Конец войны
Вторая война в Южном Индокитае закончилась. Америка понесла самое сильное поражение за всю свою историю, и Вьетнам стал синонимом «трясины». Это поражение оказало сильнейшее воздействие на американскую культуру, поскольку научило целое поколение американцев опасаться своего правительства и не доверять ему, научило лидеров страны опасаться любых потерь среди своих военнослужащих и привнесло в политический лексикон фразу «ясная стратегия выхода». До того времени, как Рональд Рейган решился использовать американские вооруженные силы для «освобождения» маленького островного государства Гренада, президенты США не расценивали вооруженную интервенцию как допустимое средство дипломатии, и даже после этого прибегали к вооруженному вмешательству только при тщательном учете внутренних интересов США, в необходимости чего убедился Билл Клинтон при выполнении своих миротворческих миссий в Сомали и Косово. Провал идеи Джонсона о «хладнокровной» войне ясно показывают цифры: в войне участвовало 3 миллиона американцев, 150000 которых были серьезно ранены, 58000 погибло и около 1000 пропало без вести, не говоря от том, что войска Северного Вьетнама и Вьетконга понесли потери почти в миллион человек, погибли 250000 военнослужащих Южного Вьетнама, а также сотни тысяч – по мнению некоторых историков, миллионы – мирных жителей.
Уроки Вьетнама
Вьетнам ставит интересную проблему перед людьми, изучающими военную и политическую историю: что, когда и где было сделано неправильно? Очевидно, что нежелание правительства США признавать свои провалы в ходе войны легко делает его «козлом отпущения», но ни одно правительство в современной истории не было полностью правдивым перед своим населением по поводу своих успехов в войнах. Одним (но не единственным) подобным примером является тщательная цензура того же правительства США информации о военных действиях во время Второй мировой войны, которую через пятьдесят в американской истории назвали «последней ‘правильной’ войной». Соблазнительно также указать в качестве важной причины провала во Вьетнаме отсутствие какой-либо военной цели, но другие не военные цели успешно достигались правительствами США и других стран без того колоссального провала, который сопровождал вьетнамскую эпопею. Более того, важно отметить, что в действительности у правительства США имелась ясная цель: оно хотело, чтобы конфликт в Южном Индокитае предотвратил падение правительства Южного Вьетнама и за счет этого приостановил распространение коммунизма. Был ли этот провал вызван нежеланием правительства США позволить военным использовать все свои возможности, как это всегда утверждал генерал Уильям Уэстморленд (William Westmoreland)? Но, безусловно, во Вьетнаме Америка потерпела не военное поражение; цифры потерь ясно показывают, что в военном отношении американцы победили.
Так в чем же заключались основные ошибки США во Вьетнаме? И, что более важно, какое все это имеет отношение к объектно-реляционному отображению (ОР-отображение, OR-Mapping)?
Вьетнам и ОР-отображение
В случае Вьетнама политический и военный аппарат Соединенных Штатов столкнулся с убийственной формой закона убывающей отдачи (Law of Diminishing Returns). В случае автоматического ОР-отображения дело обстоит аналогичным образом – первые успешные попытки применения этого подхода привели к решимости использовать ОР-отображение в тех случаях, когда успех был более сомнительным, а со временем и вовсе перестал быть успехом по причине огромных расходов времени и человеческих трудозатрат, требуемых для поддержки ОР-отображения во всех возможных случаях. По существу, главным уроком Вьетнама – для любой группы, политической или какой-либо другой – является то, что, как говорят рыбаки, «нужно знать, когда сматывать удочки». Слишком часто, как это было в случае Вьетнама, потребность в дальнейшей поддержке некоторого курса действий обосновывается тем, что отказ от этого курса приведет к обесцениванию уже понесенных затрат – в случае Вьетнама понесенных военных потерь. Привычными становятся фразы «Мы настолько далеко зашли, что должны довести это дело до конца» и «Если мы сейчас повернем назад, то все наши жертвы окажутся напрасными». В последние, мучительно горькие годы второй половины Вьетнамской войны любой человек, не поддерживавший эту войну, считался предателем, коммунистом, антиамериканцем. Такое отношение к войне полагалось неуважением всех американских ветеранов всех войн, где бы и по какой бы причине они не велись. Такого человека можно было подозревать даже в том, что он пинает ногой свою собаку. (Протестующим вредило то, что они возлагали вину за эту войну на солдат, считая их ответственными – иногда персонально – за решения, принимавшиеся военными и политическими лидерами, с большинством из которых ни протестующие, ни солдаты никогда не встречались.)
Хотя все аналогии условны, и проблема Вьетнамской войны заслуживает гораздо более глубокого обсуждения, некоторые уроки этой войны можно применить в совсем другой области. Один из основных уроков Вьетнама состоит в признании опасности того, что в разговорной речи называют «скользким путем» (slippery slope), когда некоторый курс действий вначале приводит к некоторому успеху, но продолжение следования этому курсу приносит все менее соразмерные результаты и порождает все более опасные препятствия, единственным способом преодоления которых является увеличение расходуемых ресурсов и действий. Иногда это явление называют «лекарственной западней» (drug trap), поскольку эффект приема лекарственных препаратов (как легальных, так и запрещенных) со временем может снижаться, и для достижения тех же результатов нужно все время увеличивать дозу. Другие называют это «проблемой последней мили», поскольку по мере приближения к концу процесса решения какой-либо проблемы приходится тратить все более усилий и средств для достижения полностью завершенного решения. По существу, речь всегда идет о трудности нахождения способа, позволяющего полностью разрешить проблему с требуемыми результатами.
Анализ ОР-отображения – и его взаимосвязи с Второй войной в Южном Индокитае – мы начнем с изучения причин появления этого подхода. Что побуждает разработчиков отказаться от использования традиционных реляционных инструментальных средств и предпочесть им средства, подобные ОР-отображению?
Рассогласованность объектного и реляционного подходов
Ни один разработчик, которому приходилось использовать объектные и реляционные наборы данных, не будет удивлен, если сказать ему, что эти наборы данных строятся несколько по-разному. За исключением простейших ситуаций невозможно не признать, что способ разработки реляционного хранилища данных тонко – но, тем не менее, серьезно – отличается от способа разработки объектной системы.
Объектные системы обычно характеризуются четырьмя базовыми компонентами: идентификацией (identity), состоянием (state), поведением (behavior) и инкапсуляцией (encapsulation). Идентификация в большинстве объектно-ориентированных (ОО) языков является неявным понятием, состоящим в том, что у любого объекта имеется уникальный идентификатор, отделенный от его состояния (значения внутренних полей объекта) – два объекта с одинаковым состоянием являются отдельными самостоятельными объектами, несмотря на то, что зеркально отображаются один на другой. Отсюда происходят дискуссии относительно «идентичности и эквивалентности» объектов в языках, подобных C++, C# или Java, где разработчикам необходимо различать операции «a == b» и «a.equals(b)». Поведение объекта довольно легко наблюдать, оно определяется набором операций, которые могут вызываться клиентом для манипулирования объектами, их изучения или для какого-либо взаимодействия с ними. (Это отличает объекты от пассивных структур данных в процедурных языках, подобных C.) Инкапсуляция является важным элементом объектного подхода, предотвращающим внешнее манипулирование внутренними частями объекта и обеспечивающим возможность эволюции интерфейса объекта. (Как ни странно, применение инкапсуляции для упрощения сопровождения программного обеспечения оказалось важным побуждающим мотивом почти во всех новшествах языковой компьютерной науки (Linguistic Computer Science). Исследователи процедурной, функциональной, объектной, аспектной и даже реляционной технологий (([Date02]) и других языков указывают на «инкапсуляцию» как на один из наиболее важных факторов.) Из этих понятий можно вывести более интересные понятия, такие как тип – формальное объявление состояния и поведения объектов; ассоциация, позволяющая типам ссылаться друг на друга на основе легковесных ссылок вместо того, чтобы включать в один объект состояние другого объекта (что иногда называют композицией); и полиморфизм – возможность подстановки некоторого объекта там, где ожидается наличие объекта другого типа.
Реляционные системы предоставляют возможность хранения знаний и их выборки на основе логики предикатов и истинных утверждений. По существу, каждая строка таблицы является утверждением о некотором факте реального мира, и SQL позволяет эффективно выбирать факты с использованием логики предикатов для вывода следствий из этих фактов. Дейт ([Date04]) и Фассел ([Fussell]) полагают, что реляционная модель характеризуется понятиями отношение, атрибут, кортеж, значение отношения и переменная отношения. Отношение в своей основе является смысловым предикатом о реальном мире, утверждением о фактах (атрибутах), которые обеспечивают смысл этого предиката. Например, можно определить отношение PERSON как {SSN, Name, City}; соответствующий предикат утверждает, что «существует человек (PERSON) с номером карточки социального страхования (social security number) SSN, проживающий в городе City и именуемый Name. Заметим, что в отношении полностью отсутствует упорядоченность атрибутов. Кортеж – это истинное утверждение в контексте некоторого отношения, множество значений атрибутов, соответствующее требуемому множеству атрибутов данного отношения, например, "{PERSON SSN='123-45-6789' Name='Catherine Kennedy' City='Seattle'}. Заметим, что два кортежа считаются идентичными, если они относятся к одному и тому же отношению, и значения одноименных атрибутов в них совпадают. Тогда значение отношения – это комбинация отношения и множества кортежей, соответствующих этому отношению, а переменная отношения, как и большинство других переменных, – это место для размещения значений заданного отношения. Например, можно определить переменную отношения People для хранения значений отношения {PERSON}, и эта переменная отношения может содержать следующее значение:
{ {PERSON SSN='123-45-6789' Name='Catherine Kennedy' City='Seattle'},
<{PERSON SSN='321-54-9876' Name='Charlotte Neward' City='Redmond'},
<{PERSON SSN='213-45-6978' Name='Cathi Gero' City='Redmond'} }
Переменные отношений обычно называют таблицами, кортежи – строками, атрибуты – столбцами, а набор переменных отношений – базой данных. Эти базовые элементы можно комбинировать с использованием набора операций (более подробно описанных в гл. 7 книги [Date04]): ограничения (restrict), проекции (project), декартова произведения (product), соединения (join), деления (divide), объединения (union), пересечения (intersection) и вычитания (difference), и эти операции образуют базис формата и подхода SQL, всемирно признанного языка для взаимодействия с реляционными системами с консолей операторов или из программ на языках программирования. Использование этих операций позволяет создавать порождаемые значения отношений, отношения, которые вычисляются на основе других значений отношений, хранимых в базе данных, – например, путем применения операций проекции и ограничения к определенной выше переменной отношения People можно создать значение отношения, показывающее номера карточек социального страхования людей, живущих в конкретном городе.
Уже сейчас достаточно очевидно, что имеется четко выраженной различие между тем, как представляется «правильная» разработка системы в реляционном и объектном мирах, в с течением времени это различие станет еще более очевидным. Однако важно заметить, что, поскольку программисты предпочитают использовать для доступа в реляционным хранилищам данных объектно-ориентированное программирование, всегда будет иметься некоторая разновидность объектно-реляционного отображения – эти две модели слишком сильно различаются, чтобы можно было их потихоньку соединить. (Вероятно, то же самое верно и для объектно-ориентированного и процедурного программирования, но это совсем другая тема.) ОР-отображения могут существовать в разнообразных формах, из которых проще всего понимаются средства автоматического ОР-отображения, такие как TopLink, Hibernate/NHibernate
и Gentle.NET. В другой форме для организации ОР-отображения требуется кодирование вручную с использованием инструментальных средств, ориентированных на работу с реляционными базами данных, таких как JDBC или ADO.NET. Эти средства обеспечивают доступ к реляционным данным и их извлечение «вручную» в форме, более привлекательной для объектных разработчиков. В третьей форме реляционные данные просто принимаются в качестве модели, с которой следует работать, и объекты подстраиваются под этот подход. В своем лексиконе паттернов Фаулер называет эту форму ОР-отображения «шлюзом к табличным данным» (Table Data Gateway [Fowler, стр. 144]) или «шлюзом к строчным данным» (Row Data Gateway [Fowler, стр. 152]). Этот подход используется во многих слоях доступа к данным в Java и .NET. Его комбинирование с генерацией кода облегчает разработку этих слоев. Иногда объекты строятся вокруг реляционной/табличной модели, в них добавляются операции доступа к реляционной базе данных, и это называется «активными записями» (Active Record, [Fowler, стр. 160]).
Действительно, этот базовый подход – подчинить одну модель терминам и подходу другой модели – является традиционным решением проблемы несоответствия моделей. По сути дела, проблема решается путем игнорирования ее половины. К сожалению, авторы большинства подобных разработок, подобно администрации Кеннеди, не хотят доводить эту идею до логического конца с полным предпочтением одного подхода перед другим. Например, хотя большинство групп разработки программного обеспечения было бы счастливо принять «исключительно объектный» подход, это привело бы к использованию объектно-ориентированной системы управления базами данных (ООСУБД), что часто не одобряется высшим руководством корпоративных групп управления данными. Обратный подход – «исключительно реляционный» – почти бессмысленно рассматривать при наличии той технологии, которая используется разработчиками во время написания этой статьи.
Невозможность «разрешить объектам использовать все свои возможности», как сказал бы генерал Уэстморленд, заставляет использовать некоторый гибридный подход к организации ОР-отображения, предпочтительно, как можно более автоматизированный, чтобы разработчики могли концентрироваться на модели своей предметной области, а не на деталях ОР-отображения. И вот здесь, к сожалению, начинается потенциальная трясина.
Проблема объектно-табличного отображения
Одна из первых и наиболее очевидных проблем при использовании объектов в качестве внешнего компонента реляционного хранилища данных состоит в том, как следует отображать классы в таблицы. По началу это кажется довольно простым упражнением – таблицы соответствуют типам, столбцы – полям. Даже для типов полей подыскиваются типы столбцов таблиц, которые соответствуют им в довольно сильной степени: типы VARCHAR соответствуют типу String, тип INTEGER – типу int и т.д. Так что имеет смысл для каждого класса, определенного в объектной системе, определить таблицу с тем же или схожим именем. Или, возможно, если объектный код пишется при уже существующей схеме базы данных, то класс отображается на существующую таблицу.
Но с течением времени любой подготовленный объектно-ориентированный разработчик, естественно, будет стремиться использовать в объектной системе наследование, и для этого понадобится делать то же самое в реляционной модели. К сожалению, в реляционной модели не поддерживается какой-либо вид полиморфизма, или связи IS-A. Поэтому разработчикам, в конечном счете, приходится применять один из трех вариантов отображения наследования в реляционный мир: таблица на класс, таблица на конкретный класс и таблица на семейство классов. У каждого из этих вариантов имеются существенные потенциальные недостатки.
Вероятно, наиболее понятным является подход с отдельной таблицей для каждого класса, поскольку он направлен на нахождение минимального «расстояния» между объектной и реляционной моделями. Каждому классу в иерархии наследования соответствует отдельная таблица, и объекты порожденных типов «сшиваются» путем соединения различных таблиц, основанных на наследовании. Например, если в объектной модели имеется базовый класс Person, из которого порождается класс Student, являющийся, в свою очередь, предком класса GraduateState, то потребуются три таблицы PERSON, STUDENT и GRADUATESTUDENT, каждая из которых соответствует одноименному классу. Однако для связывания этих таблиц потребуется наличие у каждой из них независимого первичного ключа (такого, значения которого реально не хранится в сущности объекта), чтобы у каждого порожденного класса могла иметься связь по внешнему ключу с таблицей, соответствующей его суперклассу. Причина этого понятна: объект GraduateStudent, поскольку у него имеется связь IS-A с объектами Student и Person, является коллекцией трех наборов состояния, и ко времени создания объекта этого типа различие между этими классами в значительной степени теряется. Например, в Java и .NET объект представляет собой участок памяти, в котором сохраняются поля экземпляра, определенные для класса и всех суперклассов, а также таблица методов, определяемых той же иерархией. Это означает, что при расспрашивании конкретного экземпляра на реляционном уровне для сборки состояния объекта в рабочую память объектной программы потребуются, по меньшей мере, три соединения.
На самом деле, дело обстоит еще хуже. Если иерархия объектов продолжает расти, включая, например, классы Professor, Staff, Undergrad (наследует от класса Student) и всю иерархию AdjunctEmployees (наследуемую от Staff), и программа хочет найти всех людей по фамилии Smith, то соединения должны быть выполнены для всех порожденных классов системы, поскольку семантика «найти всех людей» означает, что запрос должен произвести поиск данных в таблице PERSON, а затем выполнить набор дорогостоящих операций соединения для импорта из базы данных оставшихся данных. В этих операциях должны участвовать таблицы PROFESSOR, UNDERGRAD, ADJUCTEMPLOYEE, STAFF и все другие таблицы, соответствующие классам, которые порождены от PERSON. Если учесть, что запросы с соединениями относятся к числу наиболее дорогостоящих запросов, поддерживаемых РСУБД, то становится очевидной невозможность быстрого выполнения требуемых действий.
В результате разработчики обычно применяют один из двух других подходов, являющихся на вид более сложными, но и более эффективными при работе с реляционным хранилищем. При одном из этих подходов таблица создается для каждого конкретного (замыкающего цепочку наследования, most derived) класса, здесь применяется денормализация со всеми связанными с ней накладными расходами. При другом подходе для всей иерархии наследования создается одна таблица, в которой часто заводится столбец-дискриминатор, значения которого показывают, к какому классу относится каждая строка. (Возможны и различные гибридные варианты этих схем, но обычно получаемые результаты незначительно отличаются от результатов базовых подходов.) К сожалению, накладные расходы на поддержку денормализованных таблиц часто оказываются значительными при работе с большими объемами данных, а единая таблица будет содержать значительное число пустых столбцов, и, следовательно, для этих столбцов придется допустить наличие неопределенных значений и отказаться от использования ограничений целостности, поддерживаемых РСУБД.
Проблема не исчерпывается отображением наследования: совершенно по-разному обрабатываются ассоциации между объектами, типичные ассоциации 1:n и m:n, обычно используемые и в SQL, и в UML. В объектных системах ассоциации являются однонаправленными, ведущими от ассоциирующего объекта к ассоциируемому объекту (это означает, что ассоциируемый объект в действительности ничего не знает об ассоциации, если явно не установлена двунаправленная ассоциация). В то же время в реляционных системах ассоциация на самом деле является инверсной, она ведет и от ассоциируемого объекта к ассоциирующему объекту (через столбцы внешних ключей). Это оказывается поразительно важным, поскольку означает, что для поддержки ассоциаций m:n необходимо использовать третью таблицу для хранения реальной взаимосвязи ассоциирующего и ассоциируемого объектов. Даже в более простом случае связей 1:n у ассоциатора отсутствует внутреннее знание отношений, с которыми он ассоциируется – для обнаружения этих данных требуется выполнить соединение с какой-либо или со всеми ассоциированными таблицами. (Вопрос, когда следует реально выбирать эти данные, заслуживает отдельного обсуждения, см. ниже разд. «Парадокс загрузки».)
Конфликт «схема-владелец»
В обсуждениях подходов отображения наследования и ассоциаций всплывает одна распространенная ошибка. По сути дела, во многих инструментальных средствах объектно-реляционного отображения предполагается, что схема базы данных – это нечто, что можно определить в соответствии со методами, помогающими оптимизировать выполнение запросов от этих средств к реляционным данным. Но в действительности схема базы данных часто не находится под непосредственным контролем разработчиков, ей владеет некоторая другая группа служащих компании, обычно группа администрирования баз данных (DBA). Кто отвечает за проектирование базы данных и за принятие решений о допустимости изменения ее схемы?
Во многих случаях разработчики начинают новый проект с «чистой доски», с пустой реляционной базой данных, схему которой они могут определить так, как пожелают. Но вскоре после завершения проекта (иногда даже раньше, по политическим причинам или из-за «войны за территорию») становится очевидным, что владение разработчиками схемой является, в лучшем случае, временным. Различные отделы начинают требовать отчеты о базе данных, на DBA возлагается ответственность за производительность базы данных, что дает им повод требовать проведение «рефакторинга» и денормализации данных, и другие группы разработчиков могут начать интересоваться возможностью использования данных, сохраняемых в базе данных. Вскоре схема должна быть «заморожена», что потенциально создает барьер для рефакторинга объектной модели (см. ниже обсуждение «проблемы сопряжения»). Кроме того, эти другие группы часто будут рассчитывать увидеть реляционную модель, определенную в реляционных терминах, а не в той, которая поддерживает полностью ортогональную форму персистентности. Например, трудности будет представлять столбец-дискриминатор, используемый для решения проблемы отображения наследования, и, вероятно, он окажется почти бесполезным для реляционных генераторов отчетов, таких как Crystal Reports. Это состояние дел, скорее всего, окажется неприемлемым, если только разработчики не пожелают вручную составлять отчеты (и конструировать пользовательские интерфейсы, писать код для печати и т.д.).
(Честно говоря, эта проблема является не столько технической, сколько политической, но, независимо от источника и способа решения, она является серьезной проблемой. И она представляет препятствие для достижения решения ОР-отображения.)
Проблема двойной схемы
К проблеме владения схемой примыкает еще одна родственная проблема, состоящая в том, что в решении ОР-отображения метаданные системы, по сути, сохраняются в двух разных местах: в схеме базы данных и в объектной модели (если угодно, другой схеме, выраженной средствами Jave или C#, а не DDL). При обновлении или рефакторинге одной схемы, вероятно, требуются аналогичные действия над другой схемой. Рефакторинг кода для приведения его в соответствие изменениям схемы базы данных принято считать более простым способом – при рефакторинге базы данных часто требуется какая-либо миграция и/или адаптация данных, уже присутствующих в базе данных, в то время как для кода такое требование отсутствует. (Здесь полагается, что объекты являются недолговечными, исчезающими при завершении процесса, в котором они были порождены. Если бы объекты сохранялись в некоторой форме, позволяющей им продолжать существовать вне выполняемого процесса, например, в сериализованной форме, записанной на диск, то рефакторинг объектов был бы настолько же проблематичным.)
Более важно то, что, в то время как код нередко разрабатывается для использования в единственном приложении, экземпляры баз данных часто используются в более чем одном приложении, и для компаний часто неприемлемо инициировать масштабный рефакторинг кода, поскольку при рефакторинге одного приложения потребуется аналогичный рефакторинг других приложений, вызываемый изменениями схемы базы данных. В результате по мере роста системы с течением времени на разработчиков оказывается все возрастающее давление, вынуждающее их «отключить» объектную модель от схемы базы данных, чтобы при изменении схемы не требовался аналогичный рефакторинг объектной модели и наоборот. В некоторых случаях, в которых ОР-отображение не допускает такого отключения, для приложения может потребоваться полностью индивидуальный экземпляр базы данных с точной схемой, в расчете на которую было создано решение ОР-отображения. Это приводит к созданию еще одного бункера данных в IT-среде, которой свойственно стремление к сокращению числа таких бункеров.
Проблемы идентификации сущностей
Если этих проблем недостаточно, поговорим еще об одной проблеме, проблеме идентификации объектов и отношений. Как отмечалось выше, в объектных системах используется неявная форма идентификации, обычно основанная на расположении объекта в памяти (вездесущий указатель this). Это иногда также называют OID (Object Identifier, объектный идентификатор). Обычно это принято в системах, в которых напрямую не раскрывается расположение объектов в памяти, таких как объектные базы данных (в которых указатели в основной памяти являются довольно бессмысленными в качестве идентификаторов вне процессов базы данных). Однако в реляционной модели идентификация неявно присутствует внутри самого состояния – две строки с абсолютно одинаковым состоянием обычно считаются искажением реляционных данных, поскольку один и тот же факт, занесенный в базу данных дважды, является избыточным и непродуктивным. Для справедливости здесь следует говорить более точно; в действительности в реляционной системе допускаются кортежи-дубликаты (как отмечалось выше), но они часто запрещаются явными реляционными ограничениями, такими как ограничения PRIMARY KEY. В тех ситуациях, в которых значения-дубликаты допускаются, в реляционной системе отсутствует способ определить, какая из дублирующих строк выбирается – нет никакого неявного способа идентификации кортежей отношения, кроме как на основании его атрибутов. Это не так в случае объектных систем, в которых два объекта, обладающие абсолютно идентичными состояниями в двух разных областях памяти, в действительности являются разными объектами. (Из-за этого приходится различать операции «==» и «.equals()» в языках Java и C#.) Из этого вытекает простое следствие: если требуется согласовать в объектной и реляционной системах смысл идентификации, то в реляционной системе для обеспечения соответствия объектному идентификатору должно обеспечиваться некоторая разновидность уникального идентификатора (обычно автоинкрементный столбец целого типа).
Это приводит к некоторым серьезным проблемам по отношению к автоматическим системам ОР-отображения, поскольку смысл идентификации является абсолютно разным. Если две отдельные пользовательские сессии взаимодействуют с одним и тем же отношением в реляционном хранилище, то работает подсистема управления параллельным доступом РСУБД, которая обычно основывается на метафоре транзакционности (ACID). Если система ОР-отображения выбирает отношение из хранилища данных (по существу, формируя «представление» данных), образуется второй источник идентификации данных, одним из которых является база данных (защищенная упомянутой транзакционной схемой), а другим – объектное представление данных в основной памяти, для которого отсутствует транзакционная поддержка согласованности, кроме той, которая встроена в язык (например, мониторы в Java и .NET) или основанная на использовании библиотек (например, System.Transactions в .NET 2.0). Обе последние возможности могут игнорироваться разработчиками и действительно часто игнорируются ими. Управление изоляцией и параллельным доступом представляет собой непростую проблему, и, к сожалению, языки и платформы, обычно доступные для разработчиков, не настолько согласованы и гибки, как подсистемы управления транзакциями РСУБД.
Проблему еще более усложняет то, что во многих системах ОР-отображения имеется существенная поддержка кэширования данных (применяемого для повышения производительности и во избежание повторяющихся обращений к базе данных). Когда требуется производить реальное «выталкивание» данных в базу данных, и что означает транзакционная целостность, если приложение считает, что запись в базу данных произошла, а на самом деле этого не случилось? Эта проблема, в свою очередь, усложняется в тех случаях, когда система ОР-отображения выполняется в нескольких процессах над сервером баз данных. Обычно это происходит при использовании кластерных серверов приложений. Теперь идентификация данных производится в n+1 месте, где n – число узлов сервера приложений, а 1 – это сам сервер баз данных. Каждый узел должен каким-то образом сообщать другим узлам о своем намерении произвести обновление данных для поддержки некоторого механизма, предотвращающего одновременный доступ к данным (другим экземпляром той же сессии или некоторым экземпляром другой сессии, производящим доступ к тем же данным). На это тратится время, падает производительность. Даже в случае кэшей, работающих только на чтение, информация об обновлении хранилища данных должна каким-то образом доводиться до кэшей, поддерживаемых в узлах сервера приложений. Для этого требуются коммуникации сервера с клиентами по инициативе базы данных; в текущем поколении РСУБД поддержка таких коммуникаций не очень хорошо понятна или плохо документирована.
Проблема механизма выборки данных
Пусть теперь интересующая нас сущность хранится в базе данных. Каким образом можно ее выбрать? В чистом объектно-ориентированном подходе для выборки следовало бы использовать объектный подход, в котором для идентификации объекта(ов) используется синтаксис в стиле конструктора, но, к сожалению, синтаксис конструкторов не является настолько общим, чтобы обеспечить необходимую гибкость. В частности, в нем отсутствует возможность инициализации коллекции объектов, а часто требуются именно запросы, возвращающие коллекции, а не одиночные сущности. (Несколько обращений к базе данных для выборки требуемых сущностей-членов коллекции обычно считаются слишком расточительными по отношению и ко времени задержки, и к пропускной способности, чтобы этот вариант можно было полагать разумной альтернативой. Подробности см. в разд. «Парадокс загрузки».) В результате обычно применяются подходы Query-By-Example (QBE), Query-By-API (QBA) или Query-By-Language (QBL)
Подход QBE характеризуется тем, что заполняется шаблон объекта того объектного типа, к которому направлен поиск. В поля шаблона объекта заносятся значения, используемые для фильтрации значений в процессе выполнения запроса. Например, если задается запрос к таблице объектов Person для выборки людей по фамилии Smith, то этот запрос при применении подхода QBE будет выглядеть примерно так:
Person p = new Person(); // assumes all fields are set to null by default
p.LastName = "Smith";
ObjectCollection oc = QueryExecutor.execute(p);
Проблема подхода QBE очевидна: он прекрасно подходит для простых запросов, но не является достаточно выразительным для поддержки более сложного стиля запросов, который часто требуется – «найти всех людей по фамилии Smith или Cromwell», «найти всех людей, фамилией которых не является Smith» и т.д. Хотя в принципе можно расширить подход QBE так, чтобы он справлялся с такими (и более сложными) запросами, это, безусловно, приведет к существенному усложнению API. Более важно то, что применение подхода QBE создает неудобства для прикладных объектов – в них должны будут поддерживаться поля/свойства, допускающие наличие неопределенных значений, что может быть нарушением правил прикладной области, поддержку которых стремится обеспечить объектный мир. Во многих сценариях использования человек без имени не является слишком полезным объектом, а именно этого потребует подход QBE от прикладных объектов, хранимых в соответствующей базе данных. (Профессионалы QBE могут возразить, что в реализации объектов неразумно принимать это во внимание, но это, опять же, нелегко и часто не делается.)
В результате второй шаг обычно состоит в поддержке объектной системой подхода «Query-By-API», в котором запросы конструируются путем использования объектов-запросов, обычно примерно в такой форме:
Query q = new Query();
q.From("PERSON").Where(
new EqualsCriteria("PERSON.LAST_NAME", "Smith"));
ObjectCollection oc = QueryExecutor.execute(q);
Здесь запрос основывается не на пустом «шаблоне» выбираемого объекта, а на наборе «объектов-запросов», которые совместно используются для определения объекта в стиле команды, предназначенной для выполнения над базой данных. Несколько критериев комбинируется путем использования некоторой конструкции, обычно соединяющей через «And» и «Or» объекты, каждый из которых содержит уникальный объект-критерий, задающий часть условия выборки. К концу запроса могут быть добавлены вызовы объектов фильтрации/манипулирования, такие как «OrderBy(field-name)» или «GroupBy(field-name)». В некоторых случая эти вызовы методов в действительности ведут в объекты, конструируемые программистом и явно связываемые между собой.
Разработчики часто отмечают, что подход QBA является намного более многословным, чем традиционный подход SQL, и при использовании этого подхода намного более трудно (если не невозможно) представить некоторые виды запросов (в частности, внешние соединения).
Кроме этого, имеется более тонкая проблема: в надежде на дисциплину разработчиков как имя таблицы (PERSON), так и имя столбца в критерии (PERSON.LAST_NAME) представляются в виде стандартных строк символов, принимаемых в том виде, как они задаются разработчиком. Это приводит к тому, что до времени выполнения объектной системы не может быть произведена какая-либо проверка правильности запросов. Это является классической проблемой программирования, ошибкой «толстых пальцев», которая выражается в том, что запрос направляется не к желаемой таблице PERSON, а к какой-то другой таблице PRESON. Хотя эта ошибка будет быстро обнаружена при тестировании соответствующего компонента совместно с реальным экземпляром базы данных, это предполагает наличие двух обстоятельств – то, что разработчики добросовестно относятся к тестированию компонентов, и то, что тестирование компонентов производится с использованием реальной базы данных. Хотя первая ситуация становится более или менее гарантированной по мере того, как все больше разработчиков становится «тест-инфицированными» (следуя терминологии Гаммы и Бека, Erich Gamma, Kent Beck), возможность наличия второго обстоятельства все еще является предметом обсуждений, поскольку налаживание и разборка экземпляра базы данных, пригодного для тестирования компонентов, остается трудным делом. (Хотя имеется несколько способов преодоления этой проблемы, как кажется, на практике они используются незначительно.)
Также имеется та проблема, что от разработчиков требуется большее понимание логического (или физического) представления данных. Вместо того чтобы просто сосредоточиться на том, как объекты связаны между собой (через простые ассоциации, такие как массивы или коллекции экземпляров), разработчику теперь требуется больше знать о том, как объекты хранятся, что делает систему более уязвимой к изменениям схемы базы данных. Эту проблему иногда можно обойти за счет применения гибридного подхода, при котором система берет на себя ответственность за интерпретацию ассоциаций, и разработчик может писать примерно следующее:
Query q = new Query();
Field lastNameFieldFromPerson =
Person.class.getDeclaredField("lastName");
q.From(Person.class).Where(new
EqualsCriteria(lastNameFieldFromPerson, "Smith"));
ObjectCollection oc = QueryExecutor.execute(q);
Здесь частично решаются проблемы потребности знания схемы и «толстых пальцев», но не исчезает многословность, и по-прежнему остается трудным составление более сложных запросов, таких как запрос над несколькими таблицами (или, если угодно, классами), соединяемыми разными способами по нескольким критериям.
Поэтому следующая задача состоит в построении подхода «Query-By-Language», в котором создается новый язык, похожий на SQL, но в чем-то его «превосходящий», для поддержки сложных и мощных запросов, обычно поддерживаемых SQL. Примерами такого языка являются OQL и HQL. Здесь проблемой является то, что эти языки часто являются подмножествами SQL, и поэтому они не обладают полной мощностью SQL. Более важно то, что в слое ОР-отображения теперь теряется важный «выигрышный момент» – мантра «объекты и только объекты», которая породила этот подход. Использование SQL-подобного языка – это почти использование самого SQL, и как такой язык может быть более «объектным»? Хотя разработчикам может не потребоваться осведомленность о физической схеме модели данных (интерпретатор/исполнитель языка запросов может произвести упомянутое выше отображение), разработчикам понадобится знание того, как в языке представляются ассоциации и свойства, и подмножество объектных возможностей внутри языка запросов. Например, будет ли возможным написать следующее?
SELECT Person p1, Person p2
FROM Person
WHERE p1.getSpouse() == null
AND p2.getSpouse() == null
AND p1.isThisAnAcceptableSpouse(p2)
AND p2.isThisAnAcceptableSpouse(p1);
Другими словами, здесь требуется просканировать базу данных и найти все пары людей, являющихся приемлемыми по отношению друг к другу. Хотя метод «isThisAnAcceptableSpouse», очевидно, относится к классу Person (у каждого экземпляра этого класса может иметься собственный критерий, по которому оценивается приемлемость другого экземпляра – он может быть блондином, брюнетом, рыжим, зарабатывать больше 100000 долларов в год и т.д.), непонятно, можно ли выполнять этот метод в языке запросов, и должна ли существовать такая возможность. Даже в наиболее тривиальных реализациях производительности может быть нанесен серьезный урон, в особенности, если слой ОР-отображения должен для выполнения запроса преобразовывать данные реляционных столбцов в объекты. Кроме того, отсутствует гарантия того, что разработчик напишет этот метод полностью эффективно, и нет никакого способа заставить его выполнить эффективную реализацию.
(Критики могут возразить, что это решаемая проблема, и предложить два возможных решения. Одно из них состоит в том, чтобы поместить данные о предпочтениях в отдельную таблицу и включить эту таблицу в запрос. Однако это приведет к ужасно сложному запросу, текст которого займет несколько страниц, и при потребности добавления нового критерия предпочтения для распутывания этого запроса придется привлекать эксперта по SQL. Другим решения является размещение реализации «приемлемости» в хранимой процедуре базы данных. Тогда соответствующий код будет полностью удален из объектной модели, что оставит разработчиков вообще без объектного решения. Это решение допустимо, но только если принять допущение, что в объектной модели может присутствовать не вся реализация. Но это противоречит исходному условию «объекты и ничего, кроме объектов», на котором многие защитники ОР-отображения основывают свои доводы.)
Проблема частичных объектов и парадокс времени загрузки
Давно известно, что для сетевого взаимодействия, типа того, который требуется при выполнении традиционного запроса на SQL, требуется значительное время. (Грубые тесты показывают, что это время на три-пять порядков превосходит время, требуемое для обработки одного вызова метода на платформе Java или .NET. (В этом случае сравнивалось время вызова метода через Java RMI со временем локального вызова. Similar results are pretty easily Аналогичные результаты можно получить для доступа к данным на основе SQL, сравнив время выполнения межпроцессных вызовов со временем внутрипроцессных вызовов с использованием средства управления базами данных, в котором поддерживаются оба типа вызовов, например, Cloudscape/Derby или HSQL (Hypersonic SQL). Примерная аналогия состоит в следующем: если время пути из дома на работу составляет двадцать минут, и мы будем считать, что это время соответствует времени обработки локального вызова метода, то время, соответствующее времени обработки сетевого вызова составит пятнадцать лет. За это время можно добраться от Земли до Плутона.) Очевидно, что эти расходы нетривиальны, и поэтому разработчики пытаются минимизировать их за счет оптимизации числа проходов по сети и объема выбираемых данных.
В SQL эта оптимизация достигается путем тщательной структуризации SQL-запросов, обеспечивающей выборку только требуемых столбцов и/или таблиц, а не таблиц целиком или наборов таблиц. Например, при построении традиционного пользовательского интерфейса для детализации данных разработчик представляет пользователю сводное отображение всех записей, из которых он может выбрать одну запись, и тогда разработчик отобразит полный набор данных для этой конкретной записи. Если, например, требуется произвести детализацию описанного раньше реляционного типа Persons, то это можно сделать с помощью следующих двух запросов (в предположении, что пользователь выбирает первую запись):
SELECT id, first_name, last_name FROM person;
SELECT * FROM person WHERE id = 1;
Заметим, в частности, что на каждом шаге этого процесса выбираются только требуемые данные. В первом запросе выбирается необходимая сводная информация и идентификатор (в следующем запросе имя и фамилия не могут идентифицировать конкретного человека), а во втором запросе выбирается оставшаяся информация о запрашиваемом человеке. В действительности, большинство экспертов SQL остерегается использовать метасимвол «*», предпочитая явно именовать в запросе каждый столбец. Это делается по соображениям как производительности, так и удобства сопровождения – производительности, поскольку в этом случае РСУБД будет лучше оптимизировать запрос, удобства сопровождения, поскольку в этом случае будет меньше шансов получить избыточные столбцы, если DBA или разработчики изменять или произведут рефакторинг соответствующей таблицы. Возможность получения части таблицы (хотя все еще в реляционной форме, которая важна для обеспечения упоминавшейся ранее замкнутости) является существенной для упомянутой оптимизации – для большинства запросов требуется только часть всего отношения.
Это представляет проблему для большинства, если не для всех, систем ОР-отображения: целью любой такой системы является обеспечение разработчику возможности видеть «объекты и ничего, кроме объектов», но, тем не менее, слой ОР-отображения не позволяет указать, как будут использоваться объекты, полученные при выполнении запроса. Например, вполне правдоподобно, что большинство разработчиков захочет написать что-то вроде этого:
Person[] all = QueryManager.execute(...);
Person selected = DisplayPersonsForSelection(all);
DisplayPersonData(selected);
Другими словами, это означает, что после того, как из массива людей выбран конкретный человек, никаких дополнительных действий по выборке данных больше делать не надо – в конце концов, требуемый объект имеется, а больше ничего и не нужно.
Здесь проблема состоит в том, что данные, которые должны отображаться в первом вызове Display...(), не являются полными данными о людях. Здесь мы сталкиваемся с тем, что в объектно-ориентированной системе, написанной на C# или Java, невозможно возвращать «части» объекта – объект есть объект, и если объект Person состоит из 12 полей, то в любом таком возвращаемом объекте будут представлены все 12 полей. Это значит, что в системе приходится выбирать один из трех неудобных вариантов: (1) потребовать, чтобы в объектах Person могли содержаться поля, допускающие наличие неопределенных значений, независимо от возможных ограничений прикладной области; (2) возвращать объекты Person, содержащие все данные, присущие людям; (3) обеспечить некоторую разновидность загрузки по требованию, которая будет заполнять поля объектов только тогда, когда к ним производится доступ, возможно, косвенный, через вызов метода.
(Заметим, что в некоторых объектных языках, таких как ECMAScript, объекты представляются не так, как в языках, основанных на классах (например, C# и Java), и в результате в таких языках разрешается возвращать объекты, содержащие переменное число полей. Однако, с другой стороны, этот подход применяется лишь в немногих языках, он не применяется даже в любимом всеми и являющемся образцом для подражания динамическом языке Ruby, и пока такие языки не станут широко распространенными, их обсуждение в данном контексте не имеет смысла.)
Для большинства слоев ОР-отображения это означает, что объекты и/или поля объектов должны выбираться в манере отложенной (lazy) загрузки с предоставлением данных в полях по требованию, поскольку в обсуждаемом сценарии выборка всех полей всех объектов/отношений Person, «очевидно» привела бы в громадной потере пропускной способности. Обычно имеет смысл выбирать полный набор полей при доступе к любому еще на заполненному данными полю. (Это подход предпочтительнее подхода с выборкой данных отдельных требуемых полей, поскольку он с меньшей вероятностью приводит к «проблеме N+1-го запроса», когда при выборке всех данных объекта требуются один запрос для выборки первичного ключа плюс N запросов для выборки данных всех остальных полей по одиночке. При подходе с выборкой данных индивидуальных полей минимизируется потребление пропускной способности для выборки данных – данные неиспользуемых полей не выбираются, – но, очевидным образом, не удается минимизировать число сетевых взаимодействий.)
К сожалению, поля объектов являются только частью проблемы – другая часть проблемы, с которой приходится сталкиваться, состоит в том, что объекты часто ассоциированы с другими объектами с разными степенями связи (один-к-одному, один-ко-многим, многие-к-одному, многие-ко-многим), и в системе ОР-отображения должны быть приняты некоторые решения о том, когда следует выбирать эти ассоциированные объекты. И, несмотря на все усилия разработчиков системы ОР-отображения, всегда найдутся распространенные сценарии использования, в которых выбранное решение окажется неверным. В большинстве систем ОР-отображения обеспечивается некоторая управляемая разработчиками поддержка принятия решений, обычно некоторый файл конфигурации или отображения, определяющий используемую политику выборки, но соответствующий режим устанавливается на уровне классов, и его невозможно изменять в зависимости от ситуации.
Резюме
Притом, что объектно-реляционное отображение является настоятельной потребностью современных корпоративных систем, как можно утверждать, что это трясина, из которой нет выхода? Здесь снова полезной аналогией является Вьетнам – в то время как ситуация в Южном Индокитае требовала реакции со стороны американцев, у администраций Кеннеди и Джонсона имелись различные варианты возможных действий, включая вариант реакции США на недавнее падение режима Сухарто в Малайзии, которая, честно говоря, просто отсутствовала. (Напомним, что Эйзенхауэр и Даллес не считали, что Южный Индокитай затрагивается «теорией домино»; их гораздо больше беспокоили Япония и Европа.)
Существует несколько возможных решений проблемы ОР-отображения, некоторые из которых требуют «глобальных» действий со стороны сообщества в целом, а некоторые более доступны для отдельных групп разработчиков.
1. Добровольный отказ. Разработчики просто полностью отказываются от объектов и возвращаются к модели программирования, не порождающей объектно-реляционной потери соответствия. Хотя это и неприятно говорить, но в некоторых сценариях объектно-ориентированный подход порождает больше накладных расходов, чем позволяет их сэкономить, и в этих случаях просто отсутствует возврат инвестиций (ROI), оправдывающий создание развитой объектной модели. (Более глубоко этот вопрос обсуждается в книге [Fowler].) Это полностью устраняет проблему, поскольку если нет объектов, то нет и несоответствия.
2. Полное принятие. Разработчики просто полностью отказываются от реляционного хранилища и используют модель хранения, которая соответствует видению мира, принятому в используемых ими языках. Проблему решают системы хранения объектов, такие как db4o, за счет непосредственного хранения объектов на диске, что устраняет многие (но не все) упомянутые выше проблемы. Например, в этом случае отсутствует «вторая схема», поскольку единственной используемой схемой являются определения самих объектов. Хотя это может сильно шокировать администраторов баз данных, но во все более сервис-ориентированном мире, в котором воздерживаются от применения идеи непосредственного доступа к данным и взамен этого требуют, чтобы весь доступ к данным происходил через сервисные шлюзы, инкапсулируя механизм хранения данных от надоедливых глаз, становится совершенно реальным позволить разработчикам хранить данные в той форме, в которой их проще использовать именно им, а не DBA.
3. Отображение вручную. Разработчики просто соглашаются с тем, что проблема не настолько сложна, чтобы нельзя было ее решить вручную, и пишут прямой код для доступа к реляционным базам данных, обеспечивающий доступ к кортежам и позволяющий сохранять в базе данных данные объектов. Во многих случаях этот код может быть автоматически сгенерирован некоторым инструментальным средством на основе анализа метаданных базы данных, частично устраняя тем самым основу для критики этого подхода («слишком много кода нужно написать и сопровождать»).
4. Принятие ограничений ОР-отображения. Разработчики просто соглашаются с тем, что нет способа эффективно и просто решить проблему объектно-реляционного несоответствия и используют ОР-отображение для решения 85% проблемы (или 50%, или 95% в зависимости от ситуации), а в тех случаях, когда ОР-отображение может само породить проблемы, используют SQL и «примитивные» средства доступа в реляционным базам данных (такие как JDBC или ADO.NET). Однако у этого подхода имеется собственный риск, поскольку разработчики, использующие ОР-отображение, должны быть осведомлены о любом виде кэширования, производимом в используемом решении ОР-отображения, поскольку «примитивные» средства доступа к реляционной базе данных, очевидно, не будут использовать возможности этого кэширования.
5. Интеграция реляционных понятий с языками. Разработчики просто соглашаются с тем, что имеется проблема, которую следует решать на уровне языка, а не библиотеки или «оболочки». В течение первого десятилетия работ над ОР-отображением усилия сосредотачивались на попытках подтащить объекты поближе к базе данных, чтобы разработчики могли фокусироваться на программировании в рамках одной парадигмы (в качестве которой выступали, конечно, объекты). Однако в следующие несколько лет интерес к «скриптовым» языкам с более сильной поддержкой множеств и списков, таким как Ruby, побудил идею о возможной пригодности другого решения: ввести реляционные понятия (которые, по своей сути, ориентированы на множества) в массовые языки программирования, облегчив тем самым устранение разрыва между «множествами» и «объектами». В этой области велось не очень много работ, в основном исследовательские проекты и/или языки, «выходящие за рамки общепринятого», но некоторые работы стали популярными в сообществе, например, функционально-объектные гибридные языки Scala
и F#, а также прямая интеграция с традиционными объектно-ориентированными языками, обеспечиваемая в проекте
LINQ
компании Microsoft для C# и Visual Basic. К сожалению, одну из таких работ, а именно, стратегию SQL/J, постигла неудача. Даже в этом случае подход был ограниченным. В нем не предпринимались попытки внедрить множества в Java, а просто обеспечивались возможности препроцессорной обработки встроенных вызовов SQL и их трансляции в код JDBC.
6. Интеграция реляционных понятий с оболочками. Разработчики просто соглашаются с тем, что эта проблема является разрешимой, но только при изменении ее видения. Вместо того чтобы полагаться на язык или библиотеку, разрабатываемые для решения этой проблемы, разработчики принимают другое представление «объектов», более реляционное по своей природе, образуя прикладные оболочки, более близкие к реляционным конструкциям. Например, вместо создания класса Person, в котором данные экземпляров сохраняются непосредственно в полях объектов, разработчики создают класс Person, в котором данные его экземпляров сохраняются в экземпляре RowSet (Java) или DataSet (C#). Этот экземпляр может собираться вместе с другими экземплярами RowSet/DataSet в блоки, которые могут легко использоваться для обновления базы данных или выбираться из базы данных и распаковываться в индивидуальные объекты.
Заметим, что порядок этого списка возможных решений не является существенным. Хотя одни решения являются более привлекательными, чем другие, решать, какое из них лучше, должны сами разработчики.
Подобно тому, как США, вероятно, могли бы достичь какого-нибудь «успеха» во Вьетнаме, если бы у правительства имелись ясная стратегия и более отчетливое понимание связи между обязательствами и результатами (если хотите, ROI), вероятно, можно справиться с проблемой объектно-реляционной потери соответствия на основе тщательного и разумного применения некоторой стратегии при полном понимании ее собственных ограничений. Разработчики должны стремится к полной «победе» там, где они могут победить, и не попадать на скользкую дорожку, пытаясь создавать решения, которые все дороже обходятся и все меньше приносят пользы. К сожалению, как показывает история войны во Вьетнаме, даже осознание опасностей скользкого пути часто бывает недостаточным для того, чтобы избежать увязания в трясине. Хуже того, эта трясина просто слишком привлекательна, чтобы обойти ее стороной, и разработчиков среди утесов продолжают соблазнять песни сирен, доносящиеся из разных компаний (включая Microsoft, IBM, Oracle, and Sun). Если вам хочется слушать эти песни, привяжитесь к мачте, но позвольте матросам грести.
Литература
[Fussell]: Foundations of Object Relational Mapping, by Mark L. Fussell, v0.2 (mlf-970703)
[Fowler] Patterns of Enterprise Application Architecture, by Martin Fowler
[Date04]: Introduction to Database Systems, 8th Edition, by Chris Date
[Neward04]: Effective Enterprise Java