Проблема частичных объектов и парадокс времени загрузки
Давно известно, что для сетевого взаимодействия, типа того, который требуется при выполнении традиционного запроса на 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 запросов для выборки данных всех остальных полей по одиночке. При подходе с выборкой данных индивидуальных полей минимизируется потребление пропускной способности для выборки данных – данные неиспользуемых полей не выбираются, – но, очевидным образом, не удается минимизировать число сетевых взаимодействий.)
К сожалению, поля объектов являются только частью проблемы – другая часть проблемы, с которой приходится сталкиваться, состоит в том, что объекты часто ассоциированы с другими объектами с разными степенями связи (один-к-одному, один-ко-многим, многие-к-одному, многие-ко-многим), и в системе ОР-отображения должны быть приняты некоторые решения о том, когда следует выбирать эти ассоциированные объекты. И, несмотря на все усилия разработчиков системы ОР-отображения, всегда найдутся распространенные сценарии использования, в которых выбранное решение окажется неверным. В большинстве систем ОР-отображения обеспечивается некоторая управляемая разработчиками поддержка принятия решений, обычно некоторый файл конфигурации или отображения, определяющий используемую политику выборки, но соответствующий режим устанавливается на уровне классов, и его невозможно изменять в зависимости от ситуации.