Интеграция сайта на Struts и Webix c базой данных

Эта статья является последней частью руководства, в котором рассказывалось о разработке сайта с использованием библиотеки Webix UI и Java-фреймворка Struts 2. Если вы еще не знакомы с предыдущими частями руководства, то мы советуем вам прочитать о “Разработке базовой функциональности сайта” и о “Создании страниц и форм” с Webix и Struts 2.

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

Структура базы данных проста (она представлена на изображении ниже). Для хранения событий и докладов будем использовать две таблицы. Каждая запись в таблице speakers содержит идентификатор события event_id, к которому относится доклад.

webix and struts database

Для создания базы данных используйте следующие запросы:

CREATE TABLE IF NOT EXISTS `events` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
`date` date NOT NULL,
`location` varchar(255) NOT NULL,
`photo` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;

INSERT INTO `events` (`id`, `name`, `description`, `date`, `location`, `photo`) VALUES
(1, 'Front-end #1', 'На предстоящем событии мы рассмотрим разные аспекты разработки веб-приложения: Promises, AntiAliasing, HTML-импорт, использование DevTools по полной! Ждем вас!', '2014-06-06', 'Минск, ул.Центральная, д.1, Синий конференц-зал', 'frontend1.png'),
(2, 'Front-end #2', 'Планируются интереснейшие доклады по Front-end разработке! Их представят настояние гуру JavaScript-программирования!', '2014-06-20', 'Минск, ул.Центральная, д.1, Синий конференц-зал', 'frontend2.png'),
(3, 'Front-end #3', 'Планируются интереснейшие доклады по Front-end разработке! Их представят настояние гуру JavaScript-программирования!', '2014-07-04', 'Минск, ул.Центральная, д.1, Синий конференц-зал', 'frontend3.png'),
(4, 'Front-end #4', 'Планируются интереснейшие доклады по Front-end разработке! Их представят настояние гуру JavaScript-программирования!', '2014-07-18', 'Минск, ул.Центральная, д.1, Синий конференц-зал', 'frontend4.png'),
(5, 'Front-end #5', 'Планируются интереснейшие доклады по Front-end разработке! Их представят настояние гуру JavaScript-программирования!', '2014-08-01', 'Минск, ул.Центральная, д.1, Синий конференц-зал', 'frontend5.png'),
(6, 'Front-end #6', 'Планируются интереснейшие доклады по Front-end разработке! Их представят настояние гуру JavaScript-программирования!', '2014-08-15', 'Минск, ул.Центральная, д.1, Синий конференц-зал', 'frontend6.png');

CREATE TABLE IF NOT EXISTS `speakers` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`author` varchar(255) NOT NULL,
`topic` varchar(255) NOT NULL,
`photo` varchar(255) NOT NULL,
`event_id` bigint(20) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;

INSERT INTO `speakers` (`id`, `author`, `topic`, `photo`, `event_id`, `description`) VALUES
(1, 'Железный человек', 'JavaScript Promises - Туда и обратно', 'ironman.jpg', 1, 'Одна из новых особенностей которые нам готовят разработчики браузеров совместно с группами разработчиков пишущих спецификации — JavaScript Promises — полюбившийся многим шаблон написания асинхронного кода обзаводится нативной поддержкой. Что же такое обещания и с чем их едят?'),
(2, 'Халк', 'Избегаем ненужных перерисовок', 'halk.jpg', 2, 'Отрисовка элементов для сайта или приложения может быть долгой, и может иметь негативное влияние на производительность. В этом докладе мы рассмотрим, что может вызывать перерисовку в браузере и как избежать ненужных вызовов.'),
(3, 'Человек-паук', 'Использование вашего терминала в DevTools', 'spiderman.jpg', 1, 'DevTools Terminal - это новое расширение для браузера Chrome, которое оргагинзует работу командной строки прямо в вашем браузере.'),
(4, 'Тор', 'Высокопроизводительная анимация', 'thor.jpg', 2, 'Глубокое погружение в быструю анимацию в ваших проектах. Мы узнаем, почему современные браузеры могут легко анимировать следующие характеристики: позиция, масштаб, поворот и прозрачность.'),
(5, 'Бэтмэн', 'AntiAliasing. Начало.', 'batman.jpg', 1, 'Введение в antialiasing, объяснение, как отображать отображать векторные фигуры и текст красиво.'),
(6, 'Капитан Америка', 'HTML-импорт', 'captainamerica.jpg', 1, 'HTML-импорт - это способ включать одни HTML документы в другие. Вы не ограничиваетесь только разметкой, вы можете также включать CSS, JavaScript или что угодно, что может содержаться в .html файле.');

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

