EJB Advocate: Часть 2. Создание производительных EJB-компонентов управления данными

Как обсуждалось в статье прошлого месяца, использование некорректно разработанных EJB™-компонентов может привести к серьезным проблемам производительности при тестировании системы, или (что еще хуже) во время эксплуатации. В данной статье EJB Advocate показывает, как использовать CMR для получения преимуществ от использования нескольких связанных CMP в единице работы.

Джефф Хэмбрик, инженер, IBM

Джефф Хэмбрик (Geoff Hambrick) является ведущим консультантом IBM Software Services для группы обеспечения WebSphere (WebSphere Enablement Team). Он живет в Раунд Рок, Техас (недалеко от Остина). Общей задачей группы обеспечения является поддержка предпродажных процессов, которая осуществляется с помощью подробного технического инструктажа и кратковременных совещаний по проверке концепций. За свою работу по созданию и распространению передового опыта для разработки приложений J2EE на базе IBM WebSphere Application Server в марте 2004 года Джефф был удостоен звания инженера с отличием IBM (IBM Distinguished Engineer).



11.04.2008

Из IBM WebSphere Developer Technical Journal.

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

Проблема

В прошлой статье я упомянул, что на этот раз мы попытаемся поговорить о Service Data Objects, но по последней статье о производительности пришел вопрос, решить который намного важнее.

Уважаемый EJB Advocate!

Я прочитал Вашу последнюю статью и, также как и No Longer a Fan, смягчил свою позицию насчет EJB-компонента управления данными CMP. Я разработал типичный пример использования из нашего приложения, модель данных в котором выглядит так:

Рисунок 1. Модель данных
Рисунок 1. Модель данных

Я создал CMP для каждого объекта в приведенной выше диаграмме классов с объектами Key и Data Transfer Object (DTO), возвращаемыми специальным методом, именно так, как Вы предложили, для того чтобы минимизировать "болтливость" между уровнями. DTO, отображающий взаимосвязь, имеет свойство, которое ссылается либо на целевой DTO (если количество элементов не больше одного), либо на массив элементов (если количество элементов больше одного). Все эти DTO объявляются экспортируемыми (externalizable) для ускорения времени маршаллизации, и я даже создал некоторые специальные обнаружители для облегчения доступа к CMP без постоянного создания объектов первичного ключа. Затем я использовал локальный интерфейс к ним внутри фасада сессии, чтобы гарантировать выполнение только одной транзакции. Сессионный компонент имел метод, который предполагал, что локальные home-методы соответствующих CMP были извлечены в методе ejbCreate() и установлены как переменные экземпляра. Рассматриваемый метод выглядел так:

public CustomerData  getOpenOrderForCustomer(int cId) {
	// Получить DTO Customer
	Customer cRef = cHome.findByCustomerId(cId);
	CustomerData cData = cRef.getData();

	// Проверить, есть ли открытый заказ
	int oId = cData.getOpenOrderId();
	if (oId == 0) {
		throw new OrderNotOpenException(cId);
	}

	// Получить логический объект Order и DTO-объект	
	Order oRef = oHome.findByOrderId(oId);
	OrderData oData = oRef.getData();
	cData.setOrder(oData);
	
	// Настроить массив DTO-объектов Line Items
	Collection liList = liHome.findAllItemsForOrderId(oId);
	int liSize = liList.size();
	LineItemData[] liArray = new LineItemData[liSize];
	oData.setLineItems(liArray);
	Iterator liIterator = liList.iterator();
	LineItem liRef = null;
	LineItemData liData = null;
	Product pRef = null;
	for (int i = 0; i < liSize; i++) {
		// Получить DTO Line Item
		liRef = (LineItem) liIterator.next();
		liData = liRef.getData();

		// Получить DTO Product
		pRef = pHome.findByProductId(pId);
		liData.setProduct(pRef.getData());
		liArray[i] = liData;
	}
	
	return cData;
}

Когда я запустил эту сессию и набор CMP вместо написанной вручную JDBC-программы, ее производительность была значительно ниже (почти в 2 раза). Излишне говорить, что меня не впечатлило то, что я сделал.

Won't Get Fooled Again (Не хочу опять быть обманутым)

Я предположил, что как и в ситуации с No Longer a Fan в прошлом месяце, сравнение было некорректным. Но в этом примере оказалось много интересного.

