GWT+EXTJS подключаем MySQL

Очень долго, чуть больше недели готовил эту статью, начал с того что написал по статье об установке каждого используемого компонента, а теперь не знаю с чего начать :)

Начну с описания того, что буду делать. Я попробую создать тонкий клиент используя GWT и GWT-Ext. Кто уже на этом месте начал кричать «GWT-Ext устарела надо использовать GXT» может смело закрывать вкладку браузера [x]. Остальные если не знают что такое и как установить IntelliJ IDEA и Maven отвлекаются от статьи на 20 минут на прочтение инструкций и установку софта. Инструкция об установке Google Web Toolkit тоже имеется, а вот по установке MySQL мне было лень писать.

Теперь собственно о тонком клиенте, он будет немного не доделан. Дело в том что в одну статью все не впихнуть и в этой будут продемонстрированы только некоторые части интерфейса, потому что основной темой будет взаимодействие с базой данных MySQL. Все известные мне мануалы рассказываю как передавать данные в GWT используя XML или JSON сгенерированные PHP, а я хочу чтобы GWT получал данные напрямую из базы и передавал на клиент в сериализированом виде.

UPD Тут были какието ссылки для скачивания, но они уже жавно не работают.

Теперь об используемых компанентах: помимо всего прочего я буду использовать Spring Framefork, Hibernate, Log4j, JUnit и базу данных MySQL. Для того чтобы не разруливать немыслимое количество зависимостей в этих компонентах я и предложил вам поставить Maven, а еще он удобен для сборки билда. Я написал конфигурационный файл в который включил все необходимые зависимости и плагины для сборки. Я, если честно, не очень силен в конфигурации Maven’а, поэтому большая часть настроек была скопирована из мануала по GWT. Конфиг очень большой и его нет смысла выкладывать здесь, поэтому чтоб его посмотреть придется скачать проект.

Когда загрузите проект откроете закладку Maven’а и нажмете на кнопку закачать все артефакты

Control Panel

Если у вас медленное соединение или еще какие-то проблемы Maven может закачать не все и отказаться скачивать повторно. В общем я советую сразу через командную строку зайти в папку с проектом и выполнить команду:


mvn eclipse:eclipse -Declipse.downloadSources=true -DdownloadJavadocs=true

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

В начале статьи была ссылка на EXT-JS, нужно скачать именно версию 2.0.2 (или 2.0.5 но тогда вы нарушите лицензию) c более поздними GWT-EXT работать не хочет. EXT-JS нужно распаковать в D:ideaHelloWorldsrcmainjavauakievmabppublicjsext , позже я объясню почему именно сюда.

Теперь когда у вас есть все исходники, нужно настроить базу данных, откройте файл D:ideaHelloWorldsrcmainresourcesdb.properties и начинайте править настройки подключения.


jdbc.driverClassName=com.mysql.jdbc.Driver			<- не менять
jdbc.url=jdbc:mysql://localhost:3306/helloworld		<- helloworld это имя базы
jdbc.username=root						<- пользователь
jdbc.password=qwerty					<- пароль
hibernate.dialect=org.hibernate.dialect.MySQL5Dialect	<- диалект
hibernate.default_schema=helloworld			<- схема, можно не трогать
Первую строку строгать не надо, во второй, после последнего слеша надо написать название БД, в третей - имя пользователя, в четвертой - пароль, разрабатывал на MySQL 5.1 если у вас 4 то в пятой надо убрать цифру 5, шестую можно не трогать. Структура таблицы и дамп данных наже:


SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

CREATE TABLE IF NOT EXISTS `car` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `model` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=43 ;

