Русский English Тэги View Sergey Zolotaryov's profile on LinkedIn Вход
Сортировка и постраничная разбивка больших таблиц в JSF
Постоянная ссылка 12-10-2007 anydoby java

Сегодня поговорим по постраничной разбивке в JSF для больших таблиц. Если честно, для меня это всегда был больной вопрос, потому что ни MyFaces, ни Tomahawk не предлагают нормально работающих компонентов для отображения большой таблицы с сортировкой и постраничной разбивкой. Все они рассчитаны на небольшие объемы данных, которые держатся в памяти. Если же объем данных не позволяет держать все в памяти, предлагается прием, описанный подробно на apache wiki.

Этот пример хорош для таблиц, которые не надо сортировать. Если включить в компоненте таблицы сортировку, то ваша база буквально ляжет, такой шквал запросов в нее посыпется. И не мудрено, ведь для того, чтобы отсортировать в памяти страничку, нужно достать все записи из соответствующей таблицы, пусть даже и порциями. Очень не хотелось писать самому компонент, который будет правильно разбивать на странички и доставать уже отсортированные данные из базы, поэтому я выкрутился на какое-то время, добавив несколько кнопок внизу таблицы и добавив h:commandLink на заголовках таблицы, реализовав сортировку и кнопки "вперед", "назад" самостоятельно. Не хватало только линков 1,2,3,4,5.... :)

Однако, все хорошее когда-нибудь кончается и кастомер захотел, чтобы разбивка на страницы выглядела "как в Google". Так что я снова вспомнил о . И тут уж пришлось засесть за JSF и написать свой компонент, который делает все так, как я хочу. И вот, что у меня получилось.

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

За основу можно взять код, приведенный на apache wiki для работы с большими таблицами, с небольшими изменениями:

Это страница, содержащая элементы таблицы:


package com.anydoby.jsfpager;

import java.io.Serializable;
import java.util.List;

/**
 * A simple class that represents a "page" of data out of a longer set, ie
 * a list of objects together with info to indicate the starting row and
 * the full size of the dataset. EJBs can return instances of this type
 * when returning subsets of available data.
 */
public class DataPage<T> implements Serializable {

	private static final long serialVersionUID = -5211047724709570419L;
	private int startRow;
	private List<T> data;

	/**
	 * Create an object representing a sublist of a dataset.
	 * 
	 * @param startRow is the index within the complete dataset
	 * of the first element in the data list.
	 * 
	 * @param data is a list of consecutive objects from the
	 * dataset.
	 */
	public DataPage(int startRow, List<T> data) {
		this.startRow = startRow;
		this.data = data;
	}

	/**
	 * Return the offset within the full dataset of the first
	 * element in the list held by this object.
	 */
	public int getStartRow() {
		return startRow;
	}

	/**
	 * Return the list of objects held by this object, which
	 * is a continuous subset of the full dataset.
	 */
	public List<T> getData() {
		return data;
	}
}

Это DataModel, обеспечивающая постраничную разбивку:


package com.anydoby.jsfpager;

import java.io.Serializable;

import javax.faces.model.DataModel;

/**
 * A special type of JSF DataModel to allow a datatable and datascroller
 * to page through a large set of data without having to hold the entire
 * set of data in memory at once.
 * <p>
 * Any time a managed bean wants to avoid holding an entire dataset,
 * the managed bean should declare an inner class which extends this
 * class and implements the fetchData method. This method is called
 * as needed when the table requires data that isn't available in the
 * current data page held by this object.
 * <p>
 * This does require the managed bean (and in general the business
 * method that the managed bean uses) to provide the data wrapped in
 * a DataPage object that provides info on the full size of the dataset.
 */
public abstract class PagedListDataModel<T> extends DataModel implements Serializable {

	int pageSize;
	int rowIndex = -1;
	DataPage<T> page;
	private int datasetSize = -1;
    private int lastStart;
    private int lastSize;

	/*
	 * Create a datamodel that pages through the data showing the specified
	 * number of rows on each page.
	 */
	public PagedListDataModel(int pageSize) {
		this.pageSize = pageSize;
	}

	/**
	 * Not used in this class; data is fetched via a callback to the
	 * fetchData method rather than by explicitly assigning a list.
	 */
	@Override
	public void setWrappedData(Object o) {
		throw new UnsupportedOperationException("setWrappedData");
	}

	@Override
	public int getRowIndex() {
		return rowIndex;
	}

	/**
	 * Specify what the "current row" within the dataset is. Note that
	 * the UIData component will repeatedly call this method followed
	 * by getRowData to obtain the objects to render in the table.
	 */
	@Override
	public void setRowIndex(int index) {
		rowIndex = index;
	}

	/**
	 * Return the total number of rows of data available (not just the
	 * number of rows in the current page!).
	 */
	@Override
	public int getRowCount() {
		if (datasetSize == -1) {
			datasetSize = getDatasetSize(); 
		}
		return datasetSize;
	}

    /**
     * Return the total number of rows of data available (not just the
     * number of rows in the current page!).
     */
	protected abstract int getDatasetSize();