Для работы с Hibernate, необходимо добавить зависимости hibernate-core и mysql-драйвер mysql-connector-java в файл pom.xml:



org.hibernate
hibernate-core
4.3.5.Final

mysql
mysql-connector-java
5.1.6

После этого надо выполнить Maven — Update project, чтобы Maven загрузил новые библиотеки.

Создаем конфигурационный файл src/main/resources/hibernate.cfg.xml:

<!--?xml version='1.0' encoding='utf-8'?-->

com.mysql.jdbc.Driver jdbc:mysql://localhost:3306/myapp true UTF-8 UTF-8 root 1 org.hibernate.dialect.MySQLDialect thread org.hibernate.cache.NoCacheProvider true validate

В этом файле мы настраиваем соединение с базой данных:

  • connection.url — строка подключения к базе данных в формате jdbc:driver://host:port/dbname;
  • connection.username — имя пользователя базы данных;
  • connection.password — пароль пользователя базы данных;

Также в этом файле нужно указать, какие классы будут привязаны к базе данных. В нашем случае это классы Event и Speaker. Это значит, что один объект класса Event будет соответствовать одной записи в таблице events базы данных. Аналогичная логика действует и для класса Speaker.

Класс Event уже существует, однако его надо немного отредактировать. Добавьте к нему аннотации, в которых будет указано, к какой таблице базы данных надо привязать класс и как свойства класса соотносятся с полями таблицы:

package com.myapp.model;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import org.apache.struts2.json.annotations.JSON;

@Entity
@Table(name="events")
public class Event {

private Long id;
private String name;
private String description;
private Date date;
private String location;
private String photo;

public Event() {

}

public Event(Long id, String name, String description, Date date, String location, String photo) {
this.id = id;
this.name = name;
this.description = description;
this.date = date;
this.location = location;
this.photo = photo;
}

@Id
@GeneratedValue
@Column(name="id")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}

@Column(name="name")
public String getName() {
return name;
}

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

@Column(name="description", columnDefinition="TEXT")
public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

@Column(name="date")
@JSON(format = "yyyy-MM-dd")
public Date getDate() {
return date;
}

public void setDate(Date date) {
this.date = date;
}

@Column(name="location")
public String getLocation() {
return location;
}

public void setLocation(String location) {
this.location = location;
}

@Column(name="photo")
public String getPhoto() {
return photo;
}

public void setPhoto(String photo) {
this.photo = photo;
}

}

Аннотация @Table(name=»events») перед объявлением класса указывает, какую таблицу использовать, а @Column(name=»location») перед свойством location — какое поле из базы данных сопоставить этому свойству.

Создадим аналогичный класс Speaker:

package com.myapp.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="speakers")
public class Speaker {

private Long id;
private String author;
private String topic;
private String description;
private String photo;
private Long event_id;

public Speaker() {

}

public Speaker(Long id, String author, String topic, String description, String photo, Long event_id) {
this.id = id;
this.author = author;
this.topic = topic;
this.description = description;
this.photo = photo;
this.event_id = event_id;
}

@Id
@GeneratedValue
@Column(name="id")
public Long getId() {
return id;
}

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

@Column(name="author")
public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

@Column(name="topic")
public String getTopic() {
return topic;
}

public void setTopic(String topic) {
this.topic = topic;
}

@Column(name="description", columnDefinition="TEXT")
public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

@Column(name="photo")
public String getPhoto() {
return photo;
}

public void setPhoto(String photo) {
this.photo = photo;
}

@Column(name="event_id")
public Long getEvent_id() {
return event_id;
}

public void setEvent_id(Long event_id) {
this.event_id = event_id;
}
}

Для работы с Hibernate нам понадобится класс, который будет создавать фабрику сессий, или возвращать уже существующую. Создадим его в пакете com.myapp.util и назовем HibernateUtil:

