Merge pull request #3324 from ebean-orm/feature/postgres_insertOnConflict

Add support for Postgres INSERT ON CONFLICT update | nothing
This commit is contained in:
Rob Bygrave
2024-02-10 12:08:44 +13:00
committed by GitHub
15 changed files with 811 additions and 40 deletions
@@ -0,0 +1,104 @@
package io.ebean;
final class DInsertOptionsBuilder implements InsertOptions.Builder {
private Boolean getGeneratedKeys;
private boolean onConflictUpdate;
private boolean onConflictNothing;
private String constraint;
private String uniqueColumns;
private String updateSet;
@Override
public InsertOptions.Builder onConflictNothing() {
this.onConflictNothing = true;
return this;
}
@Override
public InsertOptions.Builder onConflictUpdate() {
this.onConflictUpdate = true;
return this;
}
@Override
public InsertOptions.Builder constraint(String constraint) {
this.constraint = constraint;
return this;
}
@Override
public InsertOptions.Builder uniqueColumns(String uniqueColumns) {
this.uniqueColumns = uniqueColumns;
return this;
}
@Override
public InsertOptions.Builder updateSet(String updateSet) {
this.updateSet = updateSet;
return this;
}
@Override
public InsertOptions.Builder getGeneratedKeys(boolean getGeneratedKeys) {
this.getGeneratedKeys = getGeneratedKeys;
return this;
}
@Override
public InsertOptions build() {
return new Options(constraint, uniqueColumns, updateSet, onConflictUpdate, onConflictNothing, getGeneratedKeys);
}
static final class Options implements InsertOptions {
private static final String UPDATE = "U";
private static final String NOTHING = "N";
private static final String NORMAL = "_";
private final String key;
private final Boolean getGeneratedKeys;
private final String constraint;
private final String uniqueColumns;
private final String updateSet;
Options(String constraint, String uniqueColumns, String updateSet, boolean onConflictUpdate, boolean onConflictNothing, Boolean getGeneratedKeys) {
this.constraint = constraint;
this.uniqueColumns = uniqueColumns;
this.updateSet = updateSet;
this.getGeneratedKeys = getGeneratedKeys;
this.key = (onConflictUpdate ? UPDATE : onConflictNothing ? NOTHING : NORMAL)
+ '+' + plus(constraint)
+ '+' + plus(uniqueColumns)
+ '+' + plus(updateSet);
}
private String plus(String val) {
return val == null ? "" : val;
}
@Override
public String key() {
return key;
}
@Override
public String constraint() {
return constraint;
}
@Override
public String uniqueColumns() {
return uniqueColumns;
}
@Override
public String updateSet() {
return updateSet;
}
@Override
public Boolean getGetGeneratedKeys() {
return getGeneratedKeys;
}
}
}
@@ -1227,22 +1227,53 @@ public interface Database {
*/
void insert(Object bean);
/**
* Insert the bean with options (ON CONFLICT DO UPDATE | DO NOTHING).
* <p>
* Currently, this is limited to use with Postgres only,
* <p>
* When using this ebean will look to determine the unique columns by looking at
* the mapping like {@code @Column(unique=true} and {@code @Index(unique=true}.
*/
void insert(Object bean, InsertOptions insertOptions);
/**
* Insert the bean with a transaction.
*/
void insert(Object bean, Transaction transaction);
/**
* Insert the beans with options (ON CONFLICT DO UPDATE | DO NOTHING) and transaction.
* <p>
* Currently, this is limited to use with Postgres only,
*/
void insert(Object bean, InsertOptions insertOptions, Transaction transaction);
/**
* Insert a collection of beans. If there is no current transaction one is created and used to
* insert all the beans in the collection.
*/
void insertAll(Collection<?> beans);
/**
* Insert the beans with options - typically ON CONFLICT DO UPDATE | DO NOTHING.
* <p>
* Currently, this is limited to use with Postgres only,
*/
void insertAll(Collection<?> beans, InsertOptions options);
/**
* Insert a collection of beans with an explicit transaction.
*/
void insertAll(Collection<?> beans, Transaction transaction);
/**
* Insert the beans with options (ON CONFLICT DO UPDATE | DO NOTHING) and transaction.
* <p>
* Currently, this is limited to use with Postgres only,
*/
void insertAll(Collection<?> beans, InsertOptions options, Transaction transaction);
/**
* Execute explicitly passing a transaction.
*/
@@ -0,0 +1,119 @@
package io.ebean;
import io.avaje.lang.Nullable;
/**
* Options to be used with insert such as ON CONFLICT DO UPDATE | NOTHING.
*/
public interface InsertOptions {
/**
* Use ON CONFLICT UPDATE with automatic determination of the unique columns to conflict on.
* <p>
* Uses mapping to determine the unique columns - {@code @Column(unique=true)} and {@code @Index(unique=true)} .
*/
InsertOptions ON_CONFLICT_UPDATE = InsertOptions.builder()
.onConflictUpdate()
.build();
/**
* Use ON CONFLICT DO NOTHING with automatic determination of the unique columns to conflict on.
* <p>
* Uses mapping to determine the unique columns - {@code @Column(unique=true)} and {@code @Index(unique=true)} .
*/
InsertOptions ON_CONFLICT_NOTHING = InsertOptions.builder()
.onConflictNothing()
.build();
/**
* Return a builder for InsertOptions.
*/
static Builder builder() {
return new DInsertOptionsBuilder();
}
/**
* Return the constraint name that is used for ON CONFLICT.
*/
@Nullable
String constraint();
/**
* Return the unique columns that is used for ON CONFLICT.
* <p>
* When not explicitly set will use mapping like {@code @Column(unique=true)} to determine the
* non-unique columns.
*/
@Nullable
String uniqueColumns();
/**
* Return the ON CONFLICT UPDATE SET clause.
* <p>
* When not set will use the non-unique columns.
*/
@Nullable
String updateSet();
/**
* Return if GetGeneratedKeys should be used to fetch the generated keys after insert.
*/
@Nullable
Boolean getGetGeneratedKeys();
/**
* Return the key for these build options.
*/
String key();
/**
* The builder for InsertOptions.
*/
interface Builder {
/**
* Use a ON CONFLICT UPDATE automatically determining the unique columns.
*/
Builder onConflictUpdate();
/**
* Use a ON CONFLICT DO NOTHING automatically determining the unique columns.
*/
Builder onConflictNothing();
/**
* Specify an explicit conflict constraint name.
* <p>
* When this is used then unique columns will not be used.
*/
Builder constraint(String constraint);
/**
* Specify the unique columns for the conflict target.
* <p>
* When not specified and constraint is also not specified then
* it will automatically determine the unique columns
* based on mapping like {@code @Column(unique=true)} and
* {@code @Index(unique=true)} .
*/
Builder uniqueColumns(String uniqueColumns);
/**
* Specify the ON CONFLICT DO UPDATE SET clause.
* <p>
* When not specified ebean will include all the non-unique columns.
*/
Builder updateSet(String updateSet);
/**
* Specify if GetGeneratedKeys should be used to return generated keys.
*/
Builder getGeneratedKeys(boolean getGeneratedKeys);
/**
* Build and return the insert options.
*/
InsertOptions build();
}
}
@@ -1651,41 +1651,50 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}, transaction);
}
/**
* Insert the bean.
*/
@Override
public void insert(Object bean) {
insert(bean, null);
persister.insert(checkEntityBean(bean), null, null);
}
@Override
public void insert(Object bean, @Nullable InsertOptions insertOptions) {
persister.insert(checkEntityBean(bean), insertOptions, null);
}
/**
* Insert the bean with a transaction.
*/
@Override
public void insert(Object bean, @Nullable Transaction transaction) {
persister.insert(checkEntityBean(bean), transaction);
persister.insert(checkEntityBean(bean), null, transaction);
}
@Override
public void insert(Object bean, InsertOptions insertOptions, Transaction transaction) {
persister.insert(checkEntityBean(bean), insertOptions, transaction);
}
/**
* Insert all beans in the collection.
*/
@Override
public void insertAll(Collection<?> beans) {
insertAll(beans, null);
insertAll(beans, null, null);
}
@Override
public void insertAll(Collection<?> beans, InsertOptions options) {
insertAll(beans, options, null);
}
/**
* Insert all beans in the collection with a transaction.
*/
@Override
public void insertAll(@Nullable Collection<?> beans, @Nullable Transaction transaction) {
insertAll(beans, null, transaction);
}
@Override
public void insertAll(@Nullable Collection<?> beans, InsertOptions options, @Nullable Transaction transaction) {
if (beans == null || beans.isEmpty()) {
return;
}
executeInTrans((txn) -> {
txn.checkBatchEscalationOnCollection();
for (Object bean : beans) {
persister.insert(checkEntityBean(bean), txn);
persister.insert(checkEntityBean(bean), options, txn);
}
return 0;
}, transaction);
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.core;
import io.ebean.InsertOptions;
import io.ebean.ValuePair;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.EntityBean;
@@ -124,6 +125,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
* Many-to-many intersection table changes that are held for later batch processing.
*/
private List<SaveMany> saveMany;
private InsertOptions insertOptions;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
@@ -1416,4 +1418,12 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private void setGeneratedId() {
beanDescriptor.setGeneratedId(entityBean, transaction);
}
public void setInsertOptions(InsertOptions insertOptions) {
this.insertOptions = insertOptions;
}
public InsertOptions insertOptions() {
return insertOptions;
}
}
@@ -1,11 +1,7 @@
package io.ebeaninternal.server.core;
import io.ebean.CallableSql;
import io.ebean.MergeOptions;
import io.ebean.Query;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
import io.ebean.Update;
import io.avaje.lang.Nullable;
import io.ebean.*;
import io.ebean.bean.EntityBean;
import io.ebean.meta.MetricVisitor;
import io.ebeaninternal.api.SpiSqlUpdate;
@@ -31,9 +27,9 @@ public interface Persister {
void update(EntityBean entityBean, Transaction t);
/**
* Force an Insert using the given bean.
* Perform an Insert using the given bean.
*/
void insert(EntityBean entityBean, Transaction t);
void insert(EntityBean entityBean, @Nullable InsertOptions insertOptions, @Nullable Transaction t);
/**
* Insert or update the bean depending on its state.
@@ -44,7 +40,6 @@ public interface Persister {
* Delete a bean given it's type and id value.
* <p>
* This will also cascade delete one level of children.
* </p>
*/
int delete(Class<?> beanType, Object id, Transaction transaction, boolean permanent);
@@ -411,7 +411,7 @@ public final class DefaultPersister implements Persister {
if (bean._ebean_getIntercept().isUpdate()) {
update(bean, t);
} else {
insert(bean, t);
insert(bean, null, t);
}
}
@@ -419,12 +419,15 @@ public final class DefaultPersister implements Persister {
* Insert this bean.
*/
@Override
public void insert(EntityBean bean, Transaction t) {
public void insert(EntityBean bean, InsertOptions insertOptions, Transaction t) {
PersistRequestBean<?> req = createRequest(bean, t, PersistRequest.Type.INSERT);
if (req.isSkipReference()) {
// skip insert on reference bean
return;
}
if (insertOptions != null) {
req.setInsertOptions(insertOptions);
}
try {
req.initTransIfRequiredWithBatchCascade();
insert(req);
@@ -1,11 +1,15 @@
package io.ebeaninternal.server.persist.dml;
import java.util.ArrayList;
import java.util.List;
/**
* Helper to support the generation of DML statements.
*/
public final class GenerateDmlRequest {
private final StringBuilder sb = new StringBuilder(100);
private final List<String> columns = new ArrayList<>();
private StringBuilder insertBindBuffer;
private String prefix;
private String prefix2;
@@ -26,7 +30,7 @@ public final class GenerateDmlRequest {
++bindColumnCount;
sb.append(prefix);
sb.append(column);
//sb.append(expr);
columns.add(column);
if (insertMode > 0) {
if (insertMode++ > 1) {
insertBindBuffer.append(',');
@@ -75,4 +79,8 @@ public final class GenerateDmlRequest {
public boolean isUpdate() {
return insertMode == 0;
}
public List<String> columns() {
return columns;
}
}
@@ -69,7 +69,7 @@ public final class InsertHandler extends DmlHandler {
SpiTransaction t = persistRequest.transaction();
// get the appropriate sql
sql = meta.getSql(withId, persistRequest.isPublish());
sql = meta.sql(withId, persistRequest.isPublish(), persistRequest.insertOptions());
PreparedStatement pstmt;
if (persistRequest.isBatched()) {
pstmt = pstmtBatch(t, sql, persistRequest, useGeneratedKeys);
@@ -134,7 +134,8 @@ public final class InsertHandler extends DmlHandler {
if (idValue != null) {
persistRequest.setGeneratedKey(idValue);
}
} else {
} else if (persistRequest.insertOptions() == null) {
// insert on conflict do nothing can not return generated key
throw new PersistenceException("Autoincrement getGeneratedKeys() returned no rows?");
}
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.persist.dml;
import io.ebean.InsertOptions;
import io.ebean.annotation.Platform;
import io.ebean.bean.EntityBean;
import io.ebean.config.dbplatform.DatabasePlatform;
@@ -36,9 +37,11 @@ final class InsertMeta {
private final Bindable shadowFKey;
private final String[] identityDbColumns;
private final Platform platform;
private final InsertMetaOptions options;
InsertMeta(DatabasePlatform dbPlatform, BeanDescriptor<?> desc, Bindable shadowFKey, BindableId id, BindableList all) {
this.platform = dbPlatform.platform();
this.options = InsertMetaPlatform.create(platform, desc, this);
this.discriminator = discriminator(desc);
this.id = id;
this.all = all;
@@ -47,8 +50,8 @@ final class InsertMeta {
String tableName = desc.baseTable();
String draftTableName = desc.draftTable();
this.sqlWithId = genSql(false, tableName, false);
this.sqlDraftWithId = desc.isDraftable() ? genSql(false, draftTableName, true) : sqlWithId;
this.sqlWithId = sql(false, tableName, false);
this.sqlDraftWithId = desc.isDraftable() ? sql(false, draftTableName, true) : sqlWithId;
// only available for single Id property
if (id.isConcatenated()) {
@@ -72,8 +75,8 @@ final class InsertMeta {
this.supportsGetGeneratedKeys = dbPlatform.dbIdentity().isSupportsGetGeneratedKeys();
this.supportsSelectLastInsertedId = desc.supportsSelectLastInsertedId();
}
this.sqlNullId = genSql(true, tableName, false);
this.sqlDraftNullId = desc.isDraftable() ? genSql(true, draftTableName, true) : sqlNullId;
this.sqlNullId = sql(true, tableName, false);
this.sqlDraftNullId = desc.isDraftable() ? sql(true, draftTableName, true) : sqlNullId;
}
}
@@ -137,9 +140,20 @@ final class InsertMeta {
}
/**
* get the sql based whether the id value(s) are null.
* Return the sql for the given options.
*/
public String getSql(boolean withId, boolean publish) {
public String sql(boolean withId, boolean publish, InsertOptions insertOptions) {
if (insertOptions == null) {
return sql(withId, publish);
}
return options.sql(withId, insertOptions);
}
String sqlFor(boolean withId) {
return withId ? sqlWithId : sqlNullId;
}
private String sql(boolean withId, boolean publish) {
if (withId) {
return publish ? sqlWithId : sqlDraftWithId;
} else {
@@ -147,12 +161,18 @@ final class InsertMeta {
}
}
private String genSql(boolean nullId, String table, boolean draftTable) {
private String sql(boolean nullId, String table, boolean draftTable) {
GenerateDmlRequest request = new GenerateDmlRequest();
sql(request, nullId, table, draftTable);
return request.toString();
}
void sql(GenerateDmlRequest request, boolean nullId, String table, boolean draftTable) {
request.setInsertSetMode();
request.append("insert into ").append(table);
if (nullId && noColumnsForInsert(draftTable)) {
return request.append(defaultValues()).toString();
request.append(defaultValues());
return;
}
request.append(" (");
if (!nullId) {
@@ -172,7 +192,6 @@ final class InsertMeta {
request.append(") values (");
request.append(request.insertBindBuffer());
request.append(")");
return request.toString();
}
private String defaultValues() {
@@ -196,5 +215,4 @@ final class InsertMeta {
&& discriminator == null
&& (draftTable ? all.isEmpty() : allExcludeDraftOnly.isEmpty());
}
}
@@ -0,0 +1,14 @@
package io.ebeaninternal.server.persist.dml;
import io.ebean.InsertOptions;
/**
* Generator for insert SQL with options.
*/
interface InsertMetaOptions {
/**
* Generate the SQL for the given insert options.
*/
String sql(boolean withId, InsertOptions insertOptions);
}
@@ -0,0 +1,113 @@
package io.ebeaninternal.server.persist.dml;
import io.ebean.InsertOptions;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Postgres specific generation of insert on conflict.
*/
final class InsertMetaOptionsPostgres implements InsertMetaOptions {
private final InsertMeta meta;
private final BeanDescriptor<?> desc;
private final String baseTable;
private final Map<String, String> sqlCache = new ConcurrentHashMap<>();
InsertMetaOptionsPostgres(InsertMeta meta, BeanDescriptor<?> desc) {
this.meta = meta;
this.desc = desc;
this.baseTable = desc.baseTable();
}
@Override
public String sql(boolean withId, InsertOptions options) {
String key = withId + options.key();
return sqlCache.computeIfAbsent(key, k -> generate(withId, options));
}
private String generate(boolean withId, InsertOptions options) {
char type = options.key().charAt(0);
switch (type) {
case 'U':
return generate(withId, false, options);
case 'N':
return generate(withId, true, options);
default:
return meta.sqlFor(withId);
}
}
private String generate(boolean withId, boolean doNothing, InsertOptions options) {
GenerateDmlRequest request = new GenerateDmlRequest();
meta.sql(request, !withId, baseTable, false);
request.append(" on conflict ");
List<String> uniqueColumns = desc.uniqueProps().stream()
.flatMap(Arrays::stream)
.map(BeanProperty::dbColumn)
.collect(Collectors.toList());
String constraintName = options.constraint();
if (constraintName != null) {
request.append("on constraint ").append(constraintName);
} else {
request.append("(");
String cols = options.uniqueColumns();
if (cols != null) {
request.append(cols);
} else {
appendUniqueColumns(uniqueColumns, request);
}
request.append(")");
}
if (doNothing) {
request.append(" do nothing");
return request.toString();
}
request.append(" do update set ");
String updateSet = options.updateSet();
if (updateSet != null) {
request.append(updateSet);
} else {
setColumns(withId, request, uniqueColumns);
}
return request.toString();
}
private void setColumns(boolean withId, GenerateDmlRequest request, List<String> uniqueColumns) {
List<String> columns = request.columns();
columns.removeAll(uniqueColumns);
if (withId) {
BeanProperty idProperty = desc.idProperty();
if (idProperty != null && !idProperty.isEmbedded()) {
columns.remove(idProperty.dbColumn());
}
}
for (int i = 0; i < columns.size(); i++) {
if (i > 0) {
request.append(", ");
}
String col = columns.get(i);
request.append(col).append("=excluded.").append(col);
}
}
private static void appendUniqueColumns(List<String> uniqueColumns, GenerateDmlRequest request) {
if (uniqueColumns.isEmpty()) {
throw new IllegalStateException("Unable to identify unique columns for INSERT ON CONFLICT - Add mapping like @Column(unique=true) or @Index(unique=true)");
}
for (int i = 0; i < uniqueColumns.size(); i++) {
if (i > 0) {
request.append(", ");
}
request.append(uniqueColumns.get(i));
}
}
}
@@ -0,0 +1,29 @@
package io.ebeaninternal.server.persist.dml;
import io.ebean.InsertOptions;
import io.ebean.annotation.Platform;
import io.ebeaninternal.server.deploy.BeanDescriptor;
final class InsertMetaPlatform {
private static final NotSupported NOT_SUPPORTED = new NotSupported();
static InsertMetaOptions create(Platform platform, BeanDescriptor<?> desc, InsertMeta meta) {
switch (platform.base()) {
case POSTGRES:
case YUGABYTE:
case COCKROACH:
return new InsertMetaOptionsPostgres(meta, desc);
default:
return NOT_SUPPORTED;
}
}
static final class NotSupported implements InsertMetaOptions {
@Override
public String sql(boolean withId, InsertOptions insertOptions) {
throw new UnsupportedOperationException("InsertOptions not supported on this database platform");
}
}
}
@@ -453,21 +453,41 @@ public class TDSpiServer implements SpiServer {
}
@Override
public void insert(Object bean, InsertOptions insertOptions) {
}
@Override
public void insert(Object bean, Transaction transaction) {
}
@Override
public void insert(Object bean, InsertOptions insertOptions, Transaction transaction) {
}
@Override
public void insertAll(Collection<?> beans) {
}
@Override
public void insertAll(Collection<?> beans, InsertOptions options) {
}
@Override
public void insertAll(Collection<?> beans, Transaction transaction) {
}
@Override
public void insertAll(Collection<?> beans, InsertOptions options, Transaction transaction) {
}
@Override
public int execute(SqlUpdate updSql, Transaction transaction) {
return 0;
@@ -0,0 +1,297 @@
package org.tests.insert;
import io.ebean.DB;
import io.ebean.Database;
import io.ebean.InsertOptions;
import io.ebean.Transaction;
import io.ebean.annotation.Platform;
import io.ebean.test.LoggedSql;
import io.ebean.xtest.BaseTestCase;
import io.ebean.xtest.ForPlatform;
import org.junit.jupiter.api.Test;
import org.tests.update.EPersonOnline;
import java.util.List;
import static io.ebean.InsertOptions.ON_CONFLICT_NOTHING;
import static io.ebean.InsertOptions.ON_CONFLICT_UPDATE;
import static org.assertj.core.api.Assertions.assertThat;
class TestInsertOnConflict extends BaseTestCase {
InsertOptions onConflictDoUpdateAndGetGeneratedKeys = InsertOptions.builder()
.onConflictUpdate()
.getGeneratedKeys(true)
.build();
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void insertOnConflictUpdateExplicitTransaction() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a@b.com");
try (Transaction txn = DB.createTransaction()) {
db.insert(bean, ON_CONFLICT_UPDATE, txn);
txn.commit();
}
assertThat(bean.getId()).isNotNull();
var bean2 = newBean("a@b.com");
bean2.setOnlineStatus(false);
try (Transaction txn = DB.createTransaction()) {
db.insert(bean2, ON_CONFLICT_UPDATE, txn);
txn.commit();
}
assertThat(bean2.getId()).isEqualTo(bean.getId());
var sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
List<EPersonOnline> list = db.find(EPersonOnline.class).findList();
assertThat(list).hasSize(1);
assertThat(list.get(0).getWhenUpdated()).isEqualTo(bean2.getWhenUpdated());
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void insertOnConflictUpdate_when_noIdValue() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a@b.com");
db.insert(bean, onConflictDoUpdateAndGetGeneratedKeys);
assertThat(bean.getId()).isNotNull();
var bean2 = newBean("a@b.com");
bean2.setOnlineStatus(false);
db.insert(bean2, onConflictDoUpdateAndGetGeneratedKeys);
assertThat(bean2.getId()).isEqualTo(bean.getId());
var sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
List<EPersonOnline> list = db.find(EPersonOnline.class).findList();
assertThat(list).hasSize(1);
assertThat(list.get(0).getWhenUpdated()).isEqualTo(bean2.getWhenUpdated());
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void insertOnConflictUpdate_when_idValueSupplied() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a@b.com");
bean.setId(40_042L);
db.insert(bean, ON_CONFLICT_UPDATE);
assertThat(bean.getId()).isNotNull();
var bean2 = newBean("a@b.com");
bean2.setId(40_043L); // not expected but can be different
bean2.setOnlineStatus(false);
db.insert(bean2, onConflictDoUpdateAndGetGeneratedKeys);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("insert into e_person_online (id, email, online_status, when_updated) values (?,?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains("insert into e_person_online (id, email, online_status, when_updated) values (?,?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
List<EPersonOnline> list = db.find(EPersonOnline.class).findList();
assertThat(list).hasSize(1);
assertThat(list.get(0).getWhenUpdated()).isEqualTo(bean2.getWhenUpdated());
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void insertAll_onConflictUpdate_when_noIdValue() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a1@b.com");
var bean2 = newBean("a2@b.com");
var bean3 = newBean("a3@b.com");
db.insertAll(List.of(bean, bean2, bean3), ON_CONFLICT_UPDATE);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(5);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains(" -- bind");
assertThat(sql.get(2)).contains(" -- bind");
assertThat(sql.get(3)).contains(" -- bind");
assertThat(sql.get(4)).contains(" -- executeBatch()");
var bean4 = newBean("a1@b.com");
var bean5 = newBean("a5@b.com");
db.insertAll(List.of(bean4, bean5), ON_CONFLICT_UPDATE);
List<EPersonOnline> list = db.find(EPersonOnline.class).orderBy("id").findList();
assertThat(list).hasSize(4);
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void insertAll_onConflictUpdate_explicitTransaction() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
try (Transaction txn = DB.createTransaction()) {
txn.setBatchSize(3);
var bean = newBean("a1@b.com");
var bean2 = newBean("a2@b.com");
var bean3 = newBean("a3@b.com");
var bean4 = newBean("a4@b.com");
db.insertAll(List.of(bean, bean2, bean3, bean4), ON_CONFLICT_UPDATE, txn);
txn.commit();
}
var sql = LoggedSql.stop();
assertThat(sql).hasSize(8);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains(" -- bind");
assertThat(sql.get(2)).contains(" -- bind");
assertThat(sql.get(3)).contains(" -- bind");
assertThat(sql.get(4)).contains(" -- executeBatch()");
assertThat(sql.get(5)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(6)).contains(" -- bind");
assertThat(sql.get(7)).contains(" -- executeBatch()");
List<EPersonOnline> list = db.find(EPersonOnline.class).orderBy("id").findList();
assertThat(list).hasSize(4);
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void explicitConstraint() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a1@b.com");
var bean2 = newBean("a2@b.com");
var bean3 = newBean("a3@b.com");
var ON_CONFLICT_ = InsertOptions.builder()
.onConflictUpdate()
.constraint("uq_e_person_online_email")
.build();
db.insertAll(List.of(bean, bean2, bean3), ON_CONFLICT_);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(5);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict on constraint uq_e_person_online_email do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains(" -- bind");
assertThat(sql.get(2)).contains(" -- bind");
assertThat(sql.get(3)).contains(" -- bind");
assertThat(sql.get(4)).contains(" -- executeBatch()");
List<EPersonOnline> list = db.find(EPersonOnline.class).orderBy("id").findList();
assertThat(list).hasSize(3);
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void explicitUniqueColumns() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a1@b.com");
var bean2 = newBean("a2@b.com");
var bean3 = newBean("a3@b.com");
var ON_CONFLICT_ = InsertOptions.builder()
.onConflictUpdate()
.uniqueColumns(" email ")
.build();
db.insertAll(List.of(bean, bean2, bean3), ON_CONFLICT_);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(5);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict ( email ) do update set online_status=excluded.online_status, when_updated=excluded.when_updated");
assertThat(sql.get(1)).contains(" -- bind");
assertThat(sql.get(2)).contains(" -- bind");
assertThat(sql.get(3)).contains(" -- bind");
assertThat(sql.get(4)).contains(" -- executeBatch()");
List<EPersonOnline> list = db.find(EPersonOnline.class).orderBy("id").findList();
assertThat(list).hasSize(3);
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void explicitUpdateSet() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a1@b.com");
var bean2 = newBean("a2@b.com");
var ON_CONFLICT_ = InsertOptions.builder()
.onConflictUpdate()
.updateSet("when_updated=excluded.when_updated, online_status=true")
.build();
db.insertAll(List.of(bean, bean2), ON_CONFLICT_);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(4);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do update set when_updated=excluded.when_updated, online_status=true");
assertThat(sql.get(1)).contains(" -- bind");
assertThat(sql.get(2)).contains(" -- bind");
assertThat(sql.get(3)).contains(" -- executeBatch()");
List<EPersonOnline> list = db.find(EPersonOnline.class).orderBy("id").findList();
assertThat(list).hasSize(2);
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void insertOnConflictNothing_when_noIdValue() {
Database db = DB.getDefault();
db.truncate(EPersonOnline.class);
LoggedSql.start();
var bean = newBean("a@b.com");
db.insert(bean, ON_CONFLICT_NOTHING);
assertThat(bean.getId()).isNotNull();
var bean2 = newBean("a@b.com");
bean2.setOnlineStatus(false);
db.insert(bean2, ON_CONFLICT_NOTHING);
assertThat(bean2.getId()).isNull();
var sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do nothing");
assertThat(sql.get(1)).contains("insert into e_person_online (email, online_status, when_updated) values (?,?,?) on conflict (email) do nothing");
List<EPersonOnline> list = db.find(EPersonOnline.class).findList();
assertThat(list).hasSize(1);
assertThat(list.get(0).getWhenUpdated()).isEqualTo(bean.getWhenUpdated());
}
private static EPersonOnline newBean(String email) {
EPersonOnline bean = new EPersonOnline();
bean.setEmail(email);
bean.setOnlineStatus(true);
return bean;
}
}