Интеграция сайта на 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:

<dependencies>
    …
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>4.3.5.Final</version>
    </dependency>
       
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
    </dependency>
<dependencies>

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

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

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
      "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
      "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
 
<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/myapp</property>
        <property name="hibernate.connection.useUnicode">true</property>
        <property name="hibernate.connection.characterEncoding">UTF-8</property>
        <property name="hibernate.connection.charSet">UTF-8</property>
        <property name="connection.username">root</property>
        <property name="connection.password"></property>
        <property name="connection.pool_size">1</property>
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
        <property name="current_session_context_class">thread</property>
        <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
        <property name="show_sql">true</property>
        <property name="hbm2ddl.auto">validate</property>
       
        <mapping class="com.myapp.model.Event" />
        <mapping class="com.myapp.model.Speaker" />
    </session-factory>
</hibernate-configuration>

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

  • 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:

<package name="default" namespace="/" extends="json-default">
    <action name="events" class="com.myapp.action.EventAction" method="getEvents">
        <result type="json" />
    </action>
    <action name="speakers" class="com.myapp.action.SpeakerAction" method="getSpeakers">
        <result type="json" />
    </action>
    <action name="lastSpeakers" class="com.myapp.action.SpeakerAction" method="getLastSpeakers">
        <result type="json" />
    </action>
</package>

Чтобы 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:

<action name="saveEvent" class="com.myapp.action.SaveEventAction" method="saveEvent">
    <result type="json" />
</action>
<action name="saveSpeaker" class="com.myapp.action.SaveSpeakerAction" method="saveSpeaker">
    <result type="json" />
</action>

На странице 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.