• 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, 12:11
Теги:      gwt ibatis

В цій статті ми розглянемо посібник створення GWT-додатків, що працюють з базою даних за допомогою iBATIS. Цей матеріал буде корисний тим, хто цікавиться розробкою користувацьких інтерфейсів до баз даних.

Чому iBATIS?

Типовою є задача створення зручного та функціонального інтерфейсу роботи з даними (ми вже про це відмічали як раніше, так й опублікували наше рішення в цій галузі - GWT-PF). GWT відмінно підходить для вирішення завдань такого типу, як зі сторони створення самого інтерфейсу так й для забезпечення взаємодії з сервером. Для цього в 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 class="net.pleso.GWTiBatis.server.customer.CustomerServiceImpl" path="/customer"></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>

В наведеному прикладі ми використали наш клас Customet з 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 - створіть відповідний метод на рівні бізнес-логіки, що вибере необхідні дані за вказаним ідентифікатором. Якщо потрібно змішувати дані із Customer з City зв'язно у вигляді однієї таблиці - створіть відповідний 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 type="net.pleso.framework.client.dal.types.BigInt" alias="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.