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
Добавить комментарий »


.jpg)



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