View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.jdbc.core.simple;
18  
19  import java.sql.Connection;
20  import java.sql.PreparedStatement;
21  import java.sql.ResultSet;
22  import java.sql.SQLException;
23  import java.sql.Statement;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import javax.sql.DataSource;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  
35  import org.springframework.dao.DataAccessException;
36  import org.springframework.dao.DataIntegrityViolationException;
37  import org.springframework.dao.InvalidDataAccessApiUsageException;
38  import org.springframework.dao.InvalidDataAccessResourceUsageException;
39  import org.springframework.jdbc.core.BatchPreparedStatementSetter;
40  import org.springframework.jdbc.core.ConnectionCallback;
41  import org.springframework.jdbc.core.JdbcTemplate;
42  import org.springframework.jdbc.core.PreparedStatementCreator;
43  import org.springframework.jdbc.core.SqlTypeValue;
44  import org.springframework.jdbc.core.StatementCreatorUtils;
45  import org.springframework.jdbc.core.metadata.TableMetaDataContext;
46  import org.springframework.jdbc.core.namedparam.SqlParameterSource;
47  import org.springframework.jdbc.support.GeneratedKeyHolder;
48  import org.springframework.jdbc.support.JdbcUtils;
49  import org.springframework.jdbc.support.KeyHolder;
50  import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
51  import org.springframework.util.Assert;
52  
53  /**
54   * Abstract class to provide base functionality for easy inserts
55   * based on configuration options and database metadata.
56   * This class provides the base SPI for {@link SimpleJdbcInsert}.
57   *
58   * @author Thomas Risberg
59   * @author Juergen Hoeller
60   * @since 2.5
61   */
62  public abstract class AbstractJdbcInsert {
63  
64  	/** Logger available to subclasses */
65  	protected final Log logger = LogFactory.getLog(getClass());
66  
67  	/** Lower-level class used to execute SQL */
68  	private final JdbcTemplate jdbcTemplate;
69  
70  	/** Context used to retrieve and manage database metadata */
71  	private final TableMetaDataContext tableMetaDataContext = new TableMetaDataContext();
72  
73  	/** List of columns objects to be used in insert statement */
74  	private final List<String> declaredColumns = new ArrayList<String>();
75  
76  	/** The names of the columns holding the generated key */
77  	private String[] generatedKeyNames = new String[0];
78  
79  	/**
80  	 * Has this operation been compiled? Compilation means at least checking
81  	 * that a DataSource or JdbcTemplate has been provided.
82  	 */
83  	private boolean compiled = false;
84  
85  	/** The generated string used for insert statement */
86  	private String insertString;
87  
88  	/** The SQL type information for the insert columns */
89  	private int[] insertTypes;
90  
91  
92  	/**
93  	 * Constructor to be used when initializing using a {@link DataSource}.
94  	 * @param dataSource the DataSource to be used
95  	 */
96  	protected AbstractJdbcInsert(DataSource dataSource) {
97  		this.jdbcTemplate = new JdbcTemplate(dataSource);
98  	}
99  
100 	/**
101 	 * Constructor to be used when initializing using a {@link JdbcTemplate}.
102 	 * @param jdbcTemplate the JdbcTemplate to use
103 	 */
104 	protected AbstractJdbcInsert(JdbcTemplate jdbcTemplate) {
105 		Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
106 		this.jdbcTemplate = jdbcTemplate;
107 		setNativeJdbcExtractor(jdbcTemplate.getNativeJdbcExtractor());
108 	}
109 
110 
111 	//-------------------------------------------------------------------------
112 	// Methods dealing with configuration properties
113 	//-------------------------------------------------------------------------
114 
115 	/**
116 	 * Get the configured {@link JdbcTemplate}.
117 	 */
118 	public JdbcTemplate getJdbcTemplate() {
119 		return this.jdbcTemplate;
120 	}
121 
122 	/**
123 	 * Set the name of the table for this insert.
124 	 */
125 	public void setTableName(String tableName) {
126 		checkIfConfigurationModificationIsAllowed();
127 		this.tableMetaDataContext.setTableName(tableName);
128 	}
129 
130 	/**
131 	 * Get the name of the table for this insert.
132 	 */
133 	public String getTableName() {
134 		return this.tableMetaDataContext.getTableName();
135 	}
136 
137 	/**
138 	 * Set the name of the schema for this insert.
139 	 */
140 	public void setSchemaName(String schemaName) {
141 		checkIfConfigurationModificationIsAllowed();
142 		this.tableMetaDataContext.setSchemaName(schemaName);
143 	}
144 
145 	/**
146 	 * Get the name of the schema for this insert.
147 	 */
148 	public String getSchemaName() {
149 		return this.tableMetaDataContext.getSchemaName();
150 	}
151 
152 	/**
153 	 * Set the name of the catalog for this insert.
154 	 */
155 	public void setCatalogName(String catalogName) {
156 		checkIfConfigurationModificationIsAllowed();
157 		this.tableMetaDataContext.setCatalogName(catalogName);
158 	}
159 
160 	/**
161 	 * Get the name of the catalog for this insert.
162 	 */
163 	public String getCatalogName() {
164 		return this.tableMetaDataContext.getCatalogName();
165 	}
166 
167 	/**
168 	 * Set the names of the columns to be used.
169 	 */
170 	public void setColumnNames(List<String> columnNames) {
171 		checkIfConfigurationModificationIsAllowed();
172 		this.declaredColumns.clear();
173 		this.declaredColumns.addAll(columnNames);
174 	}
175 
176 	/**
177 	 * Get the names of the columns used.
178 	 */
179 	public List<String> getColumnNames() {
180 		return Collections.unmodifiableList(this.declaredColumns);
181 	}
182 
183 	/**
184 	 * Specify the name of a single generated key column.
185 	 */
186 	public void setGeneratedKeyName(String generatedKeyName) {
187 		checkIfConfigurationModificationIsAllowed();
188 		this.generatedKeyNames = new String[] {generatedKeyName};
189 	}
190 
191 	/**
192 	 * Set the names of any generated keys.
193 	 */
194 	public void setGeneratedKeyNames(String... generatedKeyNames) {
195 		checkIfConfigurationModificationIsAllowed();
196 		this.generatedKeyNames = generatedKeyNames;
197 	}
198 
199 	/**
200 	 * Get the names of any generated keys.
201 	 */
202 	public String[] getGeneratedKeyNames() {
203 		return this.generatedKeyNames;
204 	}
205 
206 	/**
207 	 * Specify whether the parameter metadata for the call should be used.
208 	 * The default is {@code true}.
209 	 */
210 	public void setAccessTableColumnMetaData(boolean accessTableColumnMetaData) {
211 		this.tableMetaDataContext.setAccessTableColumnMetaData(accessTableColumnMetaData);
212 	}
213 
214 	/**
215 	 * Specify whether the default for including synonyms should be changed.
216 	 * The default is {@code false}.
217 	 */
218 	public void setOverrideIncludeSynonymsDefault(boolean override) {
219 		this.tableMetaDataContext.setOverrideIncludeSynonymsDefault(override);
220 	}
221 
222 	/**
223 	 * Set the {@link NativeJdbcExtractor} to use to retrieve the native connection if necessary
224 	 */
225 	public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) {
226 		this.tableMetaDataContext.setNativeJdbcExtractor(nativeJdbcExtractor);
227 	}
228 
229 	/**
230 	 * Get the insert string to be used.
231 	 */
232 	public String getInsertString() {
233 		return this.insertString;
234 	}
235 
236 	/**
237 	 * Get the array of {@link java.sql.Types} to be used for insert.
238 	 */
239 	public int[] getInsertTypes() {
240 		return this.insertTypes;
241 	}
242 
243 
244 	//-------------------------------------------------------------------------
245 	// Methods handling compilation issues
246 	//-------------------------------------------------------------------------
247 
248 	/**
249 	 * Compile this JdbcInsert using provided parameters and meta data plus other settings.
250 	 * This finalizes the configuration for this object and subsequent attempts to compile are
251 	 * ignored. This will be implicitly called the first time an un-compiled insert is executed.
252 	 * @throws InvalidDataAccessApiUsageException if the object hasn't been correctly initialized,
253 	 * for example if no DataSource has been provided
254 	 */
255 	public synchronized final void compile() throws InvalidDataAccessApiUsageException {
256 		if (!isCompiled()) {
257 			if (getTableName() == null) {
258 				throw new InvalidDataAccessApiUsageException("Table name is required");
259 			}
260 			try {
261 				this.jdbcTemplate.afterPropertiesSet();
262 			}
263 			catch (IllegalArgumentException ex) {
264 				throw new InvalidDataAccessApiUsageException(ex.getMessage());
265 			}
266 			compileInternal();
267 			this.compiled = true;
268 			if (logger.isDebugEnabled()) {
269 				logger.debug("JdbcInsert for table [" + getTableName() + "] compiled");
270 			}
271 		}
272 	}
273 
274 	/**
275 	 * Delegate method to perform the actual compilation.
276 	 * <p>Subclasses can override this template method to perform  their own compilation.
277 	 * Invoked after this base class's compilation is complete.
278 	 */
279 	protected void compileInternal() {
280 		this.tableMetaDataContext.processMetaData(
281 				getJdbcTemplate().getDataSource(), getColumnNames(), getGeneratedKeyNames());
282 		this.insertString = this.tableMetaDataContext.createInsertString(getGeneratedKeyNames());
283 		this.insertTypes = this.tableMetaDataContext.createInsertTypes();
284 		if (logger.isDebugEnabled()) {
285 			logger.debug("Compiled insert object: insert string is [" + getInsertString() + "]");
286 		}
287 		onCompileInternal();
288 	}
289 
290 	/**
291 	 * Hook method that subclasses may override to react to compilation.
292 	 * <p>This implementation is empty.
293 	 */
294 	protected void onCompileInternal() {
295 	}
296 
297 	/**
298 	 * Is this operation "compiled"?
299 	 * @return whether this operation is compiled, and ready to use.
300 	 */
301 	public boolean isCompiled() {
302 		return this.compiled;
303 	}
304 
305 	/**
306 	 * Check whether this operation has been compiled already;
307 	 * lazily compile it if not already compiled.
308 	 * <p>Automatically called by {@code validateParameters}.
309 	 */
310 	protected void checkCompiled() {
311 		if (!isCompiled()) {
312 			logger.debug("JdbcInsert not compiled before execution - invoking compile");
313 			compile();
314 		}
315 	}
316 
317 	/**
318 	 * Method to check whether we are allowed to make any configuration changes at this time.
319 	 * If the class has been compiled, then no further changes to the configuration are allowed.
320 	 */
321 	protected void checkIfConfigurationModificationIsAllowed() {
322 		if (isCompiled()) {
323 			throw new InvalidDataAccessApiUsageException(
324 					"Configuration can't be altered once the class has been compiled or used");
325 		}
326 	}
327 
328 
329 	//-------------------------------------------------------------------------
330 	// Methods handling execution
331 	//-------------------------------------------------------------------------
332 
333 	/**
334 	 * Delegate method that executes the insert using the passed-in Map of parameters.
335 	 * @param args Map with parameter names and values to be used in insert
336 	 * @return the number of rows affected
337 	 */
338 	protected int doExecute(Map<String, ?> args) {
339 		checkCompiled();
340 		List<Object> values = matchInParameterValuesWithInsertColumns(args);
341 		return executeInsertInternal(values);
342 	}
343 
344 	/**
345 	 * Delegate method that executes the insert using the passed-in {@link SqlParameterSource}.
346 	 * @param parameterSource parameter names and values to be used in insert
347 	 * @return the number of rows affected
348 	 */
349 	protected int doExecute(SqlParameterSource parameterSource) {
350 		checkCompiled();
351 		List<Object> values = matchInParameterValuesWithInsertColumns(parameterSource);
352 		return executeInsertInternal(values);
353 	}
354 
355 	/**
356 	 * Delegate method to execute the insert.
357 	 */
358 	private int executeInsertInternal(List<?> values) {
359 		if (logger.isDebugEnabled()) {
360 			logger.debug("The following parameters are used for insert " + getInsertString() + " with: " + values);
361 		}
362 		return getJdbcTemplate().update(getInsertString(), values.toArray(), getInsertTypes());
363 	}
364 
365 	/**
366 	 * Method that provides execution of the insert using the passed-in
367 	 * Map of parameters and returning a generated key.
368 	 * @param args Map with parameter names and values to be used in insert
369 	 * @return the key generated by the insert
370 	 */
371 	protected Number doExecuteAndReturnKey(Map<String, ?> args) {
372 		checkCompiled();
373 		List<Object> values = matchInParameterValuesWithInsertColumns(args);
374 		return executeInsertAndReturnKeyInternal(values);
375 	}
376 
377 	/**
378 	 * Method that provides execution of the insert using the passed-in
379 	 * {@link SqlParameterSource} and returning a generated key.
380 	 * @param parameterSource parameter names and values to be used in insert
381 	 * @return the key generated by the insert
382 	 */
383 	protected Number doExecuteAndReturnKey(SqlParameterSource parameterSource) {
384 		checkCompiled();
385 		List<Object> values = matchInParameterValuesWithInsertColumns(parameterSource);
386 		return executeInsertAndReturnKeyInternal(values);
387 	}
388 
389 	/**
390 	 * Method that provides execution of the insert using the passed-in
391 	 * Map of parameters and returning all generated keys.
392 	 * @param args Map with parameter names and values to be used in insert
393 	 * @return the KeyHolder containing keys generated by the insert
394 	 */
395 	protected KeyHolder doExecuteAndReturnKeyHolder(Map<String, ?> args) {
396 		checkCompiled();
397 		List<Object> values = matchInParameterValuesWithInsertColumns(args);
398 		return executeInsertAndReturnKeyHolderInternal(values);
399 	}
400 
401 	/**
402 	 * Method that provides execution of the insert using the passed-in
403 	 * {@link SqlParameterSource} and returning all generated keys.
404 	 * @param parameterSource parameter names and values to be used in insert
405 	 * @return the KeyHolder containing keys generated by the insert
406 	 */
407 	protected KeyHolder doExecuteAndReturnKeyHolder(SqlParameterSource parameterSource) {
408 		checkCompiled();
409 		List<Object> values = matchInParameterValuesWithInsertColumns(parameterSource);
410 		return executeInsertAndReturnKeyHolderInternal(values);
411 	}
412 
413 	/**
414 	 * Delegate method to execute the insert, generating a single key.
415 	 */
416 	private Number executeInsertAndReturnKeyInternal(final List<?> values) {
417 		KeyHolder kh = executeInsertAndReturnKeyHolderInternal(values);
418 		if (kh != null && kh.getKey() != null) {
419 			return kh.getKey();
420 		}
421 		else {
422 			throw new DataIntegrityViolationException(
423 					"Unable to retrieve the generated key for the insert: " + getInsertString());
424 		}
425 	}
426 
427 	/**
428 	 * Delegate method to execute the insert, generating any number of keys.
429 	 */
430 	private KeyHolder executeInsertAndReturnKeyHolderInternal(final List<?> values) {
431 		if (logger.isDebugEnabled()) {
432 			logger.debug("The following parameters are used for call " + getInsertString() + " with: " + values);
433 		}
434 		final KeyHolder keyHolder = new GeneratedKeyHolder();
435 		if (this.tableMetaDataContext.isGetGeneratedKeysSupported()) {
436 			getJdbcTemplate().update(
437 					new PreparedStatementCreator() {
438 						@Override
439 						public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
440 							PreparedStatement ps = prepareStatementForGeneratedKeys(con);
441 							setParameterValues(ps, values, getInsertTypes());
442 							return ps;
443 						}
444 					},
445 					keyHolder);
446 		}
447 		else {
448 			if (!this.tableMetaDataContext.isGetGeneratedKeysSimulated()) {
449 				throw new InvalidDataAccessResourceUsageException(
450 						"The getGeneratedKeys feature is not supported by this database");
451 			}
452 			if (getGeneratedKeyNames().length < 1) {
453 				throw new InvalidDataAccessApiUsageException("Generated Key Name(s) not specified. " +
454 						"Using the generated keys features requires specifying the name(s) of the generated column(s)");
455 			}
456 			if (getGeneratedKeyNames().length > 1) {
457 				throw new InvalidDataAccessApiUsageException(
458 						"Current database only supports retrieving the key for a single column. There are " +
459 						getGeneratedKeyNames().length  + " columns specified: " + Arrays.asList(getGeneratedKeyNames()));
460 			}
461 			// This is a hack to be able to get the generated key from a database that doesn't support
462 			// get generated keys feature. HSQL is one, PostgreSQL is another. Postgres uses a RETURNING
463 			// clause while HSQL uses a second query that has to be executed with the same connection.
464 			final String keyQuery = this.tableMetaDataContext.getSimulationQueryForGetGeneratedKey(
465 					this.tableMetaDataContext.getTableName(), getGeneratedKeyNames()[0]);
466 			Assert.notNull(keyQuery, "Query for simulating get generated keys can't be null");
467 			if (keyQuery.toUpperCase().startsWith("RETURNING")) {
468 				Long key = getJdbcTemplate().queryForObject(getInsertString() + " " + keyQuery,
469 						values.toArray(new Object[values.size()]), Long.class);
470 				Map<String, Object> keys = new HashMap<String, Object>(1);
471 				keys.put(getGeneratedKeyNames()[0], key);
472 				keyHolder.getKeyList().add(keys);
473 			}
474 			else {
475 				getJdbcTemplate().execute(new ConnectionCallback<Object>() {
476 					@Override
477 					public Object doInConnection(Connection con) throws SQLException, DataAccessException {
478 						// Do the insert
479 						PreparedStatement ps = null;
480 						try {
481 							ps = con.prepareStatement(getInsertString());
482 							setParameterValues(ps, values, getInsertTypes());
483 							ps.executeUpdate();
484 						}
485 						finally {
486 							JdbcUtils.closeStatement(ps);
487 						}
488 						//Get the key
489 						Statement keyStmt = null;
490 						ResultSet rs = null;
491 						Map<String, Object> keys = new HashMap<String, Object>(1);
492 						try {
493 							keyStmt = con.createStatement();
494 							rs = keyStmt.executeQuery(keyQuery);
495 							if (rs.next()) {
496 								long key = rs.getLong(1);
497 								keys.put(getGeneratedKeyNames()[0], key);
498 								keyHolder.getKeyList().add(keys);
499 							}
500 						}
501 						finally {
502 							JdbcUtils.closeResultSet(rs);
503 							JdbcUtils.closeStatement(keyStmt);
504 						}
505 						return null;
506 					}
507 				});
508 			}
509 			return keyHolder;
510 		}
511 		return keyHolder;
512 	}
513 
514 	/**
515 	 * Create a PreparedStatement to be used for an insert operation with generated keys.
516 	 * @param con the Connection to use
517 	 * @return the PreparedStatement
518 	 */
519 	private PreparedStatement prepareStatementForGeneratedKeys(Connection con) throws SQLException {
520 		if (getGeneratedKeyNames().length < 1) {
521 			throw new InvalidDataAccessApiUsageException("Generated Key Name(s) not specified. " +
522 					"Using the generated keys features requires specifying the name(s) of the generated column(s).");
523 		}
524 		PreparedStatement ps;
525 		if (this.tableMetaDataContext.isGeneratedKeysColumnNameArraySupported()) {
526 			if (logger.isDebugEnabled()) {
527 				logger.debug("Using generated keys support with array of column names.");
528 			}
529 			ps = con.prepareStatement(getInsertString(), getGeneratedKeyNames());
530 		}
531 		else {
532 			if (logger.isDebugEnabled()) {
533 				logger.debug("Using generated keys support with Statement.RETURN_GENERATED_KEYS.");
534 			}
535 			ps = con.prepareStatement(getInsertString(), Statement.RETURN_GENERATED_KEYS);
536 		}
537 		return ps;
538 	}
539 
540 	/**
541 	 * Delegate method that executes a batch insert using the passed-in Maps of parameters.
542 	 * @param batch array of Maps with parameter names and values to be used in batch insert
543 	 * @return array of number of rows affected
544 	 */
545 	protected int[] doExecuteBatch(Map<String, ?>... batch) {
546 		checkCompiled();
547 		List<List<Object>> batchValues = new ArrayList<List<Object>>(batch.length);
548 		for (Map<String, ?> args : batch) {
549 			batchValues.add(matchInParameterValuesWithInsertColumns(args));
550 		}
551 		return executeBatchInternal(batchValues);
552 	}
553 
554 	/**
555 	 * Delegate method that executes a batch insert using the passed-in {@link SqlParameterSource}s.
556 	 * @param batch array of SqlParameterSource with parameter names and values to be used in insert
557 	 * @return array of number of rows affected
558 	 */
559 	protected int[] doExecuteBatch(SqlParameterSource... batch) {
560 		checkCompiled();
561 		List<List<Object>> batchValues = new ArrayList<List<Object>>(batch.length);
562 		for (SqlParameterSource parameterSource : batch) {
563 			batchValues.add(matchInParameterValuesWithInsertColumns(parameterSource));
564 		}
565 		return executeBatchInternal(batchValues);
566 	}
567 
568 	/**
569 	 * Delegate method to execute the batch insert.
570 	 */
571 	private int[] executeBatchInternal(final List<List<Object>> batchValues) {
572 		if (logger.isDebugEnabled()) {
573 			logger.debug("Executing statement " + getInsertString() + " with batch of size: " + batchValues.size());
574 		}
575 		return getJdbcTemplate().batchUpdate(getInsertString(),
576 				new BatchPreparedStatementSetter() {
577 					@Override
578 					public void setValues(PreparedStatement ps, int i) throws SQLException {
579 						setParameterValues(ps, batchValues.get(i), getInsertTypes());
580 					}
581 					@Override
582 					public int getBatchSize() {
583 						return batchValues.size();
584 					}
585 				});
586 	}
587 
588 	/**
589 	 * Internal implementation for setting parameter values
590 	 * @param preparedStatement the PreparedStatement
591 	 * @param values the values to be set
592 	 */
593 	private void setParameterValues(PreparedStatement preparedStatement, List<?> values, int... columnTypes)
594 			throws SQLException {
595 
596 		int colIndex = 0;
597 		for (Object value : values) {
598 			colIndex++;
599 			if (columnTypes == null || colIndex > columnTypes.length) {
600 				StatementCreatorUtils.setParameterValue(preparedStatement, colIndex, SqlTypeValue.TYPE_UNKNOWN, value);
601 			}
602 			else {
603 				StatementCreatorUtils.setParameterValue(preparedStatement, colIndex, columnTypes[colIndex - 1], value);
604 			}
605 		}
606 	}
607 
608 	/**
609 	 * Match the provided in parameter values with registered parameters and parameters
610 	 * defined via metadata processing.
611 	 * @param parameterSource the parameter values provided as a {@link SqlParameterSource}
612 	 * @return Map with parameter names and values
613 	 */
614 	protected List<Object> matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) {
615 		return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(parameterSource);
616 	}
617 
618 	/**
619 	 * Match the provided in parameter values with registered parameters and parameters
620 	 * defined via metadata processing.
621 	 * @param args the parameter values provided in a Map
622 	 * @return Map with parameter names and values
623 	 */
624 	protected List<Object> matchInParameterValuesWithInsertColumns(Map<String, ?> args) {
625 		return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(args);
626 	}
627 
628 }