Уважаемый Won't Get Fooled Again!

Я должен сказать, что в ряде случаев был сильно впечатлен, и это в немалой степени показывает, что Вы действительно прочли мою статью и последовали совету!

Если серьезно, меня впечатлило то, что Вы разработали объектную модель, чтобы показать мне базовый дизайн приложения. Очень часто разработчики хотят получить полный ответ о том, что является "наилучшим", тогда как лучшее всегда зависит от ситуации. Я был очень счастлив, что Вы прислали EJB-код, и мне не пришлось догадываться, действительно ли Вы реализовали описанную методику. Ваш код, использующий EJB-компоненты, вышел за рамки того, что обычно делают разработчики!

Хорошо было бы увидеть написанный вручную SQL-код, используемый в Вашем сравнении, прежде чем дать какой-либо совет, поскольку "наилучшее" всегда сравнивается с альтернативой.

В тоже время, не могли бы Вы ответить на мой вопрос: Использовали ли Вы концепцию "управляемых контейнером взаимосвязей" ("container managed relationships" или CMR) с компонентами управления данными?

Жду Вашего ответа.

Ваш EJB Advocate


Ловите яблоко, затем другое

Уважаемый EJB Advocate!

Позвольте мне ответить сначала на Ваш вопрос по CMR, поскольку это проще всего: нет, я не использовал их, поскольку плохо с ними знаком.

Ниже приведен эквивалентный метод сессионного компонента, использующий JDBC; в нем предполагается, что драйвер был загружен в методе ejbCreate(), а исключительные ситуации времени исполнения будут обрабатываться нашей интегрированной средой delegate без необходимости объявлений в сигнатурах методов. Заранее прошу прощения за длину кода - именно поэтому я не послал его в первый раз:

