diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java index c63eea588049..e0011c7c7f99 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java @@ -16,17 +16,27 @@ package org.springframework.jdbc.support.incrementer; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + import javax.sql.DataSource; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + /** - * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given SQL Server table - * with the equivalent of an auto-increment column. Note: If you use this class, your table key - * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * A {@link DataFieldMaxValueIncrementer} for SQL Server that uses a sequence table to auto-increment a value. + * + *

This incrementer should be used with SQL Server versions 2008 and older. SQL Server 2012 introduced native + * database sequences and the {@link SqlServerSequenceMaxValueIncrementer} is a better choice when working with + * newer versions of the database. * - *

This class is intended to be used with Microsoft SQL Server. - * - *

The sequence is kept in a table. There should be one sequence table per - * table that needs an auto-generated key. + *

The sequence is kept in a table and there should be one sequence table per table that needs an auto-generated + * key. Note: If you use this class, your table key column should NOT be defined as an IDENTITY column, as + * the sequence table does the job. * *

Example: * @@ -39,7 +49,19 @@ * is rolled back, the unused values will never be served. The maximum hole size in * numbering is consequently the value of cacheSize. * - * HINT: Since Microsoft SQL Server supports the JDBC 3.0 {@code getGeneratedKeys} + *

NOTE: This class does NOT use {@code AbstractIdentityColumnMaxValueIncrementer} because + * the locking model with SQL Server may use a page/table level lock when deleting records from + * the sequence table. The delete within {@code AbstractIdentityColumnMaxValueIncrementer} can result in a + * database deadlock error on SQL Server when multiple instances of the incrementer are running (in different + * processes) and the incrementer is called within the context of an existing database transaction. + * + *

To get around the locking model, this class relies on a reaping strategy to clean up rows within the + * sequence table. A reaper interval is used to keep track of the last time the rows were removed from the table. + * Each time {@code getNextKey} is called and the reaping interval has been reached, this class will spin up a + * thread to delete the data from the table. The use of a new thread insures the delete is handled outside the + * scope of any current transaction. The default reaper interval is 20 seconds. + * + *

HINT: Since Microsoft SQL Server supports the JDBC 3.0 {@code getGeneratedKeys} * method, it is recommended to use IDENTITY columns directly in the tables and then using a * {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} or utilizing * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the @@ -50,38 +72,128 @@ * * @author Thomas Risberg * @author Juergen Hoeller + * @author Tyler Van Gorder * @since 2.5.5 */ -public class SqlServerMaxValueIncrementer extends AbstractIdentityColumnMaxValueIncrementer { +public class SqlServerMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + private long[] valueCache; + + /** The next id to serve from the value cache */ + private int nextValueIndex = -1; + + private final ReapOldValues reaper = new ReapOldValues(); + private int reaperIntervalSeconds = 20; + private long nextReapTime; /** * Default constructor for bean property style usage. + * * @see #setDataSource * @see #setIncrementerName * @see #setColumnName + * @see #setReaperInternalSeconds */ public SqlServerMaxValueIncrementer() { + nextReapTime = getReapTime(); } - + /** - * Convenience constructor. + * Convenience constructor. The default reaper interval will be 20 seconds. + * * @param dataSource the DataSource to use * @param incrementerName the name of the sequence/table to use * @param columnName the name of the column in the sequence table to use */ public SqlServerMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { super(dataSource, incrementerName, columnName); + nextReapTime = getReapTime(); + } + + private String getIncrementStatement() { + return "insert into " + getIncrementerName() + " default values"; } + private String getIdentityStatement() { + return "select @@identity"; + } @Override - protected String getIncrementStatement() { - return "insert into " + getIncrementerName() + " default values"; + protected synchronized long getNextKey() { + if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same connection (otherwise we can't be sure that @@identity + * returns the correct value) + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + this.valueCache = new long[getCacheSize()]; + this.nextValueIndex = 0; + for (int i = 0; i < getCacheSize(); i++) { + stmt.executeUpdate(getIncrementStatement()); + ResultSet rs = stmt.executeQuery(getIdentityStatement()); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("Identity statement failed after inserting"); + } + this.valueCache[i] = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + if (System.currentTimeMillis() > nextReapTime) { + //If the current time has exceeded the reap time (default 20 seconds), spin up a thread to delete the old values. + + //NOTE: This class uses a new thread to isolate the delete in a separate transaction rather than + //using a new transaction and requiring the PlatformTransactionManager as an injected dependency. + Thread reapingThread = new Thread(reaper, "Incrementer " + getIncrementerName() + " Reaping Thread"); + reapingThread.setDaemon(true); + reapingThread.start(); + nextReapTime = getReapTime(); + } + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not increment identity", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + return this.valueCache[this.nextValueIndex++]; } - @Override - protected String getIdentityStatement() { - return "select @@identity"; + private long getReapTime() { + return System.currentTimeMillis() + (reaperIntervalSeconds * 1000); } + public void setReaperInternalSeconds(int reaperInternalSeconds) { + this.reaperIntervalSeconds = reaperInternalSeconds; + } + + private class ReapOldValues implements Runnable { + + @Override + public void run() { + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + stmt.executeUpdate("DELETE FROM " + getIncrementerName()); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not delete old identity values", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerSequenceMaxValueIncrementer.java new file mode 100644 index 000000000000..e9c2af335b60 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerSequenceMaxValueIncrementer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value of a given MS SQL Server sequence. + * + *

This incrementer should be used with SQL Server versions 2012 and newer to take advantage of native + * database sequences introduced in SQL Server 2012. For older versions of SQL Server (2008 and older,) the + * {@link SqlServerMaxValueIncrementer} can be used to generate values from a dedicated sequence table. + * + * @author Tyler Van Gorder + */ +public class SqlServerSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public SqlServerSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public SqlServerSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + @Override + protected String getSequenceQuery() { + return "select NEXT VALUE for " + getIncrementerName(); + } +}