Sunday, January 1, 2012

Implementing RichFaces ExtendedDataModel for JSF Paging with Spring Data Neo4j and Scala

Satukancinta-datatable

JSF 2.1 and JBoss RichFaces 4.1.0 make it easy to do server-side paging and sorting, which improves performance of Java EE web applications significantly compared to returning list of all rows from the database and filtering it later in the application.

By implementing ExtendedDataModel, the data model can be used directly by rich:dataTable and rich:dataScroller JSF components.

ExtendedDataModel for Spring Data Neo4j Finder/Query Methods

To implement this on Neo4j graph database, by using Spring Data Neo4j finder methods we can implement ExtendedDataModel like below: (in Scala programming language)

package com.satukancinta.web

import collection.JavaConversions._
import org.ajax4jsf.model.ExtendedDataModel
import javax.faces.context.FacesContext
import org.springframework.data.domain.Page
import org.ajax4jsf.model.DataVisitor
import org.springframework.data.neo4j.repository.GraphRepository
import org.springframework.data.domain.PageRequest
import org.springframework.data.neo4j.aspects.core.NodeBacked
import org.ajax4jsf.model.Range
import org.ajax4jsf.model.SequenceRange
import org.slf4j.LoggerFactory
import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction
import org.springframework.data.domain.Pageable

abstract class FinderModel[E]() extends ExtendedDataModel[E] {
  private lazy val log = LoggerFactory.getLogger(classOf[FinderModel[E]])
 
  private lazy val rowCount: Int = {
    val result= getRowCountLazy
    log.debug("Total rows: {}", result)
    result
  }
 
  private var rowIndex: Int = _
  private var page: Page[E] = _
  private var pageData: List[E] = _
  private var lastRange: (Int, Int) = _

  log.trace("Created {}", this.getClass)
 
  def getRowCountLazy: Int
  def find(pageable: Pageable): Page[E]

  def setRowKey(key: Object): Unit = setRowIndex(key.asInstanceOf[Int])
  def getRowKey: Object = getRowIndex: java.lang.Integer
 
  private def loadData(range: Range): Unit = {
    val seqRange = range.asInstanceOf[SequenceRange]
    val curRange = (seqRange.getFirstRow, seqRange.getRows)
    if (lastRange == curRange) {
      log.debug("loadData returning cached")
      return
    }
    lastRange = curRange
   
    val pageNum = seqRange.getFirstRow / seqRange.getRows
    // ORDER BY name is painfully slow: https://groups.google.com/group/neo4j/t/f2219df41f5500a9
    val pageReq = new PageRequest(pageNum, seqRange.getRows/*, Direction.ASC, "y.name"*/)
    log.debug("loadData({}, {}) -> PageRequest({}, {})",
        Array[Object](seqRange.getFirstRow: java.lang.Long, seqRange.getRows: java.lang.Long,
            pageNum: java.lang.Long, seqRange.getRows: java.lang.Long))
    val startTime = System.currentTimeMillis
    page = find(pageReq)
    val findTime = System.currentTimeMillis - startTime
    log.debug("Page has {} rows of {} total in {} pages, took {}ms",
        Array[Object](page.getSize: java.lang.Long, page.getTotalElements: java.lang.Long,
            page.getTotalPages: java.lang.Long, findTime: java.lang.Long))
    pageData = page.toList
//    val pageIds = pageData.map( _.asInstanceOf[NodeBacked].getNodeId )
//    log.debug("Node IDs: {}", pageIds);
  }

  def walk(context: FacesContext, visitor: DataVisitor, range: Range, argument: Object): Unit = {
    loadData(range)
    for (val index <- 0 to pageData.size - 1) {
      visitor.process(context, index, argument)
    }
  }

  def isRowAvailable: Boolean = rowIndex < pageData.length

  def getRowCount: Int = rowCount

  def getRowData: E = {
    val result = pageData(rowIndex) // repository.findOne(rowKey.asInstanceOf[Long])
    val node = result.asInstanceOf[NodeBacked]
    log.trace("getRowData({}) = #{}: {}",
        Array[Object](rowIndex: java.lang.Long, node.getNodeId: java.lang.Long, node))
    result
  }

  def getRowIndex: Int = rowIndex
  def setRowIndex(index: Int): Unit = rowIndex = index

  def getWrappedData: Object = { null }
  def setWrappedData(wrappedData: Object): Unit = { /* dummy */ }

}

To use the FinderModel, it's much easier if we create a repository first and add some finder/query methods returning count and Page:

public interface InterestRepository extends GraphRepository<Interest> {

    @Query("START u=node({userId}) MATCH u-[:LIKE]->y RETURN COUNT(y)")
    public Long findUserLikeCount(@Param("userId") long userId);

    // ORDER BY name is still slow: https://groups.google.com/group/neo4j/t/f2219df41f5500a9
//    @Query("START u=node({userId}) MATCH u-[:LIKE]->y RETURN y ORDER BY y.name")
    @Query("START u=node({userId}) MATCH u-[:LIKE]->y RETURN y")
    public Page<Interest> findUserLikes(@Param("userId") long userId, Pageable pageable);

}

How to create a FinderModel instance from Java :

    @Inject InterestRepository interestRepo;
    private FinderModel<Interest> userLikesModel;

    @PostConstruct public void init() {
        userLikesModel = new FinderModel<Interest>() {

            @Override
            public int getRowCountLazy() {
                return interestRepo.findUserLikeCount(user.getNodeId()).intValue();
            }

            @Override
            public Page<Interest> find(Pageable pageable) {
                return interestRepo.findUserLikes(user.getNodeId(), pageable);
            }
        };
    }

And how to use this data model from a JSF Template .xhtml file:

<rich:dataTable id="interestTable" var="interest" value="#{userLikes.userLikesModel}" rows="20">
    <rich:column>
        <f:facet name="header">Name</f:facet>
        <h:link outcome="/interests/show?id=#{interest.nodeId}" value="#{interest.name}"/>
    </rich:column>
    <f:facet name="footer"><rich:dataScroller/></f:facet>
</rich:dataTable>

Quite practical, isn't it?


ExtendedDataModel for Spring Data Neo4j Repository

FinderModel can then be further subclassed to handle any Spring Data Neo4j repository:

class GraphRepositoryModel[E]() extends FinderModel[E] {
  private var repository: GraphRepository[E] = _
 
  def getRowCountLazy: Int = repository.count.toInt
  def find(pageable: Pageable): Page[E] = repository.findAll(pageable)

  override def getWrappedData: Object = repository
  override def setWrappedData(wrappedData: Object): Unit =
    repository = wrappedData.asInstanceOf[GraphRepository[E]]

}

And use it like this:

@Inject InterestRepository interestRepo;
private GraphRepositoryModel<Interest> interestModel;
   
@PostConstruct public void init() {
    interestModel = new GraphRepositoryModel<Interest>();
    interestModel.setWrappedData(interestRepo);
}

Hope this helps.

To learn more about Scala programming, I recommend Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition.

1 comment: