Обсуждение: контейнерно-управляемое сохранение состояния (CMP)

Если вы следовали этому учебнику по контейнерно-управляемому сохранению состояния в JBoss, то вы уже поняли, что создание сохраняемого, распределенного объекта не сложнее создания обычного несохраняемого объекта. EJB-контейнер делает всю тяжелую работу, всё что нужно сделать программисту - сообщить контейнеру какие из полей надо сохранять. Тем не менее, всё не так просто, неумелое использование CMP может привести к очень неэффективным программам. Чтобы увидеть, почему такое может случиться необходимо пнимать как EJB-сервер осуществляет сохранение состояния (CMP).

Технический обзор

В EJB есть прямая зависимость между понятиями "строка таблицы базы данных" и "экземпляр объекта". Ясно, что EJB-разработчики помнят об этом с самого начала. В то же время спецификация явно не указывает, что сохранение состояния обеспечивается с помощью таблиц базы данных, но на практике это всегда так. Более того, молчаливо предпологается, что связь между объектами и базой данных будет осуществляться с помощью SQL-запросов. Какое значение это имеет для контейнерно-управляемого сохранения состояния?

Когда создаётся экземпляр сохраняемого объекта, EJB-контейнер должен сгенерировать SQL-код который запишет строку в таблицу. Когда объект удаляется, контейнер должен сгенерировать SQL для удаления строки. Это не очень большая проблема. Когда объект запрашивает ссылку на другой, контейнер должен найти (или создать) строку в таблице для этого объекта, считать столбцы этой строки, создать экземпляр объекта в JVM и записать данные из таблицы в переменные этого объекта. Из-за того, что этот процесс может быть достаточно медленным, EJB-сервер может решить отложить это действие. Например, когда объект получает ссылку на другой объект, который управляется контейнером, объект со ссылкой может быть не инициализирован. Инициализация из базы данных произойдёт позже, возможно когда будет вызван один из его методов. Эта задержка инициализации уменьшает неэффективность возникающую при инициализации объектов, которые никогда не будут использованы, но такой метод имеет собственные проблемы, как мы увидим дальше.

Ограничения CMP

Ограничения эффективности

Основное ограничение состоит в том, что EJB-контейнер не способен генерировать запросы к базе данных с эффективностью живого программиста. Поймите следующий пример: предположим у меня есть таблица в базе данных содержащая описание моей коллекции компакт дисков. Я хочу поискать в этой коллекции диски, в которых в колонке название ("title") или в колонке заметки ("notes") есть текст "Chopin" (независимо от регистра). С помощью SQL я могу написать следующий запрос:

 SELECT FROM CD WHERE title LIKE "%chopin%" OR notes LIKE "%chopin%";          
 

Символ % это SQL-шаблон и он отвечает за поиск необходимой строки внутри колонки; оператор "LIKE" осуществляет поиск независимо от регистра. Как мы можем сделать тоже самое в контейнерно управляемом EJB? Если "CD" это EJB-объект, то предоставляемый контейнером метод "findAll()" в его домашнем интерфейсе получит список из всех существующих экземпляров объекта "CD". На практике контейнер выполнит следующий запрос:

 
  SELECT FROM CD; 
  

после этого будет создан экземпляр объекта CD для каждой найденной строки. После этого первичный ключ из каждой строки базы данных будет записан в атрибут соответствующего экземпляра CD. Теперь программа должна проверить каждый объект на соответствие необходимым критериям (т.е., слово "Chopin" в соответствующих атрибутах). Когда программа будет перебирать все объекты, сервер должен считать их атрибуты из таблицы; он не обязан был считывать атрибуты до этого момента потому что он пытался экономить память. Так что для каждого проверяемого объекта сервер сгенерирует SQL-код, похожий на этот:

 SELECT FROM CD WHERE ID=xxxx;
 

Предположим, что в системе зарегистрированно 200 CD. Вместо того чтобы выполнить один SQL-запрос чтобы получить список необходимых CD, механизм CMP выполнил более 200 SQL-запросов чтобы достичь того же эффекта. Мы не можем улучшить ситуацию используя методы findByTitle и findByNotes(), потому что эти методы осуществляют поиск записей полностью совпадающих условиям поиска.

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

 UPDATE CD SET artist="Bloggs" WHERE ID="200";
 

This is efficient, but requires the that "Artist" field really be called "artist". This makes it difficult to change the names of columns in the table. Alternatively the server could do a SELECT to get the current column values, delete the whole row, then insert a row with modified values. This allows a number of values to change at once and, because all values are written, it doesn't matter what the columns are called. This is the approach that JBoss uses. The problem is that if a class has ten persistent attributes, and they are altered one after the other, in the worst case this results in ten row deletions and ten row insertions.

Limitations of late initialization

Suppose we want to find whether a CD with a specific ID exists on the system. With CMP this corresponds to finding whether there is a row in the database table with the corresponding value of the "id" column. The code in Java might look like this:

// Get a reference to a CD Bean
Object ref  = jndiContext.lookup("cd/CD");

// Get a reference from this to the Bean's Home interface
CDHome home = (CDHome)
        PortableRemoteObject.narrow (ref, CDHome.class);

// Find the matching CD
CD cd = home.findByPrimaryKey("xxx");

What will happen if "XXX" is not the ID of a CD that exists? There would seem to be two sensible approaches. Either "findByPrimaryKey" could throw an exception, or perhaps it could return a null reference. In either case the client could easily tell whether the object exists. In practice, the EJB server may do neither of these things. It may well return a reference to a CD bean instance, which appears to be a perfectly valid object. However, none of the object's attributes will be initialized; initialization won't happen until the object is really required. This is done to improve efficiency; there is, after all, no need to initialize the object unless it will be needed. However, if the program continues to execute on the basis that "cd" refers to a valid object, an exception will be thrown later when the program tries to interact with it. This may not be a problem; if the ID had been generated from some earlier database access then we may be sure it really exists, and any failure to find it in the database represents a serious failure. However, if the data has come from the user, it is reasonable to expect some errors of typing or memory. Things can be made more predictable by always reading one of the attributes of an object after getting a reference to it, like this:

CD cd = home.findByPrimaryKey("xxx");
String dummy = cd.getId();

If there is no CD whose ID field is "XXX" then this will throw a java.rmi.NoSuchObjectException. This gets around the problem of late initialization, but at the cost of an additional SQL access.

Suitability of container-managed persistence

In many applications of object-oriented programming we have had to accept that some things that are philosophically objects are in reality implemented as something else. The "something else" may be a row of a database table, or a line of a text file, or whatever; at some point we had to code the interface between the object-oriented system and the "something elses". Entity JavaBeans goes some way towards eliminating this problem; things that are philosophically object can be modelled as objects, with methods and persistence. But this comes at a cost. It's worth asking whether the "CD" EJB in the tutorial example really is an object in a meaningful sense. It has attributes, but it doesn't do very much. We don't really gain all that much by making it an object; it could have remained a database row, and been manipulated through the "CDCollection" class. Of course this isn't as elegant, but elegance can come at a high price.

In summary then, container-managed persistence is straightforward to implement using JBoss (or any other EJB server, for that matter) but needs to be used quite carefully if serious inefficiencies are to be avoided.

Наши друзья