package com.myapp.util;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

private static final SessionFactory sessionFactory = buildSessionFactory();
private static SessionFactory buildSessionFactory() {
try {
return new Configuration()
.configure() // configures settings from hibernate.cfg.xml
.buildSessionFactory();
} catch (Throwable ex) {
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}

Для выполнения операций над объектами базы данных создадим классы EventsManager и SpeakersManager. Эти классы разместим в пакете com.myapp.controller. В задачи этих классов входит выборка списка событий/докладов (метод list), выборка последних трех записей (метод lastList), выборка записи по идентификатору (getById), добавление новой записи (метод insert), редактирование существующей записи (метод update), удаление записи (метод delete).

EventsManager.java:

package com.myapp.controller;

import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Session;

import com.myapp.model.Event;
import com.myapp.util.HibernateUtil;

public class EventsManager extends HibernateUtil {

public List list() {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
List events = null;
try {
events = (List)session.createQuery("from Event").list();
} catch (HibernateException e) {
e.printStackTrace();
session.getTransaction().rollback();
}
session.getTransaction().commit();
return events;
}

public Event getById(Long eventId) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
Event event = (Event) session.get(Event.class, eventId);
session.getTransaction().commit();
return event;
}

public Event update(Event event) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.update(event);
session.getTransaction().commit();
return event;
}

public Event delete(Event event) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.delete(event);
session.getTransaction().commit();
return event;
}

public Event insert(Event event) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.save(event);
session.getTransaction().commit();
return event;
}
}

Speakers.java:

package com.myapp.controller;

import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;

import com.myapp.model.Speaker;
import com.myapp.util.HibernateUtil;

public class SpeakersManager extends HibernateUtil {

public List list() {
return list(null);
}

public List list(Long eventId) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
List speakers = null;
try {
Query query = session.createQuery("from Speaker" + (eventId != null ? " where event_id=:event_id" : ""));
if (eventId != null) {
query.setParameter("event_id", eventId);
}
speakers = (List) query.list();
} catch (HibernateException e) {
e.printStackTrace();
session.getTransaction().rollback();
}
session.getTransaction().commit();
return speakers;
}

public List lastList() {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
List speakers = null;
try {
Query query = session.createQuery("from Speaker S ORDER BY S.id DESC");
query.setMaxResults(3);
speakers = (List) query.list();
} catch (HibernateException e) {
e.printStackTrace();
session.getTransaction().rollback();
}
session.getTransaction().commit();
return speakers;
}

public Speaker update(Speaker speaker) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.update(speaker);
session.getTransaction().commit();
return speaker;
}

public Speaker delete(Speaker speaker) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.delete(speaker);
session.getTransaction().commit();
return speaker;
}

public Speaker insert(Speaker speaker) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.save(speaker);
session.getTransaction().commit();
return speaker;
}
}

Загрузка данных в компоненты webix осуществляется через ajax-запросы. Для загрузки данных с сервера компоненту DataTable устанавливается опция url, которая указывает, на какой URL отправить ajax-запрос, чтобы загрузить данные. Ответ сервера может быть в форматах XML, JSON, CSV. Опция datatype указывает ожидаемый формат данных.

Мы будем использовать ответы в формате JSON. Для struts 2 существует плагин, который позволяет отправить ответ в JSON-формате. Он называется struts2-json-plugin Этот плагин мы подключили, когда настраивали зависимости Struts 2 в файле pom.xml.

Для загрузки данных будут использоваться следующие пути:

  • http://localhost:8080/MyApp/events — предстоящие события
  • http://localhost:8080/MyApp/speakers — доклады
  • http://localhost:8080/MyApp/lastSpeakers — последние три доклада

Настроим пути в файле struts.xml:

 

Чтобы Struts 2 не пытался отправить в ответ на запрос представление, а вернул данные в формате json, необходимо эти пути добавить в другой пакет, который имеет значение extends=”json-default”. В этом случае Struts 2 сделает сериализацию объекта EventAction или SpeakerAction и отправит его как ответ.

Отредактируем класс EventAction таким образом, чтобы он мог возвращать список событий:

package com.myapp.action;

import java.util.ArrayList;
import java.util.List;

