mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
Merge pull request #3324 from ebean-orm/feature/postgres_insertOnConflict
Add support for Postgres INSERT ON CONFLICT update | nothing
This commit is contained in:
@@ -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);
|
||||
|
||||
+9
-1
@@ -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);
|
||||
}
|
||||
+113
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user