Проблемы реализации постраничной загрузки таблиц с помощью компонент Hibernate и Java

Автор: content Понедельник, Апрель 9th, 2012 Нет комментариев

Рубрика: Язык Java

При разработке корпоративных приложений одним из сложных вопросов, является реализация возможности обеспечения работы на клиентской части с большими объемами информации. Суть проблемы заключается в том — в каком виде и каким образом осуществлять отображение на терминалах клиента необходимой информации, c возможностью выполнять основные операции с массивами данных, в первую очередь это перемещение по ним в любом направлении, а также стандартные операции редактирования. С похожим решением данной проблемы сталкивался любой пользователь, осуществлявший хотя бы раз поиск во всемирной сети. Когда при вводе критерия поиска на экране отображается только часть искомых данных, а остальные разбиваются на страницы, номера которых выводятся в этом же окне в виде ссылок, с помощью которых можно перемещаться по результирующей выборке. Данный механизм – называется пагинацией (от англ. pagination) [1] и заключается в определении общего количества соответствующих критерию поиска ссылок, разбиение их на страницы и загрузка страниц по запросам пользователей. Следует признать, что в силу специфики сетевого взаимодействия и характера выводимой информации, данный механизм в этом случае является оптимальным.

Однако при выводе большого объема табличной информации (несколько десятков тысяч записей и более) на терминал пользователя, описанный выше подход неприемлем по многим причинам. В первую очередь это связано с трудностями реализации навигационных перемещений, например, моментальный переход в конец выборки, или прокручивании (скроллинге) в любом направлении. Очевидно, что данный механизм реализован поставщиками драйверов JDBC с соответствующей СУБД, при клиент — серверном взаимодействии, для чего необходимо только определенным образом настроить драйвер, не задумываясь об этих настройках в клиентских компонентах библиотеки Swing реализующих отображение данных посредством JTable и JScrollPane. Что значительно упрощает разработку и настройку приложений.

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

Более оптимальным вариантом архитектуры корпоративного приложения является — клиент – сервер приложений – СУБД. В ОАО «РОСТОВЭНЕРГО» в настоящий момент выполняется реинженеринг существующей корпоративной информационной системы, перевод ее на многослойную архитектуру с использованием самых передовых на сегодняшний день технологий Java. Кроме того, система должна функционировать в гетерогенной среде СУБД (MS SQL Server, HSSQLDB, MySQL), а так же в силу специфики решаемых задач должна иметь и Web-интерфейс, и «толстого» клиента.

Целью данной статьи является рассмотрение способа реализации механизма пагинации при взаимодействии Swing-клиент – Hibernate – СУБД и анализ производительности системы (профайлинг) и способы ее повышения при его использовании.

Работа с объектно-ориентированным программным обеспечением и реляционной базой сопряжена с рядом трудностей. Hibernate — инструмент объектно/реляционного отображения данных (object/relational mapping tool = ORM tool) для Java окружения [2,3,4]. Термин объектно-реляционное отображение (object-relational mapping = ORM), относится к технике отображения объектно-ориентированных данных в реляционную модель со схемой данных основанной на SQL.

Hibernate не только заботится об отображении объектов Java классов в таблицы базы данных (и типов данных Java в типы данных SQL), но также обеспечивает механизм запроса и поиска данных, и значительно сокращает время разработки программного продукта, потраченное, в противном случае, на ручную обработку данных в SQL и JDBC.

Целью Hibernate является освобождение разработчика от 95 процентов общих работ, связанных с задачами программирования долгоживущих (persistence) данных. Возможно, Hibernate является наиболее полезным при использовании объектно-ориентированных моделей предметной области и бизнес логики в основанном на Java промежуточном слое (middle-tier). Hibernate помогает удалить из приложения или инкапсулировать (скрыть), зависящий от поставщика SQL-код и также помогает в решении стандартной задачи преобразования набора данных (result set) из табличного представления в объектный граф.

