JSF 大数据分页和排序研究

 
Today we are going to talk about sorting and paging very large tables using JSF standard components. To tell the truth I had no satisfying solution to this problem since I have started dealing with JSF, because neither MyFaces, nor Tomahawk offer good components for displaying large tables with sorting an paging. All the table components are fit only for small dataset sizes, which are stored in memory. If the dataset is too large to hold it in RAM, apache wiki offers a simple recipe.

This sample is very good, and demostrates the general approach to dealing with large datatables, but it is no good when you need to sort the data. If you attempt to turn on sorting within the table component, your database will be literally flooded with requests. To sort the data, all the records need to be fetched, though it is done in portions and does not consume too much memory, it awfully spams the database. I did not want to write the whole component from scratch, so I have found a temporary workaround - added sorting and paging logic to a managed bean and added commandLinks to go to next/previos page and to do sorting. What I lacked however, were the paging links 1,2,3,4,5.... which I found too difficult to incorporate into existing component.

Well everything good ends sometime and our customer requested paging to be "like at Google". So I had to turn back to the and had to study JSF once again and write my own component. This is what I've got in the end.

Having read the code of and , I realized that writing the whole component from scratch would be the worst nightmare (given the fact that I hate JSF). However I have found a way to extend the existing component and make it act as I want. In the end it appeared that the amount of code was not that great. The whole project for the component can be downloaded from here.

I took the code from apache wiki as the basis, tweaking it along the way to get the best performance.

This is the code for results page:

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;
	}
}

This DataModel ensures paging:

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);
}

The rest of the code allows us to use a custom component and sort the data manually:

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;
	}

}

Nothing too complicated heh. This is the datamodel which we will use. You will have to extend it and implement: fetchPage(int startRow, int pageSize) и int getDatasetSize(), which fetch the data page and total amount of records, accordingly.

As I have mentioned already, I needed to write a "custom" component. Actually not much to code:

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];
    }


}

The table without the sortableModel attribute will act the same as original . If sortableModel=true and the model is SortablePagedDataModel, then the sorting state is communicated to it.

And finally, the tag class which binds JSP and your component:

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;
	}

}

The attachment to this article contains the complete code for the project (except for the tomahawk.jar and myfaces), tld, faces-config.xml and jsp with usage example. You can launch build.xml, first put the required jars into the lib folder and in the dist you will get a jar with component. Place it to your WEB-INF/lib, add to the classpath of your IDE and it can be used.

If you do not want to compile, here is the compiled jar file which I use.

If preserveDataModel="false", then fetchPage will be invoked twice the first time with start=0, and the second time with current start value, therefore I would recomment making all your model classes Serializable and setting preserveDataModel="true".

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值