Merge pull request #2090 from ebean-orm/feature/2089

#2089 - Postgres - Use NO KEY with FOR UPDATE clauses with Postgres
This commit is contained in:
Rob Bygrave authored and GitHub committed 2020-11-24 21:49:28 +13:00
commit ed061fb3b1
6 files changed
+169 -12

No files matched your search

@@ -17,6 +17,11 @@ public class PlatformConfig {
private boolean allQuotedIdentifiers; private boolean allQuotedIdentifiers;
/**
* Set this to true for Postgres FOR UPDATE to include the primary key (not use NO KEY).
*/
private boolean lockWithKey;
private DbConstraintNaming constraintNaming; private DbConstraintNaming constraintNaming;
/** /**
@@ -77,6 +82,7 @@ public class PlatformConfig {
* Construct based on given config - typically for DbMigration generation with many platforms. * Construct based on given config - typically for DbMigration generation with many platforms.
*/ */
public PlatformConfig(PlatformConfig platformConfig) { public PlatformConfig(PlatformConfig platformConfig) {
this.lockWithKey = platformConfig.lockWithKey;
this.databaseBooleanFalse = platformConfig.databaseBooleanFalse; this.databaseBooleanFalse = platformConfig.databaseBooleanFalse;
this.databaseBooleanTrue = platformConfig.databaseBooleanTrue; this.databaseBooleanTrue = platformConfig.databaseBooleanTrue;
this.databaseSequenceBatchSize = platformConfig.databaseSequenceBatchSize; this.databaseSequenceBatchSize = platformConfig.databaseSequenceBatchSize;
@@ -133,14 +139,26 @@ public class PlatformConfig {
this.caseSensitiveCollation = caseSensitiveCollation; this.caseSensitiveCollation = caseSensitiveCollation;
} }
/**
* Return true if Postgres FOR UPDATE should include the primary key (or use NO KEY).
*/
public boolean isLockWithKey() {
return lockWithKey;
}
/**
* Set to true such that Postgres FOR UPDATE should include the primary key (not use NO KEY option).
*/
public void setLockWithKey(boolean lockWithKey) {
this.lockWithKey = lockWithKey;
}
/** /**
* Return a value used to represent TRUE in the database. * Return a value used to represent TRUE in the database.
* <p> * <p>
* This is used for databases that do not support boolean natively. * This is used for databases that do not support boolean natively.
* </p>
* <p> * <p>
* The value returned is either a Integer or a String (e.g. "1", or "T"). * The value returned is either a Integer or a String (e.g. "1", or "T").
* </p>
*/ */
public String getDatabaseBooleanTrue() { public String getDatabaseBooleanTrue() {
return databaseBooleanTrue; return databaseBooleanTrue;
@@ -150,10 +168,8 @@ public class PlatformConfig {
* Set the value to represent TRUE in the database. * Set the value to represent TRUE in the database.
* <p> * <p>
* This is used for databases that do not support boolean natively. * This is used for databases that do not support boolean natively.
* </p>
* <p> * <p>
* The value set is either a Integer or a String (e.g. "1", or "T"). * The value set is either a Integer or a String (e.g. "1", or "T").
* </p>
*/ */
public void setDatabaseBooleanTrue(String databaseBooleanTrue) { public void setDatabaseBooleanTrue(String databaseBooleanTrue) {
this.databaseBooleanTrue = databaseBooleanTrue; this.databaseBooleanTrue = databaseBooleanTrue;
@@ -245,7 +261,6 @@ public class PlatformConfig {
/** /**
* Add a custom type mapping. * Add a custom type mapping.
* <p>
* <pre>{@code * <pre>{@code
* *
* // set the default mapping for BigDecimal.class/decimal * // set the default mapping for BigDecimal.class/decimal
@@ -266,7 +281,6 @@ public class PlatformConfig {
/** /**
* Add a custom type mapping that applies to all platforms. * Add a custom type mapping that applies to all platforms.
* <p>
* <pre>{@code * <pre>{@code
* *
* // set the default mapping for BigDecimal/decimal * // set the default mapping for BigDecimal/decimal
@@ -294,6 +308,7 @@ public class PlatformConfig {
public void loadSettings(PropertiesWrapper p) { public void loadSettings(PropertiesWrapper p) {
idType = p.getEnum(IdType.class, "idType", idType); idType = p.getEnum(IdType.class, "idType", idType);
lockWithKey = p.getBoolean("lockWithKey", lockWithKey);
databaseSequenceBatchSize = p.getInt("databaseSequenceBatchSize", databaseSequenceBatchSize); databaseSequenceBatchSize = p.getInt("databaseSequenceBatchSize", databaseSequenceBatchSize);
databaseBooleanTrue = p.get("databaseBooleanTrue", databaseBooleanTrue); databaseBooleanTrue = p.get("databaseBooleanTrue", databaseBooleanTrue);
databaseBooleanFalse = p.get("databaseBooleanFalse", databaseBooleanFalse); databaseBooleanFalse = p.get("databaseBooleanFalse", databaseBooleanFalse);
@@ -334,7 +349,6 @@ public class PlatformConfig {
*/ */
public enum DbUuid { public enum DbUuid {
/** /**
* Store using native UUID in H2 and Postgres and otherwise fallback to VARCHAR(40). * Store using native UUID in H2 and Postgres and otherwise fallback to VARCHAR(40).
*/ */
@@ -4,6 +4,7 @@ import io.ebean.BackgroundExecutor;
import io.ebean.Query; import io.ebean.Query;
import io.ebean.annotation.PartitionMode; import io.ebean.annotation.PartitionMode;
import io.ebean.annotation.Platform; import io.ebean.annotation.Platform;
import io.ebean.config.PlatformConfig;
import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.config.dbplatform.DbType; import io.ebean.config.dbplatform.DbType;
@@ -25,6 +26,11 @@ import java.sql.Types;
*/ */
public class PostgresPlatform extends DatabasePlatform { public class PostgresPlatform extends DatabasePlatform {
// by default using NO KEY option with FOR UPDATE clauses
private String forUpdateSkipLocked = " for no key update skip locked";
private String forUpdateNowait = " for no key update nowait";
private String forUpdate = " for no key update";
public PostgresPlatform() { public PostgresPlatform() {
super(); super();
this.platform = Platform.POSTGRES; this.platform = Platform.POSTGRES;
@@ -81,6 +87,16 @@ public class PostgresPlatform extends DatabasePlatform {
dbTypeMap.put(DbType.LONGVARCHAR, dbTypeText); dbTypeMap.put(DbType.LONGVARCHAR, dbTypeText);
} }
@Override
public void configure(PlatformConfig config) {
super.configure(config);
if (config.isLockWithKey()) {
this.forUpdateSkipLocked = " for update skip locked";
this.forUpdateNowait = " for update nowait";
this.forUpdate = " for update";
}
}
@Override @Override
protected void addGeoTypes(int srid) { protected void addGeoTypes(int srid) {
dbTypeMap.put(DbType.POINT, geoType("point", srid)); dbTypeMap.put(DbType.POINT, geoType("point", srid));
@@ -116,11 +132,11 @@ public class PostgresPlatform extends DatabasePlatform {
protected String withForUpdate(String sql, Query.ForUpdate forUpdateMode) { protected String withForUpdate(String sql, Query.ForUpdate forUpdateMode) {
switch (forUpdateMode) { switch (forUpdateMode) {
case SKIPLOCKED: case SKIPLOCKED:
return sql + " for update skip locked"; return sql + forUpdateSkipLocked;
case NOWAIT: case NOWAIT:
return sql + " for update nowait"; return sql + forUpdateNowait;
default: default:
return sql + " for update"; return sql + forUpdate;
} }
} }
@@ -73,6 +73,7 @@ public class ServerConfigTest {
props.setProperty("caseSensitiveCollation", "false"); props.setProperty("caseSensitiveCollation", "false");
props.setProperty("loadModuleInfo", "true"); props.setProperty("loadModuleInfo", "true");
props.setProperty("collectQueryPlanThresholdMicros", "10000"); props.setProperty("collectQueryPlanThresholdMicros", "10000");
props.setProperty("lockWithKey", "true");
serverConfig.loadFromProperties(props); serverConfig.loadFromProperties(props);
@@ -84,6 +85,7 @@ public class ServerConfigTest {
assertTrue(serverConfig.isIdGeneratorAutomatic()); assertTrue(serverConfig.isIdGeneratorAutomatic());
assertFalse(serverConfig.getPlatformConfig().isCaseSensitiveCollation()); assertFalse(serverConfig.getPlatformConfig().isCaseSensitiveCollation());
assertTrue(serverConfig.getPlatformConfig().isLockWithKey());
assertThat(serverConfig.getNamingConvention()).isInstanceOf(MatchingNamingConvention.class); assertThat(serverConfig.getNamingConvention()).isInstanceOf(MatchingNamingConvention.class);
@@ -1,5 +1,6 @@
package io.ebean.config.dbplatform; package io.ebean.config.dbplatform;
import io.ebean.Query;
import io.ebean.config.PlatformConfig; import io.ebean.config.PlatformConfig;
import io.ebean.config.dbplatform.postgres.PostgresPlatform; import io.ebean.config.dbplatform.postgres.PostgresPlatform;
import org.junit.Test; import org.junit.Test;
@@ -20,4 +21,33 @@ public class PostgresPlatformTest {
assertThat(columnDefn).isEqualTo("uuid"); assertThat(columnDefn).isEqualTo("uuid");
} }
@Test
public void default_forUpdate_expect_noKeyUsed() {
DatabasePlatform platform = new PostgresPlatform();
PlatformConfig config = new PlatformConfig();
platform.configure(config);
assertThat(config.isLockWithKey()).isFalse();
assertThat(platform.withForUpdate("X", Query.ForUpdate.SKIPLOCKED)).isEqualTo("X for no key update skip locked");
assertThat(platform.withForUpdate("X", Query.ForUpdate.NOWAIT)).isEqualTo("X for no key update nowait");
assertThat(platform.withForUpdate("X", Query.ForUpdate.BASE)).isEqualTo("X for no key update");
}
@Test
public void lockWithKey_forUpdate() {
DatabasePlatform platform = new PostgresPlatform();
PlatformConfig config = new PlatformConfig();
config.setLockWithKey(true);
platform.configure(config);
assertThat(config.isLockWithKey()).isTrue();
assertThat(platform.withForUpdate("X", Query.ForUpdate.SKIPLOCKED)).isEqualTo("X for update skip locked");
assertThat(platform.withForUpdate("X", Query.ForUpdate.NOWAIT)).isEqualTo("X for update nowait");
assertThat(platform.withForUpdate("X", Query.ForUpdate.BASE)).isEqualTo("X for update");
}
} }
@@ -21,7 +21,6 @@ import static org.junit.Assert.assertTrue;
public class TestQueryForUpdate extends BaseTestCase { public class TestQueryForUpdate extends BaseTestCase {
@Test @Test
@ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB})
public void testForUpdate() { public void testForUpdate() {
@@ -35,6 +34,8 @@ public class TestQueryForUpdate extends BaseTestCase {
query.findList(); query.findList();
if (isSqlServer()) { if (isSqlServer()) {
assertThat(sqlOf(query)).contains("with (updlock)"); assertThat(sqlOf(query)).contains("with (updlock)");
} else if (isPostgres()) {
assertThat(sqlOf(query)).contains("for no key update");
} else { } else {
assertThat(sqlOf(query)).contains("for update"); assertThat(sqlOf(query)).contains("for update");
} }
@@ -67,7 +68,11 @@ public class TestQueryForUpdate extends BaseTestCase {
if (isH2() || isPostgres()) { if (isH2() || isPostgres()) {
assertSql(sql.get(0)).contains("from e_basic t0 where t0.id ="); assertSql(sql.get(0)).contains("from e_basic t0 where t0.id =");
assertSql(sql.get(1)).contains("from e_basic t0 where t0.id ="); assertSql(sql.get(1)).contains("from e_basic t0 where t0.id =");
assertSql(sql.get(1)).contains("for update"); if (isPostgres()) {
assertSql(sql.get(1)).contains("for no key update");
} else {
assertSql(sql.get(1)).contains("for update");
}
} }
transaction.end(); transaction.end();
@@ -91,6 +96,8 @@ public class TestQueryForUpdate extends BaseTestCase {
assertThat(sqlOf(query)).contains("for update"); assertThat(sqlOf(query)).contains("for update");
} else if (isSqlServer()) { } else if (isSqlServer()) {
assertThat(sqlOf(query)).contains("with (updlock,nowait)"); assertThat(sqlOf(query)).contains("with (updlock,nowait)");
} else if (isPostgres()) {
assertThat(sqlOf(query)).contains("for no key update nowait");
} else { } else {
assertThat(sqlOf(query)).contains("for update nowait"); assertThat(sqlOf(query)).contains("for update nowait");
} }
@@ -116,6 +123,8 @@ public class TestQueryForUpdate extends BaseTestCase {
assertThat(sqlOf(query)).contains("with (updlock,nowait)"); assertThat(sqlOf(query)).contains("with (updlock,nowait)");
} else if (isH2()) { } else if (isH2()) {
assertThat(sqlOf(query)).contains("for update"); assertThat(sqlOf(query)).contains("for update");
} else if (isPostgres()) {
assertThat(sqlOf(query)).contains("for no key update nowait");
} else { } else {
assertThat(sqlOf(query)).contains("for update nowait"); assertThat(sqlOf(query)).contains("for update nowait");
} }
@@ -0,0 +1,86 @@
package org.tests.basic;
import io.ebean.BaseTestCase;
import io.ebean.DB;
import io.ebean.annotation.ForPlatform;
import io.ebean.annotation.Platform;
import io.ebean.annotation.Transactional;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tests.model.basic.Article;
import org.tests.model.basic.Section;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
public class TestQueryForUpdatePostgresLock extends BaseTestCase {
private static final Logger log = LoggerFactory.getLogger(TestQueryForUpdatePostgresLock.class);
private long timePreInsert;
private long timePostInsert;
private long timePreLock;
private long timePostLock;
@Test
@ForPlatform(Platform.POSTGRES)
public void testForUpdatePostgresLock() throws InterruptedException {
Article article = new Article("lockTest", "auth");
DB.save(article);
final Integer id = article.getId();
ExecutorService exec = Executors.newFixedThreadPool(2);
exec.submit(() -> lockArticle(id));
exec.submit(() -> insertSection(id));
exec.awaitTermination(2, TimeUnit.SECONDS);
exec.shutdown();
// assert the lock was obtained before the insert was attempted
assertThat(timePreLock).isLessThan(timePreInsert);
// assert that the insert wasn't waiting on the lock to complete
assertThat(timePostInsert).isLessThan(timePostLock);
}
/**
* This holds row lock on article for 1 second.
* With FOR NO KEY UPDATE this does not block the insert.
*/
@Transactional
private void lockArticle(Integer id) {
timePreLock = System.currentTimeMillis();
log.info("lock start");
DB.find(Article.class).setId(id).forUpdate().findOne();
sleep(1000);
timePostLock = System.currentTimeMillis();
log.info("lock done");
}
/**
* This inserts with FK to the article that is locked.
* With FOR NO KEY UPDATE this insert does not wait on the lock.
*/
@Transactional
private void insertSection(Integer id) {
sleep(100);
log.info("insert start");
timePreInsert = System.currentTimeMillis();
Section section = new Section();
section.setArticle(DB.getReference(Article.class, id));
DB.save(section);
timePostInsert = System.currentTimeMillis();
log.info("inserted");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}