001/*
002 * Copyright 2002-2018 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.support.incrementer;
018
019import java.sql.Connection;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.sql.Statement;
023
024import javax.sql.DataSource;
025
026import org.springframework.dao.DataAccessException;
027import org.springframework.dao.DataAccessResourceFailureException;
028import org.springframework.jdbc.datasource.DataSourceUtils;
029import org.springframework.jdbc.support.JdbcUtils;
030
031/**
032 * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations
033 * which are based on identity columns in a sequence-like table.
034 *
035 * @author Juergen Hoeller
036 * @author Thomas Risberg
037 * @since 4.1.2
038 */
039public abstract class AbstractIdentityColumnMaxValueIncrementer extends AbstractColumnMaxValueIncrementer {
040
041        private boolean deleteSpecificValues = false;
042
043        /** The current cache of values. */
044        private long[] valueCache;
045
046        /** The next id to serve from the value cache. */
047        private int nextValueIndex = -1;
048
049
050        /**
051         * Default constructor for bean property style usage.
052         * @see #setDataSource
053         * @see #setIncrementerName
054         * @see #setColumnName
055         */
056        public AbstractIdentityColumnMaxValueIncrementer() {
057        }
058
059        public AbstractIdentityColumnMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) {
060                super(dataSource, incrementerName, columnName);
061        }
062
063
064        /**
065         * Specify whether to delete the entire range below the current maximum key value
066         * ({@code false} - the default), or the specifically generated values ({@code true}).
067         * The former mode will use a where range clause whereas the latter will use an in
068         * clause starting with the lowest value minus 1, just preserving the maximum value.
069         */
070        public void setDeleteSpecificValues(boolean deleteSpecificValues) {
071                this.deleteSpecificValues = deleteSpecificValues;
072        }
073
074        /**
075         * Return whether to delete the entire range below the current maximum key value
076         * ({@code false} - the default), or the specifically generated values ({@code true}).
077         */
078        public boolean isDeleteSpecificValues() {
079                return this.deleteSpecificValues;
080        }
081
082
083        @Override
084        protected synchronized long getNextKey() throws DataAccessException {
085                if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) {
086                        /*
087                        * Need to use straight JDBC code because we need to make sure that the insert and select
088                        * are performed on the same connection (otherwise we can't be sure that @@identity
089                        * returns the correct value)
090                        */
091                        Connection con = DataSourceUtils.getConnection(getDataSource());
092                        Statement stmt = null;
093                        try {
094                                stmt = con.createStatement();
095                                DataSourceUtils.applyTransactionTimeout(stmt, getDataSource());
096                                this.valueCache = new long[getCacheSize()];
097                                this.nextValueIndex = 0;
098                                for (int i = 0; i < getCacheSize(); i++) {
099                                        stmt.executeUpdate(getIncrementStatement());
100                                        ResultSet rs = stmt.executeQuery(getIdentityStatement());
101                                        try {
102                                                if (!rs.next()) {
103                                                        throw new DataAccessResourceFailureException("Identity statement failed after inserting");
104                                                }
105                                                this.valueCache[i] = rs.getLong(1);
106                                        }
107                                        finally {
108                                                JdbcUtils.closeResultSet(rs);
109                                        }
110                                }
111                                stmt.executeUpdate(getDeleteStatement(this.valueCache));
112                        }
113                        catch (SQLException ex) {
114                                throw new DataAccessResourceFailureException("Could not increment identity", ex);
115                        }
116                        finally {
117                                JdbcUtils.closeStatement(stmt);
118                                DataSourceUtils.releaseConnection(con, getDataSource());
119                        }
120                }
121                return this.valueCache[this.nextValueIndex++];
122        }
123
124
125        /**
126         * Statement to use to increment the "sequence" value.
127         * @return the SQL statement to use
128         */
129        protected abstract String getIncrementStatement();
130
131        /**
132         * Statement to use to obtain the current identity value.
133         * @return the SQL statement to use
134         */
135        protected abstract String getIdentityStatement();
136
137        /**
138         * Statement to use to clean up "sequence" values.
139         * <p>The default implementation either deletes the entire range below
140         * the current maximum value, or the specifically generated values
141         * (starting with the lowest minus 1, just preserving the maximum value)
142         * - according to the {@link #isDeleteSpecificValues()} setting.
143         * @param values the currently generated key values
144         * (the number of values corresponds to {@link #getCacheSize()})
145         * @return the SQL statement to use
146         */
147        protected String getDeleteStatement(long[] values) {
148                StringBuilder sb = new StringBuilder(64);
149                sb.append("delete from ").append(getIncrementerName()).append(" where ").append(getColumnName());
150                if (isDeleteSpecificValues()) {
151                        sb.append(" in (").append(values[0] - 1);
152                        for (int i = 0; i < values.length - 1; i++) {
153                                sb.append(", ").append(values[i]);
154                        }
155                        sb.append(")");
156                }
157                else {
158                        long maxValue = values[values.length - 1];
159                        sb.append(" < ").append(maxValue);
160                }
161                return sb.toString();
162        }
163
164}