INSERT INTO `car` (`id`, `model`) VALUES
(1, 'Audi'),
(2, 'BMW'),
(3, 'Lexus'),
(4, 'Ford'),
(5, 'Honda'),
(6, 'Chevrolet'),
(7, 'KIA'),
(8, 'Mercedes'),
(9, 'Mitsubishi'),
(10, 'Nissan'),
(11, 'Opel'),
(12, 'Rover'),
(13, 'Subaru'),
(14, 'Toyota'),
(15, 'Volkswagen'),
(16, 'Volvo'),
(17, 'Mazda'),
(18, 'Skoda'),
(19, 'Lincoln'),
(20, 'Infiniti'),
(21, 'Cadillac'),
(22, 'Renault'),
(23, 'Daevoo'),
(24, 'Jeep'),
(25, 'Pontiac'),
(26, 'Hummer'),
(27, 'Bentley'),
(28, 'Isuzu'),
(29, 'Seat'),
(30, 'Fiat'),
(31, 'GMC'),
(32, 'Land Rover'),
(33, 'Porsche'),
(34, 'Jaguar'),
(35, 'Hyundai'),
(36, 'Peugeot'),
(37, 'Aston Martin'),
(38, 'Alfa Romeo'),
(39, 'Dodge'),
(40, 'Ferrari'),
(41, 'Saab'),
(42, 'Chrysler');

Все с базой почти закончили. Теперь надо зайти в Tools->Data Source (в восьмой идеи это панелька сбоку а в седьмой новое окно) и добавить новый источник данных как показано на скриншоте:

Control Panel

И наконец надо закачать данные о структуре при помощи кнопки Refresh Tables. Вот теперь с базой все.

Предполагается что вы все делали как я описывал в статьях по установке и все находиться на диске D:Program Files , если нет заходите в Project Structure и настраивайте

В закладке GWT указываете путь к GWT, после этого нажимать на кнопку FIX не нужно, потому что у вас dll'ки лежат в одной директории (D:/Program Files/gwt-windows-1.5.3), а jar'ники в другой (D:/Program Files/apache-maven-2.1.0/repo/com/google/gwt/gwt-user1.5.3).

Control Panel

В закладке Spring скорее всего ничего править не придется потому что все три конфига подтянутся сами, но проверьте на всякий случай.

Control Panel

В закладке JPA подключите только что созданный источник данных.

Control Panel

В закладке WEB тоже ничего делать не надо там все само подхватывается.

Control Panel

Возвращайтесь в корень настроек проекта, там три закладки, на первой (Sources) отметьте папки с исходниками (main/java), тестами (test/java)

Control Panel

На второй (Paths) кажется ничего делать не надо

Control Panel

А на закладке (Dependencies) проверьте чтоб не было пакета gwt-user (это значит вы все-таки нажали на кнопку FIX на закладке GWT) и был файл javaee.jar

Control Panel

Так все настройки закончили переходим к запуску программы.

Maven и GWT навязываю нам свою структуру проекта. От Maven нам достается разделение кода на исходники (main) и тесты (test), а от GWT три пакета: client - то что в последствии станет javascript кодом, public - статические ресурсы, сюда кладется ext-js, и server - классы которые работают на сервере, например для работы с БД.

Для минимального запуска нужно всего несколько файлов. Клаcc ua.kiev.mabp.client.HelloWorld расширяет точку входа в приложение, там задаются основные layuot'ы. HelloWorld.css и HelloWorld.html нужны для каркаса с которым будут работать все js функции. В HelloWorld.gwt.xml указываеться какие модули наследует сценарий. И наконец web.xml определяет точку входа (servlet) в приложение.

В примере есть несколько layaut'ов. Первый это выбор страны и города, демонстрирует функционал связанных выпадающих списков. Второй выбор даты, календари тоже должны были не пересекаться но к сожалению в ext-js 2.0.2 этого функционала еще не было (я опишу его в следующей статье где использую ext-js 2.2.1) поэтому использована валидация полей. Выбор марки автомобиля собственно тат самый виджет ради которого вся статья. И последний это выбор цвета, там совсем все просто.


package ua.kiev.mabp.client;

import com.google.gwt.core.client.EntryPoint;
import com.gwtext.client.widgets.Panel;
import com.gwtext.client.widgets.Viewport;
import com.gwtext.client.widgets.form.FieldSet;
import com.gwtext.client.widgets.layout.AnchorLayout;
import com.gwtext.client.widgets.layout.AnchorLayoutData;
import com.gwtext.client.widgets.layout.ColumnLayout;
import com.gwtext.client.widgets.layout.ColumnLayoutData;
import ua.kiev.mabp.client.widgets.CarPanel;
import ua.kiev.mabp.client.widgets.ColorPanel;
import ua.kiev.mabp.client.widgets.DatePanel;
import ua.kiev.mabp.client.widgets.LocationPanel;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

public class HelloWorld implements EntryPoint {

