• Home
  • RSS
  • Contacts


IT консалтинг

обращайтесь для создания или развития проекта - станем консультантом и независимым аналитиком, предоставим возможность професионально проанализировать риски, спроектировать и сопровождать решение

more

Разработка

воплощаем смелые мысли - разработка изысканных web-решений и комплексных баз данных. Не стереотипный подход к реализации, создание уникальных систем на базе языков Java/Python, баз данных PostgreSQL/MySQL платформ Linux/Unix.

more

hide

Продукты

GWT-PF product

фреймворк для создания веб-фронтэндов баз данных

Pleso netNews product

решение для построения интернет-изданий

Проекты

GWT + iBATIS

Опубликовано Андрей Скалюк - 16.10.2007, 05:06
Тэги:      gwt ibatis

В этой статье мы рассмотрим способы работы GWT-приложения с базой данных на сервере при помощи iBATIS. Этот материал будет полезен тем, кто занимается разработкой GWT-приложений как пользовательских интерфейсов для баз данных.

Почему iBATIS?

Часто перед разработчиком стоит задача создания удобного и функционального интерфейса для управления данными (мы уже упоминали об этом ранее, так и опубликовали свое решение для этой области - GWT-PF). GWT отлично подходит для таких задач, как в плане написания самого интерфейса, так и для взаимодействия с сервером. Для этого существует собственный тип RPC-интерфейса для удаленного вызова процедур на сервере, или возможность использовать JSON-структуры для обмена данными с сервером.

