diff --git a/composites/ebean-clickhouse/pom.xml b/composites/ebean-clickhouse/pom.xml index ead23ca35..e459335ff 100644 --- a/composites/ebean-clickhouse/pom.xml +++ b/composites/ebean-clickhouse/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-clickhouse - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-cockroach/pom.xml b/composites/ebean-cockroach/pom.xml index b552318d7..3d312d678 100644 --- a/composites/ebean-cockroach/pom.xml +++ b/composites/ebean-cockroach/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-postgres - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-db2/pom.xml b/composites/ebean-db2/pom.xml index 5a2448995..94142c000 100644 --- a/composites/ebean-db2/pom.xml +++ b/composites/ebean-db2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-db2 - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-h2/pom.xml b/composites/ebean-h2/pom.xml index 4202f01cb..20d17eb4d 100644 --- a/composites/ebean-h2/pom.xml +++ b/composites/ebean-h2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-h2 - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-hana/pom.xml b/composites/ebean-hana/pom.xml index 9a1d3d983..008b6fdf3 100644 --- a/composites/ebean-hana/pom.xml +++ b/composites/ebean-hana/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-hana - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-mariadb/pom.xml b/composites/ebean-mariadb/pom.xml index 91c643d58..83b2911ba 100644 --- a/composites/ebean-mariadb/pom.xml +++ b/composites/ebean-mariadb/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-mariadb - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-mysql/pom.xml b/composites/ebean-mysql/pom.xml index 1fa4b5226..b383eb3fe 100644 --- a/composites/ebean-mysql/pom.xml +++ b/composites/ebean-mysql/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-mysql - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-nuodb/pom.xml b/composites/ebean-nuodb/pom.xml index d4664d6e0..343dab817 100644 --- a/composites/ebean-nuodb/pom.xml +++ b/composites/ebean-nuodb/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-nuodb - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-oracle/pom.xml b/composites/ebean-oracle/pom.xml index 079834554..9a31419e1 100644 --- a/composites/ebean-oracle/pom.xml +++ b/composites/ebean-oracle/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-oracle - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-postgis/pom.xml b/composites/ebean-postgis/pom.xml index 432e079e7..6a6c8963d 100644 --- a/composites/ebean-postgis/pom.xml +++ b/composites/ebean-postgis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -22,13 +22,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -47,19 +47,19 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-postgres - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-postgis-types - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-postgres/pom.xml b/composites/ebean-postgres/pom.xml index 6ba88c756..acb701c27 100644 --- a/composites/ebean-postgres/pom.xml +++ b/composites/ebean-postgres/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-postgres - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-sqlite/pom.xml b/composites/ebean-sqlite/pom.xml index 16bce0e77..48b30610d 100644 --- a/composites/ebean-sqlite/pom.xml +++ b/composites/ebean-sqlite/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-sqlite - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-sqlserver/pom.xml b/composites/ebean-sqlserver/pom.xml index 348bcefc0..911e6857e 100644 --- a/composites/ebean-sqlserver/pom.xml +++ b/composites/ebean-sqlserver/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-sqlserver - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean-yugabyte/pom.xml b/composites/ebean-yugabyte/pom.xml index fb1cde0b0..0e62611ec 100644 --- a/composites/ebean-yugabyte/pom.xml +++ b/composites/ebean-yugabyte/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-postgres - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/ebean/pom.xml b/composites/ebean/pom.xml index a382b901d..1a7ac9f1c 100644 --- a/composites/ebean/pom.xml +++ b/composites/ebean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta @@ -41,7 +41,7 @@ io.ebean ebean-jackson-mapper - 13.25.2-jakarta + 13.26.1-jakarta @@ -60,13 +60,13 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-platform-all - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/composites/pom.xml b/composites/pom.xml index 535238050..4524de54c 100644 --- a/composites/pom.xml +++ b/composites/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta composites diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index a0c8caba7..1c8514c4d 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean api @@ -39,7 +39,7 @@ io.avaje avaje-config - 3.9 + 3.10 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 000000000..ffec19f91 --- /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 a7427a9dc..bc3f0139b 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 @@ public interface Database { */ 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 000000000..ec0c5ab3a --- /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-api/src/main/java/io/ebean/Query.java b/ebean-api/src/main/java/io/ebean/Query.java index 28cce0506..ef941aa1f 100644 --- a/ebean-api/src/main/java/io/ebean/Query.java +++ b/ebean-api/src/main/java/io/ebean/Query.java @@ -970,6 +970,25 @@ public interface Query extends CancelableQuery { @Nullable A findSingleAttribute(); + /** + * Execute the query returning a single optional attribute value. + *

+ *

Example

+ *
{@code
+   *
+   *  Optional maybeName =
+   *    new QCustomer()
+   *      .select(name)
+   *      .id.eq(42)
+   *      .status.eq(NEW)
+   *      .findSingleAttributeOrEmpty();
+   *
+   * }
+ * + * @return an optional value for the selected property + */ +
Optional findSingleAttributeOrEmpty(); + /** * Return true if this is countDistinct query. */ diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index 5fca7b1b9..ec29f2e99 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean bom @@ -89,25 +89,25 @@ io.ebean ebean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core-type - 13.25.2-jakarta + 13.26.1-jakarta @@ -125,13 +125,13 @@ io.ebean ebean-jackson-mapper - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-ddl-generator - 13.25.2-jakarta + 13.26.1-jakarta @@ -155,37 +155,37 @@ io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta io.ebean querybean-generator - 13.25.2-jakarta + 13.26.1-jakarta io.ebean kotlin-querybean-generator - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-test - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-redis - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-spring-txn - 13.25.2-jakarta + 13.26.1-jakarta @@ -193,79 +193,79 @@ io.ebean ebean-clickhouse - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-db2 - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-h2 - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-hana - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-mariadb - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-mysql - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-nuodb - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-oracle - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-postgres - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-postgis - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-postgis-types - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-sqlite - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-sqlserver - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 57367b50b..4cc1904cf 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index 0834be897..3fa2644bd 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean-core @@ -22,7 +22,7 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta @@ -46,7 +46,7 @@ io.ebean ebean-core-type - 13.25.2-jakarta + 13.26.1-jakarta @@ -159,21 +159,21 @@ io.ebean ebean-platform-h2 - 13.25.2-jakarta + 13.26.1-jakarta test io.ebean ebean-platform-postgres - 13.25.2-jakarta + 13.26.1-jakarta test io.ebean ebean-platform-sqlserver - 13.25.2-jakarta + 13.26.1-jakarta test diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java index 89f2173eb..5669dcf57 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -654,6 +654,16 @@ public interface SpiQuery extends Query, SpiQueryFetch, TxnProfileEventCod */ ObjectGraphNode parentNode(); + /** + * Set that this is a future query that will execute in the background. + */ + void usingFuture(); + + /** + * Return true if this is a future query. + */ + boolean isUsingFuture(); + /** * Return false when this is a lazy load or refresh query for a bean. *

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 8b2dc60e3..38e8955e0 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 @@ -1273,6 +1273,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { @Override public FutureRowCount findFutureCount(SpiQuery query) { SpiQuery copy = query.copy(); + copy.usingFuture(); boolean createdTransaction = false; SpiTransaction transaction = query.transaction(); if (transaction == null) { @@ -1291,6 +1292,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { @Override public FutureIds findFutureIds(SpiQuery query) { SpiQuery copy = query.copy(); + copy.usingFuture(); boolean createdTransaction = false; SpiTransaction transaction = query.transaction(); if (transaction == null) { @@ -1309,6 +1311,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { @Override public FutureList findFutureList(SpiQuery query) { SpiQuery spiQuery = query.copy(); + spiQuery.usingFuture(); // FutureList query always run in it's own persistence content spiQuery.setPersistenceContext(new DefaultPersistenceContext()); if (!spiQuery.isDisableReadAudit()) { @@ -1651,41 +1654,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); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index 3dfca446b..1b5a1952d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -35,7 +35,6 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery private final OrmQueryEngine queryEngine; private final SpiQuery query; private final BeanFindController finder; - private final Boolean readOnly; private LoadContext loadContext; private PersistenceContext persistenceContext; private HashQuery cacheKey; @@ -52,7 +51,6 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery this.finder = beanDescriptor.beanFinder(); this.queryEngine = queryEngine; this.query = query; - this.readOnly = query.isReadOnly(); this.persistenceContext = query.persistenceContext(); } @@ -135,13 +133,6 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery return loadContext.secondaryQueriesMinBatchSize(); } - /** - * Return the Normal, sharedInstance, ReadOnly state of this query. - */ - public Boolean isReadOnly() { - return readOnly; - } - /** * Return the BeanDescriptor for the associated bean. */ 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 2d3a30bb5..f13227fc5 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) { @@ -380,6 +382,14 @@ public final class PersistRequestBean extends PersistRequest implements BeanP return intercept.dirtyPropertyNames(); } + public boolean isChangedProperty(int propertyIndex) { + if (dirtyProperties == null) { + return intercept.isChangedProperty(propertyIndex); + } else { + return dirtyProperties[propertyIndex]; + } + } + /** * Return the dirty properties on this request. */ @@ -865,8 +875,8 @@ public final class PersistRequestBean extends PersistRequest implements BeanP } setNotifyCache(); boolean isChangeLog = beanDescriptor.isChangeLog(); - if (type == Type.UPDATE && (isChangeLog || notifyCache || docStoreMode == DocStoreMode.UPDATE)) { - // get the dirty properties for update notification to the doc store + if (type == Type.UPDATE) { + // get the dirty properties for notify cache & orphanRemoval of vanilla collection detection dirtyProperties = intercept.dirtyProperties(); } if (isChangeLog) { @@ -1408,4 +1418,12 @@ public final class PersistRequestBean extends PersistRequest implements BeanP 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 46927f112..4728006da 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 f2c9805dc..c87d9e65b 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 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); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index e3ccabbff..e82c16961 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -328,7 +328,7 @@ final class SaveManyBeans extends SaveManyBase { } private boolean isChangedProperty() { - return parentBean._ebean_getIntercept().isChangedProperty(many.propertyIndex()); + return request.isChangedProperty(many.propertyIndex()); } private void removeAssocManyOrphans() { 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 bf717d968..9d5828dfd 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 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 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 d734d1a51..05f12fadb 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 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?"); } } 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 a4d9cd1f7..f55c07422 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 @@ 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()); } - } 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 000000000..48476fffa --- /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 000000000..484a4ba87 --- /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 000000000..fd82f0772 --- /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-core/src/main/java/io/ebeaninternal/server/query/CQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java index d3804e141..85d6319e9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java @@ -186,7 +186,7 @@ public final class CQuery implements DbReadContext, CancelableQuery, SpiProfi this.queryMode = query.mode(); this.loadContextBean = queryMode.isLoadContextBean() || query.getForUpdateLockType() != null; this.lazyLoadManyProperty = query.lazyLoadMany(); - this.readOnly = request.isReadOnly(); + this.readOnly = query.isReadOnly(); this.disableLazyLoading = query.isDisableLazyLoading(); this.objectGraphNode = query.parentNode(); this.profilingListener = query.profilingListener(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java index ce8f71a53..c4a3b965b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java @@ -305,6 +305,11 @@ final class DefaultFetchGroupQuery implements SpiFetchGroupQuery, SpiQuery throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); } + @Override + public
Optional findSingleAttributeOrEmpty() { + throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); + } + @Override public boolean isCountDistinct() { throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 48eb512a8..367995086 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -51,6 +51,10 @@ public final class DefaultOrmQueryEngine implements OrmQueryEngine { * Flushes the jdbc batch by default unless explicitly turned off on the transaction. */ private void flushJdbcBatchOnQuery(OrmQueryRequest request) { + if (request.query().isUsingFuture()) { + // future queries never invoke a flush + return; + } SpiTransaction t = request.transaction(); if (t.isFlushOnQuery()) { // before we perform a query, we need to flush any diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 9a583fdd8..f877be126 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -58,6 +58,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { private Type type; private String label; private Mode mode = Mode.NORMAL; + private boolean usingFuture; private Object tenantId; /** * Holds query in structured form. @@ -971,6 +972,16 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { this.mode = mode; } + @Override + public final void usingFuture() { + this.usingFuture = true; + } + + @Override + public final boolean isUsingFuture() { + return usingFuture; + } + @Override public final boolean isUsageProfiling() { return usageProfiling; @@ -1161,7 +1172,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { // add the rawSql statement - if any if (orderByIsEmpty()) { if (rawSql != null && rawSql.getSql() != null) { - order(rawSql.getSql().getOrderBy()); + orderBy(rawSql.getSql().getOrderBy()); } } if (checkPagingOrderBy()) { @@ -1517,7 +1528,6 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { } @Override - @SuppressWarnings("unchecked") public final List findSingleAttributeList() { return server.findSingleAttributeList(this); } @@ -1533,6 +1543,11 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { return !list.isEmpty() ? list.get(0) : null; } + @Override + public final Optional findSingleAttributeOrEmpty() { + return Optional.ofNullable(findSingleAttribute()); + } + @Override public final T findOne() { return server.findOne(this); diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index 2fbcb1ef8..0f0bdaa30 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean ddl generation @@ -28,14 +28,14 @@ io.ebean ebean-core-type - 13.25.2-jakarta + 13.26.1-jakarta provided io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta provided @@ -65,7 +65,7 @@ io.ebean ebean-platform-all - 13.25.2-jakarta + 13.26.1-jakarta test diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index 1e2373eee..736987ba9 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta 4.0.0 @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 13.25.2-jakarta + 13.26.1-jakarta provided diff --git a/ebean-postgis-types/pom.xml b/ebean-postgis-types/pom.xml index 1770101dd..2ea91e5b5 100644 --- a/ebean-postgis-types/pom.xml +++ b/ebean-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta provided @@ -62,7 +62,7 @@ io.ebean ebean-test - 13.25.2-jakarta + 13.26.1-jakarta test diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index 80581f1dc..91350fadc 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean querybean @@ -17,7 +17,7 @@ io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta provided @@ -63,14 +63,14 @@ io.ebean ebean-ddl-generator - 13.25.2-jakarta + 13.26.1-jakarta test io.ebean ebean-test - 13.25.2-jakarta + 13.26.1-jakarta test @@ -84,7 +84,7 @@ io.ebean querybean-generator - 13.25.2-jakarta + 13.26.1-jakarta provided diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java index f94fb267b..9a5c21f6c 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java @@ -32,7 +32,7 @@ public abstract class PBaseString extends PBaseComparable { * @param value the equal to bind value * @return the root query bean instance */ - @Deprecated + @Deprecated(forRemoval = true) public final R equalToType(T value) { expr().eq(_name, value); return _root; @@ -46,7 +46,7 @@ public abstract class PBaseString extends PBaseComparable { * @param value the equal to bind value * @return the root query bean instance */ - @Deprecated + @Deprecated(forRemoval = true) public final R notEqualToType(T value) { expr().ne(_name, value); return _root; diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java index 01f007553..48125150e 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -745,7 +745,7 @@ public abstract class TQRootBean { * Add EXISTS sub-query predicate. */ public R exists(Query subQuery) { - query.where().exists(subQuery); + peekExprList().exists(subQuery); return root; } @@ -753,7 +753,7 @@ public abstract class TQRootBean { * Add NOT EXISTS sub-query predicate. */ public R notExists(Query subQuery) { - query.where().notExists(subQuery); + peekExprList().notExists(subQuery); return root; } @@ -764,7 +764,7 @@ public abstract class TQRootBean { * @param bindValues Optional bind values if the SubQuery uses {@code ? } bind values. */ public final R exists(String sqlSubQuery, Object... bindValues) { - query().where().exists(sqlSubQuery, bindValues); + peekExprList().exists(sqlSubQuery, bindValues); return root; } @@ -775,7 +775,7 @@ public abstract class TQRootBean { * @param bindValues Optional bind values if the SubQuery uses {@code ? } bind values. */ public final R notExists(String sqlSubQuery, Object... bindValues) { - query().where().notExists(sqlSubQuery, bindValues); + peekExprList().notExists(sqlSubQuery, bindValues); return root; } @@ -1275,7 +1275,7 @@ public abstract class TQRootBean { /** * @deprecated migrate to {@link #orderBy()}. */ - @Deprecated(since = "13.19") + @Deprecated(since = "13.19", forRemoval = true) public R order() { return root; } @@ -1295,7 +1295,7 @@ public abstract class TQRootBean { /** * @deprecated migrate to {@link #orderBy(String)} */ - @Deprecated(since = "13.19") + @Deprecated(since = "13.19", forRemoval = true) public R order(String orderByClause) { return orderBy(orderByClause); } @@ -1867,6 +1867,28 @@ public abstract class TQRootBean { return query.findSingleAttribute(); } + /** + * Execute the query returning a single optional attribute value. + *

+ *

Example

+ *
{@code
+   *
+   *  Optional maybeName =
+   *    new QCustomer()
+   *      .select(name)
+   *      .id.eq(42)
+   *      .status.eq(NEW)
+   *      .findSingleAttributeOrEmpty();
+   *
+   * }
+ * + * @return an optional value for the selected property + */ + @Nullable + public
Optional findSingleAttributeOrEmpty() { + return query.findSingleAttributeOrEmpty(); + } + /** * Execute the query processing the beans one at a time. *

diff --git a/ebean-querybean/src/test/java/org/example/domain/Contact.java b/ebean-querybean/src/test/java/org/example/domain/Contact.java index 7d2554f57..505cb556a 100644 --- a/ebean-querybean/src/test/java/org/example/domain/Contact.java +++ b/ebean-querybean/src/test/java/org/example/domain/Contact.java @@ -1,8 +1,9 @@ package org.example.domain; import io.ebean.annotation.DbArray; - import jakarta.persistence.*; +import org.example.domain.finder.ContactFinder; + import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.time.ZonedDateTime; @@ -17,6 +18,11 @@ import java.util.Map; @Table(name = "be_contact") public class Contact extends BaseModel { + /** + * Convenience Finder for 'active record' style. + */ + public static final ContactFinder find = new ContactFinder(); + @DbArray List<@Size(max=20) String> phoneNumbers = new ArrayList<>(); diff --git a/ebean-querybean/src/test/java/org/example/domain/finder/ContactFinder.java b/ebean-querybean/src/test/java/org/example/domain/finder/ContactFinder.java new file mode 100644 index 000000000..42f0daccb --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/finder/ContactFinder.java @@ -0,0 +1,18 @@ +package org.example.domain.finder; + +import io.ebean.Finder; +import org.example.domain.Contact; +import org.example.domain.query.QContact; + +/** + */ +public class ContactFinder extends Finder { + + public ContactFinder() { + super(Contact.class); + } + + public QContact typed() { + return new QContact(); + } +} diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java index b930c1509..7862e0dcd 100644 --- a/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java @@ -1,6 +1,8 @@ package org.querytest; +import org.example.domain.Contact; import org.example.domain.Customer; +import org.example.domain.query.QCustomer; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -9,6 +11,8 @@ import java.time.ZoneId; import java.util.Date; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; + public class QCustomerAndOrTest { @Test @@ -55,6 +59,30 @@ public class QCustomerAndOrTest { } + @Test + public void testOrWithExists() { + QCustomer query = Customer.find.typed() + .alias("_cust") + .or() + .name.eq("Superman") + .exists(Contact.find.typed() + .alias("contact") + .firstName.eq("Superman") + .raw("contact.customer_id = _cust.id") + .query() + ) + .endOr() + .select(QCustomer.alias().id); + + query.findList(); + + assertThat(query.getGeneratedSql()).isEqualTo( + "select _cust.id from be_customer _cust where (" + + "_cust.name = ? or exists (select 1 from be_contact contact where " + + "contact.first_name = ? and contact.customer_id = _cust.id))" + ); + } + private Date fiveDaysAgo() { LocalDateTime fiveDaysAgo = LocalDate.now().atStartOfDay().minusDays(5); return new Date(fiveDaysAgo.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java index d2a8bedff..5c25c91e9 100644 --- a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java @@ -63,6 +63,25 @@ public class QCustomerTest { } + @Test + public void copy() { + var origin = new QCustomer() + .setDistinct(true) + .status.equalTo(Customer.Status.BAD); + + var copy1 = origin.copy().name.isNotNull(); + var q1 = copy1.query(); + copy1.findList(); + + assertThat(q1.getGeneratedSql()).contains("from be_customer t0 where t0.status = ? and t0.name is not null"); + + var copy2 = origin.copy().version.ge(1L); + var q2 = copy2.query(); + copy2.findList(); + + assertThat(q2.getGeneratedSql()).contains("from be_customer t0 where t0.status = ? and t0.version >= ?"); + } + @Test public void findSingleAttribute() { @@ -1050,6 +1069,32 @@ public class QCustomerTest { assertThat(maxDate).isNotNull(); } + @Test + public void findSingleAttributeOrEmpty() { + + Customer cust = new Customer(); + cust.setName("MaybeIExist yeah"); + cust.setStatus(Customer.Status.GOOD); + cust.setRegistered(new Date()); + cust.save(); + + Optional customerName = new QCustomer() + .select(name) + .status.eq(Customer.Status.GOOD) + .name.startsWith("MaybeIExist") + .findSingleAttributeOrEmpty(); + + assertThat(customerName).isPresent(); + + Optional customerName2 = new QCustomer() + .select(name) + .status.eq(Customer.Status.GOOD) + .name.eq("NahIDoNotExist") + .findSingleAttributeOrEmpty(); + + assertThat(customerName2).isEmpty(); + } + @Test public void testFetchByScalarValue() { diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index cef3e1f38..0c5f5def6 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean-redis @@ -22,28 +22,28 @@ io.ebean ebean-api - 13.25.2-jakarta + 13.26.1-jakarta provided io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta provided io.ebean ebean-querybean - 13.25.2-jakarta + 13.26.1-jakarta test io.ebean ebean-test - 13.25.2-jakarta + 13.26.1-jakarta test @@ -57,7 +57,7 @@ io.ebean querybean-generator - 13.25.2-jakarta + 13.26.1-jakarta provided diff --git a/ebean-spring-txn/pom.xml b/ebean-spring-txn/pom.xml index 55055649d..f9e9e5eec 100644 --- a/ebean-spring-txn/pom.xml +++ b/ebean-spring-txn/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean-spring-txn @@ -28,7 +28,7 @@ io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta provided @@ -77,7 +77,7 @@ io.ebean ebean-test - 13.25.2-jakarta + 13.26.1-jakarta test diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index 37e0ac0a8..cc8f2e3ab 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.25.2-jakarta + 13.26.1-jakarta ebean test @@ -33,20 +33,20 @@ io.ebean ebean-platform-h2 - 13.25.2-jakarta + 13.26.1-jakarta io.ebean ebean-core - 13.25.2-jakarta + 13.26.1-jakarta provided io.ebean ebean-ddl-generator - 13.25.2-jakarta + 13.26.1-jakarta @@ -149,14 +149,14 @@ io.ebean ebean-jackson-mapper - 13.25.2-jakarta + 13.26.1-jakarta test io.ebean ebean-platform-all - 13.25.2-jakarta + 13.26.1-jakarta test diff --git a/ebean-test/src/test/java/io/ebean/xtest/common/BeanListTest.java b/ebean-test/src/test/java/io/ebean/xtest/common/BeanListTest.java index 69aea90ff..3285b60a8 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/common/BeanListTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/common/BeanListTest.java @@ -3,6 +3,7 @@ package io.ebean.xtest.common; import io.ebean.bean.BeanCollection; import io.ebean.common.BeanList; import org.junit.jupiter.api.Test; +import org.tests.model.basic.Product; import java.util.ArrayList; import java.util.Comparator; @@ -13,22 +14,22 @@ import static org.assertj.core.api.Assertions.assertThat; public class BeanListTest { - private final Object object1 = new Object(); - private final Object object2 = new Object(); - private final Object object3 = new Object(); + private final Product object1 = new Product(1); + private final Product object2 = new Product(2); + private final Product object3 = new Product(3); private List all() { List all = new ArrayList<>(); - all.add(object1); - all.add(object2); - all.add(object3); + all.add(new Product(1)); + all.add(new Product(2)); + all.add(new Product(3)); return all; } private List some() { List some = new ArrayList<>(); - some.add(object2); - some.add(object3); + some.add(new Product(2)); + some.add(new Product(3)); return some; } @@ -163,14 +164,15 @@ public class BeanListTest { @Test public void testRemove_given_beansInAdditions() { + List all = all(); BeanList list = new BeanList<>(); list.setModifyListening(BeanCollection.ModifyListenMode.ALL); - list.addAll(all()); + list.addAll(all); assertThat(list.modifyAdditions()).containsOnly(object1, object2, object3); // act - list.remove(object2); - list.remove(object3); + list.remove(all.get(1)); + list.remove(all.get(2)); assertThat(list.modifyAdditions()).containsOnly(object1); assertThat(list.modifyRemovals()).isEmpty(); @@ -179,13 +181,14 @@ public class BeanListTest { @Test public void testRemoveAll_given_beansInAdditions() { + List all = all(); BeanList list = new BeanList<>(); list.setModifyListening(BeanCollection.ModifyListenMode.ALL); - list.addAll(all()); + list.addAll(all); assertThat(list.modifyAdditions()).containsOnly(object1, object2, object3); // act - list.removeAll(some()); + list.removeAll(List.of(all.get(1), all.get(2))); assertThat(list.modifyAdditions()).containsOnly(object1); assertThat(list.modifyRemovals()).isEmpty(); diff --git a/ebean-test/src/test/java/io/ebean/xtest/common/BeanMapTest.java b/ebean-test/src/test/java/io/ebean/xtest/common/BeanMapTest.java index 910d96661..a72a17b2a 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/common/BeanMapTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/common/BeanMapTest.java @@ -3,7 +3,7 @@ package io.ebean.xtest.common; import io.ebean.bean.BeanCollection; import io.ebean.common.BeanMap; import org.junit.jupiter.api.Test; -import org.tests.model.basic.EBasic; +import org.tests.model.basic.Product; import java.util.*; @@ -14,24 +14,24 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class BeanMapTest { - private final EBasic object1 = new EBasic("o1"); - private final EBasic object2 = new EBasic("o2"); - private final EBasic object3 = new EBasic("o3"); - private final EBasic object4 = new EBasic("o4"); - private final EBasic object5 = new EBasic("o5"); + private final Product object1 = new Product(1); + private final Product object2 = new Product(2); + private final Product object3 = new Product(3); + private final Product object4 = new Product(4); + private final Product object5 = new Product(5); - private LinkedHashMap all() { - LinkedHashMap all = new LinkedHashMap<>(); - all.put("1", object1); - all.put("2", object2); - all.put("3", object3); + private Map all() { + Map all = new LinkedHashMap<>(); + all.put("1", new Product(1)); + all.put("2", new Product(2)); + all.put("3", new Product(3)); return all; } - private LinkedHashMap some() { - LinkedHashMap all = new LinkedHashMap<>(); - all.put("2", object2); - all.put("3", object3); + private Map some() { + Map all = new LinkedHashMap<>(); + all.put("2", new Product(2)); + all.put("3", new Product(3)); return all; } @@ -73,8 +73,8 @@ public class BeanMapTest { @Test public void testAdd_given_someAlreadyIn() { - - BeanMap map = new BeanMap<>(some()); + Map some = new LinkedHashMap<>(Map.of("2", object2, "3", object3)); + BeanMap map = new BeanMap<>(some); map.setModifyListening(BeanCollection.ModifyListenMode.ALL); // act @@ -90,11 +90,13 @@ public class BeanMapTest { @Test public void testAddSome_given_someAlreadyIn() { - BeanMap map = new BeanMap<>(some()); + Map some = new LinkedHashMap<>(Map.of("2", object2, "3", object3)); + BeanMap map = new BeanMap<>(some); map.setModifyListening(BeanCollection.ModifyListenMode.ALL); // act - map.putAll(all()); + Map all = Map.of("1", object1, "2", object2, "3", object3); + map.putAll(all); assertThat(map.modifyAdditions()).containsOnly(object1); assertThat(map.modifyRemovals()).isEmpty(); @@ -179,7 +181,7 @@ public class BeanMapTest { @Test public void testClear_given_someBeansInAdditions() { - BeanMap map = newModifyListeningMap(); + BeanMap map = newModifyListeningMap(); map.put("2", object2); map.put("3", object3); @@ -193,7 +195,7 @@ public class BeanMapTest { @Test public void keySet_add_whenModifyListening() { - BeanMap map = newModifyListeningMap(); + BeanMap map = newModifyListeningMap(); assertThrows(UnsupportedOperationException.class, () -> map.keySet().add("3")); } @@ -205,7 +207,7 @@ public class BeanMapTest { @Test public void keySet_addAll_whenModifyListening() { - BeanMap map = newModifyListeningMap(); + BeanMap map = newModifyListeningMap(); assertThrows(UnsupportedOperationException.class, () -> map.keySet().addAll(asList("3", "4"))); } @@ -317,13 +319,13 @@ public class BeanMapTest { @Test public void values_add() { - BeanMap map = new BeanMap<>(); + BeanMap map = new BeanMap<>(); assertThrows(UnsupportedOperationException.class, () -> map.values().add(object3)); } @Test public void values_addAll() { - BeanMap map = new BeanMap<>(); + BeanMap map = new BeanMap<>(); assertThrows(UnsupportedOperationException.class, () -> map.values().addAll(asList(object3, object5))); } @@ -337,8 +339,8 @@ public class BeanMapTest { @Test public void entrySet_clear() { - final BeanMap map = newModifyListeningMap(); - final Set> entries = map.entrySet(); + final BeanMap map = newModifyListeningMap(); + final Set> entries = map.entrySet(); entries.clear(); assertThat(entries).isEmpty(); @@ -348,8 +350,8 @@ public class BeanMapTest { @Test public void entrySet_remove() { - final BeanMap map = newModifyListeningMap5(); - final Set> entries = map.entrySet(); + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); assertThat(map).hasSize(5); @@ -365,8 +367,8 @@ public class BeanMapTest { @Test public void entrySet_remove_whenNotEqualValue() { - final BeanMap map = newModifyListeningMap5(); - final Set> entries = map.entrySet(); + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); assertThat(map).hasSize(5); @@ -379,11 +381,11 @@ public class BeanMapTest { @Test public void entrySet_iterator_remove() { - final BeanMap map = newModifyListeningMap5(); - final Set> entries = map.entrySet(); - final Iterator> iterator = entries.iterator(); + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); + final Iterator> iterator = entries.iterator(); while (iterator.hasNext()) { - final Map.Entry entry = iterator.next(); + final Map.Entry entry = iterator.next(); if (entry.getKey().equals("2") || entry.getKey().equals("5")) { iterator.remove(); } @@ -395,8 +397,8 @@ public class BeanMapTest { @Test public void entrySet_removeAll() { - final BeanMap map = newModifyListeningMap5(); - final Set> entries = map.entrySet(); + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); entries.removeAll(asList(new AbstractMap.SimpleEntry<>("1", object1), new AbstractMap.SimpleEntry<>("3", object4), new AbstractMap.SimpleEntry<>("4", object4))); assertThat(map).hasSize(3); @@ -406,8 +408,8 @@ public class BeanMapTest { @Test public void entrySet_retainAll() { - final BeanMap map = newModifyListeningMap5(); - final Set> entries = map.entrySet(); + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); entries.retainAll(asList(new AbstractMap.SimpleEntry<>("1", object1), new AbstractMap.SimpleEntry<>("3", object4), new AbstractMap.SimpleEntry<>("4", object4))); assertThat(map).hasSize(2); @@ -415,15 +417,15 @@ public class BeanMapTest { assertThat(map.modifyRemovals()).containsOnly(object2, object3, object5); } - private BeanMap newModifyListeningMap() { - BeanMap map = new BeanMap<>(); + private BeanMap newModifyListeningMap() { + BeanMap map = new BeanMap<>(); map.put("1", object1); map.setModifyListening(BeanCollection.ModifyListenMode.ALL); return map; } - private BeanMap newModifyListeningMap5() { - BeanMap map = new BeanMap<>(); + private BeanMap newModifyListeningMap5() { + BeanMap map = new BeanMap<>(); map.put("1", object1); map.put("2", object2); map.put("3", object3); diff --git a/ebean-test/src/test/java/io/ebean/xtest/common/BeanSetTest.java b/ebean-test/src/test/java/io/ebean/xtest/common/BeanSetTest.java index ca265affd..2152af71b 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/common/BeanSetTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/common/BeanSetTest.java @@ -3,6 +3,7 @@ package io.ebean.xtest.common; import io.ebean.bean.BeanCollection; import io.ebean.common.BeanSet; import org.junit.jupiter.api.Test; +import org.tests.model.basic.Product; import java.util.LinkedHashSet; import java.util.Set; @@ -12,22 +13,22 @@ import static org.assertj.core.api.Assertions.assertThat; public class BeanSetTest { - Object object1 = new Object(); - Object object2 = new Object(); - Object object3 = new Object(); + Product object1 = new Product(1); + Product object2 = new Product(2); + Product object3 = new Product(3); - private LinkedHashSet all() { - var all = new LinkedHashSet<>(); - all.add(object1); - all.add(object2); - all.add(object3); + private Set all() { + Set all = new LinkedHashSet<>(); + all.add(new Product(1)); + all.add(new Product(2)); + all.add(new Product(3)); return all; } - private LinkedHashSet some() { - var some = new LinkedHashSet<>(); - some.add(object2); - some.add(object3); + private Set some() { + Set some = new LinkedHashSet<>(); + some.add(new Product(2)); + some.add(new Product(3)); return some; } @@ -99,7 +100,9 @@ public class BeanSetTest { BeanSet set = new BeanSet<>(); set.setModifyListening(BeanCollection.ModifyListenMode.ALL); - set.addAll(all()); + + Set all = Set.of(object1, object2, object3); + set.addAll(all); assertThat(set.modifyAdditions()).containsOnly(object1, object2, object3); // act @@ -115,11 +118,13 @@ public class BeanSetTest { BeanSet set = new BeanSet<>(); set.setModifyListening(BeanCollection.ModifyListenMode.ALL); - set.addAll(all()); + + Set all = Set.of(object1, object2, object3); + set.addAll(all); assertThat(set.modifyAdditions()).containsOnly(object1, object2, object3); // act - set.removeAll(some()); + set.removeAll(Set.of(object2, object3)); assertThat(set.modifyAdditions()).containsOnly(object1); assertThat(set.modifyRemovals()).isEmpty(); 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 6efedcacb..fefece847 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 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; diff --git a/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java b/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java index 5b331a59b..75b0c3e6c 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java +++ b/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java @@ -27,14 +27,14 @@ public class TestQueryForUpdate extends BaseTestCase { try (final Transaction transaction = DB.beginTransaction()) { query = DB.find(Customer.class) .forUpdate() - .order().desc("id"); + .orderBy().desc("id"); query.findList(); } if (isSqlServer()) { assertThat(sqlOf(query)).contains("with (updlock)"); - } else if (!isDb2()){ + } else { assertThat(sqlOf(query)).contains("for update"); } } @@ -48,21 +48,21 @@ public class TestQueryForUpdate extends BaseTestCase { query = DB.find(Customer.class) .forUpdate() .setMaxRows(3) - .order().desc("id"); + .orderBy().desc("id"); query.findList(); } if (isSqlServer()) { assertThat(sqlOf(query)).contains("with (updlock)"); - } else if (!isOracle() && !isDb2()) { + } else if (!isOracle()) { // Oracle does not support FOR UPDATE with FETCH assertThat(sqlOf(query)).contains("for update"); } } @Test - @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) + @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB, Platform.DB2}) public void testForUpdate_when_alreadyInPCAsReference() { ResetBasicData.reset(); Order o0 = DB.find(Order.class).orderBy("id").setMaxRows(1).findOne(); @@ -96,7 +96,7 @@ public class TestQueryForUpdate extends BaseTestCase { } @Test - @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) + @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB, Platform.DB2}) public void testForUpdate_when_alreadyInPCAsReference_usingLock() { ResetBasicData.reset(); Order o0 = DB.find(Order.class).orderBy("id").setMaxRows(1).findOne(); @@ -131,7 +131,7 @@ public class TestQueryForUpdate extends BaseTestCase { } @Test - @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) + @ForPlatform({Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB, Platform.DB2}) public void testForUpdate_when_alreadyInPC() { EBasic basic = new EBasic("initialValue"); @@ -193,7 +193,7 @@ public class TestQueryForUpdate extends BaseTestCase { Query query = DB.find(Customer.class) .forUpdateNoWait() - .order().desc("id"); + .orderBy().desc("id"); query.findList(); if (isOracle()) { @@ -219,7 +219,7 @@ public class TestQueryForUpdate extends BaseTestCase { Query query = DB.find(Customer.class) .forUpdateNoWait() .setMaxRows(1) - .order().desc("id"); + .orderBy().desc("id"); List list = query.findList(); Customer first = list.get(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 000000000..4490a87ef --- /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; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/Product.java b/ebean-test/src/test/java/org/tests/model/basic/Product.java index df57dadd0..4f5557660 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/Product.java +++ b/ebean-test/src/test/java/org/tests/model/basic/Product.java @@ -39,6 +39,13 @@ public class Product implements Serializable { @Version Timestamp updtime; + public Product(Integer id) { + this.id = id; + } + + public Product() { + } + /** * Return id. */ diff --git a/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListChild.java b/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListChild.java index c4b07d3cd..7ed80c302 100644 --- a/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListChild.java +++ b/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListChild.java @@ -28,6 +28,11 @@ public class OmBeanListChild extends Model { public Long getId() { return id; } + + public OmBeanListChild setId(Long id) { + this.id = id; + return this; + } } diff --git a/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListParent.java b/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListParent.java index 1aacb79d6..026199f70 100644 --- a/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListParent.java +++ b/ebean-test/src/test/java/org/tests/model/orphanremoval/OmBeanListParent.java @@ -2,10 +2,14 @@ package org.tests.model.orphanremoval; import io.ebean.Model; +import io.ebean.annotation.WhenCreated; +import io.ebean.annotation.WhenModified; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Version; + +import java.time.Instant; import java.util.List; import static jakarta.persistence.CascadeType.ALL; @@ -19,6 +23,13 @@ public class OmBeanListParent extends Model { @Version private long version; + private String name; + + @WhenCreated + private Instant whenCreated; + @WhenModified + private Instant whenModified; + @OneToMany(cascade = ALL, mappedBy = "parent", orphanRemoval = true) private List children; @@ -35,6 +46,42 @@ public class OmBeanListParent extends Model { this.children.clear(); this.children.addAll(children); } + + public void setChildren2(List children) { + this.children = children; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + public Instant getWhenModified() { + return whenModified; + } + + public void setWhenModified(Instant whenModified) { + this.whenModified = whenModified; + } + + public Instant getWhenCreated() { + return whenCreated; + } + + public void setWhenCreated(Instant whenCreated) { + this.whenCreated = whenCreated; + } } diff --git a/ebean-test/src/test/java/org/tests/model/orphanremoval/TestOrphanRemovalOverwrite.java b/ebean-test/src/test/java/org/tests/model/orphanremoval/TestOrphanRemovalOverwrite.java index 446fff4b2..d03575e8c 100644 --- a/ebean-test/src/test/java/org/tests/model/orphanremoval/TestOrphanRemovalOverwrite.java +++ b/ebean-test/src/test/java/org/tests/model/orphanremoval/TestOrphanRemovalOverwrite.java @@ -1,17 +1,20 @@ package org.tests.model.orphanremoval; +import io.ebean.DB; +import io.ebean.test.LoggedSql; import org.junit.jupiter.api.Test; import java.util.List; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -public class TestOrphanRemovalOverwrite { +class TestOrphanRemovalOverwrite { @Test - public void testOverwritingMapping() { + void testOverwritingMapping() { OmBeanListParent parent = new OmBeanListParent(); parent.save(); @@ -29,8 +32,45 @@ public class TestOrphanRemovalOverwrite { // Refreshing here generates new objects for the associated children that are referred to by the parent. parent.refresh(); - assertNotNull(childList.get(0).getId()); - assertEquals(childList.get(0).getId(), parent.getChildren().get(0).getId()); + OmBeanListChild child = childList.get(0); + assertNotNull(child.getId()); + + OmBeanListChild refreshedChild = parent.getChildren().get(0); + assertEquals(child.getId(), refreshedChild.getId()); + assertEquals(child, refreshedChild); assertEquals(childList, parent.getChildren()); } + + @Test + void clearAddAll() { + OmBeanListChild c1 = new OmBeanListChild("c1"); + OmBeanListChild c2 = new OmBeanListChild("c2"); + + OmBeanListParent parent = new OmBeanListParent(); + parent.getChildren().add(c1); + parent.getChildren().add(c2); + parent.save(); + + + OmBeanListChild c3 = new OmBeanListChild("c3"); + c3.setId(c1.getId()); + + OmBeanListParent p1 = DB.find(OmBeanListParent.class, parent.getId()); + List children = p1.getChildren(); + children.clear(); + children.add(c3); + + LoggedSql.start(); + DB.save(p1); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(7); + assertThat(sql.get(0)).contains("delete from om_bean_list_child where id=?"); + assertThat(sql.get(1)).contains(" -- bind"); + assertThat(sql.get(2)).contains(" -- bind"); + assertThat(sql.get(3)).contains(" -- executeBatch()"); + assertThat(sql.get(4)).contains(" insert into om_bean_list_child"); + assertThat(sql.get(5)).contains(" -- bind"); + assertThat(sql.get(6)).contains(" -- executeBatch()"); + } } diff --git a/ebean-test/src/test/java/org/tests/o2m/OneToManyListMarkAsDirtyTest.java b/ebean-test/src/test/java/org/tests/o2m/OneToManyListMarkAsDirtyTest.java new file mode 100644 index 000000000..ae358b85d --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/OneToManyListMarkAsDirtyTest.java @@ -0,0 +1,114 @@ +package org.tests.o2m; + +import io.ebean.DB; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; +import org.tests.model.orphanremoval.OmBeanListChild; +import org.tests.model.orphanremoval.OmBeanListParent; +import java.time.Instant; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import static org.assertj.core.api.Assertions.assertThat; + +class OneToManyListMarkAsDirtyTest extends BaseTestCase { + @Test + void usingNewListWithNonDirtyParent_expect_orphanDeleted_works() { + // setup + var parent = new OmBeanListParent(); + var a_b = new OmBeanListChild("b"); + parent.getChildren().add(a_b); + DB.save(parent); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + + // act + var secondParent = DB.find(OmBeanListParent.class, parent.getId()); + secondParent.setChildren2(new ArrayList<>()); //