[#1296] Simulate the FOR UPDATE clause for SQL Server, CUBRID, using
JDBC's ResultSet.CONCUR_UPDATABLE
This commit is contained in:
parent
8f83b6c091
commit
9c435f3ff8
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -104,6 +104,11 @@ class SQLResultQuery extends AbstractResultQuery<Record> implements BindingProvi
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
final boolean isForUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// QueryPart API
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user