Поэтому необходимо использовать возможности библиотеки Hibernate для реализации механизма пагинации не зависящего от типа поставщиков драйверов JDBC. Одним из базовых моментов Hibernate – является «ленивая» инициализация, которая заключается в том, что при запросе к базе в выборку загружаются не объекты, а ссылки на них, и только при обращении к конкретной ссылке происходит выгрузка запрошенного объекта клиенту. Кроме того, Hibernate имеет множество дополнительных возможностей, общая идея которых заключается в повышении производительности работы всей системы в целом за счет снятия нагрузки с СУБД при использовании многоуровневой архитектуры. Вместе с тем данная библиотека предоставляет возможность реализации запросов с помощью традиционного синтаксиса языка SQL или встроенного HSQL. Однако следует заметить, что слой приложения, отвечающий за доступ к СУБД, реализованный с помощью SQL запросов не обладает той гибкостью и мощью, которую можно реализовать, используя запросы – критерионы (Criteria) библиотеки Hibernate и механизмы Reflection и RTTI — Java. Критерионы позволяют реализовать запросы, используя объектный подход, получая коллекцию искомых бизнес – объектов, а так же реализуя все основные конструкции традиционного SQL – группировка, агрегация, объединение см Листинг1, 2.
Листинг 1. Метод использующий Critetia, RTTI, Reflection для загрузки бизнес объектов в табличную модель

/**
*Метод для заполнения табличной модели бизнес объектами
*@param tableModel модель таблицы которую нужно заполнить
*@param cls класс бизнес – объекта, которым нужно заполнить таблицу
*/
public void fillTable(ModelTableEntities tableModel, Class cls){
// выбираем   все методы класса
Method [] allMethods =  cls.getMethods();
//Method nesMethod = null;
List  searchMethod = new ArrayList();
// перебор всех методов класса
for (int i = 0; i < allMethods.length; i++) {
// перебор все заголовков таблицы
for(int j=0;j < tableModel.getColumnArray().length; j++) {

// проверка маской на соответствие название метода заголовку столбца…
if(
allMethods[i].getName()
.equalsIgnoreCase(«get»+tableModel.getColumnArray()[j])
) {
// если соответствует то заносим в список
searchMethod.add(allMethods[i]);
}
}
}

// далее получаем список элементов бизнес — сущности
//и заносим его в таблицу
try{
for (Object elem : this.getCollectionEntity(cls)) {
Vector rowV = new Vector();
//перебор заголовков столбцов
for(int k = 0 ; k < tableModel.getColumnArray().length ; k++) {
for (Method mtd : searchMethod) {
if(mtd.getName()
.equalsIgnoreCase(«get»+tableModel.getColumnArray()[k])) {
// формируем строку
rowV.add(mtd.invoke(elem));
}
}
}
tableModel.addRow(rowV);
}
}catch(Exception nme){this.showErrorMessage(nme);}
}

Листинг 2. Фрагмент кода метода, использующего Criteria для реализации агрегирующих запросов

//Текстовый массив запроса
String [][] strPar = new String [parAnal.size()][2];
for (int i = 0; i < parAnal.size(); i++ ) {
Object [] tmpAnal = (Object[])parAnal.get(i);
strPar[i][0] = tmpAnal[0].toString();
strPar[i][1] = tmpAnal[1].toString();
}
// Массив параметров
String [] parQ = new String [(strPar.length+2)];
parQ[0] = «accountCredit»;
Type [] typeQ = new Type[parQ.length];
typeQ[0] = Hibernate.LONG;
for (int i = 0; i < strPar.length; i++){
parQ[i+1] = strPar[i][0];
typeQ[i+1] = Hibernate.INTEGER;
}
parQ[(strPar.length+1)] = «summ»;
typeQ[(strPar.length+1)] = Hibernate.BIG_DECIMAL;

String selectSQL = «»;
String groupSQL = «»;
String whereSQL = «»;

for (int i = 0; i < (parQ.length — 1); i++) {
if(i == (parQ.length — 2))
groupSQL =  selectSQL + parQ[i];
else
groupSQL =  selectSQL + parQ[i]+», «;

selectSQL = selectSQL + parQ[i]+», «;
}
selectSQL = selectSQL +»sum(summ)as summ»;