	/**
	 * Return a DataPage object; if one is not currently available then
	 * fetch one. Note that this doesn't ensure that the datapage
	 * returned includes the current rowIndex row; see getRowData.
	 */
	private DataPage<T> getPage() {
		if (page != null) {
			return page;
        }

		int rowIndex = getRowIndex();
		int startRow = rowIndex;
		if (rowIndex == -1) {
			// even when no row is selected, we still need a page
			// object so that we know the amount of data available.
			startRow = 0;
		}

		// invoke method on enclosing class
		page = fetchPageInternal(startRow, pageSize);
		return page;
	}

	/**
	 * Return the object corresponding to the current rowIndex.
	 * If the DataPage object currently cached doesn't include that
	 * index then fetchPage is called to retrieve the appropriate page.
	 */
	@Override
	public Object getRowData() {
		if (rowIndex < 0) {
			throw new IllegalArgumentException(
					"Invalid rowIndex for PagedListDataModel; not within page");
		}

		// ensure page exists; if rowIndex is beyond dataset size, then 
		// we should still get back a DataPage object with the dataset size
		// in it...
		if (page == null) {
			page = fetchPageInternal(rowIndex, pageSize);
		}

		// Check if rowIndex is equal to startRow,
		// useful for dynamic sorting on pages

		if (rowIndex == page.getStartRow()) {
			page = fetchPageInternal(rowIndex, pageSize);
		}

		int datasetSize = getRowCount();
		int startRow = page.getStartRow();
		int nRows = page.getData().size();
		int endRow = startRow + nRows;

		if (rowIndex >= datasetSize) {
			throw new IllegalArgumentException("Invalid rowIndex");
		}

		if (rowIndex < startRow) {
			page = fetchPageInternal(rowIndex, pageSize);
			startRow = page.getStartRow();
		} else if (rowIndex >= endRow) {
			page = fetchPageInternal(rowIndex, pageSize);
			startRow = page.getStartRow();
		}

		return page.getData().get(rowIndex - startRow);
	}

	private DataPage<T> fetchPageInternal(int start, int size) {
        if (lastStart != start || lastSize != size || page == null) {
            page = fetchPage(start, size);
            lastSize = size;
            lastStart = start;
        }
        return page;
    }

    @Override
	public Object getWrappedData() {
		return getPage().getData();
	}

	/**
	 * Return true if the rowIndex value is currently set to a
	 * value that matches some element in the dataset. Note that
	 * it may match a row that is not in the currently cached 
	 * DataPage; if so then when getRowData is called the
	 * required DataPage will be fetched by calling fetchData.
	 */
	@Override
	public boolean isRowAvailable() {
        int rowCount = getRowCount();
        if (rowCount < 1) {
            return false;
        }
		int rowIndex = getRowIndex();
		if (rowIndex < 0) {
			return false;
		} else {
            return !(rowIndex >= getRowCount());
        }
	}

	/**
	 * Method which must be implemented in cooperation with the
	 * managed bean class to fetch data on demand.
	 */
	public abstract DataPage<T> fetchPage(int startRow, int pageSize);
}

А вот дальше идет то, что не упомянуто на apache wiki, модель, позволяющая сортировать содержимое самостоятельно:


package com.anydoby.jsfpager;

/**
 * This is the base class for datamodels returned by the beans, which want their
 * contents be sortable and pageable in the most efficient way. Subclasses can get
 * the sortColumn and ascending properties during resultset retrieval.
 * 
 * @author szolotaryov
 * 
 * Oct 10, 2007
 */
public abstract class SortablePagedDataModel<T> extends PagedListDataModel<T> {

	private static final long serialVersionUID = -777764054824546815L;
	private String sortColumn;
	private boolean sortAscending;

	public SortablePagedDataModel(int pageSize) {
		super(pageSize);
	}

	public final String getSortColumn() {
		return sortColumn;
	}

	public final void setSortColumn(String sortColumn) {
		this.sortColumn = sortColumn;
	}

	public final boolean isSortAscending() {
		return sortAscending;
	}

	public final void setSortAscending(boolean sortAscending) {
		this.sortAscending = sortAscending;
	}

}

Как видим, ничего сложного, но именно этот класс нужно будет использовать в качестве модели для ваших таблиц. Его нужно расширить и реализовать два метода: fetchPage(int startRow, int pageSize) и int getDatasetSize(), возвращающие, соответственно, часть результатов, и общий размер выборки (без разбивки на страницы).

Как я уже сказал, пришлось написать "свой" компонент, для того, чтобы модель работала корректно. Естественно, писать пришлось немного:


package com.anydoby.jsfpager;

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

import javax.faces.el.ValueBinding;
import javax.faces.model.DataModel;

import org.apache.myfaces.component.html.ext.HtmlDataTable;


public class SortableDataTable extends HtmlDataTable {

    public final static String COMPONENT_TYPE = "com.anydoby.jsfpager.SortableDataTable";
    private Boolean sortable;

