1 /*
2 * Copyright 2002-2007 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.orm.ibatis;
18
19 import java.sql.Connection;
20 import java.sql.SQLException;
21 import java.util.List;
22 import java.util.Map;
23
24 import javax.sql.DataSource;
25
26 import com.ibatis.common.util.PaginatedList;
27 import com.ibatis.sqlmap.client.SqlMapClient;
28 import com.ibatis.sqlmap.client.SqlMapExecutor;
29 import com.ibatis.sqlmap.client.SqlMapSession;
30 import com.ibatis.sqlmap.client.event.RowHandler;
31 import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
32
33 import org.springframework.dao.DataAccessException;
34 import org.springframework.dao.InvalidDataAccessApiUsageException;
35 import org.springframework.jdbc.CannotGetJdbcConnectionException;
36 import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException;
37 import org.springframework.jdbc.datasource.DataSourceUtils;
38 import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
39 import org.springframework.jdbc.support.JdbcAccessor;
40 import org.springframework.util.Assert;
41
42 /**
43 * Helper class that simplifies data access via the iBATIS
44 * {@link com.ibatis.sqlmap.client.SqlMapClient} API, converting checked
45 * SQLExceptions into unchecked DataAccessExceptions, following the
46 * <code>org.springframework.dao</code> exception hierarchy.
47 * Uses the same {@link org.springframework.jdbc.support.SQLExceptionTranslator}
48 * mechanism as {@link org.springframework.jdbc.core.JdbcTemplate}.
49 *
50 * <p>The main method of this class executes a callback that implements a
51 * data access action. Furthermore, this class provides numerous convenience
52 * methods that mirror {@link com.ibatis.sqlmap.client.SqlMapExecutor}'s
53 * execution methods.
54 *
55 * <p>It is generally recommended to use the convenience methods on this template
56 * for plain query/insert/update/delete operations. However, for more complex
57 * operations like batch updates, a custom SqlMapClientCallback must be implemented,
58 * usually as anonymous inner class. For example:
59 *
60 * <pre class="code">
61 * getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
62 * public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
63 * executor.startBatch();
64 * executor.update("insertSomething", "myParamValue");
65 * executor.update("insertSomethingElse", "myOtherParamValue");
66 * executor.executeBatch();
67 * return null;
68 * }
69 * });</pre>
70 *
71 * The template needs a SqlMapClient to work on, passed in via the "sqlMapClient"
72 * property. A Spring context typically uses a {@link SqlMapClientFactoryBean}
73 * to build the SqlMapClient. The template an additionally be configured with a
74 * DataSource for fetching Connections, although this is not necessary if a
75 * DataSource is specified for the SqlMapClient itself (typically through
76 * SqlMapClientFactoryBean's "dataSource" property).
77 *
78 * @author Juergen Hoeller
79 * @since 24.02.2004
80 * @see #execute
81 * @see #setSqlMapClient
82 * @see #setDataSource
83 * @see #setExceptionTranslator
84 * @see SqlMapClientFactoryBean#setDataSource
85 * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
86 * @see com.ibatis.sqlmap.client.SqlMapExecutor
87 */
88 public class SqlMapClientTemplate extends JdbcAccessor implements SqlMapClientOperations {
89
90 private SqlMapClient sqlMapClient;
91
92 private boolean lazyLoadingAvailable = true;
93
94
95 /**
96 * Create a new SqlMapClientTemplate.
97 */
98 public SqlMapClientTemplate() {
99 }
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, SqlMapClient sqlMapClient) {
116 setDataSource(dataSource);
117 setSqlMapClient(sqlMapClient);
118 afterPropertiesSet();
119 }
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("Property 'sqlMapClient' is required");
148 }
149 if (this.sqlMapClient instanceof ExtendedSqlMapClient) {
150 // Check whether iBATIS lazy loading is available, that is,
151 // whether a DataSource was specified on the SqlMapClient itself.
152 this.lazyLoadingAvailable = (((ExtendedSqlMapClient) this.sqlMapClient).getDelegate().getTxManager() != null);
153 }
154 super.afterPropertiesSet();
155 }
156
157
158 /**
159 * Execute the given data access action on a SqlMapExecutor.
160 * @param action callback object that specifies the data access action
161 * @return a result object returned by the action, or <code>null</code>
162 * @throws DataAccessException in case of SQL Maps errors
163 */
164 public Object execute(SqlMapClientCallback action) throws DataAccessException {
165 Assert.notNull(action, "Callback object must not be null");
166 Assert.notNull(this.sqlMapClient, "No SqlMapClient specified");
167
168 // We always needs to use a SqlMapSession, as we need to pass a Spring-managed
169 // Connection (potentially transactional) in. This shouldn't be necessary if
170 // we run against a TransactionAwareDataSourceProxy underneath, but unfortunately
171 // we still need it to make iBATIS batch execution work properly: If iBATIS
172 // doesn't recognize an existing transaction, it automatically executes the
173 // batch for every single statement...
174
175 SqlMapSession session = this.sqlMapClient.openSession();
176 if (logger.isDebugEnabled()) {
177 logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation");
178 }
179 Connection ibatisCon = null;
180
181 try {
182 Connection springCon = null;
183 DataSource dataSource = getDataSource();
184 boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);
185
186 // Obtain JDBC Connection to operate on...
187 try {
188 ibatisCon = session.getCurrentConnection();
189 if (ibatisCon == null) {
190 springCon = (transactionAware ?
191 dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource));
192 session.setUserConnection(springCon);
193 if (logger.isDebugEnabled()) {
194 logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation");
195 }
196 }
197 else {
198 if (logger.isDebugEnabled()) {
199 logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation");
200 }
201 }
202 }
203 catch (SQLException ex) {
204 throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
205 }
206
207 // Execute given callback...
208 try {
209 return action.doInSqlMapClient(session);
210 }
211 catch (SQLException ex) {
212 throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);
213 }
214 finally {
215 try {
216 if (springCon != null) {
217 if (transactionAware) {
218 springCon.close();
219 }
220 else {
221 DataSourceUtils.doReleaseConnection(springCon, dataSource);
222 }
223 }
224 }
225 catch (Throwable ex) {
226 logger.debug("Could not close JDBC Connection", ex);
227 }
228 }
229
230 // Processing finished - potentially session still to be closed.
231 }
232 finally {
233 // Only close SqlMapSession if we know we've actually opened it
234 // at the present level.
235 if (ibatisCon == null) {
236 session.close();
237 }
238 }
239 }
240
241 /**
242 * Execute the given data access action on a SqlMapExecutor,
243 * expecting a List result.
244 * @param action callback object that specifies the data access action
245 * @return the List result
246 * @throws DataAccessException in case of SQL Maps errors
247 */
248 public List executeWithListResult(SqlMapClientCallback action) throws DataAccessException {
249 return (List) execute(action);
250 }
251
252 /**
253 * Execute the given data access action on a SqlMapExecutor,
254 * expecting a Map result.
255 * @param action callback object that specifies the data access action
256 * @return the Map result
257 * @throws DataAccessException in case of SQL Maps errors
258 */
259 public Map executeWithMapResult(SqlMapClientCallback action) throws DataAccessException {
260 return (Map) execute(action);
261 }
262
263
264 public Object queryForObject(String statementName) throws DataAccessException {
265 return queryForObject(statementName, null);
266 }
267
268 public Object queryForObject(final String statementName, final Object parameterObject)
269 throws DataAccessException {
270
271 return execute(new SqlMapClientCallback() {
272 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
273 return executor.queryForObject(statementName, parameterObject);
274 }
275 });
276 }
277
278 public Object queryForObject(
279 final String statementName, final Object parameterObject, final Object resultObject)
280 throws DataAccessException {
281
282 return execute(new SqlMapClientCallback() {
283 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
284 return executor.queryForObject(statementName, parameterObject, resultObject);
285 }
286 });
287 }
288
289 public List queryForList(String statementName) throws DataAccessException {
290 return queryForList(statementName, null);
291 }
292
293 public List queryForList(final String statementName, final Object parameterObject)
294 throws DataAccessException {
295
296 return executeWithListResult(new SqlMapClientCallback() {
297 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
298 return executor.queryForList(statementName, parameterObject);
299 }
300 });
301 }
302
303 public List queryForList(String statementName, int skipResults, int maxResults)
304 throws DataAccessException {
305
306 return queryForList(statementName, null, skipResults, maxResults);
307 }
308
309 public List queryForList(
310 final String statementName, final Object parameterObject, final int skipResults, final int maxResults)
311 throws DataAccessException {
312
313 return executeWithListResult(new SqlMapClientCallback() {
314 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
315 return executor.queryForList(statementName, parameterObject, skipResults, maxResults);
316 }
317 });
318 }
319
320 public void queryWithRowHandler(String statementName, RowHandler rowHandler)
321 throws DataAccessException {
322
323 queryWithRowHandler(statementName, null, rowHandler);
324 }
325
326 public void queryWithRowHandler(
327 final String statementName, final Object parameterObject, final RowHandler rowHandler)
328 throws DataAccessException {
329
330 execute(new SqlMapClientCallback() {
331 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
332 executor.queryWithRowHandler(statementName, parameterObject, rowHandler);
333 return null;
334 }
335 });
336 }
337
338 /**
339 * @deprecated as of iBATIS 2.3.0
340 */
341 public PaginatedList queryForPaginatedList(String statementName, int pageSize)
342 throws DataAccessException {
343
344 return queryForPaginatedList(statementName, null, pageSize);
345 }
346
347 /**
348 * @deprecated as of iBATIS 2.3.0
349 */
350 public PaginatedList queryForPaginatedList(
351 final String statementName, final Object parameterObject, final int pageSize)
352 throws DataAccessException {
353
354 // Throw exception if lazy loading will not work.
355 if (!this.lazyLoadingAvailable) {
356 throw new InvalidDataAccessApiUsageException(
357 "SqlMapClient needs to have DataSource to allow for lazy loading" +
358 " - specify SqlMapClientFactoryBean's 'dataSource' property");
359 }
360
361 return (PaginatedList) execute(new SqlMapClientCallback() {
362 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
363 return executor.queryForPaginatedList(statementName, parameterObject, pageSize);
364 }
365 });
366 }
367
368 public Map queryForMap(
369 final String statementName, final Object parameterObject, final String keyProperty)
370 throws DataAccessException {
371
372 return executeWithMapResult(new SqlMapClientCallback() {
373 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
374 return executor.queryForMap(statementName, parameterObject, keyProperty);
375 }
376 });
377 }
378
379 public Map queryForMap(
380 final String statementName, final Object parameterObject, final String keyProperty, final String valueProperty)
381 throws DataAccessException {
382
383 return executeWithMapResult(new SqlMapClientCallback() {
384 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
385 return executor.queryForMap(statementName, parameterObject, keyProperty, valueProperty);
386 }
387 });
388 }
389
390 public Object insert(String statementName) throws DataAccessException {
391 return insert(statementName, null);
392 }
393
394 public Object insert(final String statementName, final Object parameterObject)
395 throws DataAccessException {
396
397 return execute(new SqlMapClientCallback() {
398 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
399 return executor.insert(statementName, parameterObject);
400 }
401 });
402 }
403
404 public int update(String statementName) throws DataAccessException {
405 return update(statementName, null);
406 }
407
408 public int update(final String statementName, final Object parameterObject)
409 throws DataAccessException {
410
411 Integer result = (Integer) execute(new SqlMapClientCallback() {
412 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
413 return new Integer(executor.update(statementName, parameterObject));
414 }
415 });
416 return result.intValue();
417 }
418
419 public void update(String statementName, Object parameterObject, int requiredRowsAffected)
420 throws DataAccessException {
421
422 int actualRowsAffected = update(statementName, parameterObject);
423 if (actualRowsAffected != requiredRowsAffected) {
424 throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(
425 statementName, requiredRowsAffected, actualRowsAffected);
426 }
427 }
428
429 public int delete(String statementName) throws DataAccessException {
430 return delete(statementName, null);
431 }
432
433 public int delete(final String statementName, final Object parameterObject)
434 throws DataAccessException {
435
436 Integer result = (Integer) execute(new SqlMapClientCallback() {
437 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
438 return new Integer(executor.delete(statementName, parameterObject));
439 }
440 });
441 return result.intValue();
442 }
443
444 public void delete(String statementName, Object parameterObject, int requiredRowsAffected)
445 throws DataAccessException {
446
447 int actualRowsAffected = delete(statementName, parameterObject);
448 if (actualRowsAffected != requiredRowsAffected) {
449 throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(
450 statementName, requiredRowsAffected, actualRowsAffected);
451 }
452 }
453
454 }