diff --git a/ebean-api/src/main/java/io/ebean/DInsertOptionsBuilder.java b/ebean-api/src/main/java/io/ebean/DInsertOptionsBuilder.java
new file mode 100644
index 0000000000..ffec19f91f
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/DInsertOptionsBuilder.java
@@ -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;
+ }
+ }
+}
diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java
index a7427a9dc6..bc3f0139b0 100644
--- a/ebean-api/src/main/java/io/ebean/Database.java
+++ b/ebean-api/src/main/java/io/ebean/Database.java
@@ -1227,22 +1227,53 @@ static DatabaseBuilder builder() {
*/
void insert(Object bean);
+ /**
+ * Insert the bean with options (ON CONFLICT DO UPDATE | DO NOTHING).
+ *
+ * Currently, this is limited to use with Postgres only,
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * Currently, this is limited to use with Postgres only,
+ */
+ void insertAll(Collection> beans, InsertOptions options, Transaction transaction);
+
/**
* Execute explicitly passing a transaction.
*/
diff --git a/ebean-api/src/main/java/io/ebean/InsertOptions.java b/ebean-api/src/main/java/io/ebean/InsertOptions.java
new file mode 100644
index 0000000000..ec0c5ab3af
--- /dev/null
+++ b/ebean-api/src/main/java/io/ebean/InsertOptions.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * When this is used then unique columns will not be used.
+ */
+ Builder constraint(String constraint);
+
+ /**
+ * Specify the unique columns for the conflict target.
+ *
+ * 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.
+ *
+ * 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();
+
+ }
+}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
index 8b2dc60e31..34fc985de6 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
@@ -1651,41 +1651,50 @@ public void updateAll(@Nullable Collection> beans, @Nullable Transaction trans
}, 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);
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
index 2d3a30bb5b..57ec8a2a0b 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
@@ -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 extends PersistRequest implements BeanP
* Many-to-many intersection table changes that are held for later batch processing.
*/
private List saveMany;
+ private InsertOptions insertOptions;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
@@ -1408,4 +1410,12 @@ public void setSaveRecurse() {
private void setGeneratedId() {
beanDescriptor.setGeneratedId(entityBean, transaction);
}
+
+ public void setInsertOptions(InsertOptions insertOptions) {
+ this.insertOptions = insertOptions;
+ }
+
+ public InsertOptions insertOptions() {
+ return insertOptions;
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/Persister.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/Persister.java
index 46927f1122..4728006da2 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/Persister.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/Persister.java
@@ -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.
*
* This will also cascade delete one level of children.
- *
*/
int delete(Class> beanType, Object id, Transaction transaction, boolean permanent);
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java
index f2c9805dcd..c87d9e65b3 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java
@@ -411,7 +411,7 @@ public void save(EntityBean bean, Transaction t) {
if (bean._ebean_getIntercept().isUpdate()) {
update(bean, t);
} else {
- insert(bean, t);
+ insert(bean, null, t);
}
}
@@ -419,12 +419,15 @@ public void save(EntityBean bean, Transaction t) {
* 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);
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/GenerateDmlRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/GenerateDmlRequest.java
index bf717d9687..9d5828dfd9 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/GenerateDmlRequest.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/GenerateDmlRequest.java
@@ -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 columns = new ArrayList<>();
private StringBuilder insertBindBuffer;
private String prefix;
private String prefix2;
@@ -26,7 +30,7 @@ public void appendColumn(String column, String bind) {
++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 @@ void setUpdateSetMode() {
public boolean isUpdate() {
return insertMode == 0;
}
+
+ public List columns() {
+ return columns;
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertHandler.java
index d734d1a51b..05f12fadbb 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertHandler.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertHandler.java
@@ -69,7 +69,7 @@ public void bind() throws SQLException {
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 @@ private void setGeneratedKey(ResultSet rset) throws SQLException {
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?");
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMeta.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMeta.java
index a4d9cd1f73..f55c074220 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMeta.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMeta.java
@@ -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 @@ public void bind(DmlHandler request, EntityBean bean, boolean withId, boolean pu
}
/**
- * 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 @@ public String getSql(boolean withId, boolean publish) {
}
}
- 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 @@ private String genSql(boolean nullId, String table, boolean draftTable) {
request.append(") values (");
request.append(request.insertBindBuffer());
request.append(")");
- return request.toString();
}
private String defaultValues() {
@@ -196,5 +215,4 @@ private boolean noColumnsForInsert(boolean draftTable) {
&& discriminator == null
&& (draftTable ? all.isEmpty() : allExcludeDraftOnly.isEmpty());
}
-
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaOptions.java
new file mode 100644
index 0000000000..48476fffac
--- /dev/null
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaOptions.java
@@ -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);
+}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaOptionsPostgres.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaOptionsPostgres.java
new file mode 100644
index 0000000000..484a4ba871
--- /dev/null
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaOptionsPostgres.java
@@ -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 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 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 uniqueColumns) {
+ List 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 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));
+ }
+ }
+}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaPlatform.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaPlatform.java
new file mode 100644
index 0000000000..fd82f0772f
--- /dev/null
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/InsertMetaPlatform.java
@@ -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");
+ }
+ }
+
+}
diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java
index 6efedcacbb..fefece8476 100644
--- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java
+++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java
@@ -453,21 +453,41 @@ public void insert(Object bean) {
}
+ @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;
diff --git a/ebean-test/src/test/java/org/tests/insert/TestInsertOnConflict.java b/ebean-test/src/test/java/org/tests/insert/TestInsertOnConflict.java
new file mode 100644
index 0000000000..4490a87ef0
--- /dev/null
+++ b/ebean-test/src/test/java/org/tests/insert/TestInsertOnConflict.java
@@ -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 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 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 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 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 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 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 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 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 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;
+ }
+}