    @SuppressWarnings("unchecked")
    @Override
    protected DataModel createDataModel() {
        DataModel model;
        boolean sortable = isSortableModel();
        if (!sortable) {
            model = super.createDataModel();
        } else {
            Object value = getValue();
            if (value instanceof SortablePagedDataModel) {
                SortablePagedDataModel sortableModel = (SortablePagedDataModel) value;

                String sortColumn = getSortProperty();
                boolean sortAscending = isSortAscending();
                sortableModel.setSortAscending(sortAscending);
                sortableModel.setSortColumn(sortColumn);

                model = sortableModel;
            } else {
                throw new IllegalArgumentException(
                        "If table is sortable you should provide an implementation of SortablePageDataModel as value");
            }
        }

        return model;
    }

    public void setSortableModel(boolean sortable) {
        this.sortable = sortable ? Boolean.TRUE : Boolean.FALSE;
    }

    @SuppressWarnings("deprecation")
    public boolean isSortableModel() {
        if (this.sortable != null)
            return this.sortable.booleanValue();
        ValueBinding vb = getValueBinding("sortableModel");
        Boolean v = vb != null ? (Boolean) vb.getValue(getFacesContext()) : null;
        return v != null ? v.booleanValue() : false;
    }

    @Override
    public Object saveState(FacesContext context) {
        Object[] objs = (Object[]) super.saveState(context);
        List<Object> list = new ArrayList<Object>(Arrays.asList(objs));
        list.add(sortable);
        objs = list.toArray(new Object[list.size()]);
        return objs;
    }
    
    @Override
    public void restoreState(FacesContext context, Object state) {
        super.restoreState(context, state);
        Object[] objs = (Object[])state;
        sortable = (Boolean) objs[objs.length - 1];
    }


}

Как видим, если таблица без атрибута sortableModel, то будет работать обычная логика компонента . Если же sortableModel=true и модель SortablePagedDataModel, то ей передается состояние сортировки.

И напоследок тэг, который связывает наш компонент с JSP:


package com.anydoby.jsfpager;

import org.apache.myfaces.taglib.html.ext.HtmlDataTableTag;

public class SortableDataTableTag extends HtmlDataTableTag {

	private String sortableModel;

	@Override
	public String getComponentType() {
		return SortableDataTable.COMPONENT_TYPE;
	}

	@Override
	public void release() {
		super.release();
		sortableModel = null;
	}

	@Override
	protected void setProperties(UIComponent component) {
		super.setProperties(component);
		setBooleanProperty(component, "sortableModel", sortableModel);
	}

	public String getSortableModel() {
		return sortableModel;
	}

	public void setSortableModel(String sortableModel) {
		this.sortableModel = sortableModel;
	}

}

В аттачменте к этой статье вы найдете полный код проекта (кроме библиотек, от которых он зависит - tomahawk.jar), tld, faces-config.xml и jsp с примером использования тэга. Можно запустить build.xml, предварительно положив в lib все нужные либы и в dist появится jar с компонентом - его можно положить к себе в WEB-INF/lib, добавить в classpath и пользоваться.

Если лень компилировать, то вот здесь уже готовый jar, которым я пользуюсь.

Да, если preserveDataModel="false", то fetchPage будет вызван дважды - первый раз с start=0, а второй раз с новым значением, поэтому рекомендую сделать все объекты модели Serializable и поставить preserveDataModel="true".

Всем спасибо за внимание :)

Добавить комментарий

oledz
24-07-2008

Спасибо, Сергей! Именно то, что я искал (пару дней причем). И вообще, один из лучших блогов по Java. А откуда так хорошо знаешь English и ... Javanese? Олег

anydoby
24-07-2008

Большое спасибо за приятные слова :)
Дело в том, что я сам искал довольно долго и ничего не нашел. Пришлось самому писать. English учил в школе :), а Java уже сам, после института.

qqq
21-08-2008

Багрепорт :) - Так что я снова вспомнил о "t:dataScroller"... - Поизучав немного код "t:dataScroller" и "t:dataTable"...

astarovoitov
05-01-2009

Здравствуйте! Я пытаюсь использовать Вашу jar-ку , поместив её в либы моего проекта. На одной из страниц я написал код, аналогичный коду со страницы table из вашего тестового проекта, но при рендеренге страницы таблица не отображается а в коде страницы теги таблицы не заменены html тегами. Похоже на то что не подхватываются файлы faces-context и jsfpager.tld из jar-ки jsfpager которую я у вас скачал. Объясните, то ли я должен копировать код из Вашего faces-context в аналогичный файл моего проекта , то или при деплое должен происходить автоматический мерж. Вобщем, если не сложно, объясните куда какие файлы ложить .

anydoby
07-01-2009

Все очень просто - посмотрите, как это сделано, например, в tomahawk :) И сделайте так же - магии ж, к сожалению, не бывает.

astarovoitov
07-01-2009

Спасибо за ответ. Проблема была в том, что я работал с фэйслетами. В этом случае надо писать .tadlib.xml файл с описание нашего кастомного тега и писать ссылку на него в web.xml

Предыдущая статья Ошибка при старте Tomcat с APR Следующая статья За что я не люблю JSF