001/* 002 * Copyright 2006-2015 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.batch.item.database.support; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Map; 022import java.util.Map.Entry; 023 024import org.springframework.batch.item.database.Order; 025import org.springframework.util.StringUtils; 026 027/** 028 * Utility class that generates the actual SQL statements used by query 029 * providers. 030 * 031 * @author Thomas Risberg 032 * @author Dave Syer 033 * @author Michael Minella 034 * @since 2.0 035 */ 036public class SqlPagingQueryUtils { 037 038 /** 039 * Generate SQL query string using a LIMIT clause 040 * 041 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 042 * implementation specifics 043 * @param remainingPageQuery is this query for the remaining pages (true) as 044 * opposed to the first page (false) 045 * @param limitClause the implementation specific limit clause to be used 046 * @return the generated query 047 */ 048 public static String generateLimitSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, 049 String limitClause) { 050 StringBuilder sql = new StringBuilder(); 051 sql.append("SELECT ").append(provider.getSelectClause()); 052 sql.append(" FROM ").append(provider.getFromClause()); 053 buildWhereClause(provider, remainingPageQuery, sql); 054 buildGroupByClause(provider, sql); 055 sql.append(" ORDER BY ").append(buildSortClause(provider)); 056 sql.append(" " + limitClause); 057 058 return sql.toString(); 059 } 060 061 /** 062 * Generate SQL query string using a LIMIT clause 063 * 064 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 065 * implementation specifics 066 * @param remainingPageQuery is this query for the remaining pages (true) as 067 * opposed to the first page (false) 068 * @param limitClause the implementation specific limit clause to be used 069 * @return the generated query 070 */ 071 public static String generateLimitGroupedSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, 072 String limitClause) { 073 StringBuilder sql = new StringBuilder(); 074 sql.append("SELECT * "); 075 sql.append(" FROM ("); 076 sql.append("SELECT ").append(provider.getSelectClause()); 077 sql.append(" FROM ").append(provider.getFromClause()); 078 sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); 079 buildGroupByClause(provider, sql); 080 sql.append(") AS MAIN_QRY "); 081 sql.append("WHERE "); 082 buildSortConditions(provider, sql); 083 sql.append(" ORDER BY ").append(buildSortClause(provider)); 084 sql.append(" " + limitClause); 085 086 return sql.toString(); 087 } 088 089 /** 090 * Generate SQL query string using a TOP clause 091 * 092 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 093 * implementation specifics 094 * @param remainingPageQuery is this query for the remaining pages (true) as 095 * opposed to the first page (false) 096 * @param topClause the implementation specific top clause to be used 097 * @return the generated query 098 */ 099 public static String generateTopSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, 100 String topClause) { 101 StringBuilder sql = new StringBuilder(); 102 sql.append("SELECT ").append(topClause).append(" ").append(provider.getSelectClause()); 103 sql.append(" FROM ").append(provider.getFromClause()); 104 buildWhereClause(provider, remainingPageQuery, sql); 105 buildGroupByClause(provider, sql); 106 sql.append(" ORDER BY ").append(buildSortClause(provider)); 107 108 return sql.toString(); 109 } 110 111 /** 112 * Generate SQL query string using a TOP clause 113 * 114 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 115 * implementation specifics 116 * @param remainingPageQuery is this query for the remaining pages (true) as 117 * opposed to the first page (false) 118 * @param topClause the implementation specific top clause to be used 119 * @return the generated query 120 */ 121 public static String generateGroupedTopSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, 122 String topClause) { 123 StringBuilder sql = new StringBuilder(); 124 sql.append("SELECT ").append(topClause).append(" * FROM ("); 125 sql.append("SELECT ").append(provider.getSelectClause()); 126 sql.append(" FROM ").append(provider.getFromClause()); 127 sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); 128 buildGroupByClause(provider, sql); 129 sql.append(") AS MAIN_QRY "); 130 sql.append("WHERE "); 131 buildSortConditions(provider, sql); 132 sql.append(" ORDER BY ").append(buildSortClause(provider)); 133 134 return sql.toString(); 135 } 136 137 /** 138 * Generate SQL query string using a ROW_NUM condition 139 * 140 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 141 * implementation specifics 142 * @param remainingPageQuery is this query for the remaining pages (true) as 143 * opposed to the first page (false) 144 * @param rowNumClause the implementation specific row num clause to be used 145 * @return the generated query 146 */ 147 public static String generateRowNumSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, 148 String rowNumClause) { 149 150 return generateRowNumSqlQuery(provider, provider.getSelectClause(), remainingPageQuery, rowNumClause); 151 152 } 153 154 /** 155 * Generate SQL query string using a ROW_NUM condition 156 * 157 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 158 * implementation specifics 159 * @param selectClause {@link String} containing the select portion of the query. 160 * @param remainingPageQuery is this query for the remaining pages (true) as 161 * opposed to the first page (false) 162 * @param rowNumClause the implementation specific row num clause to be used 163 * @return the generated query 164 */ 165 public static String generateRowNumSqlQuery(AbstractSqlPagingQueryProvider provider, String selectClause, 166 boolean remainingPageQuery, String rowNumClause) { 167 StringBuilder sql = new StringBuilder(); 168 sql.append("SELECT * FROM (SELECT ").append(selectClause); 169 sql.append(" FROM ").append(provider.getFromClause()); 170 sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); 171 buildGroupByClause(provider, sql); 172 sql.append(" ORDER BY ").append(buildSortClause(provider)); 173 sql.append(") WHERE ").append(rowNumClause); 174 if(remainingPageQuery) { 175 sql.append(" AND "); 176 buildSortConditions(provider, sql); 177 } 178 179 return sql.toString(); 180 181 } 182 183 public static String generateRowNumSqlQueryWithNesting(AbstractSqlPagingQueryProvider provider, 184 String selectClause, boolean remainingPageQuery, String rowNumClause) { 185 return generateRowNumSqlQueryWithNesting(provider, selectClause, selectClause, remainingPageQuery, rowNumClause); 186 } 187 188 public static String generateRowNumSqlQueryWithNesting(AbstractSqlPagingQueryProvider provider, 189 String innerSelectClause, String outerSelectClause, boolean remainingPageQuery, String rowNumClause) { 190 191 StringBuilder sql = new StringBuilder(); 192 sql.append("SELECT ").append(outerSelectClause).append(" FROM (SELECT ").append(outerSelectClause) 193 .append(", ").append(StringUtils.hasText(provider.getGroupClause()) ? "MIN(ROWNUM) as TMP_ROW_NUM" : "ROWNUM as TMP_ROW_NUM"); 194 sql.append(" FROM (SELECT ").append(innerSelectClause).append(" FROM ").append(provider.getFromClause()); 195 buildWhereClause(provider, remainingPageQuery, sql); 196 buildGroupByClause(provider, sql); 197 sql.append(" ORDER BY ").append(buildSortClause(provider)); 198 sql.append(")) WHERE ").append(rowNumClause); 199 200 return sql.toString(); 201 202 } 203 204 /** 205 * Generate SQL query string using a LIMIT clause 206 * 207 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 208 * implementation specifics 209 * @param limitClause the implementation specific top clause to be used 210 * @return the generated query 211 */ 212 public static String generateLimitJumpToQuery(AbstractSqlPagingQueryProvider provider, String limitClause) { 213 StringBuilder sql = new StringBuilder(); 214 sql.append("SELECT ").append(buildSortKeySelect(provider)); 215 sql.append(" FROM ").append(provider.getFromClause()); 216 sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); 217 buildGroupByClause(provider, sql); 218 sql.append(" ORDER BY ").append(buildSortClause(provider)); 219 sql.append(" " + limitClause); 220 221 return sql.toString(); 222 } 223 224 /** 225 * Generate SQL query string using a TOP clause 226 * 227 * @param provider {@link AbstractSqlPagingQueryProvider} providing the 228 * implementation specifics 229 * @param topClause the implementation specific top clause to be used 230 * @return the generated query 231 */ 232 public static String generateTopJumpToQuery(AbstractSqlPagingQueryProvider provider, String topClause) { 233 StringBuilder sql = new StringBuilder(); 234 sql.append("SELECT ").append(topClause).append(" ").append(buildSortKeySelect(provider)); 235 sql.append(" FROM ").append(provider.getFromClause()); 236 sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); 237 buildGroupByClause(provider, sql); 238 sql.append(" ORDER BY ").append(buildSortClause(provider)); 239 240 return sql.toString(); 241 } 242 243 /** 244 * Generates ORDER BY attributes based on the sort keys. 245 * 246 * @param provider the {@link AbstractSqlPagingQueryProvider} to be used for 247 * used for pagination. 248 * @return a String that can be appended to an ORDER BY clause. 249 */ 250 public static String buildSortClause(AbstractSqlPagingQueryProvider provider) { 251 return buildSortClause(provider.getSortKeys()); 252 } 253 254 /** 255 * Generates ORDER BY attributes based on the sort keys. 256 * 257 * @param sortKeys {@link Map} where the key is the name of the column to be 258 * sorted and the value contains the {@link Order}. 259 * @return a String that can be appended to an ORDER BY clause. 260 */ 261 public static String buildSortClause(Map<String, Order> sortKeys) { 262 StringBuilder builder = new StringBuilder(); 263 String prefix = ""; 264 265 for (Map.Entry<String, Order> sortKey : sortKeys.entrySet()) { 266 builder.append(prefix); 267 268 prefix = ", "; 269 270 builder.append(sortKey.getKey()); 271 272 if(sortKey.getValue() != null && sortKey.getValue() == Order.DESCENDING) { 273 builder.append(" DESC"); 274 } 275 else { 276 builder.append(" ASC"); 277 } 278 } 279 280 return builder.toString(); 281 } 282 283 /** 284 * Appends the where conditions required to query for the subsequent pages. 285 * 286 * @param provider the {@link AbstractSqlPagingQueryProvider} to be used for 287 * pagination. 288 * @param sql {@link StringBuilder} containing the sql to be used for the 289 * query. 290 */ 291 public static void buildSortConditions( 292 AbstractSqlPagingQueryProvider provider, StringBuilder sql) { 293 List<Map.Entry<String, Order>> keys = new ArrayList<Map.Entry<String,Order>>(provider.getSortKeys().entrySet()); 294 List<String> clauses = new ArrayList<String>(); 295 296 for(int i = 0; i < keys.size(); i++) { 297 StringBuilder clause = new StringBuilder(); 298 299 String prefix = ""; 300 for(int j = 0; j < i; j++) { 301 clause.append(prefix); 302 prefix = " AND "; 303 Entry<String, Order> entry = keys.get(j); 304 clause.append(entry.getKey()); 305 clause.append(" = "); 306 clause.append(provider.getSortKeyPlaceHolder(entry.getKey())); 307 } 308 309 if(clause.length() > 0) { 310 clause.append(" AND "); 311 } 312 clause.append(keys.get(i).getKey()); 313 314 if(keys.get(i).getValue() != null && keys.get(i).getValue() == Order.DESCENDING) { 315 clause.append(" < "); 316 } 317 else { 318 clause.append(" > "); 319 } 320 321 clause.append(provider.getSortKeyPlaceHolder(keys.get(i).getKey())); 322 323 clauses.add(clause.toString()); 324 } 325 326 sql.append("("); 327 String prefix = ""; 328 329 for (String curClause : clauses) { 330 sql.append(prefix); 331 prefix = " OR "; 332 sql.append("("); 333 sql.append(curClause); 334 sql.append(")"); 335 } 336 sql.append(")"); 337 } 338 339 private static String buildSortKeySelect(AbstractSqlPagingQueryProvider provider) { 340 StringBuilder select = new StringBuilder(); 341 342 String prefix = ""; 343 344 for (Map.Entry<String, Order> sortKey : provider.getSortKeys().entrySet()) { 345 select.append(prefix); 346 347 prefix = ", "; 348 349 select.append(sortKey.getKey()); 350 } 351 352 return select.toString(); 353 } 354 355 private static void buildWhereClause(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, 356 StringBuilder sql) { 357 if (remainingPageQuery) { 358 sql.append(" WHERE "); 359 if (provider.getWhereClause() != null) { 360 sql.append("("); 361 sql.append(provider.getWhereClause()); 362 sql.append(") AND "); 363 } 364 365 buildSortConditions(provider, sql); 366 } 367 else { 368 sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); 369 } 370 } 371 372 private static void buildGroupByClause(AbstractSqlPagingQueryProvider provider, StringBuilder sql) { 373 if(StringUtils.hasText(provider.getGroupClause())) { 374 sql.append(" GROUP BY "); 375 sql.append(provider.getGroupClause()); 376 } 377 } 378 379}