import com.myapp.controller.EventsManager;
import com.myapp.model.Event;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

public class EventAction extends ActionSupport {

private List data = new ArrayList();
private String eventId;
private Event event = null;

public String getEventById() {
if (eventId != null) {
EventsManager eventsManager = new EventsManager();
event = eventsManager.getById(Long.parseLong(eventId, 10));
return Action.SUCCESS;
} else {
return Action.ERROR;
}

}

public String getEvents() {
EventsManager eventsManager = new EventsManager();
data = eventsManager.list();
return Action.SUCCESS;
}

public List getData() {
return data;
}
public void setData(List lists) {
this.data = lists;
}

public String getEventId() {
return eventId;
}
public void setEventId(String eventId) {
this.eventId = eventId;
}

public Event getEvent() {
return event;
}
public void setEvent(Event event) {
this.event = event;
}
}

Создадим также класс SpeakerAction, который возвращает список событий:

SpeakerAction.java:

package com.myapp.action;

import java.util.ArrayList;
import java.util.List;

import com.myapp.controller.SpeakersManager;
import com.myapp.model.Speaker;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

public class SpeakerAction extends ActionSupport {

private List data = new ArrayList();
private String eventId;

public String getSpeakers() {
SpeakersManager speakersManager = new SpeakersManager();
if (eventId != null) {
data = speakersManager.list(Long.parseLong(eventId, 10));
} else {
data = speakersManager.list();
}
return Action.SUCCESS;
}

public String getLastSpeakers() {
SpeakersManager speakersManager = new SpeakersManager();
data = speakersManager.lastList();
return Action.SUCCESS;
}

public List getData() {
return data;
}
public void setData(List lists) {
this.data = lists;
}

public String getEventId() {
return eventId;
}
public void setEventId(String eventId) {
this.eventId = eventId;
}
}

После этого можно открыть страницу http://localhost:8080/MyApp/events в браузере и увидеть события в JSON-формате:

события в JSON-формате

Для того чтобы начать использовать реальные данные вместо тестовых, надо всего лишь внести небольшие изменения в настройки компонентов:

  • а странице index.jsp заменить:
    data: events

    на

    datatype:"json",
    url:"events?nocache=" + (new Date()).valueOf()
  • в файле myapp.js заменить:
    data: lastSpeakers

    на

    datatype: "json",
    url: "lastSpeakers?nocache=" + (new Date()).valueOf()
  • в файлеadd.jsp поменять:
    data: events

    на

    datatype: "json",
    url: "events?nocache=" + (new Date()).valueOf()

    и

    $$("speakers").parse(speakers);

    на

    $$("speakers").load("speakers?nocache=" + (new Date()).valueOf());
  • в файле event.jsp заменить:
    data: speakers

    into

    datatype: "json",
    url:"speakers?eventId=&amp;nocache=" + (new Date()).valueOf()

Теперь данные в таблицах — это не тестовые данные из файла tempdata.js (который можно уже удалить), а реальная информация из базы данных!

Теперь добавим возможность сохранять события и доклады, создав два отдельных класса: SaveEventAction и SaveSpeakerAction.

SaveEventAction:

package com.myapp.action;

import java.util.Date;
import java.util.Locale;

import com.myapp.controller.EventsManager;
import com.myapp.model.Event;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

import java.text.ParseException;
import java.text.SimpleDateFormat;

public class SaveEventAction extends ActionSupport {

private String id;
private String name;
private String description;
private String date;
private String location;
private String photo;
private String webix_operation;

public String saveEvent() {
Event event = new Event();
event.setId(id!=null ? Long.parseLong(id, 10) : null);
event.setName(name);
event.setDescription(description);
Date eventDate = null;
try {
eventDate = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date);
} catch (ParseException e) {
e.printStackTrace();
}
event.setDate(eventDate);
event.setLocation(location);
event.setPhoto(photo);

EventsManager eventsManager = new EventsManager();
if (webix_operation.equals("update"))
event = eventsManager.update(event);
else if (webix_operation.equals("delete"))
event = eventsManager.delete(event);
else if (webix_operation.equals("insert"))
event = eventsManager.insert(event);

id = event.getId().toString();
return Action.SUCCESS;
}

public String getId() {
return id;
}

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