//Выбор с группировкой суммы проводок по кредитовым счетам

list = this.getSession().createCriteria(Entry.class)
.add(Restrictions.and(
Restrictions.eq(«accountCredit»,account),
Restrictions.and(
Restrictions.ge( «entryDate»,beg),
Restrictions.lt( «entryDate»,end)))
)
.setProjection(Projections.projectionList()
.add(Projections.sqlGroupProjection(selectSQL,
groupSQL,
parQ,typeQ))
)
.list();

Следует помнить, что данный подход дает преимущества только при больших объемах данных и распределенном характере приложения. В остальных случаях использование Hibernate не приведет к существенному повышению производительности, а скорее наоборот будет ее снижать.

Таким образом, для реализации пагинации необходимо, чтобы компонент библиотеки Hibernate имел методы для быстрого перемещения по выборке в любом направлении, позволял работать с большими объемами данных, при этом постараться свеcти к минимуму количество обращений к СУБД. В данном случае в качестве СУБД используется MySQL v.5, тестовая таблица состоит из 102 тысяч записей, размер таблицы 60 Мб. Были проанализированы возможные варианты решения. В частности, одним из способов является явное указание, в запросе границ выборки используя методы setFirstResult(int numRow) и q.setMaxResults(int numRow) для объекта Criteria, которым в качестве параметров передавать нужные строки. Однако такой вариант во–первых при листании будет постоянно генерировать запросы к СУБД, во – вторых невозможно установить общее количество бизнес – объектов, так как попытки загрузки всей коллекции вызывали переполнение памяти. Наиболее оптимальным компонентом является интерфейс ScrollableResults, который имеет все необходимые методы для перемещения по выборке, а использование класса DetachedCriteria, позволяет после выборки объектов закрыть сессию с СУБД см. Листинг 3.
Листинг 3

/**
* Метод для выборки всей коллекции бизнес сущности
*@param cls класс объекта который нужно извлечь
*@return list коллекция объектов
*/
public ScrollableResults getScrollCollection(Class cls){
ScrollableResults  list = null;
DetachedCriteria dc = DetachedCriteria.forClass(cls);
try {
HbnSessionUtil.beginTransaction();
list = dc.getExecutableCriteria(this.getSession())
.scroll();
} catch (Exception exTr) {
HbnSessionUtil.resolveTransaction();
this.showErrorMessage(exTr);
}
return list;
}

Таким образом, использование данного компонента библиотеки Hibernate в слое отвечающим за взаимодействия приложения с СУБД является оптимальным вариантом.

Далее исходя из модели M-V-C (модель – вид -контроллер), рассмотрим данную реализацию на клиентской части. Общая идея алгоритма пагинации на клиентской части следующая. В зависимости от размера области видимого контента, который представляет из себя окно класса JInternallFrame с панелью JScrollPane и таблицей JTable определяется размер страницы, в которые заносится информация о бизнес объектах одного класса из соответствующей таблицы СУБД. Эмпирически установлено, что количество строк в странице, должно быть примерно в три раза больше чем в видимом контенте. Для хранения содержимого таблицы используется класс на базе DefaultTableModel. При инициализации которого, создается табличная модель с заданной структурой столбцов и количеством пустых строк, соответствующему количеству записей в таблице СУБД, полученным с помощью метода см. Листинг4.
Листинг 4

/**
*Метод, устанавливающий количество строк в выборке
*при инициализации класса
*/
private int getLastNum(){
setScRes(queryDB.getScrollCollection(Entry.class));
// определяем количество объектов в выборке
// перемещаемся на последний элемент выборки
getScRes().last();
// Задаем количество строк в выборке
this.setNumRow(getScRes().getRowNumber()-1);
// Перемещаем указатель в начало
getScRes().first();
return  this.getNumRow();
}

Далее происходит заполнение первой страницы объектами СУБД см. Листинг 5.
Листинг 5

