С точки зрения дизайна, было сделано то, что мы ввели сессионный компонент ShowMahager, являющийся простым façade (фасадом), который могут использовать клиенты для создания, удаления фильмов в системе, за которым располагаются наши entity bean'ы. Ясно, что сессионный компонент ShowManager предназначен для управления системой JavaTheater - предполагается, что обычные пользователи не будут изменять списочную информацию.
Теперь мы перейдем к созданию второго сессионного компонента, задачей которого является возвращение информации о фильме, но не предоставление метода для изменения списка фильмов. Этот компонент, к которому можно безопасно открыть доступ для всех пользователей, мы назовем ShowListing. Однако возвращаемая информация в распределенной системе может использовать слишком широкий канал, если она сделана неправильно. Определенно вам не нужно, например, создание клиентов, один из которых получает список названий фильмов, другой получает директоров картин и тому подобное. Вместо этого нам нужен клиент для получения всей необходимой информации о фильме в одном едином сетевом запросе.
Такой метод является тем, что мы называем групповой аксессор, метод, который получает доступ к групповой информации в едином обращении - информация может быть передана в компонент или возвращена из компонента. Этой массе информации необходима некоторая структуризация, но так как нет конструктора для структур в Java, групповой аксессор всегда работает в связке с объектами значения. Существуют простые, обычные интерфейсы Java, которые сериализуются и предоставляют публичные поля для чтения и записи значений - также называемые объектами значения или, в зависимости от автора, объектами данных или объект-посыльный.
Приведенный ниже код является примером объекта значения, предназначенного для передачи информации о фильме, который определен в своем собственном пакете, называемом javatheater.valueobject, и будет доступен и для клиентского кода и для кода компонент.
package javatheater.valueobject;
//:example09/src/ejb-tier/javatheater/valueobject/MovieItem.java
/**
* MovieItem является объектом данных, используемый для передачи
* структурированной информации о Movie между клиентом и ejb.
*/
public class MovieItem implements java.io.Serializable {
public Integer pk;
public String title;
public MovieItem(Integer pk, String title) {
this.pk = pk;
this.title = title;
}
}
// /:~
Код, как вы видите, очень прост: это просто публичный класс с парой публичных полей для информации о фильме. Сам по себе класс сериализуем и предоставляет соответствующий конструктор. Этот класс будет использоваться в реализации сессионного компонента ShowListing, код которого приведен ниже:
//:example10/src/ejb-tier/javatheater/ejb/implementation/MovieBean.java
package javatheater.ejb.implementation;
import javax.ejb.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/**
* Этот код реализации сущностного компонента <code>Movie</code>.
*
* @see javatheater.ejb.MovieHome
* @ejb:bean name="Movie" local-jndi-name="javatheater/Movie" view-type="local"
* type="CMP" primkey-field="id" reentrant="False"
* @ejb:pk class="java.lang.Integer"
* @ejb:home local-class="javatheater.ejb.MovieHome"
* @ejb:interface local-class="javatheater.ejb.Movie"
* @ejb:finder signature="java.util.Collection findByTitle(java.lang.String
* title)" query="SELECT OBJECT(m) FROM Movie m WHERE m.title=?1"
* @ejb:finder signature="java.util.Collection findAll()" query="SELECT
* OBJECT(m) FROM Movie m"
* @ejb:env-entry name="CounterName" value="MoviePKCounter"
* type="java.lang.String"
*/
public abstract class MovieBean implements EntityBean {
/**
* Проверяем первичный ключ и инициализируем параметры компонента.
*
* @ejb:create-method
*/
public Integer ejbCreate(Integer id, String title) throws CreateException {
if (id == null)
throw new CreateException("Primary key cannot be null");
if (id.intValue() == 0)
throw new CreateException("Primary key cannot be zero");
setId(id);
setTitle(title);
return null;
}
/**
* Ничего не делаем.
*/
public void ejbPostCreate(Integer id, String title) {
}
/**
* Домашний метод возвращает имя счетчика (String) для генерации уникального
* значения первичного ключа фильма. Реальное имя определяется как
* переменная окружения компонента Movie.
*
* @ejb:home-method
*/
public String ejbHomeGetCounterName() {
try {
Context initial = new InitialContext();
return (String) initial.lookup("java:comp/env/CounterName");
}
catch (NamingException e) {
throw new EJBException(e);
}
}
/**
* Возвращает Movie id, который также является первичным ключем.
*
* @ejb:pk-field
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public Integer getId();
/**
* Этот метод не является частью удаленного интерфейса но ejbdoclet будет
* все равно генерировать его реализацию. Этот методы вызывается из
* ejbCreate().
*/
abstract public void setId(Integer id);
/**
* Возвращает заголовок фильма.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public String getTitle();
/**
* Устанавливает заголовок фильма.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public void setTitle(String title);
}
// /:~
Как вы видите из тэгов EJBDoclet'а, это просто обычный сессионный компонент без состояний с удаленным клиентским представлением. Метод ejbCreate( ) не делает ничего специального, за исключением получения домашнего интерфейса entity компонента Movie, который будет использоваться позже в методе getMovie(…). getMovie(…) реализуется для: получения экземпляра Movie по значению первичного ключа, который передан в качестве аргумента; создания объекта MovieItem, заполняющегося информацией о фильме; и возвращения объекта MovieItem.
Я умышленно упростил этот код, но, тем не менее, он демонстрирует использование группового аксессора и объекта значений для получения структурированной информации в одном цельном RMI вызове. Помните, что вы точно также можете использовать групповой аксессор для передачи информации в компонент. В следующем примере мы будем реализовывать дополнительный групповой аксессор, предназначенный для получения информации по различным критериям.
И, наконец, пожалуйста, обратите внимание, что в этом примере мы реализовали групповой аксессор в сессионном компоненте, но вы можете сделать то же самое и для entity компонента. В обоих случаях вы можете пожелать, чтобы ваш компонент имел и обычный доступ, так чтобы ваш удаленный клиент не получал огромный, структурированный объект по сети в том случае, если вы интересуетесь только одним полем.
Поисковые методы и EJB-QL
Вы, конечно, уже заметили, что есть только один способ для клиентского кода найти существующий экземпляра entity компонента: вызвать метод findByPrimaryKey(…), который подразумевает, что клиент обязан знать значение первичного ключа определенного фильма прежде, чем он сможет получить его. Это, конечно же, неприемлемо: даже если клиентское приложение на своей стороне сети будет хранить список первичных ключей для всех записей, которые он создал (уродливый подход), как он может получить первичные ключи для всех записей, которые были созданы другими клиентами на этом же самом сервере? Или другой пример, как вы можете получить ссылки на все фильмы, название которых содержит слово "Java"?
Ответ содержится в настраиваемых методах поиска. Концептуально поиск не сильно отличается от выполнения SQL запроса на реляционной базе данных: вы указываете некоторые критерии для поиска (выражение WHERE в SQL запросе), место поиска (выражение FROM) и то, что вас интересует (выражение SELECT), и вы получаете назад список записей, возможно пустой. Для EJB вы получаете не записи - фактически, вы даже не знаете о существовании реляционной базы данных - а ссылки на компонентные интерфейсы сущностей, которые вы ищите.
Думаю, что здесь есть другой открытый вопрос: как вы укажите критерии для поиска ваших сущностей, если вы даже не знаете, какого рода хранилище данных используется за кадром? Какой синтаксис вы предполагаете использовать для указания вашего критерия поиска? В EJB 1.1 различные поставщики имели разные способы для предоставления контейнеру такого рода информации, но в EJB 2.0 процесс был стандартизован путем введения языка запросов EJB (EJB Query Language).
EJB-QL это простой язык, синтаксис которого похож на SQL, но полностью отличается тем, над чем он работает. В то время, как SQL запрос используется для поиска данных в таблице реляционной базе данных, EJB-QL используется для поиска свойств и постоянных объектов (сущностных компонентов), а эти объекты представляются абстрактной схемой. Различия между выполнением поиска в таблицах от поиска объектов это то, что вы должны держать в уме.
Еще раз, у меня нет здесь места для детального объяснения EJB-QL, но я дам вам простой пример, который должен помочь прояснить концепцию. За дополнительной информацией я отсылаю вас к документам, перечисленным в конце этой главы. Теперь давайте взглянем на код сущностного компонента Movie, и на новые специфические тэги EJBDoclet'а (Java код не изменился):
//:example10/src/ejb-tier/javatheater/ejb/implementation/MovieBean.java
package javatheater.ejb.implementation;
import javax.ejb.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/**
* Этот код реализации сущностного компонента <code>Movie</code>.
*
* @see javatheater.ejb.MovieHome
* @ejb:bean name="Movie" local-jndi-name="javatheater/Movie" view-type="local"
* type="CMP" primkey-field="id" reentrant="False"
* @ejb:pk class="java.lang.Integer"
* @ejb:home local-class="javatheater.ejb.MovieHome"
* @ejb:interface local-class="javatheater.ejb.Movie"
* @ejb:finder signature="java.util.Collection findByTitle(java.lang.String
* title)" query="SELECT OBJECT(m) FROM Movie m WHERE m.title=?1"
* @ejb:finder signature="java.util.Collection findAll()" query="SELECT
* OBJECT(m) FROM Movie m"
* @ejb:env-entry name="CounterName" value="MoviePKCounter"
* type="java.lang.String"
*/
public abstract class MovieBean implements EntityBean {
/**
* Проверяем первичный ключ и инициализируем параметры компонента.
*
* @ejb:create-method
*/
public Integer ejbCreate(Integer id, String title) throws CreateException {
if (id == null)
throw new CreateException("Primary key cannot be null");
if (id.intValue() == 0)
throw new CreateException("Primary key cannot be zero");
setId(id);
setTitle(title);
return null;
}
/**
* Ничего не делаем.
*/
public void ejbPostCreate(Integer id, String title) {
}
/**
* Домашний метод возвращает имя счетчика (String) для генерации уникального
* значения первичного ключа фильма. Реальное имя определяется как
* переменная окружения компонента Movie.
*
* @ejb:home-method
*/
public String ejbHomeGetCounterName() {
try {
Context initial = new InitialContext();
return (String) initial.lookup("java:comp/env/CounterName");
}
catch (NamingException e) {
throw new EJBException(e);
}
}
/**
* Возвращает Movie id, который также является первичным ключем.
*
* @ejb:pk-field
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public Integer getId();
/**
* Этот метод не является частью удаленного интерфейса но ejbdoclet будет
* все равно генерировать его реализацию. Этот методы вызывается из
* ejbCreate().
*/
abstract public void setId(Integer id);
/**
* Возвращает заголовок фильма.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public String getTitle();
/**
* Устанавливает заголовок фильма.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public void setTitle(String title);
}
// /:~
Мы использовали два новых тэга ejb:finder для EJBDoclet'а, чтобы он сгенерировал собственные методы поиска. Собственные поисковики являются методами домашнего интерфейса сущностного компонента, которые может использовать клиент для нахождения сущностей. До этого мы видели только один поисковик - это findByPrimaryKey(…), но EJB спецификация позволяет создать дополнительные поисковики, определяемые поставщиком компонента (bean provider). Только findByPrimaryKey(…) возвращает единственную ссылку на компонент; все остальные поисковики возвращают коллекцию Java - это имеет смысл, поскольку только первичный ключ может уникально идентифицировать одну сущность.
Причина, по которой мы реализуем различные поисковики состоит в том, что они позволят клиенту находить сущности, используя различные критерии. В случае компонента Movie, например, пользователь может пожелать найти фильмы по заголовку, по директору, по актерам, по жанру и так далее; по всем возможным критериям, большинству пользователей не желательно использование первичного ключа компонента, поисковик по которому мы имеем изначально.
Метод поиска предназначен для получения информации из постоянного хранилища. Так как EJB спецификация позволяет разные типы хранилищ данных, реализация поисковиков может значительно различаться. Если вы реализуете поисковик для сущностного компонента BMP, вы отвечаете за реализацию всех деталей при доступе к базе данных и информировании ее правильным образом (что, без сомнения, может быть сложным, в зависимости от природы вашего сущностного компонента и его взаимосвязей с другими компонентами). С другой стороны, если вы реализуете сущностный компонент CMP, вы не кодируете для одной определенной базы данных (помните, что CMP существует для повышения уровня портируемости), но вам все равно необходимо указать критерии поиска.
Хорошая новость состоит в том, что контейнер генерирует реализацию поисковых методов за вас. Все, что вам нужно сделать, это сказать контейнеру, где он должен искать и что. Этот вопрос подводит нас к основной теме абстрактной схемы сущностей и EJB-QL, соответственно. Абстрактная схема является простой концепцией: ваш сущностный компонент представляет собой постоянный объект, так? Это означает, что его состояние должно сохраняться в некотором постоянном хранилище, и состояние должно быть организовано в некотором формате (таблицы и записи, например). Однако, платформа EJB изначально разрабатывалась независимой от хранилища данных. Так что нам необходим, как и всегда, когда вещи становятся сложными, дополнительный уровень абстракции, который позволит нам ассоциировать состояние сущностного компонента с постоянной информацией, и этот уровень абстракции называется схемой абстракции. Абстрактная схема, в свою очередь определяет информацию, которая может быть запрошена с помощью EJB-QL.
На практике, каждый сущностный компонент CMP имеет ассоциированную схему имен, которую вы можете использовать в выражениях EJB-QL, а компонент выдает информацию через свои свойства, как определено в спецификации JavaBean'ов. Прежде, чем вы продолжите читать, вам необходимо собрать example10, поскольку мы должны взглянуть на код, сгенерированный EJBDoclet'ом - используйте Ant скрипт как и всегда для построения примера.
Прежде всего, давайте взглянем на домашний интерфейс для сущностного компонента Movie, который вы найдете в gen/javatheater/ejb/MovieHome.java в директории example10. Вы сразу же заметите, что два новых тэга ejb:finder в исходном коде реализации компонента стали причиной того, что EJBDoclet сгенерировал два новых поисковых метода, в домашнем интерфейсе Movie.
Теперь давайте взглянем на дескриптор развертывания, расположенный в каталоге gen/META-INF. Если вы посмотрите на элемент <entity>, который определяет сущностный компонент Movie, вы увидите такую строку:
Этот элемент ассоциирует сущностный компонент Movie с именем, которое вы можете использовать в запросах EJB-QL (более-менее похоже на то, если бы компонент был SQL таблицей). По умолчанию EJBDoclet использует имя компонента в качестве абстрактного схемного имени, но вы можете указать другое имя, если хотите. Поля, которые вы можете запрашивать из компонента Movie, являются частью постоянных параметров, предоставляемых компонентом Movie (и параметров, которые являются частью связей, но мы рассмотрим это позже).
Посмотрите расположенное ниже в дескрипторе развертки определение компонента Movie , вы также увидите код, сгенерированный EJBDoclet'ом:
<query> <query-method> <method-name>findByTitle</method-name> <method-params> <method-param>java.lang.String</method-param> </method-params> </query-method> <ejb-ql> <![CDATA[SELECT OBJECT(m) FROM Movie m WHERE m.title=?1]]> </ejb-ql> </query>
Здесь определяется запрос для поисковика findByTitle(…), сигнатуру которого мы определили в тэге ejb:finder, наряду с аргументами запроса и строкой запроса. Сама строка запроса вот:
SELECT OBJECT(m) FROM Movie m WHERE m.title=?1
Эта строка не сильно отличается от строки запроса SQL, и она означает, что вы хотите получить ("SELECT") все экземпляры компонентов ("OBJECT(m)") из абстрактной схемы типа "Movie", называемой "m" в данном запросе, при этом вы хотите только те экземпляры "m" ("WHERE"), свойство "title" которого равно "?1", что обозначает первый аргумент, передаваемый в поисковый метод (в данном случае это строка).
Другой поисковик в компоненте Movie, findAll( ), имеет очень похожий запрос, но без выражения WHERE, что означает, что вы хотите получить все экземпляры без какой-либо фильтрации.
Это почти все, что необходимо знать контейнеру для нахождения экземпляров Movie в соответствии с критериями, передаваемыми в запросе. Отсутствующая часть - это то, как отразить абстрактную схему данных на специфическую, конкретную базу данных. На этом спецификация останавливается, и это отображение оставляется на совесть поставщика контейнера. Другими словами, различные контейнеры предлагают разные пути и разные инструменты для отображения сущностных компонентов на базу данных - это работа Bean deployer'а. Многие контейнеры поставляются с некоторыми отображениями по умолчанию, к таким контейнерам относится и JBoss; поэтому мы можем не заботится, в этом примере, относительно отображения: мы используем базу данных по умолчанию и отображение по умолчанию, которые поставляются со стандартным пакетом установки JBoss. Если вы хотите что-то поднастроить по своим нуждам, вы должны обратиться к документации вашего контейнера.
Однако способность реализовать компонент и указать критерии поиска полностью независимым от базы данных способом является достижением. Польза заключается не только в том, что вам не нужно писать какой-то код, специфичный для базы данных, но и в том, что вы имеете портируемость, плюс тот факт, что контейнер может оптимизировать ваш запрос лучше, чем это сделаете вы, поскольку он обладает внутренним знанием и интеграцией с определенной базой данных.
Теперь, когда мы имеем пару дополнительных поисковиков, мы можем использовать их для реализации дополнительной функциональности в сессионном компоненте типа façade, особенно по причине того, что наш сущностный компонент более не доступен клиенту напрямую. Следующий далее код реализации сессионного компонента ShowListing имеет два дополнительных метода, которые клиенты могут использовать для получения фильмов по их заголовку или для получения всех фильмов:
//:example10/src/ejb-tier/javatheater/ejb/implementation/ShowListingBean.java
package javatheater.ejb.implementation;
import javatheater.ejb.*;
import javatheater.valueobject.MovieItem;
import javax.ejb.*;
import javax.naming.*;
import java.util.Collection;
import java.util.Iterator;
/**
* @ejb:bean type = "Stateless" name = "ShowListing" jndi-name =
* "javatheater/ShowListing" reentrant = "false"
* @ejb:home remote-class="javatheater.ejb.ShowListingHome"
* @ejb:interface remote-class="javatheater.ejb.ShowListing"
*/
public abstract class ShowListingBean implements SessionBean {
MovieHome movieHome = null;
/**
* @ejb:create-method
*/
public void ejbCreate() throws CreateException {
try {
movieHome = (MovieHome) new InitialContext()
.lookup("javatheater/Movie");
}
catch (NamingException e) {
throw new EJBException(e);
}
}
/**
* @ejb:interface-method
*/
public MovieItem getMovie(Integer moviePK) {
try {
Movie movie = movieHome.findByPrimaryKey(moviePK);
return new MovieItem(movie.getId(), movie.getTitle());
}
catch (FinderException e) {
return null;
}
}
/**
* @ejb:interface-method
*/
public MovieItem[] getMovie(String title) {
try {
Collection foundMovies = movieHome.findByTitle(title);
MovieItem[] items = new MovieItem[foundMovies.size()];
int index = 0;
Iterator movies = foundMovies.iterator();
while (movies.hasNext()) {
Movie movie = (Movie) movies.next();
items[index++] = new MovieItem(movie.getId(), movie.getTitle());
}
return items;
}
catch (FinderException e) {
throw new EJBException(e);
}
}
/**
* @ejb:interface-method
*/
public MovieItem[] getMovies() {
try {
Collection foundMovies = movieHome.findAll();
MovieItem[] items = new MovieItem[foundMovies.size()];
int index = 0;
Iterator movies = foundMovies.iterator();
while (movies.hasNext()) {
Movie movie = (Movie) movies.next();
items[index++] = new MovieItem(movie.getId(), movie.getTitle());
}
return items;
}
catch (FinderException e) {
throw new EJBException(e);
}
}
}
// /:~
Есть еще много чего сказать о поисковиках и о EJB-QL (например, уровень доступа поисковиков или как выполнять более сложные запросы), но это только введение в EJB, мы не можем вдаваться во все детали. Однако вам было продемонстрирована сущность собственных поисковиков и EJB-QL.
В качестве последней ремарки укажу, что очень похожими на поисковики и EJB-QL являются методы выбора. Метод выбора очень похож на поисковый и он определяется аналогичным образом при использовании запроса. Однако есть два главных отличия между поисковым методом и методом выбора: 1) поисковик всегда возвращает ссылку на компонентный интерфейс сущностного компонента, в то время как метод выбора может возвращать любой тип постоянных свойств, имеющихся у компонента, и 2) методы выбора никогда не доступны клиенту, независимо от типа реализованного клиентского представления. Так, например, вы можете иметь метод выбора, который возвращает все названия фильмов (строки), в то время, как метод поиска должен возвращать фильмы. Методы выбора не предназначены для клиентов и могут быть использованы только для реализации части внутренней логики вашего сущностного компонента.
Взаимосвязь компонентов
Последний раздел, который мы рассмотрим здесь, это, возможно, наиболее значительное улучшение, которое появилось в EJB 2.0, наряду с локальными интерфейсами. Эта способность, которая позволяет разработчикам компонентов устанавливать взаимосвязи между сущностными компонентами путем описания их в дескрипторе развертывания. Позвольте мне объяснить.
До сих пор мы реализовывали сущностные компоненты, такие как Movie и AutoCounter, которые были полностью самодостаточными. То есть, они содержали внутри всю информацию, которая им необходима, чтобы быть целостным блоком информации. Например, сущностный компонент Movie содержал всю информацию о фильме. Однако, к сожалению, в реальном мире приложения состоят не только из таких простых объектов. Даже наше приложение JavaTheater, как только мы начнем добавлять такие компоненты, как показ, станет гораздо сложнее.
Показ, фактически, формируется из фильма, времени показа и кинотеатра. Опираясь на это описание, безопасно сказать, что показ может иметь только один фильм, но один и тот же фильм может быть в разных показах - классическое отношение один ко многим. С точки зрения объектного подхода, реализация такого отношения тривиальна: объект показа содержит указатель на объект фильма, а объект фильма содержит коллекцию ссылок на объекты показа.
Проблема, на мой взгляд, состоит в том, что наши объекты являются постоянными сущностями, так что ссылки, представленные взаимосвязями, должны быть трансформированы во что-то такое, что может храниться в базе данных, а позднее быть получено и трансформировано обратно в ссылку на сущность, которую она изначально представляла. Этот процесс гораздо сложнее, чем это кажется, и EJB 1.1 предоставлял небольшую помощь разработчику; но EJB 2.0 покрыла эту недостаточность, и ввела концепцию взаимосвязей, управляемых контейнером. Благодаря этой возможности, разработчик может указать в дескрипторе развертывания, как связаны два сущностных компонента (в нашем случае это Movie и Show), какие поля участвуют в связях, какие множатся. Здесь указывается, сколько сущностей существует на каждой стороне такой связи.
Как всегда, мы будем использовать практический пример для прояснения концепции, но прежде чем вы построите и развернете код, я рекомендую, чтобы вы полностью очистили базу данных, которую вы использовали в предыдущих примерах. Самый простой способ сделать это, просто удалите все файлы, которые хранят ваши данные (и не беспокойтесь, они будут восстановлены заново, когда вы перезапустите JBoss). Выгрузите JBoss а затем, в каталоге установки JBoss, найдите каталог server/default/db/hypersonic (пожалуйста, обратите внимание, что если вы используете версию JBoss отличную от 3.04, структура директорий может значительно отличаться). Этот директорий содержит все файлы данных HypersonicSQL, имена которых имеют различные расширения. Просто удалите их все (или перенесите их в другой директорий, если хотите их сохранить) и перезапустите JBoss. Проверьте, что файлы снова восстановлены, а затем продолжите упражнение.
Далее мы займемся тем, что начнем реализовывать новый сущностный CMP компонент, который представляет показ. Показ получает доступ к одному фильму, а фильм получает доступ к нескольким показам. Ниже вы можете видеть реализацию нового сущностного компонента для нашего приложения, компонента Show, который вы найдете в директории example11. В нашем дизайне Show состоит из Фильма, времени показа (строка) и количества посадочных мест (целое). Я умышленно оставил театр за пределами рассмотрения для простоты.
//:example11/src/ejb-tier/javatheater/ejb/implementation/ShowBean.java
package javatheater.ejb.implementation;
import javax.ejb.*;
import javax.naming.*;
import javatheater.ejb.*;
/**
* Это код реализации сущностного компонента <code>Show</code> Показ имеет
* идентификатор id (который также является первичным ключом), фильм, время
* показа и число посадочных мест.
*
* @see javatheater.ejb.ShowHome
* @see javatheater.ejb.Show
* @ejb:bean name="Show" local-jndi-name="javatheater/Show" view-type="local"
* type="CMP" primkey-field="id" reentrant="False"
* @ejb:pk class="java.lang.Integer"
* @ejb:home local-class="javatheater.ejb.ShowHome"
* @ejb:interface local-class="javatheater.ejb.Show"
* @ejb:finder signature="Collection findByMovie(java.lang.Integer moviePK)"
* query="SELECT OBJECT(s) FROM Show s WHERE s.movie.id=?1"
* @ejb:finder signature="java.util.Collection findAll()" query="SELECT
* OBJECT(s) FROM Show s"
* @ejb:env-entry name="CounterName" value="ShowPKCounter"
* type="java.lang.String"
*/
public abstract class ShowBean implements EntityBean {
/**
* Проверка первичного ключа и инициализация параметров компонента.
*
* @ejb:create-method
*/
public Integer ejbCreate(Integer id, Movie movie, String showTime,
int availableSeats) throws CreateException {
if (id == null)
throw new CreateException("Id can not be null");
if (id.intValue() == 0)
throw new CreateException("Id can not be zero");
if (movie == null)
throw new CreateException("A movie must be provided");
setId(id);
setShowTime(showTime);
setAvailableSeats(availableSeats);
// Обратите внимание, что поля взаимосвязей не могут быть
// модифицированны
// или установлены в ejbCreate(). Так что мы инициализируем
// поля фильма в методе ejbPostCreate() (ниже).
return null;
}
public void ejbPostCreate(Integer id, Movie movie, String showTime,
int availableSeats) {
setMovie(movie);
}
/**
* Домашний метод возвращает имя счетчика (строка) для генерации уникального
* первичного ключа для Show. Реальное имя определяется, как переменная
* окружения для компонента Show.
*
* @ejb:home-method
*/
public String ejbHomeGetCounterName() {
try {
Context initial = new InitialContext();
return (String) initial.lookup("java:comp/env/CounterName");
}
catch (NamingException e) {
throw new EJBException(e);
}
}
/**
* Возвращается Show id, который также является первичным ключом для показа.
*
* @ejb:pk-field
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public Integer getId();
/**
* @ejb:persistent-field
*/
abstract public void setId(Integer id);
/**
* Устанавливает фильм из этого показа. Обратите внимание, что это не CMP
* поле, а поле Взаимосвязей, Управляемое Контенером (Container Managed
* Releationship (CMR)) (смотрите сгенерированный дескриптор развертывания).
*
* @ejb:interface-method
* @ejb:relation name="Show-Movie" role-name="Show-has-a-Movie"
*/
abstract public void setMovie(Movie movie);
/**
* Получает фильм из этого показа. Обратите вниманиен, что параметр фильма -
* это поле CMR.
*
* @see #setMovie(Movie)
* @ejb:interface-method
*/
abstract public Movie getMovie();
/**
* Возвращает время показа.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public String getShowTime();
/**
* Устанавливает время показа.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public void setShowTime(String showTime);
/**
* Возвращает доступное количество мест.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public int getAvailableSeats();
/**
* Устанавливает доступное количество мест.
*
* @ejb:persistent-field
* @ejb:interface-method
*/
abstract public void setAvailableSeats(int availableSeats);
} // /:~
В первой части кода нет ничего нового или странного. Это простой сущностный компонент CMP с локальным представлением и парой методов поиска. Метод ejbCreate(…) принимает всю информацию, которая необходима для создания нового просмотра: первичный ключ, ссылку на Фильм, время показа и количество посадочных мест. Вы, наверное, ожидали, что вся эта информация будет сохранена в свойствах, но здесь есть кое-что новое: ссылка на Movie не устанавливается в ejbCreate(…), а вместо этого устанавливается в ejbPostCreate(…). Это происходит потому, что поле movie компонента Show представляет собой постоянные связи, так что оно зависит от правильных первичных ключей обоих задействованных партнеров: Show и Movie; а как вы знаете, вы не можете полагаться на значение первичного ключа компонента в процессе нахождения в методе ejbCreate(…). Оставшаяся часть кода просто определяет поля CMP компонента Show, за исключением поля movie, которое является чем-то новым для нас.
Поле movie в компоненте Show не является CMP полем, а является полем взаимосвязи, управляемое Контейнером (Container Managed Relationship (CMR)), оно обрабатывается контейнером не так, как другие поля. Оно требует отличного описания в дескрипторе развертывания, от CMP полей, так что EJBDoclet работает с тэгами, специально предназначенными для описания взаимосвязей (ejb:relation).
Прежде чем объяснить, что означает этот тэг и информация в дескрипторе развертывания, нам необходимо понять, как моделируется взаимосвязь сущностей в EJB. Прежде всего, взаимосвязь сущностей сделана, конечно, для двух сущностных компонентов; второе, только определенные поля каждого компонента участвуют в связи; третье, каждая сторона имеет величину множества (например, фильм имеет много показов, но показ имеет только один фильм). Вся эта информация должна находится в дескрипторе развертки, но это не все: так как обычно существует несколько взаимосвязей, управляемых контейнером, они должны быть поименованы для контейнера и (особенно) для развертывателя компонента (Bean Deployer).
Когда вы используете EJBDoclet, все что вам нужно сделать - это указать, что поле представляет взаимосвязь, управляемую контейнером, и вы должны сделать это для обеих составляющих пары, вовлеченных во взаимосвязь. В зависимости от типа полей, описывающих взаимосвязь (единственная ссылка на сущность или коллекция) EJBDoclet сгенерирует информацию в дескрипторе развертывания с правильной величиной множества. Однако, вы должны указать имя взаимосвязи и имя роли. Вот что в точности делает вырезанный код из приведенного выше примера (я только удалил прозу из комментариев):
/**
* @ejb:interface-method
* @ejb:relation name="Show-Movie" role-name="Show-has-a-Movie"
*/
abstract public void setMovie(Movie movie);
В этом коде мы сообщаем, что хотим создать поле movie в сущностном компоненте Show (определяется по имени метода-сеттера), которое будет являться взаимосвязью, управляемой контейнером. Мы также сообщаем, что эта взаимосвязь называется "Show-Movie" и что имя роли в этой взаимосвязи будет "Show-has-a-Movie". Эти имена могут быть использованы, например, для ассоциации уровня доступа по безопасности с ролью. Мощность множества определяется типом поля, который EJBDoclet может получить из сигнатуры сеттера. В данном случае тип является "ссылкой на Movie", так что EJBDoclet знает, что на этой стороне взаимосвязи мощность множества равна "единице".
Взаимосвязь существует только между двумя партнерами, и другой участник - это, конечно, компонент Movie. Ниже вы можете увидеть новый код, который будет добавлен в сущностный компонент Movie из example11:
/**
* @ejb:interface-method
* @ejb:relation name="Show-Movie" role-name="Movie-has-many-Shows"
*/
public abstract Collection getShows();
/**
* @ejb:interface-method
*/
public abstract void setShows(Collection shows);
Здесь мы определяем новое поле для компонента Movie с названием show. Это поле является коллекцией, так как один фильм может участвовать во многих показах. Поле участвует в той же самой взаимосвязи ("Show-Movie"), которую мы описали для компонента Show, но здесь определяем другую роль ("Movie-has-many-Shows").
Основываясь на имени взаимосвязи, которое вы указали для обоих компонентов, и на типах полей, вовлеченных во взаимосвязь (единственный объект или коллекция), EJBDoclet генерирует следующую информацию в дескрипторе развертывания:
<relationships> <ejb-relation> <ejb-relation-name> Show-Movie </ejb-relation-name> <ejb-relationship-role> <ejb-relationship-role-name> Movie-has-many-Shows </ejb-relationship-role-name> <multiplicity>One</multiplicity> <relationship-role-source> <ejb-name>Movie</ejb-name> </relationship-role-source> <cmr-field> <cmr-field-name> shows </cmr-field-name> <cmr-field-type> java.util.Collection </cmr-field-type> </cmr-field> </ejb-relationship-role> <ejb-relationship-role> <ejb-relationship-role-name> Show-has-a-Movie </ejb-relationship-role-name> <multiplicity>Many</multiplicity> <relationship-role-source> <ejb-name>Show</ejb-name> </relationship-role-source> <cmr-field> <cmr-field-name> movie </cmr-field-name> </cmr-field> </ejb-relationship-role> </ejb-relation> </relationships>
Это новый раздел дескриптора развертывания, который не входит в круг знакомых элементов <enterprise-beans>, виденных вами до этого, и он служит для перечисления всех взаимосвязей, управляемых контейнером, в вашем приложении. В нашем случае есть только одна взаимосвязь, имя которой "Show-Movie". Две роли во взаимосвязи описываются своими собственными соответствующими элементами, наряду с мощностью множества, именем и типом вовлеченного поля.
Используя эту информацию, контейнер будет заботиться о постоянстве взаимосвязей в вашей базе данных, и он также будет генерировать реализацию сеттеров и геттеров для CMR поля вашего компонента. Как реально взаимосвязь отражается на базу данных, зависит от комбинации нескольких факторов: реализации контейнера, базы данных, которую вы используете, интеграции между этими двумя элементами и отображения, которое вы предоставили (также специфичное для контейнера). В нашем случае, при использовании JBoss с его базой данных по умолчанию (HypersonicSQL - простейшая база данных) и отображением по умолчанию, контейнер генерирует дополнительное поле в таблице, ассоциированное с сущностным компонентом Show, и в этом поле хранит первичный ключ фильма, ассоциированный с этим показом. В других таблицах нет дополнительных полей, и в этом случае нет ассоциированных таблиц, так как взаимосвязь между фильмом и показом достаточно проста, чтобы быть смоделированной таким образом. Когда вы вызываете метод getMovie( ) для экземпляра используемого вами Show, сгенерированная реализация метода выполняет SQL запрос для таблицы Show, который ищет записи с тем же самым первичным ключом, который имеет используемый вами экземпляр Show, и возвращает первичный ключ Movie; первичный ключ Movie используется затем, чтобы создать в памяти экземпляр Movie, который инициализируется информацией о фильме, и в конце концов эта ссылка возвращается клиенту. Если вы вызовите метод getShows( ) для экземпляра Movie, выполняется другой запрос, который получает все первичные ключи Show, которые содержат тот же первичный ключ Movie, что и экземпляр, для которого вы вызвали метод.
Все эти сложности, конечно же, спрятаны от вас контейнером, плюс к этому, при использовании EJBDoclet'а вам даже ненужно беспокоится о содержимом дескриптора развертывания. Все, что вам нужно сделать - это указать абстрактный метод для обеих сторон взаимосвязи (обоих вовлеченных компонентов) и использовать соответствующие тэги EJBDoclet'а. Теперь мы можем использовать новый метод, имеющийся в компонентах Movie и Show, и проверить, что взаимосвязь работает, как это и ожидается. Так как оба компонента не имеют удаленного представления, и не могут использоваться клиентом напрямую, я добавил несколько методов в наш façade компонент. Я также ввел новый объект-значение, ShowItem, который мы используем для передачи структурированной информации о показе между уровнями. Вот код нового класса ShowItem:
//:example11/src/ejb-tier/javatheater/valueobject/ShowItem.java
package javatheater.valueobject;
import java.io.Serializable;
/**
* ShowItem является объектом-значением, используемом для передачи информации
* между уровнем клиента и уровнем ejb.
*/
public class ShowItem implements Serializable {
public Integer pk;
public MovieItem movie;
public String showTime;
public int availableSeats;
public ShowItem(Integer pk, MovieItem movie, String showTime,
int availableSeats) {
this.pk = pk;
this.movie = movie;
this.showTime = showTime;
this.availableSeats = availableSeats;
}
public String toString() {
return "[" + pk.toString() + "] " + movie.title + ", " + showTime
+ ", " + availableSeats;
}
} // /:~
Концептуально этот класс не сильно отличается от класса MovieItem. Отличие заметно только в одном публичном поле, являющемся ссылкой на объект MovieItem, вместо присутствия простого типа данных или класса. То, что вы здесь видите, происходит при наличии взаимосвязей между компонентами, объекты-значения также должны отражать эту связь, и могут быть структурированы, как web объекты, а не как набор простых типов данных. Иногда это называется "простейшей агрегацией".
Ниже приведена реализация сессионного компонента без состояний ShowManager, который может использоваться для изменения содержимого каталога. Он отличается от предыдущего примера тем, что поддерживает новый сущностный компонент Show.
//:example11/src/ejb-tier/javatheater/ejb/implementation/ShowManagerBean.java
package javatheater.ejb.implementation;
import javatheater.ejb.*;
import javax.ejb.*;
import javax.naming.*;
import java.rmi.RemoteException;
/**
* @ejb:bean type = "Stateless" name = "ShowManager" jndi-name =
* "javatheater/ShowManager" reentrant = "false"
* @ejb:home remote-class="javatheater.ejb.ShowManagerHome"
* @ejb:interface remote-class="javatheater.ejb.ShowManager"
*/
public abstract class ShowManagerBean implements SessionBean {
AutoCounterHome autoCounterHome = null;
MovieHome movieHome = null;
ShowHome showHome = null;
String movieCounterName = null;
String showCounterName = null;
/**
* @ejb:create-method
*/
public void ejbCreate() throws CreateException {
try {
Context initial = new InitialContext();
// Получает домашний интерйес для EJB, который мы будем использовать
// позже
autoCounterHome = (AutoCounterHome) initial
.lookup("javatheater/AutoCounter");
movieHome = (MovieHome) initial.lookup("javatheater/Movie");
showHome = (ShowHome) initial.lookup("javatheater/Show");
movieCounterName = movieHome.getCounterName();
showCounterName = showHome.getCounterName();
}
catch (NamingException e) {
throw new EJBException(e);
}
}
/**
* Создаем новый фильм и возвращаем значение первичного ключа для фильма.
*
* @ejb:interface-method
*/
public Integer createMovie(String movieName) throws TheaterException {
try {
Integer pk = new Integer(getCounter(movieCounterName).getNext());
movieHome.create(pk, movieName);
return pk;
}
catch (CreateException e) {
throw new TheaterException(e);
}
catch (RemoteException e) {
throw new EJBException(e);
}
}
/**
* @ejb:interface-method
*/
public Integer createShow(Integer moviePK, String showtime, int seats)
throws TheaterException {
try {
Movie movie = movieHome.findByPrimaryKey(moviePK);
Integer showPK = new Integer(getCounter(showCounterName).getNext());
showHome.create(showPK, movie, showtime, seats);
return showPK;
}
catch (FinderException e) {
throw new TheaterException(e);
}
catch (CreateException e) {
throw new TheaterException(e);
}
catch (RemoteException e) {
throw new EJBException(e);
}
}
/**
* Удаляет фильм по заданному значению первичного ключа.
*
* @ejb:interface-method
*/
public void deleteMovie(Integer moviePk) throws TheaterException {
try {
Movie movie = movieHome.findByPrimaryKey(moviePk);
if (movie.getShows().size() != 0)
throw new TheaterException(
"Can't delete movie: it appears in one or more shows");
movie.remove();
}
catch (FinderException e) {
throw new TheaterException(e);
}
catch (RemoveException e) {
throw new TheaterException(e);
}
}
/**
* Удаляет показ по заданному первичному ключу.
*
* @ejb:interface-method
*/
public void deleteShow(Integer showPK) throws TheaterException {
try {
Show show = showHome.findByPrimaryKey(showPK);
show.remove();
}
catch (FinderException e) {
throw new TheaterException(e);
}
catch (RemoveException e) {
throw new TheaterException(e);
}
}
/**
* Вспомогательный метод для получения генератора первичного ключа для
* компонента Movie.
*/
AutoCounter getCounter(String counterName) throws RemoteException,
CreateException {
AutoCounter counter = null;
try {
counter = autoCounterHome.findByPrimaryKey(counterName);
}
catch (FinderException e) {
}
if (counter == null)
counter = autoCounterHome.create(counterName);
return counter;
}
}
// /:~
Эта новая реализация содержит два метода для создания и удаления показов в системе. Реализация этих методов достаточно проста и не содержит ничего удивительного для вас. Более интересным является изменение реализации существующего до этого метода deleteMovie(…), поскольку это изменение явно выделено. Как вы можете видеть из кода, в этот метод была добавлена проверка, что фильм, который вы хотите удалить, не участвует в показах. Это имеет смысл: если оставить показ, но удалить фильм, это не сделает аудиторию счастливой. Это классический пример нарушения ссылочной целостности системы. Другими словами, попытка удаления сущности фильма, являющегося частью взаимосвязи с показом, может привести к несостоятельности данных в системе.
Вы видите в методе deleteMovie(…) одно из возможных решений этой проблемы: реализация правил ссылочной целостности вашей системы в façade-компоненте (компоненте-фасаде). Это не единственный возможный подход, особенно, если ссылочная целостность реализуется в базе данных, как об этом знают многие DB администраторы. Это особый случай, когда вы помещаете ваш новый сущностный компонент поверх существующей базы данных, которая также может использовать хранимые процедуры для реализации определенных бизнес-функций. В таком случае, ваши сущностные и сессионные компоненты могут получить развитие логики в базе данных, при этом не потребуется дополнительного Java кода в компоненте для слежения за ссылочной целостностью, за исключением того кода, который перехватывает исключения, сгенерированные базой данных при срабатывании ограничительных правил.
Последними изменениями в EJB из example11 заключаются в добавлении в сессионный компонент ShowListing метода получения всех существующих показов. Вот его код:
//:example11/src/ejb-tier/javatheater/ejb/implementation/ShowListingBean.java
package javatheater.ejb.implementation;
import javatheater.ejb.*;
import javatheater.valueobject.*;
import javax.ejb.*;
import javax.naming.*;
import java.util.Collection;
import java.util.Iterator;
/**
* @ejb:bean type = "Stateless" name = "ShowListing" jndi-name =
* "javatheater/ShowListing" reentrant = "false"
* @ejb:home remote-class="javatheater.ejb.ShowListingHome"
* @ejb:interface remote-class="javatheater.ejb.ShowListing"
*/
public abstract class ShowListingBean implements SessionBean {
MovieHome movieHome = null;
ShowHome showHome = null;
/**
* @ejb:create-method
*/
public void ejbCreate() throws CreateException {
try {
Context initialContext = new InitialContext();
movieHome = (MovieHome) initialContext.lookup("javatheater/Movie");
showHome = (ShowHome) initialContext.lookup("javatheater/Show");
}
catch (NamingException e) {
throw new EJBException(e);
}
}
/**
* @ejb:interface-method
*/
public MovieItem getMovie(Integer moviePK) {
try {
Movie movie = movieHome.findByPrimaryKey(moviePK);
return new MovieItem(movie.getId(), movie.getTitle());
}
catch (FinderException e) {
return null;
}
}
/**
* @ejb:interface-method
*/
public MovieItem[] getMovie(String title) {
try {
Collection foundMovies = movieHome.findByTitle(title);
MovieItem[] items = new MovieItem[foundMovies.size()];
int index = 0;
Iterator movies = foundMovies.iterator();
while (movies.hasNext()) {
Movie movie = (Movie) movies.next();
items[index++] = new MovieItem(movie.getId(), movie.getTitle());
}
return items;
}
catch (FinderException e) {
throw new EJBException(e);
}
}
/**
* @ejb:interface-method
*/
public MovieItem[] getMovies() {
try {
Collection foundMovies = movieHome.findAll();
MovieItem[] items = new MovieItem[foundMovies.size()];
int index = 0;
Iterator movies = foundMovies.iterator();
while (movies.hasNext()) {
Movie movie = (Movie) movies.next();
items[index++] = new MovieItem(movie.getId(), movie.getTitle());
}
return items;
}
catch (FinderException e) {
throw new EJBException(e);
}
}
/**
* @ejb:interface-method
*/
public ShowItem[] getShows() {
try {
Collection foundShows = showHome.findAll();
ShowItem[] items = new ShowItem[foundShows.size()];
int index = 0;
Iterator shows = foundShows.iterator();
while (shows.hasNext()) {
Show show = (Show) shows.next();
Movie movie = show.getMovie();
items[index++] = new ShowItem(show.getId(), new MovieItem(movie
.getId(), movie.getTitle()), show.getShowTime(), show
.getAvailableSeats());
}
return items;
}
catch (FinderException e) {
throw new EJBException(e);
}
}
}
// /:~
Метод использует поисковик findAll(…) из домашнего интерфейса для получения всех показов, а для каждого показа он создает объект ShowItem, который хранится в возвращаемом массиве. Также, поскольку ShowItem содержит MovieItem, для каждого показа метод запрашивает ассоциированные фильмы с помощью CMP поля фильма (вызывается метод getMovie( )) и создается MovieItem по полученной информации.
Других модификаций кода EJB в example11 нет. Однако, клиентское приложение должно быть полностью переписано, и теперь оно должно быть сопряжено с графическим пользовательским интерфейсом, который вы сможете использовать для создания и удаления фильмов и показов в системе. Хотя способности клиентского приложения минимальны, вы можете пожелать поиграть с ним или, может быть, использовать его, как исходную точку для реализации дополнительных возможностей, таких как поиск показа определенного фильма, или поиск показа по определенному времени показа. Это также потребует, чтобы вы добавили больше возможностей в façade сессионный компонент и, возможно, в сущностный компонент, но это будет хорошим упражнением.
Заключение
То, что мы имеем здесь, это взгляд с высоты птичьего полета на Enterprise JavaBean, и многое осталось за пределами этой картины. Мы не рассмотрели некоторые главные темы, такие как безопасность, транзакции и message-driven beans. Мы также не рассмотрели сессионные компоненты с использованием состояний и постоянство, управляемое компонентом. Однако, теперь у вас есть концепция для понимания того, что такое EJB, и вы также имеете некоторые технические инструменты и навыки, чтобы продолжить исследования самостоятельно.
Enterprise JavaBeans является главной, часто упротребляемой Java технологией, работающей на стороне сервера, и дающей наибольшие преимущества при совмещении с другими J2EE технологиями, такими как Сервлеты и JavaServer Pages (JSP), чтобы предоставлять интерфейс к вашей EJB бизнес-модели через web уровень, а web службы выставляют ваш бизнес-объект через HTTP. Когда вы начинаете построение системы, употребляя эти технологии, необходимо руководство, и то, что вам нужно - это дизайн шаблонов J2EE, очаровательная тема, которую я рекомендую вам исследовать.
Если вы продолжите движение по дороге создания Enterprise JavaBeans, перечисленные ниже документы составят вам хорошую компанию.
Ресурсы
Здесь перечислена рекомендованная литература для получения исчерпывающей информации относительно Enterprise JavaBeans и сопутствующих объектов. Если документация доступна в онлайне, приведен URL.
- Enterprise
Java Beans ™ Specification
(Sun Microsystems, Inc.)
http://java.sun.com/j2ee - The J2EE ™ Tutorial
(Sun Microsystems, Inc.)
http://java.sun.com/j2ee - Java ™ Blueprints
(Sun Microsystems, Inc.)
http://java.sun.com/blueprints - Richard
Monson-Haefel, Enterprise Java Beans 3rd ed.
(O’Reilly and Associates, Inc., 2001) - Deepak Alur,
John Crupi, Dan Malks, Core J2EE Patterns
(Sun Microsystems Press, 2001) - Floyd
Marinescu, EJB ™ Design Patterns
(John Wiley & Sons, Inc., 2002)
http://theserverside.com
← | Ваш первый Enterprise JavaBean | XML | → |