public String getName() {
return name;
}

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

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public String getDate() {
return date;
}

public void setDate(String date) {
this.date = date;
}

public String getLocation() {
return location;
}

public void setLocation(String location) {
this.location = location;
}

public String getPhoto() {
return photo;
}

public void setPhoto(String photo) {
this.photo = photo;
}

public String getWebix_operation() {
return webix_operation;
}

public void setWebix_operation(String webix_operation) {
this.webix_operation = webix_operation;
}

}

SaveSpeakerAction.java:

package com.myapp.action;

import java.util.Date;
import java.util.Locale;

import com.myapp.controller.EventsManager;
import com.myapp.controller.SpeakersManager;
import com.myapp.model.Event;
import com.myapp.model.Speaker;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

import java.text.ParseException;
import java.text.SimpleDateFormat;

public class SaveSpeakerAction extends ActionSupport {

private String id;
private String author;
private String topic;
private String description;
private String photo;
private String event_id;
private String webix_operation;

public String saveSpeaker() {
Speaker speaker = new Speaker();
speaker.setId(id!=null ? Long.parseLong(id, 10) : null);
speaker.setAuthor(author);
speaker.setTopic(topic);
speaker.setDescription(description);
speaker.setPhoto(photo);
speaker.setEvent_id(Long.parseLong(event_id, 10));
Date eventDate = null;

SpeakersManager speakersManager = new SpeakersManager();
if (webix_operation.equals("update"))
speaker = speakersManager.update(speaker);
else if (webix_operation.equals("delete"))
speaker = speakersManager.delete(speaker);
else if (webix_operation.equals("insert"))
speaker = speakersManager.insert(speaker);

id = speaker.getId().toString();
return Action.SUCCESS;
}

public String getId() {
return id;
}

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

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getTopic() {
return topic;
}

public void setTopic(String topic) {
this.topic = topic;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public String getPhoto() {
return photo;
}

public void setPhoto(String photo) {
this.photo = photo;
}

public String getEvent_id() {
return event_id;
}

public void setEvent_id(String event_id) {
this.event_id = event_id;
}

public String getWebix_operation() {
return webix_operation;
}

public void setWebix_operation(String webix_operation) {
this.webix_operation = webix_operation;
}
}

Все значения, которые приходят в запросе, автоматически устанавливаются в переменные Action-класса с соответствующими именами. Таким образом, для сохранения нового события нам достаточно создать новый объект new Event(), установить в него принятые значения и передать его в метод EventsManager.insert(…).

Помимо данных о событии, в запросе также отправляется параметр webix_operation, который позволяет однозначно определить, какое действие должно быть выполнено с событием.

Добавим настройки в файл struts.xml:

 

На странице add.jsp надо добавить свойство save для таблиц:

{
id: "events",
view:"datatable",
columns:[
{ id:"date", header:"Date" , width:80 },
{ id:"name", header:"Name", fillspace: true },
{ id:"location",header:"Location", width:400 },
{ id:"edit",header:"", width: 34, template: ""},
{ id:"remove", header:"", width: 34, template: ""}
],
onClick: {
removeEvent: removeEventClick,
editEvent: editEventClick
},
autoheight:true,
select:"row",
datatype: "json",
url: "events?nocache=" + (new Date()).valueOf(),
save: "saveEvent"
},

{ width: 10 },
{
id: "speakers",
view:"datatable",
columns:[
{ id:"author", header:"Author", width:150 },
{ id:"topic", header:"Topic", width:300 },
{ id:"edit",header:"", width: 34, template: ""},
{ id:"remove", header:"", width: 34, template: ""}
],
onClick: {
removeSpeaker: removeSpeakerClick,
editSpeaker: editSpeakerClick
},
select: "row",
autoheight:true,
autowidth:true,
datatype: "json",
save: "saveSpeaker"
}

Готово! Теперь у вас есть сайт со страницами.

Подводя итог, хочется отметить, что Webix зарекомендовал себя, как очень удобный инструмент для быстрого создания сайта. Он позволяет строить не только статические странички, но и полноценные системы управления данными. При этом Webix легко поддается изменениям, хорошо интегрируется с другими библиотеками и разнообразными технологиями, а также оставляет возможность быстро делать разметку на привычном html.