/**
* Метод для заполнения страницы модели  данными
* @param first первая строка
* @param last последняя строка
*/
public void fillPage(int first, int last){
for (int i = first; i < last; i++) {
// получаем из выборки бизнес объект
Entry entry = (Entry)getScRes().get(0);
// заносим его в таблицу
this.setValueAt(entry, i,0);
this.setValueAt(entry.getId(), i, 1);;
this.setValueAt(entry.getEntryDate().getTime(), i,2);
this.setValueAt(entry.getAccountDebit(), i,3);
this.setValueAt(entry.getAccountCredit(), i, 4);
this.setValueAt(entry.getSumm(), i ,5);
// перемещаемся указатель на следующий объект
getScRes().next();
}
}

При последующем листании (перемещению по выборке) анализируются относительные номера строк видимого контента, т.е. номера строк в рамках загруженной страницы. И если при листании вниз, номер строки превышает 95%, а при листании вверх менее 5% от общего объема строк страницы, то выполняется подгрузка следующей страницы с помощью метода Листинг 5, а также очистка строк от предыдущей страницы, с помощью метода Листинг 6, загруженной в модель.
Листинг 6

/**
*Метод для очистки таблицы от старого контента
*@param first строка начала очистки
*@param last строка конца очистки
*/
public void clearTable(int first, int last){
while(first < last)
{
HbnSessionUtil.evict(this.getValueAt(first,0));
this.setValueAt(null,first,0);
this.setValueAt(null,first,1);
this.setValueAt(null,first,2);
this.setValueAt(null,first,3);
this.setValueAt(null,first,4);
this.setValueAt(null,first,5);
first++;
}
queryDB.getSession().clear();
}

Следует заметить, что для удобства пользователей, табличная информация об объекте не содержит большого количества столбцов, но при необходимости можно на экран вывести панель свойств, в которую выводится подробная информация о всех свойствах объекта. Для реализации такой возможности в табличную модель в первый столбец загружается сам бизнес объект, а при выводе на экран в JTable данный столбец скрывается. Так же в классе табличной модели необходимо реализовать три метода для заполнения модели данными. Первые два метода отвечают за подгрузку данных при листании с помощью клавиш, и соответственно отличаются направлением листания. При листании вниз, необходимо данные подгружать в конец текущей страницы и контролировать достижения конца выборки Листинг 7.
Листинг 7

/**
*Метод для реализации постарничного листания модели вниз
*@param first номер первой видимой строки модели данных  в контексте
*@param last номер повледней видимой строки модели данных  в контексте
*/
public void pageDown(int first, int last)
{
// очищаем таблицу от строк до первой видимой в контексте
this.clearTable(this.getFirstRow(), first-1);
// переводим курсор на последнюю загруженную сущность
getScRes().setRowNumber(this.getLastRow());
// Заполняем модель данными, если разность общего количества и
// номера текущей  записи меньше размера страницы, то подгружаем все до конца
if(this.getNumRow()- getScRes().getRowNumber()< this.getPageSize())
this.fillPage(getScRes().getRowNumber(), this.getNumRow());
else this.fillPage(getScRes().getRowNumber(), this.getLastRow()+this.getPageSize());
// Устанавливаем номера загруженных строк
this.setFirstRow(first);
this.setLastRow(getScRes().getRowNumber());
}

При листании вверх, наоборот, необходимо данные подгружать в начало текущей страницы и контролировать достижение начало выборки Листинг 8.
Листинг 8

/**
*Метод для реализации постраничного листания модели вверх
*@param first номер первой видимой строки модели данных  в контексте
*@param last номер повледней видимой строки модели данных  в контексте
*/
public void pageUp(int first, int last)
{
this.clearTable(last+1,this.getLastRow());
// Заполняем модель данными, если разность общего количества и
// номера текущей  записи меньше размера страницы, то подгружаем все до конца
if(this.getFirstRow()< this.getPageSize())
this.setFirstRow(0);
else this.setFirstRow(first-this.getPageSize());
// переводим курсор на последнюю загруженную сущность
getScRes().setRowNumber(this.getFirstRow());
// Загружаем в таблицу страницу с данными
this.fillPage(this.getFirstRow(), first);
this.setLastRow(last);
}

