001/* 002 * Copyright 2002-2019 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.jdbc.core.simple; 018 019import java.util.ArrayList; 020import java.util.LinkedHashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024 025import javax.sql.DataSource; 026 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029 030import org.springframework.dao.InvalidDataAccessApiUsageException; 031import org.springframework.jdbc.core.CallableStatementCreator; 032import org.springframework.jdbc.core.CallableStatementCreatorFactory; 033import org.springframework.jdbc.core.JdbcTemplate; 034import org.springframework.jdbc.core.RowMapper; 035import org.springframework.jdbc.core.SqlParameter; 036import org.springframework.jdbc.core.metadata.CallMetaDataContext; 037import org.springframework.jdbc.core.namedparam.SqlParameterSource; 038import org.springframework.lang.Nullable; 039import org.springframework.util.Assert; 040import org.springframework.util.StringUtils; 041 042/** 043 * Abstract class to provide base functionality for easy stored procedure calls 044 * based on configuration options and database meta-data. 045 * 046 * <p>This class provides the base SPI for {@link SimpleJdbcCall}. 047 * 048 * @author Thomas Risberg 049 * @author Juergen Hoeller 050 * @since 2.5 051 */ 052public abstract class AbstractJdbcCall { 053 054 /** Logger available to subclasses. */ 055 protected final Log logger = LogFactory.getLog(getClass()); 056 057 /** Lower-level class used to execute SQL. */ 058 private final JdbcTemplate jdbcTemplate; 059 060 /** Context used to retrieve and manage database meta-data. */ 061 private final CallMetaDataContext callMetaDataContext = new CallMetaDataContext(); 062 063 /** List of SqlParameter objects. */ 064 private final List<SqlParameter> declaredParameters = new ArrayList<>(); 065 066 /** List of RefCursor/ResultSet RowMapper objects. */ 067 private final Map<String, RowMapper<?>> declaredRowMappers = new LinkedHashMap<>(); 068 069 /** 070 * Has this operation been compiled? Compilation means at least checking 071 * that a DataSource or JdbcTemplate has been provided. 072 */ 073 private volatile boolean compiled = false; 074 075 /** The generated string used for call statement. */ 076 @Nullable 077 private String callString; 078 079 /** 080 * A delegate enabling us to create CallableStatementCreators 081 * efficiently, based on this class's declared parameters. 082 */ 083 @Nullable 084 private CallableStatementCreatorFactory callableStatementFactory; 085 086 087 /** 088 * Constructor to be used when initializing using a {@link DataSource}. 089 * @param dataSource the DataSource to be used 090 */ 091 protected AbstractJdbcCall(DataSource dataSource) { 092 this.jdbcTemplate = new JdbcTemplate(dataSource); 093 } 094 095 /** 096 * Constructor to be used when initializing using a {@link JdbcTemplate}. 097 * @param jdbcTemplate the JdbcTemplate to use 098 */ 099 protected AbstractJdbcCall(JdbcTemplate jdbcTemplate) { 100 Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); 101 this.jdbcTemplate = jdbcTemplate; 102 } 103 104 105 /** 106 * Get the configured {@link JdbcTemplate}. 107 */ 108 public JdbcTemplate getJdbcTemplate() { 109 return this.jdbcTemplate; 110 } 111 112 /** 113 * Set the name of the stored procedure. 114 */ 115 public void setProcedureName(@Nullable String procedureName) { 116 this.callMetaDataContext.setProcedureName(procedureName); 117 } 118 119 /** 120 * Get the name of the stored procedure. 121 */ 122 @Nullable 123 public String getProcedureName() { 124 return this.callMetaDataContext.getProcedureName(); 125 } 126 127 /** 128 * Set the names of in parameters to be used. 129 */ 130 public void setInParameterNames(Set<String> inParameterNames) { 131 this.callMetaDataContext.setLimitedInParameterNames(inParameterNames); 132 } 133 134 /** 135 * Get the names of in parameters to be used. 136 */ 137 public Set<String> getInParameterNames() { 138 return this.callMetaDataContext.getLimitedInParameterNames(); 139 } 140 141 /** 142 * Set the catalog name to use. 143 */ 144 public void setCatalogName(@Nullable String catalogName) { 145 this.callMetaDataContext.setCatalogName(catalogName); 146 } 147 148 /** 149 * Get the catalog name used. 150 */ 151 @Nullable 152 public String getCatalogName() { 153 return this.callMetaDataContext.getCatalogName(); 154 } 155 156 /** 157 * Set the schema name to use. 158 */ 159 public void setSchemaName(@Nullable String schemaName) { 160 this.callMetaDataContext.setSchemaName(schemaName); 161 } 162 163 /** 164 * Get the schema name used. 165 */ 166 @Nullable 167 public String getSchemaName() { 168 return this.callMetaDataContext.getSchemaName(); 169 } 170 171 /** 172 * Specify whether this call is a function call. 173 * The default is {@code false}. 174 */ 175 public void setFunction(boolean function) { 176 this.callMetaDataContext.setFunction(function); 177 } 178 179 /** 180 * Is this call a function call? 181 */ 182 public boolean isFunction() { 183 return this.callMetaDataContext.isFunction(); 184 } 185 186 /** 187 * Specify whether the call requires a return value. 188 * The default is {@code false}. 189 */ 190 public void setReturnValueRequired(boolean returnValueRequired) { 191 this.callMetaDataContext.setReturnValueRequired(returnValueRequired); 192 } 193 194 /** 195 * Does the call require a return value? 196 */ 197 public boolean isReturnValueRequired() { 198 return this.callMetaDataContext.isReturnValueRequired(); 199 } 200 201 /** 202 * Specify whether parameters should be bound by name. 203 * The default is {@code false}. 204 * @since 4.2 205 */ 206 public void setNamedBinding(boolean namedBinding) { 207 this.callMetaDataContext.setNamedBinding(namedBinding); 208 } 209 210 /** 211 * Should parameters be bound by name? 212 * @since 4.2 213 */ 214 public boolean isNamedBinding() { 215 return this.callMetaDataContext.isNamedBinding(); 216 } 217 218 /** 219 * Specify whether the parameter meta-data for the call should be used. 220 * The default is {@code true}. 221 */ 222 public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { 223 this.callMetaDataContext.setAccessCallParameterMetaData(accessCallParameterMetaData); 224 } 225 226 /** 227 * Get the call string that should be used based on parameters and meta-data. 228 */ 229 @Nullable 230 public String getCallString() { 231 return this.callString; 232 } 233 234 /** 235 * Get the {@link CallableStatementCreatorFactory} being used. 236 */ 237 protected CallableStatementCreatorFactory getCallableStatementFactory() { 238 Assert.state(this.callableStatementFactory != null, "No CallableStatementCreatorFactory available"); 239 return this.callableStatementFactory; 240 } 241 242 243 /** 244 * Add a declared parameter to the list of parameters for the call. 245 * <p>Only parameters declared as {@code SqlParameter} and {@code SqlInOutParameter} will 246 * be used to provide input values. This is different from the {@code StoredProcedure} 247 * class which - for backwards compatibility reasons - allows input values to be provided 248 * for parameters declared as {@code SqlOutParameter}. 249 * @param parameter the {@link SqlParameter} to add 250 */ 251 public void addDeclaredParameter(SqlParameter parameter) { 252 Assert.notNull(parameter, "The supplied parameter must not be null"); 253 if (!StringUtils.hasText(parameter.getName())) { 254 throw new InvalidDataAccessApiUsageException( 255 "You must specify a parameter name when declaring parameters for \"" + getProcedureName() + "\""); 256 } 257 this.declaredParameters.add(parameter); 258 if (logger.isDebugEnabled()) { 259 logger.debug("Added declared parameter for [" + getProcedureName() + "]: " + parameter.getName()); 260 } 261 } 262 263 /** 264 * Add a {@link org.springframework.jdbc.core.RowMapper} for the specified parameter or column. 265 * @param parameterName name of parameter or column 266 * @param rowMapper the RowMapper implementation to use 267 */ 268 public void addDeclaredRowMapper(String parameterName, RowMapper<?> rowMapper) { 269 this.declaredRowMappers.put(parameterName, rowMapper); 270 if (logger.isDebugEnabled()) { 271 logger.debug("Added row mapper for [" + getProcedureName() + "]: " + parameterName); 272 } 273 } 274 275 276 //------------------------------------------------------------------------- 277 // Methods handling compilation issues 278 //------------------------------------------------------------------------- 279 280 /** 281 * Compile this JdbcCall using provided parameters and meta-data plus other settings. 282 * <p>This finalizes the configuration for this object and subsequent attempts to compile are 283 * ignored. This will be implicitly called the first time an un-compiled call is executed. 284 * @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't 285 * been correctly initialized, for example if no DataSource has been provided 286 */ 287 public final synchronized void compile() throws InvalidDataAccessApiUsageException { 288 if (!isCompiled()) { 289 if (getProcedureName() == null) { 290 throw new InvalidDataAccessApiUsageException("Procedure or Function name is required"); 291 } 292 try { 293 this.jdbcTemplate.afterPropertiesSet(); 294 } 295 catch (IllegalArgumentException ex) { 296 throw new InvalidDataAccessApiUsageException(ex.getMessage()); 297 } 298 compileInternal(); 299 this.compiled = true; 300 if (logger.isDebugEnabled()) { 301 logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") + 302 " [" + getProcedureName() + "] compiled"); 303 } 304 } 305 } 306 307 /** 308 * Delegate method to perform the actual compilation. 309 * <p>Subclasses can override this template method to perform their own compilation. 310 * Invoked after this base class's compilation is complete. 311 */ 312 protected void compileInternal() { 313 DataSource dataSource = getJdbcTemplate().getDataSource(); 314 Assert.state(dataSource != null, "No DataSource set"); 315 this.callMetaDataContext.initializeMetaData(dataSource); 316 317 // Iterate over the declared RowMappers and register the corresponding SqlParameter 318 this.declaredRowMappers.forEach((key, value) -> this.declaredParameters.add(this.callMetaDataContext.createReturnResultSetParameter(key, value))); 319 this.callMetaDataContext.processParameters(this.declaredParameters); 320 321 this.callString = this.callMetaDataContext.createCallString(); 322 if (logger.isDebugEnabled()) { 323 logger.debug("Compiled stored procedure. Call string is [" + this.callString + "]"); 324 } 325 326 this.callableStatementFactory = new CallableStatementCreatorFactory( 327 this.callString, this.callMetaDataContext.getCallParameters()); 328 329 onCompileInternal(); 330 } 331 332 /** 333 * Hook method that subclasses may override to react to compilation. 334 * This implementation does nothing. 335 */ 336 protected void onCompileInternal() { 337 } 338 339 /** 340 * Is this operation "compiled"? 341 * @return whether this operation is compiled and ready to use 342 */ 343 public boolean isCompiled() { 344 return this.compiled; 345 } 346 347 /** 348 * Check whether this operation has been compiled already; 349 * lazily compile it if not already compiled. 350 * <p>Automatically called by {@code doExecute}. 351 */ 352 protected void checkCompiled() { 353 if (!isCompiled()) { 354 logger.debug("JdbcCall call not compiled before execution - invoking compile"); 355 compile(); 356 } 357 } 358 359 360 //------------------------------------------------------------------------- 361 // Methods handling execution 362 //------------------------------------------------------------------------- 363 364 /** 365 * Delegate method that executes the call using the passed-in {@link SqlParameterSource}. 366 * @param parameterSource parameter names and values to be used in call 367 * @return a Map of out parameters 368 */ 369 protected Map<String, Object> doExecute(SqlParameterSource parameterSource) { 370 checkCompiled(); 371 Map<String, Object> params = matchInParameterValuesWithCallParameters(parameterSource); 372 return executeCallInternal(params); 373 } 374 375 /** 376 * Delegate method that executes the call using the passed-in array of parameters. 377 * @param args array of parameter values. The order of values must match the order 378 * declared for the stored procedure. 379 * @return a Map of out parameters 380 */ 381 protected Map<String, Object> doExecute(Object... args) { 382 checkCompiled(); 383 Map<String, ?> params = matchInParameterValuesWithCallParameters(args); 384 return executeCallInternal(params); 385 } 386 387 /** 388 * Delegate method that executes the call using the passed-in Map of parameters. 389 * @param args a Map of parameter name and values 390 * @return a Map of out parameters 391 */ 392 protected Map<String, Object> doExecute(Map<String, ?> args) { 393 checkCompiled(); 394 Map<String, ?> params = matchInParameterValuesWithCallParameters(args); 395 return executeCallInternal(params); 396 } 397 398 /** 399 * Delegate method to perform the actual call processing. 400 */ 401 private Map<String, Object> executeCallInternal(Map<String, ?> args) { 402 CallableStatementCreator csc = getCallableStatementFactory().newCallableStatementCreator(args); 403 if (logger.isDebugEnabled()) { 404 logger.debug("The following parameters are used for call " + getCallString() + " with " + args); 405 int i = 1; 406 for (SqlParameter param : getCallParameters()) { 407 logger.debug(i + ": " + param.getName() + ", SQL type "+ param.getSqlType() + ", type name " + 408 param.getTypeName() + ", parameter class [" + param.getClass().getName() + "]"); 409 i++; 410 } 411 } 412 return getJdbcTemplate().call(csc, getCallParameters()); 413 } 414 415 416 /** 417 * Get the name of a single out parameter or return value. 418 * Used for functions or procedures with one out parameter. 419 */ 420 @Nullable 421 protected String getScalarOutParameterName() { 422 return this.callMetaDataContext.getScalarOutParameterName(); 423 } 424 425 /** 426 * Get a List of all the call parameters to be used for call. 427 * This includes any parameters added based on meta-data processing. 428 */ 429 protected List<SqlParameter> getCallParameters() { 430 return this.callMetaDataContext.getCallParameters(); 431 } 432 433 /** 434 * Match the provided in parameter values with registered parameters and 435 * parameters defined via meta-data processing. 436 * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource} 437 * @return a Map with parameter names and values 438 */ 439 protected Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { 440 return this.callMetaDataContext.matchInParameterValuesWithCallParameters(parameterSource); 441 } 442 443 /** 444 * Match the provided in parameter values with registered parameters and 445 * parameters defined via meta-data processing. 446 * @param args the parameter values provided as an array 447 * @return a Map with parameter names and values 448 */ 449 private Map<String, ?> matchInParameterValuesWithCallParameters(Object[] args) { 450 return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); 451 } 452 453 /** 454 * Match the provided in parameter values with registered parameters and 455 * parameters defined via meta-data processing. 456 * @param args the parameter values provided in a Map 457 * @return a Map with parameter names and values 458 */ 459 protected Map<String, ?> matchInParameterValuesWithCallParameters(Map<String, ?> args) { 460 return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); 461 } 462 463}