	public void onModuleLoad() {
		Panel panel = new Panel();
		panel.setBorder(false);
		panel.setPaddings(15);
		panel.setLayout(new AnchorLayout());

		panel.add(new FieldSet(), new AnchorLayoutData("100%"));

		Panel wrapperPanel = new Panel();
		wrapperPanel.setBorder(false);

		Panel columnOnePanel = new Panel();
		columnOnePanel.setBorder(false);
		columnOnePanel.setLayout(new AnchorLayout());
		columnOnePanel.add(new LocationPanel(), new AnchorLayoutData("100%"));
		columnOnePanel.add(new DatePanel(), new AnchorLayoutData("100%"));
		columnOnePanel.add(new CarPanel(), new AnchorLayoutData("100%"));

		Panel columnTwoPanel = new Panel();
		columnTwoPanel.setBorder(false);
		columnTwoPanel.setLayout(new AnchorLayout());
		columnTwoPanel.add(new ColorPanel(), new AnchorLayoutData("100%"));

		wrapperPanel.setLayout(new ColumnLayout());
		wrapperPanel.add(columnOnePanel, new ColumnLayoutData(.5));
		wrapperPanel.add(columnTwoPanel, new ColumnLayoutData(.5));

		panel.add(wrapperPanel, new AnchorLayoutData("100%"));
		panel.add(new FieldSet(), new AnchorLayoutData("100%"));

		new Viewport(panel);
	}
}

Немного поигравшись с layaut'ами можно начать прикручивать БД, данные из которой получает выпадающий список.


package ua.kiev.mabp.client.widgets;

import com.gwtext.client.core.Template;
import com.gwtext.client.core.UrlParam;
import com.gwtext.client.data.*;
import com.gwtext.client.widgets.Panel;
import com.gwtext.client.widgets.form.ComboBox;
import com.gwtext.client.widgets.form.FieldSet;
import com.gwtext.client.widgets.form.event.ComboBoxListenerAdapter;
import ua.kiev.mabp.client.proxies.MyProxy;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

public class CarPanel extends FieldSet {
	public CarPanel(){
	super("Select your car");

		// определяе вид записи
		FieldDef[] fieldDefs = new FieldDef[]{new StringFieldDef("id"), new StringFieldDef("model")};
		RecordDef recordDef = new RecordDef(fieldDefs);

		// создаем хранилище
		final Store store = new Store(new MyProxy(), new ArrayReader(recordDef), true);
		store.setBaseParams(new UrlParam[]{new UrlParam("paramName", "paramValue")});

		// создаем новую панель
		// сюда будет дублироваться выбраный результат
		final Panel instructionPanel = new Panel();
		instructionPanel.setBorder(false);
		instructionPanel.setHeight(24);
		instructionPanel.setHtml("Car model doesn't selected yet");

		// создаем новый шаблон
		final Template template = new Template("<div class="x-combo-list-item">" +
				"<table><tbody><tr>" +
				"<td style="width:50px;border-right:2px dotted #A3BAE9;">{id}</td>" +
				"<td style="width:200px;border-right:2px dotted #A3BAE9;">{model}</td>" +
				"</tr></tbody></table>" +
				"<div class="x-clear"></div></div>");

		// создаем новый выпадающий список 
		// и присваеваем ему хранилище и шаблон
		final ComboBox carComboBox = new ComboBox();
		carComboBox.setMinChars(1);
		carComboBox.setFieldLabel("Label");
		carComboBox.setStore(store);
		carComboBox.setDisplayField("model");
		carComboBox.setMode(ComboBox.LOCAL);
		carComboBox.setTriggerAction(ComboBox.ALL);
		carComboBox.setForceSelection(true);
		carComboBox.setEmptyText("Choose car model");
		carComboBox.setTypeAhead(true);
		carComboBox.setSelectOnFocus(true);
		carComboBox.setResizable(true);
		carComboBox.setTpl(template);
		carComboBox.setHideLabel(true);

		// устанавливаем листенер
		carComboBox.addListener(new ComboBoxListenerAdapter() {
			public void onSelect(ComboBox comboBox, Record record, int index) {
				instructionPanel.setHtml(record.getAsString("id") + " " + record.getAsString("model"));
			}
		});

		// добавляем все в текущий layout
		add(carComboBox);
		add(instructionPanel);

		// загружаем в список 5 первых элементов
		store.load(0, 5);
	}
}

Итак кроме того что виджет CarPanel создает ComboBox с листенером он использует для получения данных прокси MyProxy.

Итак кроме того что виджет CarPanel создает ComboBox с листенером он использует для получения данных прокси MyProxy.


package ua.kiev.mabp.client.proxies;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.gwtext.client.core.UrlParam;
import ua.kiev.mabp.client.HelloWorldService;
import ua.kiev.mabp.client.beans.ArrayBean;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

public class MyProxy extends GWTProxy {

	/**
	 * Загружает данные в объект
	 *
	 * @param start позиция первого элемента
	 * @param limit количество элемента
	 * @param sort сортировка
	 * @param dir направление сортировки
	 * @param javaScriptObject объект
	 */
	public void load(int start, int limit, String sort, String dir, JavaScriptObject javaScriptObject) {
		load(start, limit, sort, dir, javaScriptObject, null);
	}
	
	/**
	 * Загружает данные в объект
	 *
	 * @param start позиция первого элемента
	 * @param limit количество элемента
	 * @param sort сортировка
	 * @param dir направление сортировки
	 * @param o объект
	 * @param baseParams параметры запроса
	 */
	public void load(int start, int limit, String sort, String dir, final JavaScriptObject o, UrlParam[] baseParams) {
		String[][] params = new String[0][];
		HelloWorldService.App.getInstance().getCars(start, limit, sort, dir, params, new AsyncCallback() {
			public void onFailure(Throwable caught) {
				loadResponse(o, false, 0, (JavaScriptObject) null);
			}
			
			public void onSuccess(Object result) {
				ArrayBean response = (ArrayBean) result;
				loadResponse(o, true, response.totalPages, response.data);
			}
		});
	}
}

Класс MyProxy наследует класс GWTProxy который был выдран мной из проекта gwt-ext-ux (com.gwtextux.client.data.GWTProxy), который я счел не нужным подключать из-за одного класса. Самый большой интерес в классе MyProxy представляет вызов метода HelloWorldService.App.getInstance().getCars() .


package ua.kiev.mabp.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import ua.kiev.mabp.client.beans.ArrayBean;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

public interface HelloWorldService extends RemoteService {

	public static class App {
		private static HelloWorldServiceAsync ourInstance = null;

		public static synchronized HelloWorldServiceAsync getInstance() {
			if (ourInstance == null) {
				ourInstance = (HelloWorldServiceAsync) GWT.create(HelloWorldService.class);
				((ServiceDefTarget) ourInstance).setServiceEntryPoint(GWT.getModuleBaseURL() + "ua.kiev.mabp.HelloWorld/HelloWorldService");
			}
			return ourInstance;
		}
	}

	public ArrayBean getCars(int start, int limit, String sort, String dir, String params[][]);
}

HelloWorldService это интерфейс описанный в пакете client и имеющий реализацию в пакете server. Внутренний класс App знает о бэкенде у которого можно получить данные. Этот бекэнд описывается в конфигах в HelloWorld.gwt.xml и web.xml .


package ua.kiev.mabp.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import ua.kiev.mabp.server.beans.DaoManager;
import ua.kiev.mabp.server.beans.ArrayHelper;
import ua.kiev.mabp.server.beans.entity.Car;
import ua.kiev.mabp.client.HelloWorldService;
import ua.kiev.mabp.client.beans.ArrayBean;
import java.util.Collection;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

public class HelloWorldServiceImpl extends RemoteServiceServlet implements HelloWorldService {
	DaoManager daoManager = DaoManager.getInstance();
	
	public ArrayBean getCars(int start, int limit, String sort, String dir, String params[][]) {
		ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:ApplicationConfig.xml");
		Collection<Car> col = daoManager.getCarDao().getCarByCriteria(start, limit, "ASC".equals(dir), sort);
		return new ArrayBean(ArrayHelper.collectionToArrayString(col), 30);
	}
}

Реализующий интерфейс HelloWorldService класс HelloWorldServiceImpl, знает о конфигах Spring. Этот конфиг знает о файле db.properties в котором описано как подключиться к базе. Я не буду опубликовывать тут весь конфиг, только самую интересную его часть.