Третий метод принципиально отличается от первых двух, тем что направление листание не имеет принципиального значения и он отвечает за подгрузку данных при выполнении манипуляций с помощью мыши, либо прокручивание колесика, либо перемещение «бегунка» на панели прокрутки см. Листинг 9.
Листинг 9

/**
*Метод для прокрутки страниц
*@param first первая строка
*@param last последняя строка
*/
public void scrollPage(int first, int last){
// Проверка на предмет достижения начала или конца таблицы
if(first < 0 )
{
first = 0;
last = 100;
}
else if (last >= this.getNumRow())
{
first = this.getNumRow()-100;
last = this.getNumRow();
}

// очистка контента
this.clearTable(this.getFirstRow(), this.getLastRow());
// переводим курсор на последнюю загруженную сущность
getScRes().setRowNumber(first);
this.fillPage(first, last);
this.setFirstRow(first);
this.setLastRow(last);
}

Далее была решена проблема реализации функций контроллера вызывающего описанные выше методы. При традиционном подходе, данная функция выполняется с помощью слушателя событий скроллирующей панели (JScrollPane), интерфейса AdjustmentListener. Однако, все дальнейшие попытки, адаптировать данный интерфейс для инициализации методов загрузки страниц не принесли результатов. Реализация метода интерфейса adjustmentValueChanged(AdjustmentEvent evt), показала, что он вне зависимости от типа события, интерпретирует их в одно AdjustmentEvent.TRACK (перемещение «бегунка»), см. Листинг 10.

Листинг 10

