Создание Ajax-проекта с использованием инструментария Google Web Toolkit, Apache Derby и Eclipse: Часть 2. Надежная серверная часть

Apache Derby - твердый фундамент для разработки ваших приложений

В этом материале, втором из серии статей о создании приложений на основе технологии Asynchronous JavaScript + XML (Ajax) с помощью Google Web Toolkit (GWT), рассказывается о разработке базы данных Apache Derby для вашего приложения, и об ее использовании совместно с GWT. Первая часть данной серии знакомит вас с инструментарием GWT и рассказывает о том, как создать простой и функциональный интерфейс для web-приложения. В этот раз мы узнаем, как создать серверную часть, используя нашу базу данных, и код, который будет конвертировать данные в понятный GWT-формат. К концу статьи мы сможем наладить взаимосвязь между клиентской и серверной частями.

Ноэл Рэппин, старший инженер-программист, Motorola, Inc.

Ноэл Рэппин (Noel Rappin) имеет степень доктор философии по графике, визуализации и используемости от Georgia Institute of Technology. Он работает старшим инженером-программистом в Motorola, Inc. Является соавтором книг "wxPython в действии" (Manning Publications, март 2006) и "Основные элементы Jython" (O'Reilly, март 2002).



30.01.2008

В этой статье мы установим и сконфигурируем базу данных - серверную часть нашего Web-приложения, - создадим схему базы данных, и освоим несколько простых средств для наполнения ее информацией. Мы будем использовать Apache Derby, реляционную базу данных на основе Java™, разрабатывавшуюся изначально под маркой Cloudscape™. Позднее код Cloudscape был приобретен IBM®, и затем ее бесплатная версия вошла в состав проекта Apache. Аналогичный продукт распространяется Sun Microsystems под маркой JavaDB.

На странице центра ресурсов Ajax вы сможете найти информацию об Ajax-программировании, включая различные руководства и статьи, форумы, блоги, новости, и любые другие необходимые материалы.

К преимуществам Derby можно отнести ее небольшой размер и легкость конфигурации. В отличие от большинства реляционных баз данных, Derby может работать с помощью той же виртуальной машины Java, что и ваш серверный Java-код. (Конечно, при желании ее можно запустить на отдельной JVM). Это облегчает процесс разработки и инсталляции проекта, кроме того, Derby достаточно быстра для удовлетворения потребностей Web-приложения среднего размера.

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

Как получить Derby

Derby входит в состав проекта Apache DB. На момент написания статьи, ее последней версией является 10.1.3.1. Если вы предпочитаете работать с интегрированной средой разработки Eclipse, вам будут необходимы только два плагина: derby_core_plugin и derby_ui_plugin. В противном случае, выберите тот дистрибутив, который наиболее отвечает вашим нуждам. Различные варианты дистрибутивов содержат только файлы библиотек, файлы библиотек и документацию, библиотеку с отладочной информацией, или просто исходный код. Derby основана исключительно на технологии Java и будет работать на любой JVM версии 1.3 или более поздней. Примеры кода в данной статье рассчитаны на версию Java 1.4.

Установка Derby без Eclipse

Если вы не используете Eclipse, распакуйте скачанный дистрибутив в любое удобное место. Убедитесь, что файлы lib/derby.jar и lib/derbytools.jar находятся в вашей переменной classpath. Вы можете сделать это на уровне системы, в этом случае будет полезно указать в переменной окружения директорию DERBY_INSTALL, в которой расположена Derby (включая саму директорию Derby, например, /opt/bin/db-derby-10.1.3.1-bin). Вы так же можете сделать это через IDE или скрипт запуска. Если вы планируете использовать Derby в режиме клиента/сервера вдобавок к встроенному режиму, файлы lib/derbyclient.jar и lib/derbynet.jar должны также присутствовать в вашем classpath.

Настройка Derby с помощью Eclipse

Если вы используете Eclipse, настроить Derby для разработки будет немного проще. Для этого необходимо выполнить следующие шаги:

  1. Распакуйте два файла плагинов. Каждый из них имеет директорию с именем plugin.
  2. Скопируйте содержимое этих директорий в папку плагинов Eclipse.
  3. Откройте ваш проект в Eclipse.
  4. Нажмите Project > Add Apache Derby Nature, при этом в classpath будут добавлены четыре файла библиотек, а вам открыт доступ к командной строке ij.

На рисунке 1 изображено меню Derby после выполнения этих действий.

Рисунок 1. Меню Eclipse Derby
Меню Eclipse Derby

Даже если вы используете Eclipse для разработки, соответствующие JAR-файлы должны быть доступны при развертке вашего приложения. В более поздних статьях мы рассмотрим этот вопрос подробнее.

Создание шаблона базы данных

Перед использованием базы данных нам необходимо определиться со структурой хранящейся в ней информации. Мы еще не обсуждали требования к приложению Slickr, поэтому давайте предположим, что наша база данных в первую очередь должна содержать информацию о покупателях и заказах.

На данной ранней стадии нашего продукта смысл работы с базой заключается в реализации ее максимально простым и использующим наименьшее количество особенностей системы способом, даже если это означает создание дополнительного Java-кода. База данных является важной независимой частью системы, но важно не допускать ситуаций, когда БД управляет остальными ее частями. Нам необходимо минимизировать точки контакта между программой и базой данных таким образом, чтобы изменение любой части системы было легко осуществимо. Проблема в том, что улучшение производительности базы данных приводит к использованию определенной системы, так что данный вид оптимизации стоит осуществлять как можно позже.

Основа шаблона базы данных достаточно проста. Покупатели размещают заказы. Заказы состоят из одной или нескольких упаковок пиццы (забудем пока о том, что ресторан может продавать другую еду). Пицца содержит несколько (или ни одной) начинок, которые могут располагаться на половине пиццы или на всей пицце целиком.

Создание таблицы заказчиков

Теперь нам надо позаботиться о структуре информации о заказчиках, достаточной для доставки и подтверждения заказов, например так, как показано в листинге 1.

Листинг 1. Создание таблицы заказчиков
CREATE TABLE customers (
    id int generated always as identity constraint cust_pk primary key,
    first_name varchar(255),
    last_name varchar(255),
    phone varchar(15),
    address_1 varchar(200),
    address_2 varchar(200),
    city varchar(100),
    state varchar(2),
    zip varchar(10)
)

Синтаксис команды CREATE имеет один нестандартный параметр. Мы создаем колонку ID, которая должна поддерживать функционал автоинкрементации для каждого нового ряда. Выражение для обозначения этого поведения выглядит так:

id int generated always as identity

Другая опция для этой колонки могла бы выглядеть так:

generate by default as identity

Разница в том, что generate by default позволяет вам поместить ваше собственное значение в колонку, тогда как generate always - нет. Вы также выбираете колонку ID в качестве первичного ключа таблицы.

Всегда рекомендуется выбирать ID в базе данных без какой-либо привязки к реальным значениям. Возможно, кто-нибудь посоветует вам использовать телефонный номер в качестве ключа, потому что по нему можно однозначно определить заказчика; однако наверняка вы бы не хотели обновлять всю базу целиком из-за того, что кто-то переехал и изменил свой номер.

Создание таблицы заказов

В таблице заказов (см. листинг 2), нам необходимо указать покупателя, дату заказа и возможность скидки. Сумму к оплате можно рассчитать программно.

Листинг 2. Таблица заказов
CREATE TABLE orders ( 
	id int generated always as identity constraint ord_pk primary key,
    customer_id int constraint cust_foreign_key references customers, 
    order_time timestamp,
    discount float
)

Вдобавок к первичному ключу id, мы обьявили поле customer_id, которое будет внешним ключом, ссылающимся на таблицу customers. (Если мы не включаем внешнюю колонку в объявление поля, Derby предполагает, что мы ссылаемся на первичный ключ другой таблицы.) Это означает, что Derby будет проверять любое значение customer_id, добавленное в таблицу, на соответствие какому-либо заказчику в системе. Любой администратор баз данных расскажет вам, почему это следует делать. Однако бывают некоторые случаи, в которых нежелательна постоянная точная проверка. Например, нам необходимо ввести данные прежде, чем мы узнаем значение внешнего ключа; или понадобится удалить запись во внешней таблице, сохранив данные в текущей. В таком случае, например, нам нужно будет удалить заказчика, но сохранить данные о его заказах в целях сбора статистики. В Derby это возможно, однако могут возникнуть проблемы с переносом базы в другие системы БД.

Создание таблицы с данными о наполнителях.

Последние данные, о которых нам необходимо позаботиться - это информация о пицце и начинке. Схема хранения данных о начинках показана в листинге 3.

Листинг 3. Таблица с информацией о начинках
CREATE TABLE toppings(
    id int generated always as identity constraint top_pk primary key,
    name varchar(100), 
    price float
)

Теперь нам необходимо определить структуру взаимосвязей между информацией о пицце и о наполнителях. Данные о пицце должны содержать номер заказа, размер пиццы и набор наполнителей. Опираясь на классическую нормализацию баз данных, нам бы следовало создать таблицу Pizza, а затем установить отношение многие-ко-многим между ID пиццы и ID наполнителей. Такой подход имеет множество приятных сторон, например, он позволяет использовать любое количество наполнителей. Однако управление отношениями между таблицами отрицательно влияет на производительность. Если нам не требуется бесконечное количество начинок, мы можем включить в таблицу Pizza несколько полей для хранения данных о наполнителях (topping_1, topping_2, и т.д.). Это немного проще, но может сделать неудобным, например, сбор данных для определения наиболее популярных наполнителей. При желании, вы можете создать одно поле для данных о начинках и хранить там кодовую строку или битовую карту, но я бы не рекомендовал такой подход.

Создание таблицы с данными о пицце

После небольших раздумий, я решил остановиться на полностью нормальной форме. Если используется большое количество наполнителей для одной пиццы, помещение данных о них в ту же таблицу будет выглядеть достаточно уродливо. Таким образом, давайте используем код, показанный в листинге 4.

Листинг 4. Таблица Pizza
CREATE TABLE pizzas (
    id int generated always as identity constraint piz_pk primary key,
    order_id int constraint order_foreign_key references orders,
    size int 
)

CREATE TABLE pizza_topping_map (
    id int generated always as identity constraint ptmap_pk primary key,
    pizza_id int constraint pizza_fk references pizzas,
    topping_id int constraint topping_fk references toppings,
    placement int
)

Для внесения ясности отметим, что значения размеров 1, 2, 3, и 4 будут обозначать маленький, средний, большой и очень большой размеры соответственно. Расположение начинки (placement) равно -1, 0, 1 для левой половины, пиццы целиком, правой половины соответственно. А в силу того, что для каждого соответствия будет использоваться отдельный ID, заказ на пиццу с двойными анчоусами можно хранить, создав ссылку на эту начинку еще раз для той же пиццы.

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

На этом мы заканчиваем создание шаблона базы, теперь нам необходимо заполнить ее данными.

Заполнение базы данных информацией

После того, как мы получили шаблон базы данных, нам необходимо заполнить ее начальной информацией. Мы создадим небольшую отдельную программу, которая выполнит эту работу. Конечно, такой подход не является единственно верным - мы можем использовать командную строку Derby ij для прямого ввода SQL-команд, или любой графический SQL-интерфейс. Однако программный подход позволит нам увидеть, как запускается Derby, и чем она отличается от остальных JDBC баз данных. На практике, нам стоит хранить шаблон базы данных в качестве SQL-скрипта.

Мы начнем с некоторых относительно постоянных данных - списка начинок для пиццы, которые присутствовали на странице Slickr (см. Часть 1). Данный подход работает главным образом потому, что мы добавляем в базу статические данные. Мы заполним таблицу Toppings, в которой укажем для каждой начинки ее название и базовую цену. Соответствующий код приведен в листинге 5. Для начала предположим, что все начинки имеют одну и ту же цену.

Листинг 5. Заполнение таблицы Toppings в Derby
public class SlicrPopulatr {

    public static final String[] TOPPINGS = new String[] {
        "Anchovy", "Gardineria", "Garlic", 
        "Green Pepper", "Mushrooms", "Olives", 
        "Onions", "Pepperoni", "Pineapple", 
        "Sausage", "Spinach"
    }
    
    public void populateDatabase() throws Exception {
        Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
        Connection con = DriverManager.getConnection(
                "jdbc:derby:slicr;create=true");
        con.setAutoCommit(false);
        Statement s = con.createStatement();
        s.execute("DROP TABLE toppings");
        s.execute("CREATE TABLE toppings(" +
                "id int generated always as identity constraint top_pk primary key, " +
                "name varchar(100), " +
                "price float)");
        //        
        // Все эти утверждения созданных выше таблиц должны находиться здесь ...
        //
        for (int i = 0; i < TOPPINGS.length; i++) {
            s.execute("insert into toppings values (DEFAULT, '" +
                    TOPPINGS[i] + "', 1.25)");
        }
        con.commit();
        con.close();
        try {
            DriverManager.getConnection("jdbc:derby:;shutdown=true");
        } catch (SQLException ignore) {}
    }    
    
    public static void main(String[] args) throws Exception {
        (new SlicrPopulatr()).populateDatabase();
    }

Если вы знакомы с JDBC, большая часть этого кода вам будет понятна. Однако я отмечу пару особенностей, характерных для базы данных Derby. Мы начинаем с загрузки класс-драйвера, используя идиому Class.forName. Поскольку мы используем встроенную версию Derby, имя класса драйвера будет выглядеть как org.apache.derby.jdbc.EmbeddedDriver. Затем создадим строку соединения (connectionstring), для Derby она выглядит следующим образом:

jdbc:derby:database name;[attr=value]

database name -- это название базы данных, с помощью которого мы будем на нее ссылаться. Совершенно неважно, каким оно будет, главное, чтобы в серверном коде название везде было одинаковым.

После создания соединения мы попадаем в обычную среду JDBC. Создадим Statement для работы с командами удаления и пересоздания таблицы. Это позволит программно очистить базу данных, если она будет испорчена. (Иначе Derby создаст исключительную ситуацию, попытавшись создать таблицу, которая уже существует). После создания таблицы, мы будем использовать отдельную команду insert для добавления каждой записи в массив с "начинками".

В SQL-запросе insert присутствует одна особенность, которая может показаться вам неожиданной. Я использовал ключевое слово DEFAULT в качестве структурного нуля для колонки Identity. Derby будет ожидать в поле Identity это ключевое слово, если в запросе insert мы не определим список колонок.

В момент запуска программы, мы делаем специальный вызов для установления соединения с помощью URL "jdbc:derby:;shutdown=true" - базу данных указывать не нужно. Этот вызов просит Derby закрыть и освободить все возможные активные соединения.

После запуска этой маленькой программы в корневой директории вашего приложения появится папка, называющаяся derbyDb. Эта папка хранит бинарные файлы данных Derby. Ни в коем случае не изменяйте эти файлы.

Подготовка данных для GWT

После создания шаблона базы данных и загрузки статической информации, нам необходимо позаботиться о передаче данных клиенту и обратно. Фактически, нам необходимо упорядочить данные в рамках соединения клиент-сервер. Для того чтобы данное преобразование работало, наши фактические классы данных должны быть доступны для GWT, что означает, что они должны быть определены в пакете client и готовы для компиляции с помощью GWT-компилятора Java в JavaScript.

Для клиентского класса, который будет преобразован, существуют некоторые дополнительные ограничения. Во-первых, класс должен реализовывать интерфейс com.google.gwt.user.client.rpc.IsSerializable, который является маркерным интерфейсом, не определяющим никаких методов. Более того, все поля данных в классе так же должны быть преобразованы. (Как и в случае с обычным преобразованием в Java, мы можем освободить некоторые поля от преобразования, отметив их как transient.)

Что означает "упорядоченное поле"? Во-первых, это поле должно являться экземпляром типа, наследующего интерфейс IsSerializable, или иметь такой суперкласс. В другом случае, поле должно быть одним из базовых типов, которые включают в себя примитивы Java, все основные интерфейсные классы, Date и String. Массив или коллекция упорядоченных типов также упорядочены. Однако если мы попытаемся упорядочить Collection или List, GWT будет необходима аннотация с помощью комментария Javadoc, указывающая фактический тип, чтобы компилятор мог его оптимизировать. Листинг 6 показывает соответствующий пример для поля и метода.

Листинг 6. Упорядоченные поле и метод
/**
 * @gwt.typeArgs <java.lang.Integer>
 */
private List aList; 

/**
 * @gwt.typeArgs <java.lang.Double>
 * @gwt.typeArgs argument <java.lang.String>
 */
public List doSomethingThatReturnsAList(List argument) {
    // Здесь идет заполнение
}

Обратите внимание: Имя аргумента метода указано в комментарии, в отличие от возвращаемого значения.

Заметьте, что все, что имеет отношение к java.sql и JDBC, не находится в списке преобразованных объектов. Все операции между выборкой и объектом данных должны проводиться на стороне сервера.

С этого момента мы попадаем в мир Object-Relational Mapping (ORM), то есть перевода данных из структуры реляционной БД в объектно-ориентированную структуру нашей Java-программы. При дальнейшей работе вы, возможно, захотите использовать одну из существующих мощных систем ORM, таких, как Hibernate или Castor. Обе данные системы автоматически загружают данные из базы в необходимые объекты Java. Однако они требуют достаточно сложной конфигурации перед тем, как будут готовы к использованию. Так как данная статья сфокусирована на Derby и GWT, я предлагаю использовать простой конвертер, который поможет нам в начале разработки. Конечно, вы можете поменять его на более мощный инструмент.

Простой ORM конвертер

Во-первых, создадим bean-классы для всех таблиц с данными. Я использую класс Topping в качестве примера, потому что он простой и уже имеет данные. Используя стандартные наименования для каждого поля, уберем знаки подчеркивания (к примеру, topping_id станет getToppingId). Листинг 7 показывает класс Topping.

Листинг 7. Класс Topping
package com.ibm.examples.client;

import com.google.gwt.user.client.rpc.IsSerializable;

public class Topping implements IsSerializable {

    private Integer id;
    private String name;
    private Double price;

    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;
    }
    public Double getPrice() {
        return price;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
}

Следующим будет наш ORM-инструмент, как показано в листинге 8.

Листинг 8. Простой ORM-инструмент
package com.ibm.examples.server;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class ObjectFactory {

    public static String convertPropertyName(String name) {
        String lowerName = name.toLowerCase();
        String[] pieces = lowerName.split("_");
        if (pieces.length == 1) {
            return lowerName;
        }
        StringBuffer result = new StringBuffer(pieces[0]);
        for (int i = 1; i < pieces.length; i++) {
            result.append(Character.toUpperCase(pieces[i].charAt(0)));
            result.append(pieces[i].substring(1));
        }
        return result.toString();
    }

    public static List convertToObjects(ResultSet rs, Class cl) {
        List result = new ArrayList();
        try {
            int colCount = rs.getMetaData().getColumnCount();
            while (rs.next()) {
                Object item = cl.newInstance();
                for (int i = 1; i <= colCount; i += 1 ) {
                    String colName = rs.getMetaData().getColumnName(i);
                    String propertyName = convertPropertyName(colName);
                    Object value = rs.getObject(i);
                    PropertyDescriptor pd = new PropertyDescriptor(propertyName, cl);
                    Method mt = pd.getWriteMethod();
                    mt.invoke(item, new Object[] {value});
                }
                result.add(item);
            } 
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return result;
    }
}

Метод convertToObjects() проходит через результирующую выборку, получает имя get-методов и устанавливает все значения. Метод convertPropertyName() устанавливает соответствие между SQL-наименованиями (с_подчеркиваниями) и Java-именованием (сЗаглавнымиБуквами).

Что не может сделать данный инструмент ORM

Функционал данного инструмента лишен множества полезных особенностей, например:

  • Защита от создания нескольких версий одного объекта.
  • Запись в базу данных.
  • Высокая производительность.

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

Листинг 9 показывает данный инструмент в действии, считывая обратно все экземпляры Topping, созданные ранее.

Листинг 9. Испытание ORM-инструмента
public class ToppingTestr {

    public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";

    public static final String PROTOCOL = "jdbc:derby:slicr;";

    public static void main(String[] args) throws Exception {
        try {
            Class.forName(DRIVER).newInstance();
            Connection con = DriverManager.getConnection(PROTOCOL);
            Statement s = con.createStatement();
            ResultSet rs = s.executeQuery("SELECT * FROM toppings");
            List result = ObjectFactory.convertToObjects(rs, Topping.class);
            for (Iterator itr = result.iterator(); itr.hasNext();) {
                Topping t = (Topping) itr.next();
                System.out.println("Topping " + t.getId() + ": " +
                        t.getName() + " is $" + t.getPrice());
            }
        } finally {
            try {
                DriverManager.getConnection("jdbc:derby:;shutdown=true");
            } catch (SQLException ignore) {}
        }
    }

}

Эта тестовая программа создает Derby-соединение с базой данных Slickr. Мы выполняем несложный SQL-запрос, и затем получаем результаты для дальнейшей обработки; после чего мы можем просмотреть полученные данные и выйти из базы.

В следующей статье

Теперь ваша база установлена и сконфигурирована должным образом. Мы создали схему базы данных и разобрались с некоторыми простыми методами, позволяющими заполнить ее данными. После прочтения двух статей из данной серии, ваш проект Slickr имеет простые, но функциональные клиентскую и серверную части. Следующим шагом будет являться налаживание взаимодействия между ними. В третьей статье из этой серии, вы узнаете о механизме, используемом в GWT для простого создания и управления удаленными вызовами процедур (RPC).

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

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=Open source, Information Management, Технология Java
ArticleID=284859
ArticleTitle=Создание Ajax-проекта с использованием инструментария Google Web Toolkit, Apache Derby и Eclipse: Часть 2. Надежная серверная часть
publish-date=01302008