[#1296] Simulate the FOR UPDATE clause for SQL Server, CUBRID, using

JDBC's ResultSet.CONCUR_UPDATABLE
This commit is contained in:
Lukas Eder 2012-04-30 23:55:29 +02:00
parent 8f83b6c091
commit 9c435f3ff8
10 changed files with 180 additions and 36 deletions

View File

@ -659,6 +659,10 @@ public abstract class BaseTest<
return delegate.getConnection();
}
protected final Connection getNewConnection() {
return delegate.getConnection0(null, null);
}
protected final Factory create() {
return delegate.create();
}
@ -671,6 +675,13 @@ public abstract class BaseTest<
return delegate.internal(q);
}
protected final void sleep(long millis) {
try {
Thread.sleep(millis);
}
catch (InterruptedException ignore) {}
}
@SuppressWarnings("unchecked")
protected Sequence<? extends Number> SAuthorID() throws IllegalAccessException, NoSuchFieldException {
return (Sequence<? extends Number>) cSequences().getField("S_AUTHOR_ID").get(cSequences());

View File

@ -35,6 +35,7 @@
*/
package org.jooq.test._.testcases;
import static java.util.Arrays.asList;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.jooq.impl.Factory.count;
@ -42,6 +43,8 @@ import static org.jooq.impl.Factory.countDistinct;
import static org.jooq.impl.Factory.trim;
import static org.jooq.impl.Factory.val;
import java.util.Vector;
import org.jooq.Field;
import org.jooq.Record;
import org.jooq.Result;
@ -50,6 +53,8 @@ import org.jooq.SelectQuery;
import org.jooq.Table;
import org.jooq.TableRecord;
import org.jooq.UpdatableRecord;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.Factory;
import org.jooq.test.BaseTest;
import org.jooq.test.jOOQAbstractTest;
@ -342,25 +347,91 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
@Test
public void testForUpdateClauses() throws Exception {
switch (getDialect()) {
case CUBRID:
case SQLITE:
case SQLSERVER:
log.info("SKIPPING", "FOR UPDATE tests");
return;
}
// Just checking for syntax correctness. Locking should be OK
Result<Record> result = create().select(TAuthor_ID())
.from(TAuthor())
.forUpdate()
.fetch();
assertEquals(2, result.size());
Result<A> result2 = create().selectFrom(TAuthor())
.forUpdate()
.fetch();
assertEquals(2, result2.size());
// Checking for syntax correctness and locking behaviour
// -----------------------------------------------------
final Factory create1 = create();
final Factory create2 = create();
create2.setConnection(getNewConnection());
create2.getConnection().setAutoCommit(false);
final Vector<String> execOrder = new Vector<String>();
try {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sleep(2000);
execOrder.add("t1-block");
try {
create1
.select(TAuthor_ID())
.from(TAuthor())
.forUpdate()
.fetch();
}
// Some databases fail on locking, others lock for a while
catch (DataAccessException ignore) {
}
finally {
execOrder.add("t1-fail-or-t2-commit");
}
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
execOrder.add("t2-exec");
Result<A> result2 = create2
.selectFrom(TAuthor())
.forUpdate()
.fetch();
assertEquals(2, result2.size());
execOrder.add("t2-signal");
sleep(4000);
execOrder.add("t1-fail-or-t2-commit");
try {
create2.getConnection().commit();
create2.getConnection().close();
}
catch (Exception e) {}
}
});
// This is the test case:
// 0.0s: Both threads start
// 0.0s: t1 sleeps for 2s
// 0.0s: t2 locks the T_AUTHOR table
// 0.1s: t2 sleeps for 4s
// 2.0s: t1 blocks on the T_AUTHOR table
// ???s: t1 fails
// 4.0s: t2 commits and unlocks T_AUTHOR
t1.start();
t2.start();
t1.join();
t2.join();
assertEquals(asList("t2-exec", "t2-signal", "t1-block", "t1-fail-or-t2-commit", "t1-fail-or-t2-commit"), execOrder);
}
finally {
try {
create2.getConnection().close();
}
catch (Exception e) {}
}
// Check again with limit / offset clauses
// ---------------------------------------
switch (getDialect()) {
case INGRES:
case ORACLE:
@ -398,7 +469,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
break;
default: {
result = create().select(TAuthor_ID())
Result<Record> result = create().select(TAuthor_ID())
.from(TAuthor())
.forUpdate()
.wait(2)
@ -418,7 +489,7 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
assertEquals(2, result.size());
result2 = create().selectFrom(TAuthor())
Result<A> result2 = create().selectFrom(TAuthor())
.forUpdate()
.of(TAuthor_LAST_NAME(), TAuthor_FIRST_NAME())
.wait(2)
@ -452,14 +523,14 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
case INGRES:
case ORACLE:
case SYBASE: {
result = create().select(TAuthor_ID())
Result<Record> result = create().select(TAuthor_ID())
.from(TAuthor())
.forUpdate()
.of(TAuthor_LAST_NAME(), TAuthor_FIRST_NAME())
.fetch();
assertEquals(2, result.size());
result2 = create().selectFrom(TAuthor())
Result<A> result2 = create().selectFrom(TAuthor())
.forUpdate()
.of(TAuthor_LAST_NAME(), TAuthor_FIRST_NAME())
.fetch();
@ -470,14 +541,14 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
// Postgres only supports the OF clause with tables as parameters
case POSTGRES: {
result = create().select(TAuthor_ID())
Result<Record> result = create().select(TAuthor_ID())
.from(TAuthor())
.forUpdate()
.of(TAuthor())
.fetch();
assertEquals(2, result.size());
result2 = create().selectFrom(TAuthor())
Result<A> result2 = create().selectFrom(TAuthor())
.forUpdate()
.of(TAuthor())
.fetch();
@ -491,13 +562,13 @@ extends BaseTest<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
switch (getDialect()) {
case MYSQL:
case POSTGRES: {
result = create().select(TAuthor_ID())
Result<Record> result = create().select(TAuthor_ID())
.from(TAuthor())
.forShare()
.fetch();
assertEquals(2, result.size());
result2 = create().selectFrom(TAuthor())
Result<A> result2 = create().selectFrom(TAuthor())
.forShare()
.fetch();
assertEquals(2, result2.size());

View File

@ -54,8 +54,6 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.swing.UIManager;
import org.jooq.ArrayRecord;
import org.jooq.DataType;
import org.jooq.ExecuteType;
@ -76,7 +74,6 @@ import org.jooq.conf.RenderMapping;
import org.jooq.conf.Settings;
import org.jooq.conf.SettingsTools;
import org.jooq.debug.DebugListener;
import org.jooq.debug.console.Console;
import org.jooq.debug.console.DatabaseDescriptor;
import org.jooq.debug.console.remote.RemoteDebuggerServer;
import org.jooq.impl.Factory;
@ -441,10 +438,10 @@ public abstract class jOOQAbstractTest<
};
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
Console console = new Console(descriptor, true);
console.setLoggingActive(true);
console.setVisible(true);
// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Console console = new Console(descriptor, true);
// console.setLoggingActive(true);
// console.setVisible(true);
}
catch (Exception ignore) {}
}
@ -471,7 +468,7 @@ public abstract class jOOQAbstractTest<
return connectionMultiSchemaUnused;
}
private final Connection getConnection0(String jdbcUser, String jdbcPassword) {
final Connection getConnection0(String jdbcUser, String jdbcPassword) {
try {
String configuration = System.getProperty("jdbc.properties");
if (configuration == null) {

View File

@ -36,6 +36,7 @@
package org.jooq;
import static org.jooq.SQLDialect.ASE;
import static org.jooq.SQLDialect.CUBRID;
import static org.jooq.SQLDialect.DB2;
import static org.jooq.SQLDialect.DERBY;
import static org.jooq.SQLDialect.H2;
@ -44,8 +45,12 @@ import static org.jooq.SQLDialect.INGRES;
import static org.jooq.SQLDialect.MYSQL;
import static org.jooq.SQLDialect.ORACLE;
import static org.jooq.SQLDialect.POSTGRES;
import static org.jooq.SQLDialect.SQLSERVER;
import static org.jooq.SQLDialect.SYBASE;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Collection;
/**
@ -59,7 +64,8 @@ public interface LockProvider {
/**
* Sets the "FOR UPDATE" flag onto the query
* <p>
* This has been observed to be supported by any of these dialects:
* <h3>Native implementation</h3> This has been observed to be supported by
* any of these dialects:
* <ul>
* <li><a href=
* "http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/index.jsp?topic=/com.ibm.db2.luw.sql.ref.doc/doc/r0000879.html"
@ -82,12 +88,23 @@ public interface LockProvider {
* "http://www.postgresql.org/docs/9.0/static/sql-select.html#SQL-FOR-UPDATE-SHARE"
* >Postgres FOR UPDATE / FOR SHARE</a></li>
* </ul>
* <h3>Simulation</h3> These dialects can simulate the
* <code>FOR UPDATE</code> clause using a cursor. The cursor is handled by
* the JDBC driver, at {@link PreparedStatement} construction time, when
* calling {@link Connection#prepareStatement(String, int, int)} with
* {@link ResultSet#CONCUR_UPDATABLE}. jOOQ handles simulation of a
* <code>FOR UPDATE</code> clause using <code>CONCUR_UPDATABLE</code> for
* these dialects:
* <ul>
* <li> {@link SQLDialect#CUBRID}</li>
* <li> {@link SQLDialect#SQLSERVER}</li>
* </ul>
* <p>
* These dialects are known not to support the <code>FOR UPDATE</code>
* clause in regular SQL:
* Note: This simulation may not be efficient for large result sets!
* <h3>Not supported</h3> These dialects are known not to support the
* <code>FOR UPDATE</code> clause in regular SQL:
* <ul>
* <li> {@link SQLDialect#SQLITE}</li>
* <li> {@link SQLDialect#SQLSERVER}</li>
* </ul>
* <p>
* If your dialect does not support this clause, jOOQ will still render it,
@ -98,7 +115,7 @@ public interface LockProvider {
*
* @param forUpdate The flag's value
*/
@Support({ ASE, DB2, DERBY, H2, HSQLDB, INGRES, MYSQL, ORACLE, POSTGRES, SYBASE })
@Support({ ASE, CUBRID, DB2, DERBY, H2, HSQLDB, INGRES, MYSQL, ORACLE, POSTGRES, SQLSERVER, SYBASE })
void setForUpdate(boolean forUpdate);
/**

View File

@ -36,6 +36,7 @@
package org.jooq;
import static org.jooq.SQLDialect.ASE;
import static org.jooq.SQLDialect.CUBRID;
import static org.jooq.SQLDialect.DB2;
import static org.jooq.SQLDialect.DERBY;
import static org.jooq.SQLDialect.H2;
@ -44,6 +45,7 @@ import static org.jooq.SQLDialect.INGRES;
import static org.jooq.SQLDialect.MYSQL;
import static org.jooq.SQLDialect.ORACLE;
import static org.jooq.SQLDialect.POSTGRES;
import static org.jooq.SQLDialect.SQLSERVER;
import static org.jooq.SQLDialect.SYBASE;
/**
@ -101,7 +103,7 @@ public interface SelectForUpdateStep extends SelectFinalStep {
*
* @see LockProvider#setForUpdate(boolean) see LockProvider for more details
*/
@Support({ ASE, DB2, DERBY, H2, HSQLDB, INGRES, MYSQL, ORACLE, POSTGRES, SYBASE })
@Support({ ASE, CUBRID, DB2, DERBY, H2, HSQLDB, INGRES, MYSQL, ORACLE, POSTGRES, SQLSERVER, SYBASE })
SelectForUpdateOfStep forUpdate();
/**

View File

@ -35,8 +35,13 @@
*/
package org.jooq.impl;
import static java.sql.ResultSet.CONCUR_UPDATABLE;
import static java.sql.ResultSet.TYPE_SCROLL_SENSITIVE;
import static java.util.Arrays.asList;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.jooq.SQLDialect.ASE;
import static org.jooq.SQLDialect.CUBRID;
import static org.jooq.SQLDialect.SQLSERVER;
import java.lang.reflect.Array;
import java.sql.Connection;
@ -114,7 +119,17 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
@Override
protected final void prepare(ExecuteContext ctx) throws SQLException {
super.prepare(ctx);
// [#1296] These dialects do not implement FOR UPDATE. But the same
// effect can be achieved using ResultSet.CONCUR_UPDATABLE
if (isForUpdate() && asList(CUBRID, SQLSERVER).contains(ctx.getDialect())) {
ctx.statement(ctx.getConnection().prepareStatement(ctx.sql(), TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE));
}
// Regular behaviour
else {
ctx.statement(ctx.getConnection().prepareStatement(ctx.sql()));
}
// [#1263] Allow for negative fetch sizes to support some non-standard
// MySQL feature, where Integer.MIN_VALUE is used
@ -228,6 +243,11 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
*/
abstract boolean isSelectingRefCursor();
/**
* Subclasses should indicate whether they want an updatable {@link ResultSet}
*/
abstract boolean isForUpdate();
@Override
public final Result<R> fetch() {
execute();

View File

@ -35,6 +35,8 @@
*/
package org.jooq.impl;
import static java.util.Arrays.asList;
import static org.jooq.SQLDialect.CUBRID;
import static org.jooq.SQLDialect.SQLSERVER;
import static org.jooq.impl.Factory.literal;
import static org.jooq.impl.Factory.one;
@ -248,7 +250,8 @@ implements
toSQLReference0(context);
}
if (forUpdate) {
// [#1296] FOR UPDATE is simulated in some dialects using ResultSet.CONCUR_UPDATABLE
if (forUpdate && !asList(CUBRID, SQLSERVER).contains(context.getDialect())) {
context.formatSeparator()
.keyword("for update");
@ -815,6 +818,11 @@ implements
this.hint = hint;
}
@Override
final boolean isForUpdate() {
return forUpdate;
}
// -------------------------------------------------------------------------
// Utility classes
// -------------------------------------------------------------------------

View File

@ -1242,6 +1242,14 @@ class CursorImpl<R extends Record> implements Cursor<R> {
try {
if (!isClosed && rs.next()) {
// [#1296] Force a row-lock by updating the row if the
// FOR UPDATE clause is simulated
if (rs.getConcurrency() == ResultSet.CONCUR_UPDATABLE) {
rs.updateObject(1, rs.getObject(1));
rs.updateRow();
}
record = Util.newRecord(type, fields, ctx.configuration());
ctx.record(record);

View File

@ -104,6 +104,11 @@ class SQLResultQuery extends AbstractResultQuery<Record> implements BindingProvi
return false;
}
@Override
final boolean isForUpdate() {
return false;
}
// ------------------------------------------------------------------------
// QueryPart API
// ------------------------------------------------------------------------

View File

@ -129,4 +129,9 @@ class Union<R extends Record> extends AbstractSelect<R> {
public final void bind(BindContext context) {
context.bind(queries);
}
@Override
final boolean isForUpdate() {
return false;
}
}