public void adjustmentValueChanged(AdjustmentEvent evt) {

int type = evt.getAdjustmentType();
switch (type) {
case AdjustmentEvent.UNIT_INCREMENT:
System.out.println(«Incremented by one Unit»);
break;
case AdjustmentEvent.UNIT_DECREMENT:
System.out.println(«Decremented by one Unit»);
break;
case AdjustmentEvent.BLOCK_INCREMENT:
System.out.println(«Incremented by one Block»);
break;
case AdjustmentEvent.BLOCK_DECREMENT:
System.out.println(«Decremented by one Block»);
break;
case AdjustmentEvent.TRACK:
System.out.println(«Track Event»);
break;
}

Дальнейший анализ показал, что ошибка в данном компоненте зафиксирована на сайте фирмы Sun [5], и по заверениям производителей, данная ошибка в версии Java 1.5 исправлена. Однако, как показали мои дальнейшие изыскания по данной теме, как на сайте Sun [6], так и практической реализации, данная ошибка, к сожалению, до сих пор не исправлена. Поэтому была предпринята попытка использовать в корне другой подход. Проанализировав компоненты, участвующие в данном процессе, оказалось, возможным отслеживание событий перемещения с помощью интерфейса ChangeListener связав его с моделью класса BoundedRangeModel, которая определяет основные характеристики линеек прокрутки JScrollBar. Поэтому реализация заключалась в создании метода см. Листинг 11 и его вызов в слушателе компонет ScrollBar в stateChanged(ChangeEvent e).
Листинг 11

1.    public void onVerticalScrolling(ChangeEvent e) {
2.    if (!(e.getSource() instanceof DefaultBoundedRangeModel))      return;
3.    JViewport vp = spEntry.getViewport();
4.    Point p = tbEntry.getVisibleRect().getLocation();
5.    int h = 0 ;
6.    DefaultBoundedRangeModel rm = (DefaultBoundedRangeModel)e.getSource();
7.    int firstVisibleRow = tbEntry.rowAtPoint(p);
8.    int lastVisibleRow = tbEntry.rowAtPoint(new Point(0,rm.getValue()+vp.getExtentSize().height)) — 1;
9.    h=(int)(rm.getMaximum()/tbEntry.getRowCount());
10.    if (rm.getValueIsAdjusting())  {
11.    firstVisibleRow = (int)(rm.getValue()/h)-50;
12.    lastVisibleRow = (int)(rm.getValue()/h)+50;
13.    modelTableEntry.scrollPage(firstVisibleRow, lastVisibleRow);
14.    tbEntry.setRowSelectionInterval((int)(rm.getValue()/h)+1,(int)(rm.getValue()/h)+1);
15.    }
16.    else if (rm.getValue() == rm.getMinimum())                modelTableEntry.scrollPage(0, 100);
17.    else if (rm.getValue()+rm.getExtent() >= rm.getMaximum()-1)
18.    modelTableEntry.scrollPage(modelTableEntry.getNumRow()-100,modelTableEntry.getNumRow());
19.    else{
20.    if (rm.getValue() > this.oldValue) {
21.    if ((modelTableEntry.getLastRow()-lastVisibleRow ) < 10) {
22.    modelTableEntry.pageDown(firstVisibleRow, lastVisibleRow);
23.    }
24.    }
25.    else if(rm.getValue() < this.oldValue) {
26.    if ( modelTableEntry.getFirstRow()-firstVisibleRow < 10) {
27.    modelTableEntry.pageUp(firstVisibleRow, lastVisibleRow);
28.    }
29.    }
30.    }
31.    oldValue = rm.getValue();
32.    }

В данном методе в строке 2 контролируется объект-источник события. В строках 3-4 определяется область видимого конента. В строках 7-8 определяется номер первой и последней строк видимого контента. В строке 9, определяется размер отображения в пикселях одной строки. Строки 10-15 обработка событий «скроллинга» с помощью мыши. Строка 16 – событие быстрого перемещения в начало выборки (нажатие сочетания клавиш Ctrl+Home). Строки 17-18 – событие быстрого перемещения в конец выборки (нажатие сочетания клавиш Ctrl+End). Строка 20-22, отслеживание «листания» вниз с помощью клавиш. Строка 25-27, отслеживание «листания» вверх с помощью клавиш. Как видно из листинга [] при отслеживании событий перемещения, вызываются соответствующие методы табличной модели.

Следующим этапом данной работы, являлся анализ распределения ресурсов при работе программы на клиентской машине с помощью профайлера NetBeans v 5.5 и механизма логгирования библиотеки log4j. При этом анализировался объем занимаемой памяти в процессе выполнения программы, быстродействие выполнения операций пагинации а также нагрузка на СУБД. Были получены следующие результаты. Загруженное приложение в памяти занимает порядка 40 Мб. После загрузки коллекции объектов и вывода их в рабочее окно размер памяти увеличивается до 115 Мб, быстрое перемещение в конец-начало выборки, практически не изменяет объем используемой памяти. Пролистывание страниц с помощью клавиш и мыши приводят к постепенному увеличению объема памяти, но доходя до определенной отметки, в среднем примерно до 135-138 Мб в дальнейшем объем памяти остается неизменным. При пролистывании отсутствует эффект «замедленной реакции», все выполняется мгновенно. Анализ логгирования к СУБД, показал, что при этом выполняется только один запрос к СУБД на выгрузку всей коллекции. Однако если выводится на экран окно свойств объекта, то идут постоянные запросы к базе по получению конкретного бизнес объекта, но и в этом случае в работе системы не замечается каких либо замедлений. При этом конфигурация клиентской машины CPU P — IV 2,2 Ггц, ОЗУ 256 Мб, сеть – STP – 100Мбит/сек.

Таким образом, можно сделать следующие выводы:

Экспериментальным путем установлено, что непосредственной загрузкой табличных данных в объект JTable можно загрузить в среднем не более 65000 строк, в противном случае возникает переполнение памяти. Поэтому при выводе в клиентское приложение больших объемов табличных данных, необходимо использовать механизм пагинации описанный выше.
Использование библиотеки Hibernate целесообразно только в случае клиент-серверного взаимодействия при работе с большими объемами данных.
В качестве объекта позволяющего работать с большими объемами данных следует использовать компонент библиотеки Hibernate – ScrollableResults.
К недостаткам данного механизма следует отнести выявленное в результате экспериментальных исследований ограничение на максимально возможное количество строк, при создании табличной модели данных, не более 1,5 млн.

Источник: http://www.javaportal.ru/java/articles/upload_table_Hibernate.html
Автор: Жмайлов Б.Б.

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты