001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.orm.ibatis;
018:
019: import java.sql.Connection;
020: import java.sql.SQLException;
021: import java.util.List;
022: import java.util.Map;
023:
024: import javax.sql.DataSource;
025:
026: import com.ibatis.common.util.PaginatedList;
027: import com.ibatis.sqlmap.client.SqlMapClient;
028: import com.ibatis.sqlmap.client.SqlMapExecutor;
029: import com.ibatis.sqlmap.client.SqlMapSession;
030: import com.ibatis.sqlmap.client.event.RowHandler;
031: import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
032:
033: import org.springframework.dao.DataAccessException;
034: import org.springframework.dao.InvalidDataAccessApiUsageException;
035: import org.springframework.jdbc.CannotGetJdbcConnectionException;
036: import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException;
037: import org.springframework.jdbc.datasource.DataSourceUtils;
038: import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
039: import org.springframework.jdbc.support.JdbcAccessor;
040: import org.springframework.util.Assert;
041:
042: /**
043: * Helper class that simplifies data access via the iBATIS
044: * {@link com.ibatis.sqlmap.client.SqlMapClient} API, converting checked
045: * SQLExceptions into unchecked DataAccessExceptions, following the
046: * <code>org.springframework.dao</code> exception hierarchy.
047: * Uses the same {@link org.springframework.jdbc.support.SQLExceptionTranslator}
048: * mechanism as {@link org.springframework.jdbc.core.JdbcTemplate}.
049: *
050: * <p>The main method of this class executes a callback that implements a
051: * data access action. Furthermore, this class provides numerous convenience
052: * methods that mirror {@link com.ibatis.sqlmap.client.SqlMapExecutor}'s
053: * execution methods.
054: *
055: * <p>It is generally recommended to use the convenience methods on this template
056: * for plain query/insert/update/delete operations. However, for more complex
057: * operations like batch updates, a custom SqlMapClientCallback must be implemented,
058: * usually as anonymous inner class. For example:
059: *
060: * <pre class="code">
061: * getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
062: * public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
063: * executor.startBatch();
064: * executor.update("insertSomething", "myParamValue");
065: * executor.update("insertSomethingElse", "myOtherParamValue");
066: * executor.executeBatch();
067: * return null;
068: * }
069: * });</pre>
070: *
071: * The template needs a SqlMapClient to work on, passed in via the "sqlMapClient"
072: * property. A Spring context typically uses a {@link SqlMapClientFactoryBean}
073: * to build the SqlMapClient. The template an additionally be configured with a
074: * DataSource for fetching Connections, although this is not necessary if a
075: * DataSource is specified for the SqlMapClient itself (typically through
076: * SqlMapClientFactoryBean's "dataSource" property).
077: *
078: * @author Juergen Hoeller
079: * @since 24.02.2004
080: * @see #execute
081: * @see #setSqlMapClient
082: * @see #setDataSource
083: * @see #setExceptionTranslator
084: * @see SqlMapClientFactoryBean#setDataSource
085: * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
086: * @see com.ibatis.sqlmap.client.SqlMapExecutor
087: */
088: public class SqlMapClientTemplate extends JdbcAccessor implements
089: SqlMapClientOperations {
090:
091: private SqlMapClient sqlMapClient;
092:
093: private boolean lazyLoadingAvailable = true;
094:
095: /**
096: * Create a new SqlMapClientTemplate.
097: */
098: public SqlMapClientTemplate() {
099: }
100:
101: /**
102: * Create a new SqlMapTemplate.
103: * @param sqlMapClient iBATIS SqlMapClient that defines the mapped statements
104: */
105: public SqlMapClientTemplate(SqlMapClient sqlMapClient) {
106: setSqlMapClient(sqlMapClient);
107: afterPropertiesSet();
108: }
109:
110: /**
111: * Create a new SqlMapTemplate.
112: * @param dataSource JDBC DataSource to obtain connections from
113: * @param sqlMapClient iBATIS SqlMapClient that defines the mapped statements
114: */
115: public SqlMapClientTemplate(DataSource dataSource,
116: SqlMapClient sqlMapClient) {
117: setDataSource(dataSource);
118: setSqlMapClient(sqlMapClient);
119: afterPropertiesSet();
120: }
121:
122: /**
123: * Set the iBATIS Database Layer SqlMapClient that defines the mapped statements.
124: */
125: public void setSqlMapClient(SqlMapClient sqlMapClient) {
126: this .sqlMapClient = sqlMapClient;
127: }
128:
129: /**
130: * Return the iBATIS Database Layer SqlMapClient that this template works with.
131: */
132: public SqlMapClient getSqlMapClient() {
133: return this .sqlMapClient;
134: }
135:
136: /**
137: * If no DataSource specified, use SqlMapClient's DataSource.
138: * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource()
139: */
140: public DataSource getDataSource() {
141: DataSource ds = super .getDataSource();
142: return (ds != null ? ds : this .sqlMapClient.getDataSource());
143: }
144:
145: public void afterPropertiesSet() {
146: if (this .sqlMapClient == null) {
147: throw new IllegalArgumentException(
148: "Property 'sqlMapClient' is required");
149: }
150: if (this .sqlMapClient instanceof ExtendedSqlMapClient) {
151: // Check whether iBATIS lazy loading is available, that is,
152: // whether a DataSource was specified on the SqlMapClient itself.
153: this .lazyLoadingAvailable = (((ExtendedSqlMapClient) this .sqlMapClient)
154: .getDelegate().getTxManager() != null);
155: }
156: super .afterPropertiesSet();
157: }
158:
159: /**
160: * Execute the given data access action on a SqlMapExecutor.
161: * @param action callback object that specifies the data access action
162: * @return a result object returned by the action, or <code>null</code>
163: * @throws DataAccessException in case of SQL Maps errors
164: */
165: public Object execute(SqlMapClientCallback action)
166: throws DataAccessException {
167: Assert.notNull(action, "Callback object must not be null");
168: Assert.notNull(this .sqlMapClient, "No SqlMapClient specified");
169:
170: // We always needs to use a SqlMapSession, as we need to pass a Spring-managed
171: // Connection (potentially transactional) in. This shouldn't be necessary if
172: // we run against a TransactionAwareDataSourceProxy underneath, but unfortunately
173: // we still need it to make iBATIS batch execution work properly: If iBATIS
174: // doesn't recognize an existing transaction, it automatically executes the
175: // batch for every single statement...
176:
177: SqlMapSession session = this .sqlMapClient.openSession();
178: if (logger.isDebugEnabled()) {
179: logger.debug("Opened SqlMapSession [" + session
180: + "] for iBATIS operation");
181: }
182: Connection ibatisCon = null;
183:
184: try {
185: Connection springCon = null;
186: DataSource dataSource = getDataSource();
187: boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);
188:
189: // Obtain JDBC Connection to operate on...
190: try {
191: ibatisCon = session.getCurrentConnection();
192: if (ibatisCon == null) {
193: springCon = (transactionAware ? dataSource
194: .getConnection() : DataSourceUtils
195: .doGetConnection(dataSource));
196: session.setUserConnection(springCon);
197: if (logger.isDebugEnabled()) {
198: logger.debug("Obtained JDBC Connection ["
199: + springCon + "] for iBATIS operation");
200: }
201: } else {
202: if (logger.isDebugEnabled()) {
203: logger.debug("Reusing JDBC Connection ["
204: + ibatisCon + "] for iBATIS operation");
205: }
206: }
207: } catch (SQLException ex) {
208: throw new CannotGetJdbcConnectionException(
209: "Could not get JDBC Connection", ex);
210: }
211:
212: // Execute given callback...
213: try {
214: return action.doInSqlMapClient(session);
215: } catch (SQLException ex) {
216: throw getExceptionTranslator().translate(
217: "SqlMapClient operation", null, ex);
218: } finally {
219: try {
220: if (springCon != null) {
221: if (transactionAware) {
222: springCon.close();
223: } else {
224: DataSourceUtils.doReleaseConnection(
225: springCon, dataSource);
226: }
227: }
228: } catch (Throwable ex) {
229: logger.debug("Could not close JDBC Connection", ex);
230: }
231: }
232:
233: // Processing finished - potentially session still to be closed.
234: } finally {
235: // Only close SqlMapSession if we know we've actually opened it
236: // at the present level.
237: if (ibatisCon == null) {
238: session.close();
239: }
240: }
241: }
242:
243: /**
244: * Execute the given data access action on a SqlMapExecutor,
245: * expecting a List result.
246: * @param action callback object that specifies the data access action
247: * @return the List result
248: * @throws DataAccessException in case of SQL Maps errors
249: */
250: public List executeWithListResult(SqlMapClientCallback action)
251: throws DataAccessException {
252: return (List) execute(action);
253: }
254:
255: /**
256: * Execute the given data access action on a SqlMapExecutor,
257: * expecting a Map result.
258: * @param action callback object that specifies the data access action
259: * @return the Map result
260: * @throws DataAccessException in case of SQL Maps errors
261: */
262: public Map executeWithMapResult(SqlMapClientCallback action)
263: throws DataAccessException {
264: return (Map) execute(action);
265: }
266:
267: public Object queryForObject(String statementName)
268: throws DataAccessException {
269: return queryForObject(statementName, null);
270: }
271:
272: public Object queryForObject(final String statementName,
273: final Object parameterObject) throws DataAccessException {
274:
275: return execute(new SqlMapClientCallback() {
276: public Object doInSqlMapClient(SqlMapExecutor executor)
277: throws SQLException {
278: return executor.queryForObject(statementName,
279: parameterObject);
280: }
281: });
282: }
283:
284: public Object queryForObject(final String statementName,
285: final Object parameterObject, final Object resultObject)
286: throws DataAccessException {
287:
288: return execute(new SqlMapClientCallback() {
289: public Object doInSqlMapClient(SqlMapExecutor executor)
290: throws SQLException {
291: return executor.queryForObject(statementName,
292: parameterObject, resultObject);
293: }
294: });
295: }
296:
297: public List queryForList(String statementName)
298: throws DataAccessException {
299: return queryForList(statementName, null);
300: }
301:
302: public List queryForList(final String statementName,
303: final Object parameterObject) throws DataAccessException {
304:
305: return executeWithListResult(new SqlMapClientCallback() {
306: public Object doInSqlMapClient(SqlMapExecutor executor)
307: throws SQLException {
308: return executor.queryForList(statementName,
309: parameterObject);
310: }
311: });
312: }
313:
314: public List queryForList(String statementName, int skipResults,
315: int maxResults) throws DataAccessException {
316:
317: return queryForList(statementName, null, skipResults,
318: maxResults);
319: }
320:
321: public List queryForList(final String statementName,
322: final Object parameterObject, final int skipResults,
323: final int maxResults) throws DataAccessException {
324:
325: return executeWithListResult(new SqlMapClientCallback() {
326: public Object doInSqlMapClient(SqlMapExecutor executor)
327: throws SQLException {
328: return executor.queryForList(statementName,
329: parameterObject, skipResults, maxResults);
330: }
331: });
332: }
333:
334: public void queryWithRowHandler(String statementName,
335: RowHandler rowHandler) throws DataAccessException {
336:
337: queryWithRowHandler(statementName, null, rowHandler);
338: }
339:
340: public void queryWithRowHandler(final String statementName,
341: final Object parameterObject, final RowHandler rowHandler)
342: throws DataAccessException {
343:
344: execute(new SqlMapClientCallback() {
345: public Object doInSqlMapClient(SqlMapExecutor executor)
346: throws SQLException {
347: executor.queryWithRowHandler(statementName,
348: parameterObject, rowHandler);
349: return null;
350: }
351: });
352: }
353:
354: /**
355: * @deprecated as of iBATIS 2.3.0
356: */
357: public PaginatedList queryForPaginatedList(String statementName,
358: int pageSize) throws DataAccessException {
359:
360: return queryForPaginatedList(statementName, null, pageSize);
361: }
362:
363: /**
364: * @deprecated as of iBATIS 2.3.0
365: */
366: public PaginatedList queryForPaginatedList(
367: final String statementName, final Object parameterObject,
368: final int pageSize) throws DataAccessException {
369:
370: // Throw exception if lazy loading will not work.
371: if (!this .lazyLoadingAvailable) {
372: throw new InvalidDataAccessApiUsageException(
373: "SqlMapClient needs to have DataSource to allow for lazy loading"
374: + " - specify SqlMapClientFactoryBean's 'dataSource' property");
375: }
376:
377: return (PaginatedList) execute(new SqlMapClientCallback() {
378: public Object doInSqlMapClient(SqlMapExecutor executor)
379: throws SQLException {
380: return executor.queryForPaginatedList(statementName,
381: parameterObject, pageSize);
382: }
383: });
384: }
385:
386: public Map queryForMap(final String statementName,
387: final Object parameterObject, final String keyProperty)
388: throws DataAccessException {
389:
390: return executeWithMapResult(new SqlMapClientCallback() {
391: public Object doInSqlMapClient(SqlMapExecutor executor)
392: throws SQLException {
393: return executor.queryForMap(statementName,
394: parameterObject, keyProperty);
395: }
396: });
397: }
398:
399: public Map queryForMap(final String statementName,
400: final Object parameterObject, final String keyProperty,
401: final String valueProperty) throws DataAccessException {
402:
403: return executeWithMapResult(new SqlMapClientCallback() {
404: public Object doInSqlMapClient(SqlMapExecutor executor)
405: throws SQLException {
406: return executor.queryForMap(statementName,
407: parameterObject, keyProperty, valueProperty);
408: }
409: });
410: }
411:
412: public Object insert(String statementName)
413: throws DataAccessException {
414: return insert(statementName, null);
415: }
416:
417: public Object insert(final String statementName,
418: final Object parameterObject) throws DataAccessException {
419:
420: return execute(new SqlMapClientCallback() {
421: public Object doInSqlMapClient(SqlMapExecutor executor)
422: throws SQLException {
423: return executor.insert(statementName, parameterObject);
424: }
425: });
426: }
427:
428: public int update(String statementName) throws DataAccessException {
429: return update(statementName, null);
430: }
431:
432: public int update(final String statementName,
433: final Object parameterObject) throws DataAccessException {
434:
435: Integer result = (Integer) execute(new SqlMapClientCallback() {
436: public Object doInSqlMapClient(SqlMapExecutor executor)
437: throws SQLException {
438: return new Integer(executor.update(statementName,
439: parameterObject));
440: }
441: });
442: return result.intValue();
443: }
444:
445: public void update(String statementName, Object parameterObject,
446: int requiredRowsAffected) throws DataAccessException {
447:
448: int actualRowsAffected = update(statementName, parameterObject);
449: if (actualRowsAffected != requiredRowsAffected) {
450: throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(
451: statementName, requiredRowsAffected,
452: actualRowsAffected);
453: }
454: }
455:
456: public int delete(String statementName) throws DataAccessException {
457: return delete(statementName, null);
458: }
459:
460: public int delete(final String statementName,
461: final Object parameterObject) throws DataAccessException {
462:
463: Integer result = (Integer) execute(new SqlMapClientCallback() {
464: public Object doInSqlMapClient(SqlMapExecutor executor)
465: throws SQLException {
466: return new Integer(executor.delete(statementName,
467: parameterObject));
468: }
469: });
470: return result.intValue();
471: }
472:
473: public void delete(String statementName, Object parameterObject,
474: int requiredRowsAffected) throws DataAccessException {
475:
476: int actualRowsAffected = delete(statementName, parameterObject);
477: if (actualRowsAffected != requiredRowsAffected) {
478: throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(
479: statementName, requiredRowsAffected,
480: actualRowsAffected);
481: }
482: }
483:
484: }
|