1616
1717package org .springframework .cloud .dataflow .server .repository ;
1818
19+ import java .lang .reflect .Field ;
1920import java .sql .ResultSet ;
2021import java .sql .SQLException ;
2122import java .time .Instant ;
4748import org .springframework .batch .core .repository .dao .JdbcJobExecutionDao ;
4849import org .springframework .batch .item .database .Order ;
4950import org .springframework .batch .item .database .PagingQueryProvider ;
51+ import org .springframework .batch .item .database .support .AbstractSqlPagingQueryProvider ;
52+ import org .springframework .batch .item .database .support .Db2PagingQueryProvider ;
53+ import org .springframework .batch .item .database .support .OraclePagingQueryProvider ;
5054import org .springframework .batch .item .database .support .SqlPagingQueryProviderFactoryBean ;
55+ import org .springframework .batch .item .database .support .SqlPagingQueryUtils ;
56+ import org .springframework .batch .item .database .support .SqlServerPagingQueryProvider ;
5157import org .springframework .cloud .dataflow .core .DataFlowPropertyKeys ;
5258import org .springframework .cloud .dataflow .core .database .support .DatabaseType ;
5359import org .springframework .cloud .dataflow .rest .job .JobInstanceExecutions ;
7581import org .springframework .jdbc .core .RowMapper ;
7682import org .springframework .util .Assert ;
7783import org .springframework .util .ObjectUtils ;
84+ import org .springframework .util .ReflectionUtils ;
7885import org .springframework .util .StringUtils ;
7986
8087/**
@@ -802,7 +809,7 @@ private PagingQueryProvider getPagingQueryProvider(String fields, String fromCla
802809 * @throws Exception if page provider is not created.
803810 */
804811 private PagingQueryProvider getPagingQueryProvider (String fields , String fromClause , String whereClause , Map <String , Order > sortKeys ) throws Exception {
805- SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean ();
812+ SqlPagingQueryProviderFactoryBean factory = new SafeSqlPagingQueryProviderFactoryBean ();
806813 factory .setDataSource (dataSource );
807814 fromClause = "AGGREGATE_JOB_INSTANCE I JOIN AGGREGATE_JOB_EXECUTION E ON I.JOB_INSTANCE_ID=E.JOB_INSTANCE_ID AND I.SCHEMA_TARGET=E.SCHEMA_TARGET" + (fromClause == null ? "" : " " + fromClause );
808815 factory .setFromClause (fromClause );
@@ -811,7 +818,7 @@ private PagingQueryProvider getPagingQueryProvider(String fields, String fromCla
811818 }
812819 if (fields .contains ("E.JOB_EXECUTION_ID" ) && this .useRowNumberOptimization ) {
813820 Order order = sortKeys .get ("E.JOB_EXECUTION_ID" );
814- String orderString = Optional . ofNullable (order ). map ( orderKey -> orderKey == Order .DESCENDING ? "DESC" : "ASC" ). orElse ( "DESC" ) ;
821+ String orderString = (order == null || order == Order .DESCENDING ) ? "DESC" : "ASC" ;
815822 fields += ", ROW_NUMBER() OVER (PARTITION BY E.JOB_EXECUTION_ID ORDER BY E.JOB_EXECUTION_ID " + orderString + ") as RN" ;
816823 }
817824 factory .setSelectClause (fields );
@@ -832,4 +839,201 @@ private boolean determineSupportsRowNumberFunction(DataSource dataSource) {
832839 }
833840 return false ;
834841 }
842+
843+ /**
844+ * A {@link SqlPagingQueryProviderFactoryBean} specialization that overrides the {@code Oracle, MSSQL, and DB2}
845+ * paging {@link SafeOraclePagingQueryProvider provider} with an implementation that properly handles sort aliases.
846+ * <p><b>NOTE:</b> nested within the aggregate DAO as this is the only place that needs this specialization.
847+ */
848+ static class SafeSqlPagingQueryProviderFactoryBean extends SqlPagingQueryProviderFactoryBean {
849+
850+ private DataSource dataSource ;
851+
852+ @ Override
853+ public void setDataSource (DataSource dataSource ) {
854+ super .setDataSource (dataSource );
855+ this .dataSource = dataSource ;
856+ }
857+
858+ @ Override
859+ public PagingQueryProvider getObject () throws Exception {
860+ PagingQueryProvider provider = super .getObject ();
861+ if (provider instanceof OraclePagingQueryProvider ) {
862+ provider = new SafeOraclePagingQueryProvider ((AbstractSqlPagingQueryProvider ) provider , this .dataSource );
863+ }
864+ else if (provider instanceof SqlServerPagingQueryProvider ) {
865+ provider = new SafeSqlServerPagingQueryProvider ((SqlServerPagingQueryProvider ) provider , this .dataSource );
866+ }
867+ else if (provider instanceof Db2PagingQueryProvider ) {
868+ provider = new SafeDb2PagingQueryProvider ((Db2PagingQueryProvider ) provider , this .dataSource );
869+ }
870+ return provider ;
871+ }
872+
873+ }
874+
875+ /**
876+ * A {@link AbstractSqlPagingQueryProvider paging provider} for {@code Oracle} that works around the fact that the
877+ * Oracle provider in Spring Batch 4.x does not properly handle sort aliases when using nested {@code ROW_NUMBER}
878+ * clauses.
879+ */
880+ static class SafeOraclePagingQueryProvider extends AbstractSqlPagingQueryProvider {
881+
882+ SafeOraclePagingQueryProvider (AbstractSqlPagingQueryProvider delegate , DataSource dataSource ) {
883+ // Have to use reflection to retrieve the provider fields
884+ this .setFromClause (extractField (delegate , "fromClause" , String .class ));
885+ this .setWhereClause (extractField (delegate , "whereClause" , String .class ));
886+ this .setSortKeys (extractField (delegate , "sortKeys" , Map .class ));
887+ this .setSelectClause (extractField (delegate , "selectClause" , String .class ));
888+ this .setGroupClause (extractField (delegate , "groupClause" , String .class ));
889+ try {
890+ this .init (dataSource );
891+ }
892+ catch (Exception e ) {
893+ throw new RuntimeException (e );
894+ }
895+ }
896+
897+ private <T > T extractField (AbstractSqlPagingQueryProvider target , String fieldName , Class <T > fieldType ) {
898+ Field field = ReflectionUtils .findField (AbstractSqlPagingQueryProvider .class , fieldName , fieldType );
899+ ReflectionUtils .makeAccessible (field );
900+ return (T ) ReflectionUtils .getField (field , target );
901+ }
902+
903+ @ Override
904+ public String generateFirstPageQuery (int pageSize ) {
905+ return generateRowNumSqlQuery (false , pageSize );
906+ }
907+
908+ @ Override
909+ public String generateRemainingPagesQuery (int pageSize ) {
910+ return generateRowNumSqlQuery (true , pageSize );
911+ }
912+
913+ @ Override
914+ public String generateJumpToItemQuery (int itemIndex , int pageSize ) {
915+ int page = itemIndex / pageSize ;
916+ int offset = (page * pageSize );
917+ offset = (offset == 0 ) ? 1 : offset ;
918+ String sortKeyInnerSelect = this .getSortKeySelect (true );
919+ String sortKeyOuterSelect = this .getSortKeySelect (false );
920+ return SqlPagingQueryUtils .generateRowNumSqlQueryWithNesting (this , sortKeyInnerSelect , sortKeyOuterSelect ,
921+ false , "TMP_ROW_NUM = " + offset );
922+ }
923+
924+ private String getSortKeySelect (boolean withAliases ) {
925+ StringBuilder sql = new StringBuilder ();
926+ Map <String , Order > sortKeys = (withAliases ) ? this .getSortKeys () : this .getSortKeysWithoutAliases ();
927+ sql .append (sortKeys .keySet ().stream ().collect (Collectors .joining ("," )));
928+ return sql .toString ();
929+ }
930+
931+ // Taken from SqlPagingQueryUtils.generateRowNumSqlQuery but use sortKeysWithoutAlias
932+ // for outer sort condition.
933+ private String generateRowNumSqlQuery (boolean remainingPageQuery , int pageSize ) {
934+ StringBuilder sql = new StringBuilder ();
935+ sql .append ("SELECT * FROM (SELECT " ).append (getSelectClause ());
936+ sql .append (" FROM " ).append (this .getFromClause ());
937+ if (StringUtils .hasText (this .getWhereClause ())) {
938+ sql .append (" WHERE " ).append (this .getWhereClause ());
939+ }
940+ if (StringUtils .hasText (this .getGroupClause ())) {
941+ sql .append (" GROUP BY " ).append (this .getGroupClause ());
942+ }
943+ // inner sort by
944+ sql .append (" ORDER BY " ).append (SqlPagingQueryUtils .buildSortClause (this ));
945+ sql .append (") WHERE " ).append ("ROWNUM <= " + pageSize );
946+ if (remainingPageQuery ) {
947+ sql .append (" AND " );
948+ // For the outer sort we want to use sort keys w/o aliases. However,
949+ // SqlPagingQueryUtils.buildSortConditions does not allow sort keys to be passed in.
950+ // Therefore, we temporarily set the 'sortKeys' for the call to 'buildSortConditions'.
951+ // The alternative is to clone the 'buildSortConditions' method here and allow the sort keys to be
952+ // passed in BUT method is gigantic and this approach is the lesser of the two evils.
953+ Map <String , Order > originalSortKeys = this .getSortKeys ();
954+ this .setSortKeys (this .getSortKeysWithoutAliases ());
955+ try {
956+ SqlPagingQueryUtils .buildSortConditions (this , sql );
957+ }
958+ finally {
959+ this .setSortKeys (originalSortKeys );
960+ }
961+ }
962+ return sql .toString ();
963+ }
964+ }
965+
966+ /**
967+ * A {@link SqlServerPagingQueryProvider paging provider} for {@code MSSQL} that works around the fact that the
968+ * MSSQL provider in Spring Batch 4.x does not properly handle sort aliases when generating jump to page queries.
969+ */
970+ static class SafeSqlServerPagingQueryProvider extends SqlServerPagingQueryProvider {
971+
972+ SafeSqlServerPagingQueryProvider (SqlServerPagingQueryProvider delegate , DataSource dataSource ) {
973+ // Have to use reflection to retrieve the provider fields
974+ this .setFromClause (extractField (delegate , "fromClause" , String .class ));
975+ this .setWhereClause (extractField (delegate , "whereClause" , String .class ));
976+ this .setSortKeys (extractField (delegate , "sortKeys" , Map .class ));
977+ this .setSelectClause (extractField (delegate , "selectClause" , String .class ));
978+ this .setGroupClause (extractField (delegate , "groupClause" , String .class ));
979+ try {
980+ this .init (dataSource );
981+ }
982+ catch (Exception e ) {
983+ throw new RuntimeException (e );
984+ }
985+ }
986+
987+ private <T > T extractField (AbstractSqlPagingQueryProvider target , String fieldName , Class <T > fieldType ) {
988+ Field field = ReflectionUtils .findField (AbstractSqlPagingQueryProvider .class , fieldName , fieldType );
989+ ReflectionUtils .makeAccessible (field );
990+ return (T ) ReflectionUtils .getField (field , target );
991+ }
992+
993+ @ Override
994+ protected String getOverClause () {
995+ // Overrides the parent impl to use 'getSortKeys' instead of 'getSortKeysWithoutAliases'
996+ StringBuilder sql = new StringBuilder ();
997+ sql .append (" ORDER BY " ).append (SqlPagingQueryUtils .buildSortClause (this .getSortKeys ()));
998+ return sql .toString ();
999+ }
1000+
1001+ }
1002+
1003+ /**
1004+ * A {@link Db2PagingQueryProvider paging provider} for {@code DB2} that works around the fact that the
1005+ * DB2 provider in Spring Batch 4.x does not properly handle sort aliases when generating jump to page queries.
1006+ */
1007+ static class SafeDb2PagingQueryProvider extends Db2PagingQueryProvider {
1008+
1009+ SafeDb2PagingQueryProvider (Db2PagingQueryProvider delegate , DataSource dataSource ) {
1010+ // Have to use reflection to retrieve the provider fields
1011+ this .setFromClause (extractField (delegate , "fromClause" , String .class ));
1012+ this .setWhereClause (extractField (delegate , "whereClause" , String .class ));
1013+ this .setSortKeys (extractField (delegate , "sortKeys" , Map .class ));
1014+ this .setSelectClause (extractField (delegate , "selectClause" , String .class ));
1015+ this .setGroupClause (extractField (delegate , "groupClause" , String .class ));
1016+ try {
1017+ this .init (dataSource );
1018+ }
1019+ catch (Exception e ) {
1020+ throw new RuntimeException (e );
1021+ }
1022+ }
1023+
1024+ private <T > T extractField (AbstractSqlPagingQueryProvider target , String fieldName , Class <T > fieldType ) {
1025+ Field field = ReflectionUtils .findField (AbstractSqlPagingQueryProvider .class , fieldName , fieldType );
1026+ ReflectionUtils .makeAccessible (field );
1027+ return (T ) ReflectionUtils .getField (field , target );
1028+ }
1029+
1030+ @ Override
1031+ protected String getOverClause () {
1032+ // Overrides the parent impl to use 'getSortKeys' instead of 'getSortKeysWithoutAliases'
1033+ StringBuilder sql = new StringBuilder ();
1034+ sql .append (" ORDER BY " ).append (SqlPagingQueryUtils .buildSortClause (this .getSortKeys ()));
1035+ return sql .toString ();
1036+ }
1037+
1038+ }
8351039}
0 commit comments