Когда дело доходит до взаимодействия с базой данных непосредственно на серверной стороне, возникает вопрос унификации и автоматизации этого процесса. Можно воспользоваться напрямую JDBC-драйвером избранной базы данных и передавать нетипизированные данные в массиве. Такое решение неудобно, поскольку на клиенте мы имеем дело с конкретной сущностью, которая описана классом. Приведем пример: Customer - класс, который описывает покупателя, а его код выглядит следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Customer {
	public Customer() {}

	private Integer id;
	private String name;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

В базе данных мы создадим соответствующую таблицу с двумя полями id и name. Описав такой класс один раз на уровне GWT, мы хотим получить функциональность загрузки, создания, редактирования и удаления покупателей. То есть, нам нужен слой взаимодействия с базой данных, который отобразит данные из таблицы в наш класс и наоборот. Также мы хотели бы получить независимое решение от конкретной СУБД, чтобы подключив иной JDBC-драйвер, мы смогли бы сменить среду для нашего приложения. Именно эти задачи и решает iBATIS - разработка, что развивается уже 5 лет в Apache Software Foundation, и является очень удачным решением для создания независимого уровня доступа к данным. Используя XML-конфигурацию, она позволяет организовать отображение данных в Java-классы, обработку null-значений, вызов серверных процедур и функций, обработку конкретных типов данных и многое другое. В данной статье мы не рассматриваем другие решения для подобного рода задач - это темы для следующих публикаций.

Создание RPC-сервиса

Создадим пустой GWT-проект и воспользовавшись документацией реализуем RPC-сервис для загрузки списка покупателей из базы данных на сервере. Для этого выполним этапы:

1. Опишем интерфейс клиентской части:

 1
 2
 3
 4
 5
public interface CustomerService extends RemoteService {
	
	Customer[] select();

}

2. Асинхронный интерфейс:

 1
 2
 3
 4
public interface CustomerServiceAsync {

	void select(AsyncCallback callback);
}

3. Загрузчик сервиса по URL "/customer":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class CustomerServiceLoader {
	
	public static CustomerServiceAsync getService()
	{
		CustomerServiceAsync calService = (CustomerServiceAsync) GWT
		.create(CustomerService.class);
		ServiceDefTarget target = (ServiceDefTarget) calService;
		String moduleRelativeURL = GWT.getModuleBaseURL() + "customer";
		target.setServiceEntryPoint(moduleRelativeURL);
		return calService;
	}

}

4. Реализацию интерфейса на сервере:

 1
 2
 3
 4
 5
 6
 7
 8
public class CustomerServiceImpl extends RemoteServiceServlet implements CustomerService {

	public Customer[] select() {
		// TODO Auto-generated method stub
		return null;
	}

}

5. А также поправим XML-конфигурацию GWT, добавим строчку:

 1
<servlet path="/customer" class="net.pleso.GWTiBatis.server.customer.CustomerServiceImpl"></servlet>

6. Класс Customer должен реализовать интерфейс IsSerializable:

 1
public class Customer implements IsSerializable

RPC-сервис готов.

Подключение iBATIS

Создадим папку lib в корне проекта и скопируем в нее последнюю версию iBATIS. Также в эту папку необходимо поместить JDBC-драйвер используемой базы данных. В приведеном примере мы используем PostgreSQL, поэтому XML-конфигурация будет создана для этой СУБД. Также оба jar-файла (ibatis и postgre-jdbc) нужно добавить в Build Path проекта.

Теперь создадим файл XML-конфигурации iBATIS с названием SqlMapConfig.xml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE sqlMapConfig      
    PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"      
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlmapconfig>
<!-- Configure a built-in transaction manager.  If you\'re using an 
       app server, you probably want to use its transaction manager 
       and a managed datasource -->
<transactionmanager type="JDBC" commitrequired="true">
<datasource type="SIMPLE">
<property name="JDBC.Driver" value="org.postgresql.Driver">
</property><property name="JDBC.ConnectionURL" value="jdbc:postgresql://${host}:${port}/${database}">
</property><property name="JDBC.Username" value="${login}">
</property><property name="JDBC.Password" value="${password}">
</property></datasource>
</transactionmanager>
<!-- List the SQL Map XML files. They can be loaded from the 
       classpath, as they are here (com.domain.data...)-->
</sqlmapconfig>

Этот файл должен находиться на серверной стороне приложения. В даном случае мы используем SimpleDataSource из iBATIS. Вместо параметров host, port, database, login, password нужно ввести соответствующие для конкретного случая значения. Отметим также - если эти параметры изменяемые, или вы хотите вынести их отдельно, то iBATIS предоставляет эту возможность.

Создание SQL Map XML

Для каждой логической сущности приложения желательно создавать собственную SQL Map XML - карту отображения результатов SQL-запросов в классы Java и наоборот. Создадим такую карту для класса Customer (файл Customer.xml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE sqlMap      
    PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"      
    "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlmap namespace="Customer">
<typealias alias="Customer" type="net.pleso.GWTiBatis.client.customer.Customer">
<resultmap id="CustomerResult" class="Customer">
<result property="id" column="id">
</result><result property="name" column="name">
</result></resultmap>
<select id="selectCustomer" resultmap="CustomerResult"> 
 	select * from customer
  </select>
</typealias></sqlmap>

Здесь мы использовали наш класс Customer из GWT, а также простой select-запрос выбора всех покупателей из таблицы customer. resultMap указывает соответствие между колонкой в таблице и полем в классе, для нашего случая они совпадают. Добавим даную конфигурацию в SqlMapConfig.xml:

 1
<sqlmap resource="net/pleso/GWTiBatis/server/customer/Customer.xml"></sqlmap>

Реализация доступа к базе данных на серверной стороне

Как следует из документации, SqlMapClient - это класс iBATIS, с помощью которого мы уже сможем производить запросы в базу. Для получения экземпляра SqlMapClient создадим вспомогательный класс SqlMapManager:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class SqlMapManager {

	public static SqlMapClient getSqlMapClient(Properties authProps) {
		try {
			Reader reader = Resources
			.getResourceAsReader("net/pleso/GWTiBatis/server/SqlMapConfig.xml");
			SqlMapClient sqlMapper = SqlMapClientBuilder.buildSqlMapClient(reader, authProps);
	        reader.close();
	        return sqlMapper;
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage(), e);
		}
	}
}

Как видно из кода выше, статичный метод getSqlMapClient, принимая параметры аутентификации и места нахождения базы данных, загружает созданную нами SQL Map XML и создает экземпляр SqlMapClient.

Все готово для запроса в СУБД. Дописываем реализацию RPC-сервиса CustomerServiceImpl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Properties getAuthProps(){
	Properties props = new Properties();
	props.setProperty("host", "localhost");
	props.setProperty("port", "5432");
	props.setProperty("database", "gwtibatis");
	props.setProperty("login", "postgres");
	props.setProperty("password", "12345");
	
	return props;
}

public Customer[] select() {
	SqlMapClient client = SqlMapManager.getSqlMapClient(getAuthProps());
	
	try {	
		List li = client.queryForList("selectCustomer");			
		return (Customer[])li.toArray(new Customer[li.size()]);				
	} 
	catch (SQLException e){
		e.printStackTrace();
		throw new RuntimeException(e.getMessage(), e);			
	}
}

Реализация вывода полученных данных на клиентской стороне

Выводим полученные данные в GWT-клиенте:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void onModuleLoad() {
  
  final Grid grid = new Grid(1, 2);
  grid.setText(0, 0, "id");
  grid.setText(0, 1, "name");
  
  CustomerServiceAsync service = CustomerServiceLoader.getService();
  service.select(new AsyncCallback() {
	    public void onSuccess(Object result) {
	    	Customer[] customers = (Customer[]) result;
	    	for (int row = 0; row < customers.length; ++row) {
	    	    grid.resizeRows(grid.getRowCount() + 1);
	    		grid.setText(row + 1, 0, customers[row].getId().toString());
	    		grid.setText(row + 1, 1, customers[row].getName());
	        }
	    }

	      public void onFailure(Throwable caught) {
	        Window.alert("Failure RPC call");
	      }
	    }
	    );

   RootPanel.get("slot1").add(grid);
 }

Не забудьте создать таблицу в базе данных и наполнить ее тестовыми значениями:

 1
 2
 3
 4
 5
 6
 7
CREATE TABLE customer
(
  id serial NOT NULL,
  name character varying,
  CONSTRAINT customer_pk PRIMARY KEY (id)
) 
WITHOUT OIDS;

Запустив проект на выполнение, мы уже увидим загруженные значения, отображенные на компоненте Grid.

Обработка значений null

В начале даной статьи, когда создавался класс Customer, мы преднамеренно использовали классы Integer и String - для поля id можно было бы использовать простой тип int. Такое решение связано с обработкой значений null, которые имеют место при работе с базами данных. iBATIS автоматически поддерживает присвоение значения null даному полю, если оно представлено, как класс, а не как простой тип. Поэтому, рекомендуется в своих бизнес-классах использовать такие стандартные классы-обертки, как Integer, Boolean и так далее. Таким образом, проверка на null делается стандартными средствами Java.

Связные данные. Внешние ключи

Рассмотрим пример, что таблица customer имеет внешний ключ на другую таблицу - city. В базе данных это будет представлено в виде поля city_id, которое является ссылкой на внешний идентификатор и сохраняет его значение. Для работы с ним в нашем классе достаточно добавить поле city_id типа Integer (тип идентификатора) и внешний ключ готов.

Но возникает вопрос, как получить всю запись из внешней таблицы, когда идентификатора недостаточно. Если вы хотите получить весь класс City из Customer - напишите соответствующий метод на уровне бизнес-логики, которые выберет вам все данные по идентификатору. Если необходимо смешать City с Customer - создайте соответствующий View в СУБД или создайте более сложный select-запрос в конфигурации iBATIS.

Обработка типов даных с помощью TypeHandlerCallback

Приведем пример классической проблемы, с которой сталкиваются разработчики на GWT - отсутствие типа long (bigint) в JavaScript - эта особенность описана в Google Web Toolkit Language Support. Значение типа long представляются в JavaScript, как "double-precision floating point values". Таким образом, если вы используете большие числа в СУБД (например bigint в PostgreSQL), есть вероятность несовпадения переданных значений между сервером и GWT-клиентом за счет округления в double типе. Эту проблему можно решить, передавая значения типа long в GWT как String - в этом случае мы используем большие числа только как идентификаторы и отказываемся производить вычисления на клиенте, что допустимо в нашем случае. При этом продолжает база данных продолжает работать со своим внутренним числовых представлением типа (например, bigint).

Реализовать это на iBATIS можно с помощью TypeHandlerCallback. Этот интерфейс позволяет написать обработчик на собственный или существующий тип данных - это удобно для перечисляемых типов (enumerate), а также легко решает описанную проблему с типом long. Вы можете посмотреть пример внешнего для iBATIS типа BigInt (который хранит тип long, как текстовою строку) в исходном коде демо-приложения GWT-PF. Здесь представим собственно обработчик BigIntHandlerCallback, подключаемый в конфигурацию SqlMapConfig.xml следующим образом:

 1
<typealias alias="BigInt" type="net.pleso.framework.client.dal.types.BigInt"></typealias>

Вот код обработчика, что также взят из демо-приложения GWT-PF:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BigIntHandlerCallback implements TypeHandlerCallback {

	public Object getResult(ResultGetter getter) throws SQLException {
		if (getter.getObject() == null)
			return null;
		else
			return new BigInt(getter.getLong());
	}

	public void setParameter(ParameterSetter setter, Object parameter)
			throws SQLException {
		Long value = null;
		if (parameter != null)
			value = ((BigInt) parameter).getAsLong();		
		
		if (value == null)
			setter.setNull(Types.BIGINT);
		else
			setter.setLong(value.longValue());
	}

	public Object valueOf(String value) {
		if (value != null)
			return new BigInt(value);
		else
			return null;
	}
}

Таким образом, мы получаем рабочий тип BigInt без искажений при передаче значений на GWT-клиент и можем использовать его в своих бизнес-классах. Всю работу по взаимодействию с базой данных берет на себя iBATIS, руководствуясь приведеной реализацией TypeHandlerCallback

комментарии:0

Добавить комментарий »