Skip to content

Commit

Permalink
fix(pgsql): Postgres doesn't support UNION select with FOR UPDATE (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
david-leifker authored Dec 19, 2024
1 parent 4392d72 commit 48f3cc5
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,14 @@ public class EbeanAspectDao implements AspectDao, AspectMigrationsDao {
*/
private final LoadingCache<String, Lock> locks;

private final String batchGetMethod;

public EbeanAspectDao(@Nonnull final Database server, EbeanConfiguration ebeanConfiguration) {
_server = server;
this.batchGetMethod =
ebeanConfiguration.getBatchGetMethod() != null
? ebeanConfiguration.getBatchGetMethod()
: "IN";
if (ebeanConfiguration.getLocking().isEnabled()) {
this.locks =
CacheBuilder.newBuilder()
Expand Down Expand Up @@ -371,23 +377,37 @@ private List<EbeanAspectV2> batchGet(

final int totalPageCount = QueryUtils.getTotalPageCount(keys.size(), keysCount);
final List<EbeanAspectV2> finalResult =
batchGetUnion(new ArrayList<>(keys), keysCount, position, forUpdate);
batchGetSelectString(new ArrayList<>(keys), keysCount, position, forUpdate);

while (QueryUtils.hasMore(position, keysCount, totalPageCount)) {
position += keysCount;
final List<EbeanAspectV2> oneStatementResult =
batchGetUnion(new ArrayList<>(keys), keysCount, position, forUpdate);
batchGetSelectString(new ArrayList<>(keys), keysCount, position, forUpdate);
finalResult.addAll(oneStatementResult);
}

return finalResult;
}

@Nonnull
private List<EbeanAspectV2> batchGetSelectString(
@Nonnull final List<EbeanAspectV2.PrimaryKey> keys,
final int keysCount,
final int position,
boolean forUpdate) {

if (batchGetMethod.equals("IN")) {
return batchGetIn(keys, keysCount, position, forUpdate);
}

return batchGetUnion(keys, keysCount, position, forUpdate);
}

/**
* Builds a single SELECT statement for batch get, which selects one entity, and then can be
* UNION'd with other SELECT statements.
*/
private String batchGetSelect(
private String batchGetSelectString(
final int selectId,
@Nonnull final String urn,
@Nonnull final String aspect,
Expand Down Expand Up @@ -434,7 +454,7 @@ private List<EbeanAspectV2> batchGetUnion(
final Map<String, Object> params = new HashMap<>();
for (int index = position; index < end; index++) {
sb.append(
batchGetSelect(
batchGetSelectString(
index - position,
keys.get(index).getUrn(),
keys.get(index).getAspect(),
Expand Down Expand Up @@ -467,6 +487,65 @@ private List<EbeanAspectV2> batchGetUnion(
return query.findList();
}

@Nonnull
private List<EbeanAspectV2> batchGetIn(
@Nonnull final List<EbeanAspectV2.PrimaryKey> keys,
final int keysCount,
final int position,
boolean forUpdate) {
validateConnection();

// Build a single SELECT with IN clause using composite key comparison
// Query will look like:
// SELECT * FROM metadata_aspect WHERE (urn, aspect, version) IN
// (('urn0', 'aspect0', 0), ('urn1', 'aspect1', 1))
final StringBuilder sb = new StringBuilder();
sb.append(
"SELECT urn, aspect, version, metadata, systemMetadata, createdOn, createdBy, createdFor ");
sb.append("FROM metadata_aspect_v2 WHERE (urn, aspect, version) IN (");

final int end = Math.min(keys.size(), position + keysCount);
final Map<String, Object> params = new HashMap<>();

for (int index = position; index < end; index++) {
int paramIndex = index - position;
String urnParam = "urn" + paramIndex;
String aspectParam = "aspect" + paramIndex;
String versionParam = "version" + paramIndex;

params.put(urnParam, keys.get(index).getUrn());
params.put(aspectParam, keys.get(index).getAspect());
params.put(versionParam, keys.get(index).getVersion());

sb.append("(:" + urnParam + ", :" + aspectParam + ", :" + versionParam + ")");

if (index != end - 1) {
sb.append(",");
}
}

sb.append(")");

if (forUpdate) {
sb.append(" FOR UPDATE");
}

final RawSql rawSql =
RawSqlBuilder.parse(sb.toString())
.columnMapping(EbeanAspectV2.URN_COLUMN, "key.urn")
.columnMapping(EbeanAspectV2.ASPECT_COLUMN, "key.aspect")
.columnMapping(EbeanAspectV2.VERSION_COLUMN, "key.version")
.create();

final Query<EbeanAspectV2> query = _server.find(EbeanAspectV2.class).setRawSql(rawSql);

for (Map.Entry<String, Object> param : params.entrySet()) {
query.setParameter(param.getKey(), param.getValue());
}

return query.findList();
}

@Override
@Nonnull
public ListResult<String> listUrns(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class EbeanConfiguration {
private boolean autoCreateDdl;
private boolean postgresUseIamAuth;
private LockingConfiguration locking;
private String batchGetMethod;

public static final EbeanConfiguration testDefault =
EbeanConfiguration.builder().locking(LockingConfiguration.testDefault).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ ebean:
waitTimeoutMillis: ${EBEAN_WAIT_TIMEOUT_MILLIS:1000}
autoCreateDdl: ${EBEAN_AUTOCREATE:false}
postgresUseIamAuth: ${EBEAN_POSTGRES_USE_AWS_IAM_AUTH:false}
batchGetMethod: ${EBEAN_BATCH_GET_METHOD:IN} # Alternative UNION
locking:
enabled: ${EBEAN_LOCKING_ENABLED:false}
durationSeconds: ${EBEAN_LOCKING_DURATION_SECONDS:60}
Expand Down

0 comments on commit 48f3cc5

Please sign in to comment.