<bean id="car" class="ua.kiev.mabp.server.beans.dao.CarDAO">
	<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="daoManager" class="ua.kiev.mabp.server.beans.DaoManager" factory-method="getInstance">
	<property name="carDao" ref="car"/>
</bean>

Тут описывается что singleton класс DaoManager для своего создания использует метод getInstance и что ему нужно наполнить свойство carDao классом CarDAO


package ua.kiev.mabp.server.beans.dao;

import ua.kiev.mabp.server.beans.entity.Car;

import java.util.List;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

public class CarDAO extends DAOBean  {
	
	/**
	 * Сохраняет элемент
	 *
	 * @param bean элемент
	 */
	public void save(Car bean) {
		getHibernateTemplate().save(Car.class.getName(), bean);
	}
	
	/**
	 * Удаляет элемент
	 *
	 * @param bean элемент
	 */
	public void delete(Car bean) {
		getHibernateTemplate().delete(bean);
	}
	
	/**
	 * Получает список объектов
	 *
	 * @param start позиция первого элемента
	 * @param limit количество элемента
	 * @param sort сортировка
	 * @param fieldName название поля
	 * @return список
	 */
	public List<Car> getCarByCriteria(int start, int limit, boolean sort, String fieldName) {
		return getObjectsByCriteria(start, limit, sort, fieldName, Car.class);
	}
	
	/**
	 * Получает весь список операций
	 *
	 * @return коллекция операций
	 */
	public List<Car> getAllCar() {
		return getHibernateTemplate().loadAll(Car.class);
	}
	
	/**
	 * Получает список операция по производству машины
	 *
	 * @param s где произведена
	 * @return коллекция операция
	 */
	public List<Car> getCarByModel(String s) {
		return getHibernateTemplate().findByNamedQueryAndNamedParam("findByModelFromCar", "model", s);
	}
	
	/**
	 * Получает операции по id
	 *
	 * @param id номер операции
	 * @return коллекция операций
	 */
	public Car getCarById(Integer id) {
		return (Car) getHibernateTemplate().get(Car.class, id);
	}
}

Класс CarDAO содержит метод getCarByCriteria который вызывается в классе HelloWorldServiceImpl. Метод возвращает список бинов Car. Немного отвлекусь и расскажу о дополнительных возможностях. Вместо вызова getCarByCriteria можно вызывать getCarByModel передавая модель машины, это будет полезно в каскадных списках, при этом findByModelFromCar описано в классе Car в виде аннотаций.


package ua.kiev.mabp.server.beans.entity;

import javax.persistence.*;
import java.io.Serializable;

/**
 * Created by IntelliJ IDEA.
 * User: CTAPbIu_MABP
 * Date: 10.04.2009
 * Time: 22:58:15
 */

@Entity
@Table(name = "car")
@NamedQueries({
	@NamedQuery(name = "findByModelFromCar", query = "select t from Car t where t.model=:model order by t.model"),
	@NamedQuery(name = "findByIdFromCar", query = "select t from Car t where t.id=:id")
})
public class Car implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "id", unique = true, nullable = false, length = 11)
	private Integer id;
	@Column(name = "model")
	private String model;
	private static final long serialVersionUID = 5664909385244426031L;

	/**
	 * Getter for property 'id'.
	 *
	 * @return Value for property 'id'.
	 */
	public Integer getId() {
		return id;
	}
	
	/**
	 * Setter for property 'id'.
	 *
	 * @param id Value to set for property 'id'.
	 */
	public void setId(Integer id) {
		this.id = id;
	}
	
	/**
	 * Getter for property 'model'.
	 *
	 * @return Value for property 'model'.
	 */
	public String getModel() {
		return model;
	}
	
	/**
	 * Setter for property 'model'.
	 *
	 * @param model Value to set for property 'model'.
	 */
	public void setModel(String model) {
		this.model = model;
	}
}

После того как класс HelloWorldServiceImpl получит коллекцию бинов Car он должен превратить ее в двухмерный массив строк, который после сериализации можно передать клиенту. Все стек распутан, а рассказ подошел к концу. И стоит показать как выглядит то ради чего вы все это читали.

Control Panel

Все кто дочитал до конца молодец, а я готовлю следующую статью, до скорых встреч.