public CustomerData  getOpenOrderForCustomer(int cId) 
throws OrderNotOpenException
{
	Connection con = null;
	try { 
		con = DriverManager.getConnection(
			"jdbc:db2:orderentry"
      		);
	}
	catch (SQLException e) {
		throw new RuntimeException,  
 			"Connection cannot be made.", e)
 		);
 	} 	
 	try {
 		con.setAutoCommit(false);
		con.setReadOnly(true);
		con.setTransactionIsolation(
			Connection.TRANSACTION_READ_COMMITTED
		);
	 	PreparedStatement stmt1 = con.prepareStatement("
 			SELECT NAME, OPEN_ORDER_ID
   			FROM CUSTOMER_USER.CUSTOMER 
 			WHERE ID = ?
	 	");
		stmt1.setInt(1, customerId);
	 	stmt1.setMaxRows(1);
	 	stmt1.setMaxFieldSize(30);
 		ResultSet result = stmt1.executeQuery();
	 	result.next();
 		String name = result.getString(1);
	 	int openOrderId = result.getInt(2);
	 	result.close();
	 	stmt1.close();

 		// Проверить, открыт ли заказ
 		if (openOrderId == 0) {
 			throw new OrderNotOpenException(cId);
 		}

 		// Теперь получить Line Items и Products вместе 
 		PreparedStatement stmt2 = con.prepareStatement("
 			SELECT 
 				PRODUCT.SKU,
 				PRODUCT.DESC,
 				PRODUCT.PRICE,
 				LINEITEM.QUANTITY
 	   		FROM 
 				CUSTOMER_USER.LINEITEM,
 				CUSTOMER_USER.PRODUCT 
 			WHERE 
 				LINEITEM.ORDER_ID = ? 
 	   		
 			ORDER BY 
 				PRODUCT.SKU ASC
	 	");

 		stmt2.setInt(1, openOrderId);
 		stmt2.setFetchSize(10);
 		ResultSet result2 = stmt2.executeQuery();
		LineItemData liData = null;
 		ProductData pData = null;
		int productId = 0;
 		String description = null;
 		Int price = 0;
 		int quantity = 0;
 		Vector v = new Vector(10, 10);
 		while (result2.next()) {
 			// Получить поля
 			productId = result2.getInt(1);
 			description = result2.getString(2);
 			price = result2.getInt(3);
 			quantity = result2.getInt(4);

 			// Установить поля, связанные с Order
 			liData = new LineItemData();
			pData = new ProductData();
 			liData.setKey(
 				new LineItemKey(openOrderId, productId)
 			);
			liData.setQuantity(quantity);
 			liData.setAmount(price * quantity);
 			liData.setProduct(pData);

 			// Установить поля, связанные с Product
 			pData.setKey(new ProductKey(productId));
 			pData.setDescription(description);
 			pData.setPrice(price);

 			// Добавить элемент в Vector  
 			v.addElement(liData);
 		}
	
 		// Закрыть полученный набор данных и выражений
 		result2.close();
 		stmt2.close();
 		
 		// Теперь мы можем настроить Customer и Order
 		CustomerData cData = new CustomerData();
 		OrderData oData = new CustomerData();
 		cData.setKey(new CustomerKey(cid));
 		cData.setName(name);
 		cData.setOpenOrderId(openOrderId);
 		cData.setOrder(oData);

 		// Порядок данных может выбираться по умолчанию в большинстве случаев
 		oData.setKey(new OrderKey(openOrderId));
 		oData.setStatus("Open");
 		oData.setLineItems((LineItem[])v.toArray());

		// Возвратить вершину графа
 		return cData;
 	}	
 	catch (SQLException e) {
 		throw new RuntimeException(
 			"Error during processing.", e
 		);
	}
 	finally {
		try {
			con.close();
		}
		catch (SQLException e) { 
			throw new RuntimeException(
 				"Error closing connection.", e
 	 		);
		}
	}
}

Я знаю, что после ознакомления с кодом Вы посмеетесь над "работой, которую я сделал" для EJB-компонентов управления данными. Но имейте в виду, что Вы видите ВЕСЬ код, необходимый для работы метода. Я не показал Вам EJB-компоненты и дескрипторы развертывания, которые должен был написать, для того чтобы все это работало с CMP.

Все еще
Won't Get Fooled Again (Не хочу опять быть обманутым)

Ого! Этот парень крут. Он точно знает, как срезать путь.

Уважаемый Won't Get Fooled!

Опять я очень впечатлен. Да, я улыбнулся, когда увидел количество строк исходного кода, но с другой стороны его качество действительно превосходно! Он соответствует всем наилучшим методикам применения JDBC, о которых я знаю, и даже больше. Вот что я увидел:

  • Ресурсы создаются как можно позже и освобождаются как можно раньше.
  • Корректно обрабатываются ошибки, и всегда используется выражение finally для выполнения действий независимо от того, произошла исключительная ситуация или нет.
  • Вы указываете цель доступа в соединении и используете подготовленные выражения для оптимизации обмена данными. (Мне особенно понравилось использование setFetchSize() в выражении в соединении с инициализатором Vector и инкрементацией!)
  • И, конечно же, Ваши SQL-запросы везде, где это возможно, используют выбор специальных столбцов и соединений для минимизации количества и размеров запросов, передаваемых на уровень базы данных, а также объема возвращаемых данных.

Этот код довольно высоко поднимает планку для существующего кода и каких-либо возможных улучшений.

Верите или нет, я считаю, что код можно сделать лучше, чем с JDBC, если использовать CMP с CMR. Просто просмотрев код сессионного компонента и не видя кода EJB-компонента или дескрипторов развертывания, я предположил, что Вы не используете CMR в EJB-компонентах управления данными. Почему? Ваш сессионный компонент выполнен в процедурном, а не объектно-ориентированном стиле. То есть он процедурно обрабатывает всю логику "навигации" по взаимосвязям между четырьмя логическими объектами вместо того, чтобы делегировать эту задачу логическому объекту. Если бы он был объектно-ориентированным, возможно, код в Вашей сессии был бы таким:

public CustomerData  getOpenOrderForCustomer(int id) 
throws OrderNotOpenException
{
	// Получить DTO Customer	
	CustomerKey key = CustomerKey new(id)
	Customer ref = cHome.findByPrimaryKey(key);
	return ref.getDataWithOpenOrder();
}

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

Я уже говорил, что оценил факт кодирования Вами логики доступа CMP и JDBC непосредственно в сессионном компоненте вместо делегирования во вспомогательный POJO-объект. Вы должны также прочитать мою первую статью. Но независимо от того, где расположена логика вызова CMP (в сессионном компоненте или в POJO), она остается процедурной, а не объектно-ориентированной.

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

// CMP-поля
public abstract String getName();
public abstract void setName(String value);
public abstract int getOpenOrderId();
public abstract void setOpenOrderId(int value);

// Специализированные getters-методы
public CustomerData  getData() {
	CustomerData data = new CustomerData();
	data.setKey(getPrimaryKey());
	data.setOpenOrderId(getOpenOrderId());
	return data;
}
public CustomerData  getDataWithOpenOrder(int cId) {
	// Проверить, есть ли открытый заказ
	int oId = getOpenOrderId();
	if (oId == 0) {
		throw new OrderNotOpenException(cId);
	}

	// Получить логический объект Order и DTO-объект	
	Order oRef = oHome.findByOrderId(oId);
	OrderData oData = oRef.getDataWithLineItems();
	CustomerData data = getData();
	data.setOrder(oData);
	return data;
}

Этот код свидетельствовал бы, что Вы используете объектно-ориентированное делегирование, но не используете CMR. Если бы Вы использовали CMR, код выглядел бы примерно так:

// CMP-поля
public abstract String getName();
public abstract void setName(String value);
public abstract int getOpenOrderId();
public abstract void setOpenOrderId(int value);

// CMR-поля
public abstract Order getOpenOrder();
public abstract void setOpenOrder(Order value);
public abstract Collection getOrders();
public abstract void setOrders(Collection value);

// Специализированные getter-методы
public CustomerData  getData() {
	CustomerData data = new CustomerData();
	data.setKey(getPrimaryKey());
	data.setOpenOrderId(getOpenOrderId());
	return data;
}
public CustomerData  getDataWithOpenOrder() {
	// Проверить наличие открытого заказа REFERENCE
	Order oRef = getOpenOrder ();
	if (oRef == null) {
		throw new OrderNotOpenException(cId);
	}

	// Получить данные Open Order
	OrderData oData = oRef.getDataWithLineItems();
	CustomerData data = getData();
	data.setOrder(oData);
	return data;
}

Я уверен, что в соответствии с диаграммой можно применять как процедурный, так и объектно-ориентированный шаблон, и что использование CMR не зависит от сделанного выбора. Я продемонстрирую "начинку" для Order, Line Item и Product для случая объектно-ориентированного CMR (начав с Order), поскольку в конечном итоге мы хотим выполнить сравнение с описанным выше кодом процедурного JDBC:

// CMP-поля
public abstract String getStatus();
public abstract void setStatus(String value);

// CMR-поля
public abstract Customer getCustomer();
public abstract void setCustomer(Customer value);
public abstract Collection getLineItems();
public abstract void setLineItems(Collection value);

// Специализированные getter-методы
public OrderData  getData() {
	OrderData data = new OrderData();
	cData.setKey(getPrimaryKey());
	data.setStatus(getStatus());
	return data;
}
public OrderData  getDataWithLineItems() {
	// Использовать CMR для получения line items в массив
	Collection liList = getLineItems();
	int liSize = liList.size();
	LineItemData[] liArray = new LineItemData[liSize];
	Iterator liIterator = liList.iterator();
	LineItem liRef = null;
	for (int i = 0; i < liSize; i++) {
		// Получить DTO Line Item 
		liRef = (LineItem) liIterator.next();
		liArray[i] = liRef.getDataWithProduct();
	}

	// Создать объект и возвратить
	OrderData data = getData();
	data.setLineItems(liArray);
	return data;
}

Here is the Line Item:

// CMP-поля
public abstract int getQuantity();
public abstract void setQuantity(int value);
public abstract int getAmount();
public abstract void setAmount(int value);

// CMR-поля 
public abstract Order getOrder();
public abstract void setOrder(Order value);
public abstract Product getProduct();
public abstract void setProduct(Product value);

// Специализированные getter-методы
public LineItemData  getData() {
	LineItemData data = new LineItemData ();
	data.setKey(getPrimaryKey());
	data.setQuantity(getQuantity());
	data.setAmount(setAmount());
	return data;
}
public CustomerData  getDataWithProduct() {
	// Получить Product из CMR	
	Product pRef = getProduct();
	ProductData pData = pRef.getData ();
	LineItemData data = getData();
	data.setProduct(pData);
	return data;
}

И, наконец, Product:

// CMP-поля
public abstract String getDescription();
public abstract void setDescription(String value);
public abstract int getPrice();
public abstract void setPrice(int value);

// CMR-поля
public abstract Collection getLineItems();
public abstract void setLineItems(Collection value);

// Специализированные getter-методы
public ProductData  getData() {
	ProductData data = new ProductData();
	data.setKey(getPrimaryKey());
	data.setDescription(getDescription());
	data.setPrice(getPrice());
	return data;
}

Теперь мы можем по-настоящему сравнить яблоки, имея следующее их многообразие:

  • Процедурный/JDBC
  • Процедурный/CMP
  • Процедурный/CMR
  • OO/JDBC
  • OO/CMP
  • OO/CMR

Я включил вариант OO/JDBC просто для полноты изложения, поскольку он представляет из себя, по существу, любой подход, где "специализированные" объекты разрабатываются один к одному с бизнес-объектами (в данном случае четыре из них). В список должны быть включены EJB-компоненты управления данными с управляемой компонентом персистенцией (Bean Managed Persistence - BMP) как "JDO-подобные" объекты без поддержки программами или JVM. Другими словами, если Ваш специально закодированный JDBC связан с одним бизнес-объектом (в предположении, что он может вызывать другие в собственных методах и быть вызванным в контексте других), то он рассматривается как OO/JDBC.

Теперь приступим к оценке.

Прежде всего, как Вы понимаете, в любой ситуации с CMP/CMR любой индивидуальный метод намного проще, чем любой метод с напрямую закодированным JDBC (по причине сложности SQL и интегрированной среды). Упомяну также "очевидный" момент: если неожиданно переключить базы данных, JDBC-реализация больше не будет работать (пока Вы не измените код или не будете использовать источники данных, файлы свойств или переменные окружения в качестве способа получения URL соединения). И даже в том случае, если Вы уйдете от использования реляционной базы в качестве хранилища данных, работа полностью остановится.

Затем, как Вы заметили, при разработке единицы работы с использованием любого CMP/CMR-подхода (или даже OO/JDBC-подхода) имеется большее количество методов, чем при использовании процедурного/JDBC - в частности из-за включения всех специализированных методов gets и sets. Но эти специализированные объектно-ориентированные методы любого типа являются повторно-используемыми, тогда как процедурный код, особенно использующий JDBC, должен оптимизироваться для сценария вручную.

Вот пример повторного использования: можно легко отобразить различные методы getData из любого компонента управления данными (CMP или CMR) вплоть до сессии с передачей соответствующих ключевых полей. Такой сессионный метод будет настоящим фасадом, как упоминалось выше. Допустим, например, что мы хотим получить просто "автономный" DTO, связанный с данным типом логического объекта. Мы могли бы написать четыре очень простых метода фасада сессии:

public CustomerData  getCustomerData(int id) 
throws FinderException
{
	// Получить DTO Customer
	CustomerKey key = CustomerKey new(id)
	Customer ref = cHome.findByPrimaryKey(key);
	return ref.getData ();
}
public OrderData  getOrderData(int id) 
throws FinderException
{
	// Получить DTO Order	
	OrderKey key = OrderKey new(id)
	Order ref = oHome.findByPrimaryKey(key);
	return ref.getData ();
}
public LineItemData  getLineItemData(int orderId, int productId) 
throws FinderException
{
	// Получить DTO LineItem	
	LineItemKey key = LineItemKey new(orderId, productId)
	LineItem ref = liHome.findByPrimaryKey(key);
	return ref.getData ();
}
public ProductData  getProductData(int id) 
throws FinderException
{
	// Получить DTO Product	
	ProductKey key = ProductKey new(id)
	Product ref = pHome.findByPrimaryKey(key);
	return ref.getData ();
}

Хотя настоящим преимуществом повторного использования является удобство обслуживания, применяя специализированные методы, ассоциированные с порожденными Вашей моделью данных DTO-объектами, нужно всего лишь изменить методы getData() и setData() различных компонентов управления данными, когда Вы добавляете или удаляете атрибут (связан ли он с CMP-полем или CMR-полем). Каждый процедурный или OO/JDBC метод, относящийся к этим полям, должен изменяться при изменении схемы.

При использовании CMR редко нужно получать метод home и использовать специализированный обнаружитель для извлечения одной или более ссылок на логические объекты; CMR делает это автоматически за кулисами при объявлении объекта. Такое упрощение приводит к намного меньшему числу передаваемых параметров по сравнению с процедурным подходом и подходом с использованием только CMP. Фактически, при использовании CMR, даже в процедурном стиле, должен вызываться только один обнаружитель на единицу работы. Сюда входит и ссылка на не сохраняющий состояния сессионный компонент, поскольку она может кэшироваться на клиенте! Также во многих ситуациях с OO/CMR поиск логического объекта использует метод findByPrimaryKey(). Это означает, что Вы должны указать меньше специализированных обнаружителей в дескрипторах развертывания. Меньше означает больше, когда доходит до удобства обслуживания и эксплуатации.

Что касается ключей, то при использовании CMR зачастую только сессионный компонент (или "конечный клиент", например, сервлет или JSP) должен знать, какие ключи имеет логический объект; это касается даже специализированных методов самого логического объекта. Это еще упрощает логические объекты и их обслуживание, и даже больше, если CMR используется последовательно. Вы могли бы изменять ключевые поля и не делать больше ничего кроме повторного развертывания. Если CMR не используются, код должен знать ключевые поля для поиска объекта, и Вы, вероятнее всего, будете использовать в Вашем коде специализированные обнаружители, как при процедурном, так и при ОО стиле.

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

Не ясно, будет ли CMR работать также хорошо, как закодированный вручную JDBC-код, независимо от того, какой подход, процедурный или ОО, использует каждая технология. Мы считаем, что при использовании CMR в некоторых серверах приложений (например, IBM WebSphere Application Server) контейнер можно настроить для оптимизации SQL способом, аналогичным применяемым Вами при ручном кодировании. Например, могут быть указаны "цели доступа" ("access intents"), что позволяет указать размер для последующих операций чтения (read ahead size) (аналогично setFetchSize), предельные размеры операций чтения (read limit) (аналогично setMaxRows) и кэширование предварительной нагрузки (preload caching) (аналогично joins). Вы можете также "подстроить" столбцы, которые загружаются в выражениях select. (Информация по профилированию приложений приведена в документе IBM WebSphere Application Information Center.)

На случай, если Вы станете утверждать, что работа с CMR, профилями и целями доступа делает логические объекты такими же сложными, как и прямое кодирование оптимизированного JDBC, я приведу несколько соображений:

  1. CMR - это информация о модели, аналогичная внешним ключам в базе данных. Однако, в отличие от внешних ключей, CMR также делают код приложения проще, независимо от того, процедурные они или объектно-ориентированные по природе.
  2. Явно указанные цели доступа должны использоваться только тогда, когда производительность готового продукта не отвечает указанным требованиям; например, ситуация, которую мы обсуждали в прошлом месяце, когда целями единицы работы являлись один или несколько несвязанных объектов, и Вы обнаружили, что производительность находится в пределах только нескольких процентов и действительно стоит затратить усилия на упрощение программной модели.
  3. EJB-контейнеры постоянно улучшаются в плане отображения CMP с CMR на применяемые хранилища данных, что дает возможность повторно развертывать их позже и использовать преимущества производительности, недоступные при ручном кодировании JDBC.
  4. Когда цели доступа действительно должны быть указаны по соображениям производительности, код EJB-компонентов не нужно менять, а всего лишь настроить параметры. Следовательно, код будет работать всегда, даже если он не настолько производителен, как мог бы быть. Эта гарантия оказывает огромное влияние на цикл разработки и тестирования.
  5. Когда несколько единиц работы совместно используют один и тот же базовый набор целей доступа, например, все те, которым требуется заголовок заказа с клиентом (такие методы как submit, cancel и show orders), можно создать общий прикладной профиль и настроить эти функции вместе.

Без CMR Ваш процедурный/CMP или ОО/CMP код неявно выдавал бы несколько отдельных SQL-запросов: один для клиента, один для заказа, один для элементов строки и один для каждого товара. Не удивительно, что CMP не такой производительный, как закодированный вручную JDBC.

Но при полном использовании CMR приложение может иногда быть настроено простыми выражениями пути навигации по объектам на генерирование только одного SQL-запроса (с ноль или более внутренних соединений (inner joins) для обработки многоэлементных навигаций; мой друг Стэйси называет их "honking big" соединениями). Другими словами, возможно, что приложения, полностью использующие CMR, могли бы выполняться лучше, чем код, который написал бы средний JDBC-программист (я ужасно не люблю даже смотреть на SQL с внутренними соединениями, особенно когда их написал кто-то другой!).

Итак, надеюсь, мои аргументы убедили Вас использовать CMR. И, что тоже достаточно интересно, даже если Ваш код не использует их явно, Вы все равно можете добавить CMR к логическим объектам и выполнить повторное развертывание без изменения каких-либо методов. Затем, поскольку ваш код всегда использует метод findByPrimaryKey(), контейнер может попытаться использовать CMR в сгенерированных SQL-запросах. Этот момент является ключевым. Спецификация EJB 2.0 проясняет, что вызова метода специализированного обнаружителя нельзя избежать, так как внутри может скрываться важная логика работы. Взгляните на пункт 3, приведенный выше, и увидите, что это безопасный вариант, даже если Вы не изменили ни одной строки кода.

Но для Вас не составит труда вернуться назад и изменить процедурный/CMP-метод так, чтобы он был совместим с CMR, например:

public CustomerData  getOpenOrderForCustomer(int cId) {
	// Получить DTO Customer
	// Здесь используется PK, чтобы воспрепятствовать использованию
	специализированных обнаружителей
	// Но поскольку это "главный" из вызовов, это не обязательно
	Customer cRef = cHome.findByPrimaryKey(new CustomerKey(cId));
	CustomerData cData = cRef.getData();

	// Проверить, имеется ли открытый заказ
	int oId = cData.getOpenOrderId();
	if (oId == 0) {
		throw new OrderNotOpenException(cId);
	}

	// Получить логический объект Order и DTO-объект -fBPK обязательно
	Order oRef = oHome.findByPrimaryKey(new OrderKey(oId));
	OrderData oData = oRef.getData();
	cData.setOrder(oData);
	
	// Получить массив DTO Line Items 
	// Если не используется CMR, этот специализированный обнаружитель
	обязателен!
	Collection liList = liHome.findAllItemsForOrderId(oId);
	int liSize = liList.size();
	LineItemData[] liArray = new LineItemData[liSize];
	oData.setLineItems(liArray);
	Iterator liIterator = liList.iterator();
	LineItem liRef = null;
	LineItemData liData = null;
	Product pRef = null;
	for (int i = 0; i < liSize; i++) {
		// Получить DTO Line Item
		liRef = (LineItem) liIterator.next();
		liData = liRef.getData();

		// Получить DTO Product -fBPK обязательно для поддержки соединения
		pRef = pHome.findByPrimaryKey(new ProductKey(pId));
		liData.setProduct(pRef.getData());
		liArray[i] = liData;
	}
	
	return cData;
}

Этот код можно было бы настроить на генерирование не более двух SQL-запросов, потому что специализированный метод findAllItemsForOrderId() вызывается со специализированным обнаружителем, приводя к "сбросу". При такой настройке производительность процедурного CMP-CMR (это означает, что CMR определены, но явно не используются в коде) должна находиться в пределах нескольких процентов от закодированного Вами JDBC, который тоже генерирует два SQL-запроса.

Но на самом деле, однажды столкнувшись с проблемой специфицирования, Вы обнаружите, что проще использовать CMR (даже если код является процедурным) и нужно изменить, в основном, те же строки кода, что и выше:

public CustomerData  getOpenOrderForCustomer(int cId) {
	// Получить DTO Customer
	// Здесь используется PK, чтобы воспрепятствовать использованию
	специализированных обнаружителей
	// Но поскольку это "главный" из вызовов, это не обязательно
	Customer cRef = cHome.findByPrimaryKey(new CustomerKey(cId));
	CustomerData cData = cRef.getData();

	// Проверить, имеется ли открытый заказ
	// Обратите внимание на то, что эта проверка намного более "показательна", 
	//	поскольку код не должен интерпретировать '0' как не открытый заказ
	Order oRef = cData.getOpenOrder();
	if (oRef == null) {
		throw new OrderNotOpenException(cId);
	}

	// Получить DTO-объект Order, поскольку мы уже имеем ссылку
	OrderData oData = oRef.getData();
	cData.setOrder(oData);
	
	// Получить массив DTO-объектов Line Items, используя CMR
	Collection liList = oRef.getLineItems();
	int liSize = liList.size();
	LineItemData[] liArray = new LineItemData[liSize];
	oData.setLineItems(liArray);
	Iterator liIterator = liList.iterator();
	LineItem liRef = null;
	LineItemData liData = null;
	Product pRef = null;
	for (int i = 0; i < liSize; i++) {
		// Получить DTO Line Item
		liRef = (LineItem) liIterator.next();
		liData = liRef.getData();

		// Получить DTO Product, используя CMR
		liData.setProduct(liRef.getProduct().getData());
		liArray[i] = liData;
	}
	
	return cData;
}

Не забывайте, что можно отбросить основную часть этих надоедливых home и IC lookups в Ваших сессионных методах ejbCreate(), создавая (или еще лучше, генерируя) фасад сессии для логического объекта. Беспроигрышный вариант! После этого, надеюсь, Вы начнете подписываться как

Led to Water (Утоляющий жажду)

Я также надеюсь, что Вы пьете воду, а не что-нибудь покрепче.

Ваш EJB Advocate


Заключение

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

  1. Всегда используйте фасад сессии для начала глобальной транзакции с применяемыми логическими объектами.
  2. Всегда используйте локальный интерфейс к CMP-компоненту, чтобы избежать возможного дублирования.
  3. Даже при локальном использовании создавайте специализированные методы создания, поиска, получения и установки для минимизации "болтливости" между уровнями и поощрения повторного использования и удобства обслуживания.
  4. Стремитесь к одному (не более) вызову, после того как один элемент обнаружит home или next для итератора, извлеченного из коллекции.

Благодаря обмену мнениями в этом месяце, мы рассмотрели подробности сценария, в котором используется несколько связанных EJB-компонентов управления данными в одной транзакции. Правда заключается в том, что CMP без CMR почти всегда выполняется значительно хуже, чем написанный вручную JDBC. Это главная причина, почему EJB-компоненты управления данными до 2.0 версии не получили широкого распространения. Однако EJB 2.0 решила эту проблему для большинства сценариев.

В данном обсуждении мы рассмотрели следующие наилучшие методики использования, относящиеся к связанным логическим объектам:

  1. Вы получите все и ничего не потеряете, настроив CMR, соответствующие вашей модели данных, даже если код приложения не использует их явно.
  2. Измерьте производительность системы, и если Ваши логические объекты не отвечают ожиданиям, попробуйте настроить контейнер для оптимизации запросов. Возможно, что производительность станет приблизительно равной или даже лучше производительности вручную закодированных JDBC-процедур, но объем работы будет меньше, а возможность повторного использования и независимость от хранилища данных улучшится.
  3. Также подумайте над рефакторингом фасадов сессии, так чтобы они были связаны с одним логическим объектом-"шлюзом", являющимся естественной начальной точкой кода. Создайте профили приложения и специализированные обнаружители вокруг этих шлюзов для повторного использования параметров настройки.
  4. Если производительность все еще не соответствует ожиданиям, исследуйте использование EJB-компонентов управления данными, чтобы увидеть, можно ли использовать вызовы findByPrimaryKey() или CMR вместо специализированных обнаружителей. Другими словами, применение специализированных обнаружителей должно рассматриваться как антишаблон, за исключением первого вызова EJB-компонента управления данными в единице работы - этот компонент может считаться "шлюзом" к компонентам, ассоциированным с ним. Если имеется этот антишаблон, попробуйте снова подправить приложение, как во втором пункте.
  5. При создании приложения с нуля или при рефакторинге кода по каким-либо причинам (например, наилучшие методики 3 или 4), подумайте об использовании OO-подходов к сессионным компонентам, для того чтобы сделать их настоящими фасадами. Такой рефакторинг может быть выполнен на основе подхода метод за методом.
  6. Если производительность данной единицы работы все еще не отвечает Вашим ожиданиям, используйте OO-подход для метода, активизированного в естественных объектах-шлюзах, использованных в транзакции, и напишите BMP-методы, которые напрямую используют JDBC (см. книгу Кайла Брауна, ссылка на которую приведена в разделе "Ресурсы"). И, пожалуйста, пришлите мне пример!
  7. Постоянно проверяйте обновления продуктов, чтобы обнаружить наличие новых параметров настройки, пытаясь избавиться от одного или нескольких BMP-методов или просто повысить общую производительности системы. Конечной целью должно быть использование 100% CMP/CMR и 0% BMP. Но это может потребовать некоторого времени.

Достаточно на сегодня. Я уже достаточно опытен, поэтому не будут давать обещаний относительно содержания следующей статьи.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=WebSphere, Технология Java
ArticleID=300838
ArticleTitle=EJB Advocate: Часть 2. Создание производительных EJB-компонентов управления данными
publish-date=04112008