BATCH-760: refactoring, moving SQL snippets to PagingQueryProvider implementations, adding some database specific implementations (more to come)

This commit is contained in:
trisberg
2008-08-25 19:28:57 +00:00
parent d41464c176
commit c0c8331227
14 changed files with 654 additions and 195 deletions

View File

@@ -1,6 +1,22 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.item.database;
import org.springframework.batch.item.support.AbstractItemReaderItemStream;
import org.springframework.batch.item.database.support.PagingQueryProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ClassUtils;
import org.springframework.util.Assert;
@@ -9,9 +25,6 @@ import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -25,12 +38,13 @@ import java.sql.SQLException;
* {@link org.springframework.batch.item.ItemReader} for reading database records using JDBC in a paging
* fashion.
*
* It executes the SQL built from values specified for {@link #setSelectClause(String)} (String)},
* {@link #setFromClause(String)} (String)} and {@link #setWhereClause(String)} (String)} to retrieve requested data.
* The query is executed using paged requests of a size specified in {@link #setPageSize(int)}. Additional pages
* are requested when needed as {@link #read()} method is called, returning an object corresponding to current position.
* It executes the SQL built by the {@link PagingQueryProvider} to retrieve requested data.
* The query is executed using paged requests of a size specified in {@link #setPageSize(int)}.
* Additional pages are requested when needed as {@link #read()} method is called, returning an
* object corresponding to current position.
*
* The performance of the paging depends on the database specific features available to limit the number of returned rows.
* The performance of the paging depends on the database specific features available to limit the number
* of returned rows.
*
* Setting a fairly large page size and using a commit interval that matches the page size should provide
* better performance.
@@ -46,22 +60,12 @@ public class JdbcPagingItemReader<T> extends AbstractItemReaderItemStream<T> imp
private DataSource dataSource;
private PagingQueryProvider queryProvider;
private SimpleJdbcTemplate simpleJdbcTemplate;
private ParameterizedRowMapper<T> parameterizedRowMapper;
private String databaseProductName;
private String selectClause;
private String fromClause;
private String whereClause;
private String sortKey;
private String orderClause;
private String firstPageSql;
private String remainingPagesSql;
@@ -86,53 +90,8 @@ public class JdbcPagingItemReader<T> extends AbstractItemReaderItemStream<T> imp
this.dataSource = dataSource;
}
/**
* @param selectClause SELECT clause part of SQL query string
*/
public void setSelectClause(String selectClause) {
String keyWord = "select ";
String temp = selectClause.trim();
if (temp.toLowerCase().startsWith(keyWord) && temp.length() > keyWord.length()) {
this.selectClause = temp.substring(keyWord.length());
}
else {
this.selectClause = temp;
}
}
/**
* @param fromClause FROM clause part of SQL query string
*/
public void setFromClause(String fromClause) {
String keyWord = "from ";
String temp = fromClause.trim();
if (temp.toLowerCase().startsWith(keyWord) && temp.length() > keyWord.length()) {
this.fromClause = temp.substring(keyWord.length());
}
else {
this.fromClause = temp;
}
}
/**
* @param whereClause WHERE clause part of SQL query string
*/
public void setWhereClause(String whereClause) {
String keyWord = "where ";
String temp = whereClause.trim();
if (temp.toLowerCase().startsWith(keyWord) && temp.length() > keyWord.length()) {
this.whereClause = temp.substring(keyWord.length());
}
else {
this.whereClause = temp;
}
}
/**
* @param sortKey key to use to sort and limit page content
*/
public void setSortKey(String sortKey) {
this.sortKey = sortKey;
public void setQueryProvider(PagingQueryProvider queryProvider) {
this.queryProvider = queryProvider;
}
/**
@@ -159,59 +118,15 @@ public class JdbcPagingItemReader<T> extends AbstractItemReaderItemStream<T> imp
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(dataSource);
Assert.hasLength(selectClause, "selectClause must be specified");
Assert.hasLength(fromClause, "fromClause must be specified");
Assert.hasLength(sortKey, "sortKey must be specified");
Assert.isTrue(pageSize > 0, "pageSize must be greater than zero");
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setMaxRows(pageSize);
this.simpleJdbcTemplate = new SimpleJdbcTemplate(jdbcTemplate);
initializeSqlStatements();
Assert.notNull(queryProvider);
this.firstPageSql = queryProvider.generateFirstPageQuery(pageSize);
this.remainingPagesSql = queryProvider.generateRemainingPagesQuery(pageSize);
}
private void initializeSqlStatements() throws MetaDataAccessException {
this.databaseProductName = JdbcUtils.commonDatabaseName(
JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName").toString());
String topClause = "";
String limitCondition = "";
String limitClause = "";
if ("DB2".equals(databaseProductName)) {
limitClause = " FETCH FIRST " + pageSize + " ROWS ONLY";
}
else if ("Oracle".equals(databaseProductName)) {
limitCondition = "ROWNUM <= " + pageSize;
}
else if ("MySQL".equals(databaseProductName) || "PostgreSQL".equals(databaseProductName)) {
limitClause = " LIMIT " + pageSize;
}
else if ("Microsoft SQL Server".equals(databaseProductName) || "Sybase".equals(databaseProductName) ||
"HSQL Database Engine".equals(databaseProductName)) {
topClause = "TOP " + pageSize + " ";
}
else if ("Apache Derby".equals(databaseProductName)) {
String version = JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseVersion").toString();
if ("10.4.1.3".compareTo(version) > 0) {
throw new InvalidDataAccessResourceUsageException(databaseProductName + " version " + version + " is not supported");
}
// Derby doesn't support TOP or LIMIT -- maxRows will limit the rows retrieved
}
else {
throw new InvalidDataAccessResourceUsageException(databaseProductName + " is not a supported database");
}
this.orderClause = " ORDER BY SORT_KEY";
this.firstPageSql = "SELECT " + topClause + selectClause + ", " + sortKey + " AS SORT_KEY" +
" FROM " + fromClause +
(whereClause == null ? "" : " WHERE " + whereClause) +
(limitCondition.length() == 0 ? "" : (whereClause == null ? " WHERE " : " AND ") + limitCondition) +
orderClause + limitClause;
this.remainingPagesSql = "SELECT " + topClause + selectClause + ", " + sortKey + " AS SORT_KEY" +
" FROM " + fromClause + " WHERE " + sortKey + " > ?" +
(whereClause == null ? "" : " AND " + whereClause) +
(limitCondition.length() == 0 ? "" : " AND " + limitCondition) +
orderClause + limitClause;
}
@Override
@SuppressWarnings("unchecked")
protected T doRead() throws Exception {
@@ -289,35 +204,13 @@ public class JdbcPagingItemReader<T> extends AbstractItemReaderItemStream<T> imp
page = itemIndex / pageSize;
current = itemIndex % pageSize;
int offset = (page * pageSize) - 1;
int lastRowNum = (page * pageSize);
logger.debug("Jumping to page " + page + " and index " + current);
if (page > 0) {
String windowClause = "";
String topClause = "";
String limitClause = "";
if ("DB2".equals(databaseProductName) || "Oracle".equals(databaseProductName) ||
"Microsoft SQL Server".equals(databaseProductName) || "Sybase".equals(databaseProductName) ||
"Apache Derby".equals(databaseProductName)) {
windowClause = "ROW_NUMBER() OVER (ORDER BY " + sortKey + " ASC) AS ROW_NUMBER";
}
else if ("HSQL Database Engine".equals(databaseProductName)) {
topClause = "LIMIT " + offset + " 1 ";
}
else if ("MySQL".equals(databaseProductName) || "PostgreSQL".equals(databaseProductName) ||
"HSQL Database Engine".equals(databaseProductName)) {
limitClause = " LIMIT 1 OFFSET " + offset;
}
String jumpToItemSql;
String jumpToItemSql =
(windowClause.length() > 0 ? "SELECT * FROM ( " : "") +
"SELECT " + (topClause.length() > 0 ? topClause : "") + sortKey + " AS SORT_KEY" +
(windowClause.length() > 0 ? ", " + windowClause : "") +
" FROM " + fromClause + (whereClause == null ? "" : " WHERE " + whereClause) +
(windowClause.length() > 0 ? ") WHERE ROW_NUMBER = " + lastRowNum : orderClause + limitClause);
jumpToItemSql = queryProvider.generateJumpToItemQuery(itemIndex, pageSize);
if (logger.isDebugEnabled()) {
logger.debug("SQL used for jumping: [" + jumpToItemSql + "]");

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.item.database.support;
import org.springframework.util.Assert;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import javax.sql.DataSource;
/**
* Abstract SQL Paging Query Provider to serve as a base class for all provided SQL paging query providers.
*
* Any implementation must provide a way to specify the select clause, from clause and optionally a where clause.
* In addition a way to specify a single column sort key must also be provided. This sort key will be used to
* provide the paging functinality. It is recommended that there should be an index for the sort key to provide
* better performance.
*
* Provides properties and preperation for the mandatory "selectClause" and "fromClause" as well as for the
* optional "whereClause". Also provides property for the mandatory "sortKey".
*
* @author Thomas Risberg
* @since 2.0
*/
public abstract class AbstractSqlPagingQueryProvider implements PagingQueryProvider {
private String selectClause;
private String fromClause;
private String whereClause;
private String sortKey;
/**
* @param selectClause SELECT clause part of SQL query string
*/
public void setSelectClause(String selectClause) {
String keyWord = "select ";
String temp = selectClause.trim();
if (temp.toLowerCase().startsWith(keyWord) && temp.length() > keyWord.length()) {
this.selectClause = temp.substring(keyWord.length());
}
else {
this.selectClause = temp;
}
}
/**
*
* @return the
*/
protected String getSelectClause() {
return selectClause;
}
/**
* @param fromClause FROM clause part of SQL query string
*/
public void setFromClause(String fromClause) {
String keyWord = "from ";
String temp = fromClause.trim();
if (temp.toLowerCase().startsWith(keyWord) && temp.length() > keyWord.length()) {
this.fromClause = temp.substring(keyWord.length());
}
else {
this.fromClause = temp;
}
}
/**
*
* @return
*/
protected String getFromClause() {
return fromClause;
}
/**
* @param whereClause WHERE clause part of SQL query string
*/
public void setWhereClause(String whereClause) {
String keyWord = "where ";
String temp = whereClause.trim();
if (temp.toLowerCase().startsWith(keyWord) && temp.length() > keyWord.length()) {
this.whereClause = temp.substring(keyWord.length());
}
else {
this.whereClause = temp;
}
}
/**
*
* @return
*/
protected String getWhereClause() {
return whereClause;
}
/**
* @param sortKey key to use to sort and limit page content
*/
public void setSortKey(String sortKey) {
this.sortKey = sortKey;
}
/**
*
* @return
*/
protected String getSortKey() {
return sortKey;
}
/**
* Check mandatory properties.
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void init(DataSource dataSource) throws Exception {
Assert.notNull(dataSource);
Assert.hasLength(selectClause, "selectClause must be specified");
Assert.hasLength(fromClause, "fromClause must be specified");
Assert.hasLength(sortKey, "sortKey must be specified");
}
public abstract String generateFirstPageQuery(int pageSize);
public abstract String generateRemainingPagesQuery(int pageSize);
public abstract String generateJumpToItemQuery(int itemIndex, int pageSize);
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.item.database.support;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import javax.sql.DataSource;
/**
* Derby implementation of a {@link PagingQueryProvider} using standard SQL:2003 windowing functions.
* These features are supported starting with Apache Derby version 10.4.1.3.
*
* @author Thomas Risberg
* @since 2.0
*/
public class DerbyPagingQueryProvider extends SqlWindowingPagingQueryProvider {
@Override
public void init(DataSource dataSource) throws Exception {
super.init(dataSource);
String version = JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductVersion").toString();
if ("10.4.1.3".compareTo(version) > 0) {
throw new InvalidDataAccessResourceUsageException("Apache Derby version " + version + " is not supported by this class, Only version 10.4.1.3 or later is supported");
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.item.database.support;
/**
* MySQL implementation of a {@link org.springframework.batch.item.database.support.PagingQueryProvider} using database specific features.
*
* @author Thomas Risberg
* @since 2.0
*/
public class HsqlPagingQueryProvider extends AbstractSqlPagingQueryProvider {
@Override
public String generateFirstPageQuery(int pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append("TOP ").append(pageSize).append(" ").append(getSelectClause());
sql.append(" FROM ").append(getFromClause());
sql.append(getWhereClause() == null ? "" : " WHERE " + getWhereClause());
return sql.toString();
}
@Override
public String generateRemainingPagesQuery(int pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append("TOP ").append(pageSize).append(" ").append(getSelectClause());
sql.append(" FROM ").append(getFromClause());
sql.append(" WHERE ").append(getSortKey()).append(" > ?");
sql.append(getWhereClause() == null ? "" : " AND " + getWhereClause());
return sql.toString();
}
@Override
public String generateJumpToItemQuery(int itemIndex, int pageSize) {
int page = itemIndex / pageSize;
int offset = (page * pageSize) - 1;
StringBuilder sql = new StringBuilder();
sql.append("SELECT LIMIT ").append(offset).append(" 1 ").append(getSortKey()).append(" AS SORT_KEY");
sql.append(" FROM ").append(getFromClause());
sql.append(getWhereClause() == null ? "" : " WHERE " + getWhereClause());
return sql.toString();
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2006-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.item.database.support;
/**
* MySQL implementation of a {@link PagingQueryProvider} using database specific features.
*
* @author Thomas Risberg
* @since 2.0
*/
public class MySqlPagingQueryProvider extends AbstractSqlPagingQueryProvider {
@Override
public String generateFirstPageQuery(int pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append(getSelectClause());
sql.append(" FROM ").append(getFromClause());
sql.append(getWhereClause() == null ? "" : " WHERE " + getWhereClause());
sql.append(" LIMIT ").append(pageSize);
return sql.toString();
}
@Override
public String generateRemainingPagesQuery(int pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append(getSelectClause());
sql.append(" FROM ").append(getFromClause());
sql.append(" WHERE ").append(getSortKey()).append(" > ?");
sql.append(getWhereClause() == null ? "" : " AND " + getWhereClause());
sql.append(" LIMIT ").append(pageSize);
return sql.toString();
}
@Override
public String generateJumpToItemQuery(int itemIndex, int pageSize) {
int page = itemIndex / pageSize;
int offset = (page * pageSize) - 1;
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append(getSortKey()).append(" AS SORT_KEY");
sql.append(" FROM ").append(getFromClause()).append(getWhereClause() == null ? "" : " WHERE " + getWhereClause());
sql.append(" LIMIT ").append(offset).append(" 1");
return sql.toString();
}
}

View File

@@ -15,41 +15,39 @@
*/
package org.springframework.batch.item.database.support;
import javax.sql.DataSource;
/**
* Interface defining the functionality to be provided for generating paging queries for use with JDBC.
*
* Any usage must provide the select clause, from clause and optionally a where clause. In addition a
* single column sort key must be defined. This sort key will be used to provide the paging functinality.
* It is recommended that there should be an index for the sort key to provide better performance.
* Interface defining the functionality to be provided for generating paging queries for use with Paging
* Item Readers.
*
* @author Thomas Risberg
* @since 2.0
*/
public interface JdbcPagingQueryProvider {
public interface PagingQueryProvider {
/**
* Initialize the query provider using the provided {@link DataSource} if necessary.
*
* @param dataSource DataSource to use for any initialization
*/
void init(DataSource dataSource) throws Exception;
/**
* Generate the query that will provide the first page, limited by the page size.
*
* @param pageSize number of rows to read for each page
* @param selectClause the columns that are part of the selct clause
* @param fromClause the table(s) that are part of the from clause
* @param whereClause the conditions that are part of the where clause
* @param sortKey the single column used for sorting
* @return the generated query
*/
String generateFirstPageQuery(int pageSize, String selectClause, String fromClause, String whereClause, String sortKey);
String generateFirstPageQuery(int pageSize);
/**
* Generate the query that will provide the first page, limited by the page size.
*
* @param pageSize number of rows to read for each page
* @param selectClause the columns that are part of the selct clause
* @param fromClause the table(s) that are part of the from clause
* @param whereClause the conditions that are part of the where clause
* @param sortKey the single column used for sorting
* @return the generated query
*/
String generateRemainingPagesQuery(int pageSize, String selectClause, String fromClause, String whereClause, String sortKey);
String generateRemainingPagesQuery(int pageSize);
/**
*
@@ -59,12 +57,8 @@ public interface JdbcPagingQueryProvider {
*
* @param itemIndex the index for the next item to be read
* @param pageSize number of rows to read for each page
* @param selectClause the columns that are part of the selct clause
* @param fromClause the table(s) that are part of the from clause
* @param whereClause the conditions that are part of the where clause
* @param sortKey the single column used for sorting
* @return the generated query
*/
String generateJumpToItemQuery(int itemIndex, int pageSize, String selectClause, String fromClause, String whereClause, String sortKey);
String generateJumpToItemQuery(int itemIndex, int pageSize);
}

View File

@@ -22,40 +22,40 @@ package org.springframework.batch.item.database.support;
* @author Thomas Risberg
* @since 2.0
*/
public class SqlWindowingPagingQueryProvider implements JdbcPagingQueryProvider {
public class SqlWindowingPagingQueryProvider extends AbstractSqlPagingQueryProvider {
public String generateFirstPageQuery(int pageSize, String selectClause, String fromClause, String whereClause, String sortKey) {
public String generateFirstPageQuery(int pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM ( ");
sql.append("SELECT ").append(selectClause).append(", ");
sql.append("ROW_NUMBER() OVER (ORDER BY ").append(sortKey).append(" ASC) AS ROW_NUMBER");
sql.append(" FROM ").append(fromClause).append(whereClause == null ? "" : " WHERE " + whereClause);
sql.append("SELECT ").append(getSelectClause()).append(", ");
sql.append("ROW_NUMBER() OVER (ORDER BY ").append(getSortKey()).append(" ASC) AS ROW_NUMBER");
sql.append(" FROM ").append(getFromClause()).append(getWhereClause() == null ? "" : " WHERE " + getWhereClause());
sql.append(") WHERE ROW_NUMBER <= ").append(pageSize);
return sql.toString();
}
public String generateRemainingPagesQuery(int pageSize, String selectClause, String fromClause, String whereClause, String sortKey) {
public String generateRemainingPagesQuery(int pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM ( ");
sql.append("SELECT ").append(selectClause).append(", ");
sql.append("ROW_NUMBER() OVER (ORDER BY ").append(sortKey).append(" ASC) AS ROW_NUMBER");
sql.append(" FROM ").append(fromClause).append(" WHERE ").append(sortKey).append(" > ?");
sql.append(whereClause == null ? "" : " AND " + whereClause);
sql.append("SELECT ").append(getSelectClause()).append(", ");
sql.append("ROW_NUMBER() OVER (ORDER BY ").append(getSortKey()).append(" ASC) AS ROW_NUMBER");
sql.append(" FROM ").append(getFromClause()).append(" WHERE ").append(getSortKey()).append(" > ?");
sql.append(getWhereClause() == null ? "" : " AND " + getWhereClause());
sql.append(") WHERE ROW_NUMBER <= ").append(pageSize);
return sql.toString();
}
public String generateJumpToItemQuery(int itemIndex, int pageSize, String selectClause, String fromClause, String whereClause, String sortKey) {
public String generateJumpToItemQuery(int itemIndex, int pageSize) {
int page = itemIndex / pageSize;
int lastRowNum = (page * pageSize);
StringBuilder sql = new StringBuilder();
sql.append("SELECT SORT_KEY FROM ( ");
sql.append("SELECT ").append(sortKey).append(" AS SORT_KEY, ");
sql.append("ROW_NUMBER() OVER (ORDER BY ").append(sortKey).append(" ASC) AS ROW_NUMBER");
sql.append(" FROM ").append(fromClause).append(whereClause == null ? "" : " WHERE " + whereClause);
sql.append("SELECT ").append(getSortKey()).append(" AS SORT_KEY, ");
sql.append("ROW_NUMBER() OVER (ORDER BY ").append(getSortKey()).append(" ASC) AS ROW_NUMBER");
sql.append(" FROM ").append(getFromClause()).append(getWhereClause() == null ? "" : " WHERE " + getWhereClause());
sql.append(") WHERE ROW_NUMBER = ").append(lastRowNum);
return sql.toString();