diff --git a/composites/ebean-clickhouse/pom.xml b/composites/ebean-clickhouse/pom.xml index 29758dc5e2..4c8a31f94f 100644 --- a/composites/ebean-clickhouse/pom.xml +++ b/composites/ebean-clickhouse/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-clickhouse @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-clickhouse - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-cockroach/pom.xml b/composites/ebean-cockroach/pom.xml index cae0bb35a3..62f0bc6eb7 100644 --- a/composites/ebean-cockroach/pom.xml +++ b/composites/ebean-cockroach/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-cockroach @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-postgres - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-db2/pom.xml b/composites/ebean-db2/pom.xml index cece1db865..1bcb7c848f 100644 --- a/composites/ebean-db2/pom.xml +++ b/composites/ebean-db2/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-db2 @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-db2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-h2/pom.xml b/composites/ebean-h2/pom.xml index be909354c3..61ba67edbb 100644 --- a/composites/ebean-h2/pom.xml +++ b/composites/ebean-h2/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-h2 @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-hana/pom.xml b/composites/ebean-hana/pom.xml index b16776d69f..6096877158 100644 --- a/composites/ebean-hana/pom.xml +++ b/composites/ebean-hana/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-hana @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-hana - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-mariadb/pom.xml b/composites/ebean-mariadb/pom.xml index 07112691b6..3cbb2a996c 100644 --- a/composites/ebean-mariadb/pom.xml +++ b/composites/ebean-mariadb/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-mariadb @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-mariadb - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-mysql/pom.xml b/composites/ebean-mysql/pom.xml index 7fa5080abf..f46393a4e5 100644 --- a/composites/ebean-mysql/pom.xml +++ b/composites/ebean-mysql/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-mysql @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-mysql - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-nuodb/pom.xml b/composites/ebean-nuodb/pom.xml index 01d6fb3ee7..59d292ba94 100644 --- a/composites/ebean-nuodb/pom.xml +++ b/composites/ebean-nuodb/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-nuodb @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-nuodb - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-oracle/pom.xml b/composites/ebean-oracle/pom.xml index a6451554f6..01ddd5d75b 100644 --- a/composites/ebean-oracle/pom.xml +++ b/composites/ebean-oracle/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-oracle @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,14 +35,18 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-oracle - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + ebean-parent-13.6.4-FOC1 + diff --git a/composites/ebean-postgres/pom.xml b/composites/ebean-postgres/pom.xml index e65fa5477e..52219c13cf 100644 --- a/composites/ebean-postgres/pom.xml +++ b/composites/ebean-postgres/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-postgres @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-postgres - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/composites/ebean-sqlite/pom.xml b/composites/ebean-sqlite/pom.xml index 823d3d3879..03c0f40b4d 100644 --- a/composites/ebean-sqlite/pom.xml +++ b/composites/ebean-sqlite/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-sqlite @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-sqlite - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/composites/ebean-sqlserver/pom.xml b/composites/ebean-sqlserver/pom.xml index a4702098de..f784345350 100644 --- a/composites/ebean-sqlserver/pom.xml +++ b/composites/ebean-sqlserver/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-sqlserver @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-sqlserver - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/composites/ebean-yugabyte/pom.xml b/composites/ebean-yugabyte/pom.xml index 5222eed93f..0f0709080d 100644 --- a/composites/ebean-yugabyte/pom.xml +++ b/composites/ebean-yugabyte/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-yugabyte @@ -16,13 +16,13 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -35,13 +35,13 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-postgres - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/composites/ebean/pom.xml b/composites/ebean/pom.xml index f524e247b8..dc3903cd76 100644 --- a/composites/ebean/pom.xml +++ b/composites/ebean/pom.xml @@ -4,7 +4,7 @@ composites io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean (all platforms) @@ -16,31 +16,31 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-joda-time - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-jackson-jsonnode - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-jackson-mapper - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -53,13 +53,13 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-all - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/composites/pom.xml b/composites/pom.xml index 29c05f3ca5..f1508a7d0e 100644 --- a/composites/pom.xml +++ b/composites/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT composites diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index 0770152d5f..2111b887ba 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean api diff --git a/ebean-api/src/main/java/io/ebean/DB.java b/ebean-api/src/main/java/io/ebean/DB.java index f20bcb740c..881b8cda9b 100644 --- a/ebean-api/src/main/java/io/ebean/DB.java +++ b/ebean-api/src/main/java/io/ebean/DB.java @@ -447,6 +447,19 @@ public static int saveAll(Collection beans) throws OptimisticLockException { public static int saveAll(Object... beans) throws OptimisticLockException { return getDefault().saveAll(beans); } + + /** + * This will visit all beans in the persist graph on a given object + * start. It will call the visitor for each dirty bean that would + * be saved with {@link #save(Object)} or {@link #saveAll(Collection)}. You can + * use this method to implement custom validations. + * + * @param start could be a bean, a list of beans or a map of beans. + * @param visitor the visitor + */ + public static void visitSave(Object start, PersistVisitor visitor) { + getDefault().visitSave(start, visitor); + } /** * This method checks the uniqueness of a bean. I.e. if the save will work. It will return the diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index 6dd0ab3cfa..6a8cc2d82e 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -843,6 +843,17 @@ public interface Database { */ int saveAll(Object... beans) throws OptimisticLockException; + /** + * This will visit all beans in the persist graph on a given object + * start. It will call the visitor for each dirty bean that would + * be saved with {@link #save(Object)} or {@link #saveAll(Collection)}. You can + * use this method to implement custom validations. + * + * @param start could be a bean, a list of beans or a map of beans. + * @param visitor the visitor + */ + void visitSave(Object start, PersistVisitor visitor); + /** * Delete the bean. *

@@ -1509,4 +1520,8 @@ public interface Database { */ void truncate(Class... beanTypes); + /** + * RunDdl manually. This can be used if 'db.ddl.run=false' is set and you plan to run DDL manually. + */ + void runDdl(); } diff --git a/ebean-api/src/main/java/io/ebean/PersistVisitor.java b/ebean-api/src/main/java/io/ebean/PersistVisitor.java new file mode 100644 index 0000000000..b5f1398d09 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/PersistVisitor.java @@ -0,0 +1,29 @@ +package io.ebean; + +import java.util.Collection; +import java.util.Map; + +import io.ebean.bean.EntityBean; +import io.ebean.plugin.Property; + +@FunctionalInterface +public interface PersistVisitor { + + PersistVisitor visitBean(EntityBean bean); + + default PersistVisitor visitProperty(Property prop) { + return this; + }; + + default PersistVisitor visitCollection(Collection collection) { + return this; + }; + + default PersistVisitor visitMap(Map map) { + return this; + }; + + default void visitEnd() { + } + +} diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/EntityImplements.java b/ebean-api/src/main/java/io/ebean/annotation/ext/EntityImplements.java new file mode 100644 index 0000000000..ee312bb72c --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/annotation/ext/EntityImplements.java @@ -0,0 +1,21 @@ +package io.ebean.annotation.ext; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotation to define a list of interface for which this entity is target for. + * @author Roland Praml, FOCONIS AG + */ +@Documented +@Target({ FIELD, TYPE }) +@Retention(RUNTIME) +public @interface EntityImplements { + + Class[] value(); + +} diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/EntityOverride.java b/ebean-api/src/main/java/io/ebean/annotation/ext/EntityOverride.java new file mode 100644 index 0000000000..2c166812a3 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/annotation/ext/EntityOverride.java @@ -0,0 +1,24 @@ +package io.ebean.annotation.ext; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotation to define that an entity overrides the parent entity. + * @author Roland Praml, FOCONIS AG + */ +@Documented +@Target({ FIELD, TYPE }) +@Retention(RUNTIME) +public @interface EntityOverride { + + /** + * The priority of the statement. Lower priority wins. + */ + int priority() default 0; + +} diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java b/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java new file mode 100644 index 0000000000..5749549fb9 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/annotation/ext/IntersectionFactory.java @@ -0,0 +1,29 @@ +package io.ebean.annotation.ext; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation to define a factory for an intersection model. This class MUST have a constructor or factory method with two parameters that accepts parent and property type. + * @author Roland Praml, FOCONIS AG + */ +@Documented +@Target({ FIELD, TYPE }) +@Retention(RUNTIME) +public @interface IntersectionFactory { + + /** + * The intersection model class. + */ + Class value(); + + /** + * An optional factory method. + */ + String factoryMethod() default ""; +} diff --git a/ebean-api/src/main/java/io/ebean/annotation/ext/package.html b/ebean-api/src/main/java/io/ebean/annotation/ext/package.html new file mode 100644 index 0000000000..9505c002ca --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/annotation/ext/package.html @@ -0,0 +1,8 @@ + + + Ebean Annotations + + +This classes will be moved later to the ebean-annotation module. + + diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index ca778d02ca..20c434d720 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -16,6 +16,7 @@ import io.ebean.event.changelog.ChangeLogRegister; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; +import io.ebean.plugin.CustomDeployParser; import io.ebean.meta.MetricNamingMatch; import io.ebean.util.StringHelper; @@ -400,6 +401,8 @@ public class DatabaseConfig { */ private Clock clock = Clock.systemUTC(); + private TempFileProvider tempFileProvider = new WeakRefTempFileProvider(); + private List idGenerators = new ArrayList<>(); private List findControllers = new ArrayList<>(); private List persistControllers = new ArrayList<>(); @@ -409,6 +412,7 @@ public class DatabaseConfig { private List queryAdapters = new ArrayList<>(); private final List bulkTableEventListeners = new ArrayList<>(); private final List configStartupListeners = new ArrayList<>(); + private final List customDeployParsers = new ArrayList<>(); /** * By default inserts are included in the change log. @@ -570,6 +574,14 @@ public void setClock(final Clock clock) { this.clock = clock; } + public TempFileProvider getTempFileProvider() { + return tempFileProvider; + } + + public void setTempFileProvider(final TempFileProvider tempFileProvider) { + this.tempFileProvider = tempFileProvider; + } + /** * Return the slow query time in millis. */ @@ -615,7 +627,7 @@ public boolean isDefaultOrderById() { } /** - * Put a service object into configuration such that it can be passed to a plugin. + * Put a service object into configuration such that it can be used by ebean or a plugin. *

* For example, put IgniteConfiguration in to be passed to the Ignite plugin. */ @@ -623,6 +635,28 @@ public void putServiceObject(String key, Object configObject) { serviceObject.put(key, configObject); } + /** + * Put a service object into configuration such that it can be used by ebean or a plugin. + *

+ * For example, put IgniteConfiguration in to be passed to the Ignite plugin. + * You can also override some SPI objects that should be used for that Database. Currently, the following + * objects are possible. + *

    + *
  • DataSourceAlertFactory (e.g. add different alert factories for different ebean instances)
  • + *
  • DocStoreFactory
  • + *
  • XmapService
  • + *
  • SpiLoggerFactory (e.g. add custom logger for a certain ebean instance)
  • + *
  • AutoTuneServiceProvider
  • + *
  • SpiProfileHandler
  • + *
  • SlowQueryListener (e.g. add custom query listener for a certain ebean instance)
  • + *
  • ServerCacheNotifyPlugin
  • + *
  • SpiDdlGenneratorProvider
  • + *
+ */ + public void putServiceObject(Class iface, T configObject) { + serviceObject.put(serviceObjectKey(iface), configObject); + } + /** * Return the service object given the key. */ @@ -631,7 +665,7 @@ public Object getServiceObject(String key) { } /** - * Put a service object into configuration such that it can be passed to a plugin. + * Put a service object into configuration such that it can be used by ebean or a plugin. * *
{@code
    *
@@ -656,7 +690,7 @@ private String serviceObjectKey(Class cls) {
   }
 
   /**
-   * Used by plugins to obtain service objects.
+   * Used by ebean or plugins to obtain service objects.
    *
    * 
{@code
    *
@@ -2691,6 +2725,17 @@ public List getServerConfigStartupListeners() {
     return configStartupListeners;
   }
 
+  /**
+   * Add a CustomDeployParser.
+   */
+  public void addCustomDeployParser(CustomDeployParser customDeployParser) {
+    customDeployParsers.add(customDeployParser);
+  }
+
+  public List getCustomDeployParsers() {
+    return customDeployParsers;
+  }
+
   /**
    * Register all the BeanPersistListener instances.
    * 

diff --git a/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java new file mode 100644 index 0000000000..ad257c98ee --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/DeleteOnShutdownTempFileProvider.java @@ -0,0 +1,68 @@ +package io.ebean.config; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TempFileProvider implementation, which deletes all temp files on shutdown. + * + * @author Roland Praml, FOCONIS AG + * + */ +public class DeleteOnShutdownTempFileProvider implements TempFileProvider { + + private static final Logger logger = LoggerFactory.getLogger(DeleteOnShutdownTempFileProvider.class); + + List tempFiles = new ArrayList<>(); + private final String prefix; + private final String suffix; + private final File directory; + + /** + * Creates the TempFileProvider with default prefix "db-". + */ + public DeleteOnShutdownTempFileProvider() { + this("db-", null, null); + } + + /** + * Creates the TempFileProvider. + */ + public DeleteOnShutdownTempFileProvider(String prefix, String suffix, File directory) { + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + } + + @Override + public File createTempFile() throws IOException { + File file = File.createTempFile(prefix, suffix, directory); + synchronized (tempFiles) { + tempFiles.add(file.getAbsolutePath()); + } + return file; + } + + /** + * Deletes all created files on shutdown. + */ + @Override + public void shutdown() { + synchronized (tempFiles) { + for (String path : tempFiles) { + if (new File(path).delete()) { + logger.trace("deleted {}", path); + } else { + logger.warn("could not delete {}", path); + } + } + tempFiles.clear(); + } + } + +} diff --git a/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java new file mode 100644 index 0000000000..4658b46c28 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/TempFileProvider.java @@ -0,0 +1,23 @@ +package io.ebean.config; + +import java.io.File; +import java.io.IOException; + +/** + * Creates a temp file for the ScalarTypeFile datatype. + * + * @author Roland Praml, FOCONIS AG + * + */ +public interface TempFileProvider { + + /** + * Creates a tempFile. + */ + File createTempFile() throws IOException; + + /** + * Shutdown the tempFileProvider. + */ + void shutdown(); +} diff --git a/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java b/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java new file mode 100644 index 0000000000..aa3ae82905 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/WeakRefTempFileProvider.java @@ -0,0 +1,145 @@ +package io.ebean.config; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * WeakRefTempFileProvider will delete the tempFile if all references to the returned File + * object are collected by the garbage collection. + * + * @author Roland Praml, FOCONIS AG + * + */ +public class WeakRefTempFileProvider implements TempFileProvider { + + private static final Logger logger = LoggerFactory.getLogger(WeakRefTempFileProvider.class); + + private final ReferenceQueue tempFiles = new ReferenceQueue<>(); + + private WeakFileReference root; + + private final String prefix; + private final String suffix; + private final File directory; + + /** + * We hold a linkedList of weak references. So we can remove stale files in O(1) + * + * @author Roland Praml, FOCONIS AG + */ + private static class WeakFileReference extends WeakReference { + + String path; + WeakFileReference prev; + WeakFileReference next; + + WeakFileReference(File referent, ReferenceQueue q) { + super(referent, q); + path = referent.getAbsolutePath(); + } + + boolean delete(boolean shutdown) { + File file = new File(path); + if (!file.exists()) { + logger.trace("already deleted {}", path); + return true; + } else if (file.delete()) { + logger.trace("deleted {}", path); + return true; + } else { + if (shutdown) { + logger.warn("could not delete {}", path); + } else { + logger.info("could not delete {} - will delete on shutdown", path); + } + return false; + } + } + } + + + /** + * Creates the TempFileProvider with default prefix "db-". + */ + public WeakRefTempFileProvider() { + this("db-", null, null); + } + + /** + * Creates the TempFileProvider. + */ + public WeakRefTempFileProvider(String prefix, String suffix, File directory) { + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + } + + @Override + public File createTempFile() throws IOException { + File tempFile = File.createTempFile(prefix, suffix, directory); + logger.trace("createTempFile: {}", tempFile); + synchronized (this) { + add(new WeakFileReference(tempFile, tempFiles)); + } + return tempFile; + } + + /** + * Will delete stale files. + * This is public to use in tests. + */ + public void deleteStaleTempFiles() { + synchronized (this) { + deleteStaleTempFilesInternal(); + } + } + + private void deleteStaleTempFilesInternal() { + WeakFileReference ref; + while ((ref = (WeakFileReference) tempFiles.poll()) != null) { + if (ref.delete(false)) { + remove(ref); // remove from linkedList only, if delete was successful. + } + } + } + + private void add(WeakFileReference ref) { + deleteStaleTempFilesInternal(); + + if (root == null) { + root = ref; + } else { + ref.next = root; + root.prev = ref; + root = ref; + } + } + + private void remove(WeakFileReference ref) { + if (ref.next != null) { + ref.next.prev = ref.prev; + } + if (ref.prev != null) { + ref.prev.next = ref.next; + } else { + root = ref.next; + } + } + + /** + * Deletes all created files on shutdown. + */ + @Override + public void shutdown() { + while (root != null) { + root.delete(true); + root = root.next; + } + } + +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java new file mode 100644 index 0000000000..4c854f2f1e --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java @@ -0,0 +1,15 @@ +package io.ebean.plugin; + +import io.ebean.config.dbplatform.DatabasePlatform; + +/** + * Fired after all beans are parsed. You may implement own parsers to handle custom annotations. + * (See test case for example) + * + * @author Roland Praml, FOCONIS AG + */ +@FunctionalInterface +public interface CustomDeployParser { + + void parse(DeployBeanDescriptorMeta descriptor, DatabasePlatform databasePlatform); +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java new file mode 100644 index 0000000000..4347a5057c --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java @@ -0,0 +1,36 @@ +package io.ebean.plugin; + +import java.util.Collection; + +/** + * General deployment information. This is used in {@link CustomDeployParser}. + * + * @author Roland Praml, FOCONIS AG + */ +public interface DeployBeanDescriptorMeta { + + /** + * Return a collection of all BeanProperty deployment information. + */ + public Collection propertiesAll(); + + /** + * Get a BeanProperty by its name. + */ + public DeployBeanPropertyMeta getBeanProperty(String secondaryBeanName); + + /** + * Return the DeployBeanDescriptorMeta for the given bean class. + */ + public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class propertyType); + + /** + * Returns the discriminator column, if any. + * @return + */ + public String getDiscriminatorColumn(); + + public String getBaseTable(); + + DeployBeanPropertyMeta idProperty(); +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java new file mode 100644 index 0000000000..d7d1d637a3 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java @@ -0,0 +1,23 @@ +package io.ebean.plugin; + +public interface DeployBeanPropertyAssocMeta extends DeployBeanPropertyMeta { + + /** + * Return the mappedBy deployment attribute. + *

+ * This is the name of the property in the 'detail' bean that maps back to + * this 'master' bean. + *

+ */ + String getMappedBy(); + + /** + * Return the base table for this association. + *

+ * This has the table name which is used to determine the relationship for + * this association. + *

+ */ + String getBaseTable(); + +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java new file mode 100644 index 0000000000..4ba2d3e2a1 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java @@ -0,0 +1,37 @@ +package io.ebean.plugin; + +import java.lang.reflect.Field; + +public interface DeployBeanPropertyMeta { + + /** + * Return the name of the property. + */ + String getName(); + + /** + * The database column name this is mapped to. + */ + String getDbColumn(); + + /** + * Return the bean Field associated with this property. + */ + Field getField(); + + /** + * The property is based on a formula. + */ + void setSqlFormula(String sqlSelect, String sqlJoin); + + /** + * Return the bean type. + */ + Class getOwningType(); + + /** + * Return the property type. + */ + Class getPropertyType(); + +} diff --git a/ebean-api/src/main/java/module-info.java b/ebean-api/src/main/java/module-info.java index a8fb4ea387..9349bd8882 100644 --- a/ebean-api/src/main/java/module-info.java +++ b/ebean-api/src/main/java/module-info.java @@ -41,5 +41,5 @@ exports io.ebean.text; exports io.ebean.text.json; exports io.ebean.util; - + exports io.ebean.annotation.ext; } diff --git a/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java b/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java new file mode 100644 index 0000000000..dc69cc6876 --- /dev/null +++ b/ebean-api/src/test/java/io/ebean/TestWeakRefTempFileProvider.java @@ -0,0 +1,171 @@ +package io.ebean; + + +import io.ebean.config.WeakRefTempFileProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.channels.FileLock; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the WeakRefTempFileProvider. (Note: this test relies on an aggressive garbage collection. + * if GC implementation will change, the test may fail) + * + * @author Roland Praml, FOCONIS AG + */ +public class TestWeakRefTempFileProvider { + + WeakRefTempFileProvider prov = new WeakRefTempFileProvider(); + + @AfterEach + public void shutdown() { + prov.shutdown(); + } + + /** + * Run the garbage collection and delete stale files. + */ + private void gc() throws InterruptedException { + System.gc(); + Thread.sleep(100); + prov.deleteStaleTempFiles(); + } + + @Test + public void testStaleEntries() throws Exception { + File tempFile = prov.createTempFile(); + String fileName = tempFile.getAbsolutePath(); + + gc(); + + assertThat(new File(fileName)).exists(); + + tempFile = null; // give up reference + + gc(); + + assertThat(new File(fileName)).doesNotExist(); + + + } + + @Test + public void testLinkedListForward() throws Exception { + File tempFile1 = prov.createTempFile(); + String fileName1 = tempFile1.getAbsolutePath(); + File tempFile2 = prov.createTempFile(); + String fileName2 = tempFile2.getAbsolutePath(); + File tempFile3 = prov.createTempFile(); + String fileName3 = tempFile3.getAbsolutePath(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).exists(); + + gc(); + + // give up first ref + tempFile1 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).exists(); + + // give up second ref + tempFile2 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).exists(); + + // give up third ref + tempFile3 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).doesNotExist(); + + } + + + @Test + public void testLinkedListReverse() throws Exception { + File tempFile1 = prov.createTempFile(); + String fileName1 = tempFile1.getAbsolutePath(); + File tempFile2 = prov.createTempFile(); + String fileName2 = tempFile2.getAbsolutePath(); + File tempFile3 = prov.createTempFile(); + String fileName3 = tempFile3.getAbsolutePath(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).exists(); + + gc(); + + // give up third ref + tempFile3 = null; + + gc(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).exists(); + assertThat(new File(fileName3)).doesNotExist(); + + // give up second ref + tempFile2 = null; + + gc(); + + assertThat(new File(fileName1)).exists(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).doesNotExist(); + + // give up first ref + tempFile1 = null; + + gc(); + + assertThat(new File(fileName1)).doesNotExist(); + assertThat(new File(fileName2)).doesNotExist(); + assertThat(new File(fileName3)).doesNotExist(); + + } + + @Test + @Disabled("Runs on Windows only") + public void testFileLocked() throws Exception { + File tempFile = prov.createTempFile(); + String fileName = tempFile.getAbsolutePath(); + + try (FileOutputStream os = new FileOutputStream(fileName)) { + FileLock lock = os.getChannel().lock(); + try { + os.write(42); + + tempFile = null; + gc(); + } finally { + lock.release(); + } + + } + + assertThat(new File(fileName)).exists(); + + prov.shutdown(); + + assertThat(new File(fileName)).doesNotExist(); + } +} diff --git a/ebean-autotune/pom.xml b/ebean-autotune/pom.xml index 90d0597124..fde3907e39 100644 --- a/ebean-autotune/pom.xml +++ b/ebean-autotune/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -13,8 +13,8 @@ - scm:git:git@github.com:ebean-orm/ebean.git - HEAD + scm:git:git@github.com:FOCONIS/ebean.git + ebean-parent-13.6.4-FOC1 ebean autotune @@ -26,7 +26,7 @@ io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided @@ -55,7 +55,7 @@ io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index 618e44ba02..dfa25d283a 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean bom @@ -89,106 +89,106 @@ io.ebean ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core-type - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-joda-time - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-jackson-jsonnode - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-jackson-mapper - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-ddl-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-externalmapping-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-externalmapping-xml - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-autotune - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean querybean-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean kotlin-querybean-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-postgis - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-redis - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 866d3ec569..8b5a7b6eee 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index ac73b3c697..86a115b5d4 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-core @@ -14,15 +14,15 @@ https://ebean.io/ - scm:git:git@github.com:ebean-orm/ebean.git - HEAD + scm:git:git@github.com:FOCONIS/ebean.git + ebean-parent-13.6.4-FOC1 io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -37,6 +37,12 @@ 7.1 + + io.ebean + ebean-api + 13.10.1-FOC4-SNAPSHOT + + io.ebean ebean-migration-auto @@ -46,13 +52,13 @@ io.ebean ebean-core-type - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-externalmapping-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -143,21 +149,21 @@ io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-platform-postgres - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-platform-sqlserver - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java index b3c4858139..d821e75b84 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java @@ -12,4 +12,9 @@ public interface SpiDdlGenerator { */ void execute(boolean online); + /** + * Run DDL manually. This can be used to initialize multi tenant environments or if you plan not to run + * DDL on startup + */ + void runDdl(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java index f15ab82ffc..ddafc8607e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java @@ -168,6 +168,7 @@ private BootupClasses bootupClasses(DatabaseConfig config) { bootup.addPersistListeners(config.getPersistListeners()); bootup.addQueryAdapters(config.getQueryAdapters()); bootup.addServerConfigStartup(config.getServerConfigStartupListeners()); + bootup.addCustomDeployParser(config.getCustomDeployParsers()); bootup.addChangeLogInstances(config); bootup.runServerConfigStartup(config); return bootup; 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 2c604ed988..4f82437f7c 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 @@ -79,6 +79,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { private final String serverName; private final DatabasePlatform databasePlatform; private final TransactionManager transactionManager; + private final TempFileProvider tempFileProvider; private final QueryPlanManager queryPlanManager; private final ExtraMetrics extraMetrics; private final DataTimeZone dataTimeZone; @@ -159,6 +160,7 @@ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) { this.metaInfoManager = new DefaultMetaInfoManager(this, this.config.getMetricNaming()); this.serverPlugins = config.getPlugins(); this.ddlGenerator = config.initDdlGenerator(this); + this.tempFileProvider = config.getConfig().getTempFileProvider(); this.scriptRunner = new DScriptRunner(this); configureServerPlugins(); @@ -307,7 +309,10 @@ public void initialise() { */ public void start() { if (config.isRunMigration() && TenantMode.DB != config.getTenantMode()) { - final AutoMigrationRunner migrationRunner = ServiceUtil.service(AutoMigrationRunner.class); + AutoMigrationRunner migrationRunner = config.getServiceObject(AutoMigrationRunner.class); + if (migrationRunner == null) { + migrationRunner = ServiceUtil.service(AutoMigrationRunner.class); + } if (migrationRunner == null) { throw new IllegalStateException("No AutoMigrationRunner found. Probably ebean-migration is not in the classpath?"); } @@ -388,6 +393,8 @@ private void shutdownInternal(boolean shutdownDataSource, boolean deregisterDriv backgroundExecutor.shutdown(); // shutdown DataSource (if its an Ebean one) transactionManager.shutdown(shutdownDataSource, deregisterDriver); + + tempFileProvider.shutdown(); dumpMetrics(); shutdown = true; if (shutdownDataSource) { @@ -598,7 +605,11 @@ public void clearQueryStatistics() { */ @Override public T createEntityBean(Class type) { - return descriptor(type).createBean(); + final BeanDescriptor desc = descriptor(type); + if (desc == null) { + throw new IllegalArgumentException("No bean type " + type.getName() + " registered"); + } + return desc.createBean(); } /** @@ -1581,6 +1592,11 @@ public void save(Object bean, @Nullable Transaction transaction) { persister.save(checkEntityBean(bean), transaction); } + @Override + public void visitSave(Object start, PersistVisitor visitor) { + new VisitHandler(descriptorManager).visit(start, visitor); + } + @Override public void markAsDirty(Object bean) { if (!(bean instanceof EntityBean)) { @@ -2249,4 +2265,9 @@ List queryPlanInit(QueryPlanInit initRequest) { List queryPlanCollectNow(QueryPlanRequest request) { return queryPlanManager.collect(request); } + + @Override + public void runDdl() { + ddlGenerator.runDdl(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java index 3eb6dfe3a0..7dddf0d124 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java @@ -117,7 +117,10 @@ private DataSource create(DataSourceConfig dsConfig, boolean readOnly) { * Attach DataSourceAlert via service loader if present. */ private void attachAlert(DataSourceConfig dsConfig) { - DataSourceAlertFactory alertFactory = ServiceUtil.service(DataSourceAlertFactory.class); + DataSourceAlertFactory alertFactory = config.getServiceObject(DataSourceAlertFactory.class); + if (alertFactory == null) { + alertFactory = ServiceUtil.service(DataSourceAlertFactory.class); + } if (alertFactory != null) { dsConfig.setAlert(alertFactory.createAlert()); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index bce1339783..b31b6ed44a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -4,10 +4,7 @@ import io.ebean.ExpressionFactory; import io.ebean.annotation.Platform; import io.ebean.cache.*; -import io.ebean.config.DatabaseConfig; -import io.ebean.config.ExternalTransactionManager; -import io.ebean.config.ProfilingConfig; -import io.ebean.config.SlowQueryListener; +import io.ebean.config.*; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DbHistorySupport; import io.ebean.event.changelog.ChangeLogListener; @@ -37,6 +34,7 @@ import io.ebeaninternal.server.expression.DefaultExpressionFactory; import io.ebeaninternal.server.expression.platform.DbExpressionHandler; import io.ebeaninternal.server.expression.platform.DbExpressionHandlerFactory; +import io.ebeaninternal.server.json.DJsonContext; import io.ebeaninternal.server.logger.DLogManager; import io.ebeaninternal.server.logger.DLoggerFactory; import io.ebeaninternal.server.persist.Binder; @@ -46,7 +44,6 @@ import io.ebeaninternal.server.query.*; import io.ebeaninternal.server.readaudit.DefaultReadAuditLogger; import io.ebeaninternal.server.readaudit.DefaultReadAuditPrepare; -import io.ebeaninternal.server.json.DJsonContext; import io.ebeaninternal.server.transaction.*; import io.ebeaninternal.server.type.DefaultTypeManager; import io.ebeaninternal.server.type.TypeManager; @@ -76,6 +73,7 @@ public final class InternalConfiguration { private final DatabasePlatform databasePlatform; private final DeployInherit deployInherit; private final TypeManager typeManager; + private final TempFileProvider tempFileProvider; private final DtoBeanManager dtoBeanManager; private final ClockService clockService; private final DataTimeZone dataTimeZone; @@ -116,6 +114,7 @@ public final class InternalConfiguration { this.databasePlatform = config.getDatabasePlatform(); this.expressionFactory = initExpressionFactory(config); this.typeManager = new DefaultTypeManager(config, bootupClasses); + this.tempFileProvider = config.getTempFileProvider(); this.multiValueBind = createMultiValueBind(databasePlatform.platform()); this.deployInherit = new DeployInherit(bootupClasses); this.deployCreateProperties = new DeployCreateProperties(typeManager); @@ -144,7 +143,12 @@ private InternalConfigXmlMap initExternalMapping() { } private S service(Class cls) { - return ServiceUtil.service(cls); + S service = config.getServiceObject(cls); + if (service != null) { + return service; + } else { + return ServiceUtil.service(cls); + } } private List readExternalMapping() { @@ -512,6 +516,10 @@ SpiLogManager getLogManager() { return logManager; } + public TempFileProvider getTempFileProvider() { + return tempFileProvider; + } + private ServerCachePlugin initServerCachePlugin() { if (config.isLocalOnlyL2Cache()) { localL2Caching = true; @@ -591,6 +599,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) { return new QueryPlanLoggerSqlServer(); case ORACLE: return new QueryPlanLoggerOracle(); + case DB2: + return new QueryPlanLoggerDb2(); default: return new QueryPlanLoggerExplain(); } @@ -611,6 +621,11 @@ private static class NoopDdl implements SpiDdlGenerator { this.ddlRun = ddlRun; } + @Override + public void runDdl() { + CoreLog.log.log(ERROR, "Manual DDL run not possible"); + } + @Override public void execute(boolean online) { if (online && ddlRun) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java index 0391582009..1743929189 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -121,7 +121,7 @@ public interface SpiOrmQueryRequest extends BeanQueryRequest, DocQueryRequ Map findMap(); /** - * Execute the findSingleAttributeList query. + * Execute the findSingleAttributeCollection query. */ > A findSingleAttributeCollection(A collection); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java new file mode 100644 index 0000000000..fe267898c9 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/VisitHandler.java @@ -0,0 +1,128 @@ +package io.ebeaninternal.server.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +import io.ebean.PersistVisitor; +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; +import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.BeanDescriptorManager; +import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; +import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; + +/** + * Handler to process persist graphs. It will allow you to visit a persist + * action (insert/update) and get information about all beans that will be + * affected by that action. This allows you to do validation and other things on + * a set of beans. + * + * @author Roland Praml, FOCONIS AG + * + */ +class VisitHandler { + + private final Set seen = Collections.newSetFromMap(new IdentityHashMap()); + private final BeanDescriptorManager beanDescriptorManager; + + public VisitHandler(BeanDescriptorManager beanDescriptorManager) { + this.beanDescriptorManager = beanDescriptorManager; + } + + public void visit(Object start, PersistVisitor visitor) { + if (start != null) { + if (start instanceof EntityBean) { + visitBean((EntityBean) start, visitor); + } else { + visitMany(start, visitor, true); + } + } + visitor.visitEnd(); + } + + private void visitBean(EntityBean bean, PersistVisitor visitor) { + if (!seen.add(bean)) { + return; + } + visitor = visitor.visitBean(bean); + if (visitor != null) { + BeanDescriptor desc = beanDescriptorManager.descriptor(bean.getClass()); + visitOnes(bean, visitor, desc, desc.propertiesOneImportedSave()); + visitOnes(bean, visitor, desc, desc.propertiesOneExportedSave()); + visitManys(bean, visitor, desc, desc.propertiesManySave()); + visitor.visitEnd(); + } + } + + private void visitManys(EntityBean bean, PersistVisitor visitor, BeanDescriptor desc, + BeanPropertyAssocMany[] manys) { + EntityBeanIntercept ebi = bean._ebean_getIntercept(); + for (BeanPropertyAssocMany many : manys) { + // check that property is loaded and collection should be cascaded to + if (ebi.isLoadedProperty(many.propertyIndex()) + && !many.isSkipSaveBeanCollection(bean, ebi.isNew()) + && !many.isElementCollection()) { + Object manyValue = many.getValue(bean); + if (manyValue != null) { + + PersistVisitor propertyVisitor = visitor.visitProperty(many); + if (propertyVisitor != null) { + visitMany(manyValue, propertyVisitor, many.cascadeInfo().isSave()); + propertyVisitor.visitEnd(); + } + } + } + } + } + + private void visitMany(Object many, PersistVisitor visitor, boolean cascadeSave) { + if (!seen.add(many)) { + return; + } + if (many instanceof Collection) { + Collection coll = (Collection) many; + PersistVisitor collectionVisitor = visitor.visitCollection(coll); + if (collectionVisitor != null) { + if (cascadeSave) { + for (Object elem : coll) { + visitBean((EntityBean) elem, collectionVisitor); + } + } + collectionVisitor.visitEnd(); + } + } else if (many instanceof Map) { + Map map = (Map) many; + PersistVisitor mapVisitor = visitor.visitMap(map); + if (mapVisitor != null) { + if (cascadeSave) { + for (Object elem : ((Map) many).values()) { + visitBean((EntityBean) elem, mapVisitor); + } + } + mapVisitor.visitEnd(); + } + } else { + throw new IllegalArgumentException("Object " + many + " cannot be visited in persist graph"); + } + } + + private void visitOnes(EntityBean bean, PersistVisitor visitor, BeanDescriptor desc, + BeanPropertyAssocOne[] ones) { + EntityBeanIntercept ebi = bean._ebean_getIntercept(); + for (BeanPropertyAssocOne prop : ones) { + if (ebi.isLoadedProperty(prop.propertyIndex())) { + EntityBean detailBean = prop.valueAsEntityBean(bean); + if (detailBean != null && !prop.isSaveRecurseSkippable(detailBean) && !prop.isReference(detailBean)) { + PersistVisitor propertyVisitor = visitor.visitProperty(prop); + if (propertyVisitor != null) { + visitBean(detailBean, propertyVisitor); + propertyVisitor.visitEnd(); + } + } + } + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java index 4647310249..b7bb5d4aff 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java @@ -11,6 +11,7 @@ import io.ebean.event.changelog.ChangeLogRegister; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; +import io.ebean.plugin.CustomDeployParser; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.api.CoreLog; @@ -53,6 +54,7 @@ public class BootupClasses implements Predicate> { private final List> beanPersistListenerCandidates = new ArrayList<>(); private final List> beanQueryAdapterCandidates = new ArrayList<>(); private final List> serverConfigStartupCandidates = new ArrayList<>(); + private final List> customDeployParserCandidates = new ArrayList<>(); private final List idGeneratorInstances = new ArrayList<>(); private final List beanPersistControllerInstances = new ArrayList<>(); @@ -62,6 +64,7 @@ public class BootupClasses implements Predicate> { private final List beanPersistListenerInstances = new ArrayList<>(); private final List beanQueryAdapterInstances = new ArrayList<>(); private final List serverConfigStartupInstances = new ArrayList<>(); + private final List customDeployParserInstances = new ArrayList<>(); // single objects private Class changeLogPrepareClass; @@ -171,6 +174,10 @@ public void addServerConfigStartup(List startupInstances) { add(startupInstances, serverConfigStartupInstances, serverConfigStartupCandidates); } + public void addCustomDeployParser(List customDeployParser) { + add(customDeployParser, customDeployParserInstances, customDeployParserCandidates); + } + public void addChangeLogInstances(DatabaseConfig config) { readAuditPrepare = config.getReadAuditPrepare(); readAuditLogger = config.getReadAuditLogger(); @@ -287,6 +294,10 @@ public List getBeanQueryAdapters() { return createAdd(beanQueryAdapterInstances, beanQueryAdapterCandidates); } + public List getCustomDeployParsers() { + return createAdd(customDeployParserInstances, customDeployParserCandidates); + } + /** * Return the list of Embeddable classes. */ @@ -406,6 +417,11 @@ private boolean isInterestingInterface(Class cls) { interesting = true; } + if (CustomDeployParser.class.isAssignableFrom(cls)) { + customDeployParserCandidates.add((Class) cls); + interesting = true; + } + // single instances, last assigned wins if (ChangeLogListener.class.isAssignableFrom(cls)) { changeLogListenerClass = (Class) cls; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index eddf39e5d7..64f7a25ae9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -1932,13 +1932,6 @@ public BeanDescriptor descriptor(Class otherType) { return owner.descriptor(otherType); } - /** - * Returns true, if the table is managed (i.e. an existing m2m relation). - */ - public boolean isTableManaged(String tableName) { - return owner.isTableManaged(tableName); - } - /** * Return the order column property. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 5f2792f6c1..f1f56249e8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -75,6 +75,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final BeanFinderManager beanFinderManager; private final PersistListenerManager persistListenerManager; private final BeanQueryAdapterManager beanQueryAdapterManager; + private final CustomDeployParserManager customDeployParserManager; private final NamingConvention namingConvention; private final DeployCreateProperties createProperties; private final BeanManagerFactory beanManagerFactory; @@ -88,10 +89,13 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final BootupClasses bootupClasses; private final String serverName; private final List> elementDescriptors = new ArrayList<>(); - private final Map, BeanTable> beanTableMap = new HashMap<>(); + private final Map beanTableMap = new HashMap<>(); + + private final Set managedTables = new HashSet<>(); private final Map> descMap = new HashMap<>(); private final Map> descQueueMap = new HashMap<>(); private final Map> beanManagerMap = new HashMap<>(); + private final Map descAliases = new HashMap<>(); private final Map>> tableToDescMap = new HashMap<>(); private final Map>> tableToViewDescMap = new HashMap<>(); private final DbIdentity dbIdentity; @@ -117,7 +121,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final Map draftTableMap = new HashMap<>(); // temporary collections used during startup and then cleared - private Map, DeployBeanInfo> deployInfoMap = new HashMap<>(); + private Map> deployInfoMap = new HashMap<>(); private Set> embeddedIdTypes = new HashSet<>(); private List> embeddedBeans = new ArrayList<>(); @@ -154,6 +158,7 @@ public BeanDescriptorManager(InternalConfiguration config) { this.persistListenerManager = new PersistListenerManager(bootupClasses); this.beanQueryAdapterManager = new BeanQueryAdapterManager(bootupClasses); this.beanFinderManager = new BeanFinderManager(bootupClasses); + this.customDeployParserManager = new CustomDeployParserManager(bootupClasses); this.transientProperties = new TransientProperties(); this.changeLogPrepare = config.changeLogPrepare(bootupClasses.getChangeLogPrepare()); this.changeLogListener = config.changeLogListener(bootupClasses.getChangeLogListener()); @@ -235,14 +240,22 @@ public SpiBeanType beanType(Class entityType) { } @Override - @SuppressWarnings("unchecked") public BeanDescriptor descriptor(Class entityType) { - return (BeanDescriptor) descMap.get(entityType.getName()); + BeanDescriptor ret = descriptorByClassName(entityType.getName()); + if (ret == null) { + for (Class iface : entityType.getInterfaces()) { + ret = descriptorByClassName(iface.getName()); + if (ret != null) { + return ret; + } + } + } + return ret; } @SuppressWarnings("unchecked") public BeanDescriptor descriptorByClassName(String entityClassName) { - return (BeanDescriptor) descMap.get(entityClassName); + return (BeanDescriptor) descMap.get(resolveAlias(entityClassName)); } @Override @@ -288,10 +301,12 @@ public Map deploy(List mappings) { try { createListeners(); readEntityDeploymentInitial(); + readOverridesAndAliases(); readXmlMapping(mappings); readEntityBeanTable(); readEntityDeploymentAssociations(); readInheritedIdGenerators(); + deployInfoMap.values().forEach(customDeployParserManager::parse); // creates the BeanDescriptors readEntityRelationships(); List> list = new ArrayList<>(descMap.values()); @@ -337,7 +352,7 @@ private void readEntityMapping(ClassLoader classLoader, XmapEntity entityDeploy) return; } - DeployBeanInfo info = deployInfoMap.get(entityClass); + DeployBeanInfo info = deploy(entityClass); if (info == null) { log.log(ERROR, "No entity bean for ebean.xml entry " + entityClassName); @@ -410,8 +425,7 @@ public List> beanTypes(String tableName) { @Override public boolean isTableManaged(String tableName) { - return tableToDescMap.get(tableName.toLowerCase()) != null - || tableToViewDescMap.get(tableName.toLowerCase()) != null; + return managedTables.contains(tableName); } /** @@ -551,7 +565,7 @@ public List> descriptorList() { } public BeanTable beanTable(Class type) { - return beanTableMap.get(type); + return beanTableMap.get(resolveAlias(type.getName())); } /** @@ -589,7 +603,12 @@ private String errNotRegistered(Class beanClass) { } private BeanManager beanManager(String beanClassName) { - return beanManagerMap.get(beanClassName); + return beanManagerMap.get(resolveAlias(beanClassName)); + } + + private String resolveAlias(String name) { + String altName = descAliases.get(name); + return altName == null ? name : altName; } /** @@ -614,7 +633,7 @@ private void logStatus() { */ @SuppressWarnings("unchecked") public DeployBeanInfo deploy(Class cls) { - return (DeployBeanInfo) deployInfoMap.get(cls); + return (DeployBeanInfo) deployInfoMap.get(resolveAlias(cls.getName())); } private void registerDescriptor(DeployBeanInfo info) { @@ -639,7 +658,7 @@ private void registerDescriptor(DeployBeanInfo info) { private void readEntityDeploymentInitial() { for (Class entityClass : bootupClasses.getEntities()) { DeployBeanInfo info = createDeployBeanInfo(entityClass); - deployInfoMap.put(entityClass, info); + deployInfoMap.put(entityClass.getName(), info); Class embeddedIdType = info.getEmbeddedIdType(); if (embeddedIdType != null) { embeddedIdTypes.add(embeddedIdType); @@ -647,7 +666,7 @@ private void readEntityDeploymentInitial() { } for (Class entityClass : bootupClasses.getEmbeddables()) { DeployBeanInfo info = createDeployBeanInfo(entityClass); - deployInfoMap.put(entityClass, info); + deployInfoMap.put(entityClass.getName(), info); if (embeddedIdTypes.contains(entityClass)) { // register embeddedId types early - scalar properties only // and needed for creating BeanTables (id properties) @@ -673,7 +692,10 @@ private void registerEmbeddedBean(DeployBeanInfo info) { private void readEntityBeanTable() { for (DeployBeanInfo info : deployInfoMap.values()) { BeanTable beanTable = createBeanTable(info); - beanTableMap.put(beanTable.getBeanType(), beanTable); + beanTableMap.put(beanTable.getBeanType().getName(), beanTable); + if (beanTable.getBaseTable() != null) { + managedTables.add(beanTable.getBaseTable()); + } } // register non-id embedded beans (after bean tables are created) for (DeployBeanInfo info : embeddedBeans) { @@ -697,7 +719,7 @@ private void readInheritedIdGenerators() { DeployBeanDescriptor descriptor = info.getDescriptor(); InheritInfo inheritInfo = descriptor.getInheritInfo(); if (inheritInfo != null && !inheritInfo.isRoot()) { - DeployBeanInfo rootBeanInfo = deployInfoMap.get(inheritInfo.getRoot().getType()); + DeployBeanInfo rootBeanInfo = deploy(inheritInfo.getRoot().getType()); PlatformIdGenerator rootIdGen = rootBeanInfo.getDescriptor().getIdGenerator(); if (rootIdGen != null) { descriptor.setIdGenerator(rootIdGen); @@ -738,13 +760,79 @@ private void readEntityRelationships() { } } + /** + * Finds the overrides by priority. Computes a map with the base class as key + * and the descriptor, that shoud replace that class. Modifies the deployInfoMap + * and descAliases accordingly. + */ + private void readOverridesAndAliases() { + Map, DeployBeanInfo> overrides = new HashMap<>(); + + for (DeployBeanInfo info : deployInfoMap.values()) { + DeployBeanDescriptor desc = info.getDescriptor(); + Integer prio = desc.getOverridePriority(); + if (prio != null) { + Class base = desc.getBaseBeanType(); + Class child = desc.getBeanType(); + log.log(DEBUG, "{0} overridden by {1} with priority {2}", base.getName(), child.getName(), prio); + + // check, if we already have found an override for this base and take the one + // with + // lowest prio + DeployBeanInfo override = overrides.get(base); + if (override == null) { + overrides.put(base, info); + } else { + int delta = override.getDescriptor().getOverridePriority().compareTo(prio); + if (delta > 0) { + overrides.put(base, info); + } else if (delta == 0) { + throw new IllegalStateException("There are two or more implementations for " + base.getName() + + " with priority " + prio + ". Conflicting entity: " + desc.getBeanType().getName() + " - " + + override.getDescriptor().getBeanType().getName()); + } + } + } + } + + overrides.forEach((key, value) -> { + deployInfoMap.remove(key.getName()); // remove the original descriptor + + DeployBeanDescriptor newDesc = value.getDescriptor(); + newDesc.setOverridePriority(null); // clear priority (so no discard will occur) + + deployInfoMap.put(newDesc.getBeanType().getName(), value); + descAliases.put(key.getName(), newDesc.getBeanType().getName()); + }); + + + // remove all left overrides classes (they are discarded) + deployInfoMap.values().removeIf(info -> info.getDescriptor().getOverridePriority() != null); + + for (DeployBeanInfo info : deployInfoMap.values()) { + for (Class iface : info.getDescriptor().getInterfaces()) { + String conflict = descAliases.put(iface.getName(), info.getDescriptor().getBeanType().getName()); + if (conflict != null) { + throw new IllegalStateException("There are two or more implementations for " + iface + + ". Conflicting entity: " + conflict + " - " + info.getDescriptor().getBeanType().getName()); + } + } + } + + // output alias map + if (log.isLoggable(INFO)) { + descAliases.forEach((key, value) -> log.log(INFO, "{0} alias for {1}", key, value)); + } + } + + /** * Sets the inheritance info. */ private void setInheritanceInfo(DeployBeanInfo info) { for (DeployBeanPropertyAssocOne oneProp : info.getDescriptor().propertiesAssocOne()) { if (!oneProp.isTransient()) { - DeployBeanInfo assoc = deployInfoMap.get(oneProp.getTargetType()); + DeployBeanInfo assoc = deploy(oneProp.getTargetType()); if (assoc != null) { oneProp.getTableJoin().setInheritInfo(assoc.getDescriptor().getInheritInfo()); } @@ -752,7 +840,7 @@ private void setInheritanceInfo(DeployBeanInfo info) { } for (DeployBeanPropertyAssocMany manyProp : info.getDescriptor().propertiesAssocMany()) { if (!manyProp.isTransient()) { - DeployBeanInfo assoc = deployInfoMap.get(manyProp.getTargetType()); + DeployBeanInfo assoc = deploy(manyProp.getTargetType()); if (assoc != null) { manyProp.getTableJoin().setInheritInfo(assoc.getDescriptor().getInheritInfo()); } @@ -809,7 +897,7 @@ private void checkMappedBy(DeployBeanInfo info, List targetDescriptor(DeployBeanPropertyAssoc prop) { Class targetType = prop.getTargetType(); - DeployBeanInfo info = deployInfoMap.get(targetType); + DeployBeanInfo info = deploy(targetType); if (info == null) { throw new PersistenceException("Can not find descriptor [" + targetType + "] for " + prop.getFullBeanName()); } @@ -896,7 +984,7 @@ private void makeOrderColumn(DeployBeanPropertyAssocMany oneToMany) { final InheritInfo targetInheritInfo = targetDesc.getInheritInfo(); if (targetInheritInfo != null) { for (InheritInfo child : targetInheritInfo.getChildren()) { - final DeployBeanDescriptor childDescriptor = deployInfoMap.get(child.getType()).getDescriptor(); + final DeployBeanDescriptor childDescriptor = deploy(child.getType()).getDescriptor(); childDescriptor.setOrderColumn(orderProperty); } } @@ -1161,11 +1249,12 @@ private DeployBeanInfo createDeployBeanInfo(Class beanClass) { // set bean controller, finder and listener setBeanControllerFinderListener(desc); deplyInherit.process(desc); - desc.checkInheritanceMapping(); createProperties.createProperties(desc); DeployBeanInfo info = new DeployBeanInfo<>(deployUtil, desc); readAnnotations.readInitial(info); + + desc.checkInheritanceMapping(); return info; } @@ -1442,7 +1531,7 @@ private void addPrimaryKeyJoin(DeployBeanPropertyAssocOne prop) { String baseTable = prop.getDesc().getBaseTable(); DeployTableJoin inverse = prop.getTableJoin().createInverse(baseTable); TableJoin inverseJoin = new TableJoin(inverse, prop.getForeignKey()); - DeployBeanInfo target = deployInfoMap.get(prop.getTargetType()); + DeployBeanInfo target = deploy(prop.getTargetType()); target.setPrimaryKeyJoin(inverseJoin); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 8c1b208df6..1b84c6c55b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -883,6 +883,13 @@ public boolean isImportedPrimaryKey() { return importedPrimaryKey; } + /** + * If true, this property references O2O with its primary key. + */ + public boolean isPrimaryKeyExport() { + return false; + } + @Override public boolean isAssocMany() { // Returns false - override in BeanPropertyAssocMany. @@ -1014,7 +1021,7 @@ public List dbMigrationInfos() { /** * Return the bean Field associated with this property. */ - private Field field() { + public Field field() { return field; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index feec8f8cec..c502b13551 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -38,9 +38,12 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST * Join for manyToMany intersection table. */ private final TableJoin intersectionJoin; + + private final boolean tableManaged; private final String intersectionPublishTable; private final String intersectionDraftTable; private final boolean orphanRemoval; + private final IntersectionFactoryHelp intersectionFactory; private IntersectionTable intersectionTable; /** * For ManyToMany this is the Inverse join used to build reference queries. @@ -99,10 +102,13 @@ public BeanPropertyAssocMany(BeanDescriptor descriptor, DeployBeanPropertyAss this.mapKey = deploy.getMapKey(); this.fetchOrderBy = deploy.getFetchOrderBy(); this.intersectionJoin = deploy.createIntersectionTableJoin(); + this.intersectionFactory = deploy.getIntersectionFactory(); if (intersectionJoin != null) { + this.tableManaged = deploy.isTableManaged(); this.intersectionPublishTable = intersectionJoin.getTable(); this.intersectionDraftTable = deploy.getIntersectionDraftTable(); } else { + this.tableManaged = false; this.intersectionPublishTable = null; this.intersectionDraftTable = null; } @@ -914,11 +920,11 @@ public boolean isIncludeCascadeSave() { // Note ManyToMany always included as we always 'save' // the relationship via insert/delete of intersection table // REMOVALS means including PrivateOwned relationships - return cascadeInfo.isSave() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; + return cascadeInfo.isSave() || (hasJoinTable() && !tableManaged) || ModifyListenMode.REMOVALS == modifyListenMode; } public boolean isIncludeCascadeDelete() { - return cascadeInfo.isDelete() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; + return cascadeInfo.isDelete() || (hasJoinTable() && !tableManaged) || ModifyListenMode.REMOVALS == modifyListenMode; } boolean isCascadeDeleteEscalate() { @@ -1052,9 +1058,18 @@ public void bindElementValue(SqlUpdate insert, Object value) { public boolean createJoinTable() { if (hasJoinTable() && mappedBy() == null) { // only create on other 'owning' side - return !descriptor.isTableManaged(intersectionJoin.getTable()); + return !tableManaged; } else { return false; } } + + public boolean isTableManaged() { + return tableManaged; + } + + public IntersectionFactoryHelp getIntersectionFactory() { + return intersectionFactory; + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index b97c0a75a1..67a47d9d60 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -364,6 +364,11 @@ public boolean isOrphanRemoval() { return orphanRemoval; } + @Override + public boolean isPrimaryKeyExport() { + return primaryKeyExport; + } + @Override public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean) { Object newEmb = (newBean == null) ? null : getValue(newBean); @@ -601,20 +606,27 @@ private ExportedProperty findMatch(boolean embeddedProp, BeanProperty prop) { return findMatch(embeddedProp, prop, prop.dbColumn(), tableJoin); } + /** + * If column is a primaryKeyExport colum, we can directly use our own ID and do not need to add a join if the relation is not optional + */ + boolean requiresJoin() { + return !primaryKeyExport || isNullable(); + } + @Override public void appendSelect(DbSqlContext ctx, boolean subQuery) { if (!isTransient) { - if (primaryKeyExport) { - descriptor.idProperty().appendSelect(ctx, subQuery); - } else { + if (requiresJoin()) { localHelp.appendSelect(ctx, subQuery); + } else { + descriptor.idProperty().appendSelect(ctx, subQuery); } } } @Override public void appendFrom(DbSqlContext ctx, SqlJoinType joinType, String manyWhere) { - if (!isTransient && !primaryKeyExport) { + if (!isTransient && requiresJoin()) { localHelp.appendFrom(ctx, joinType); if (sqlFormulaJoin != null) { ctx.appendFormulaJoin(sqlFormulaJoin, joinType, manyWhere); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java new file mode 100644 index 0000000000..93a86d98b7 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.deploy; + +import java.util.List; + +import io.ebean.plugin.CustomDeployParser; +import io.ebeaninternal.server.core.bootup.BootupClasses; +import io.ebeaninternal.server.deploy.parse.DeployBeanInfo; + +public class CustomDeployParserManager { + + private final List parsers; + + public CustomDeployParserManager(BootupClasses bootupClasses) { + parsers = bootupClasses.getCustomDeployParsers(); + } + + public void parse(DeployBeanInfo value) { + for (CustomDeployParser parser : parsers) { + parser.parse(value.getDescriptor(), value.getUtil().dbPlatform()); + } + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionFactoryHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionFactoryHelp.java new file mode 100644 index 0000000000..e9bafa2890 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionFactoryHelp.java @@ -0,0 +1,66 @@ +package io.ebeaninternal.server.deploy; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * Helper class to construct intersection beans. + * + * @author Roland Praml, FOCONIS AG + */ +public class IntersectionFactoryHelp { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + private final MethodHandle handle; + + public IntersectionFactoryHelp(Class clazz, Class leftSide, Class rightSide, String factoryMethod) { + try { + if (factoryMethod.isEmpty()) { + Constructor ctor = findCtor(clazz, leftSide, rightSide); + handle = LOOKUP.findConstructor(clazz, MethodType.methodType(void.class, ctor.getParameterTypes())); + } else { + Method method = findMethod(clazz, factoryMethod, leftSide, rightSide); + handle = LOOKUP.findStatic(clazz, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes())); + } + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException("The factory" + clazz.getName() + + " must define a public constructor or static factory method that accepts (" + leftSide.getName() + ", " + rightSide.getName() + ")", e); + } + } + + private Constructor findCtor(Class clazz, Class leftSide, Class rightSide) throws NoSuchMethodException { + for (Constructor constructor : clazz.getConstructors()) { + Class[] types = constructor.getParameterTypes(); + if (types.length == 2) { + // we are only interested in ctors with 2 arguments + if (types[0].isAssignableFrom(leftSide) && types[1].isAssignableFrom(rightSide)) { + return constructor; + } + } + } + throw new NoSuchMethodException("Could not find valid constructor"); + } + + private Method findMethod(Class clazz, String methodName, Class leftSide, Class rightSide) throws NoSuchMethodException { + for (Method method : clazz.getMethods()) { + Class[] types = method.getParameterTypes(); + if (types.length == 2 && method.getName().equals(methodName)) { + // we are only interested in ctors with 2 arguments + if (types[0].isAssignableFrom(leftSide) && types[1].isAssignableFrom(rightSide)) { + return method; + } + } + } + throw new NoSuchMethodException("Could not find valid method"); + } + + public Object invoke(Object left, Object right) { + try { + return handle.invoke(left, right); + } catch (Throwable e) { + throw new RuntimeException("Unexpected error creating Intersection bean", e); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java index f5480ac77f..c89ece00e8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java @@ -65,7 +65,7 @@ public SpiSqlUpdate createInsert(SpiEbeanServer server) { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { + public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode, String extraWhere) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); if (deleteMode.isHard()) { @@ -89,17 +89,33 @@ public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { bindParams.setParameter(++count, bindValue); } } + addExtraWhere(sb, extraWhere); + return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server) { + public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server, String extraWhere) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); sb.append("delete from ").append(tableName).append(" where "); setBindParams(bindParams, sb); + addExtraWhere(sb, extraWhere); return new DefaultSqlUpdate(server, sb.toString(), bindParams); } + private void addExtraWhere(StringBuilder sb, String extraWhere) { + if (extraWhere != null) { + if (extraWhere.indexOf("${ta}") == -1) { + // no table alias append ${mta} to query. + sb.append(" and ").append(extraWhere.replace("${mta}", tableName)); + } else if (extraWhere.indexOf("${mta}") != -1) { + // we have a table alias - this is not interesting for deletion. + // but if have also a m2m table alias - this is a problem now! + throw new UnsupportedOperationException("extraWhere \'" + extraWhere + "\' has both ${ta} and ${mta} - this is not yet supported"); + } + } + } + private int setBindParams(BindParams bindParams, StringBuilder sb) { int count = 0; for (Map.Entry entry : values.entrySet()) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index dfeacbd839..e1b127b4ec 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -3,6 +3,7 @@ import io.ebean.annotation.Cache; import io.ebean.annotation.DocStore; import io.ebean.annotation.DocStoreMode; +import io.ebean.annotation.ext.EntityOverride; import io.ebean.annotation.Identity; import io.ebean.config.DatabaseConfig; import io.ebean.config.TableName; @@ -15,7 +16,9 @@ import io.ebean.event.BeanPostLoad; import io.ebean.event.BeanQueryAdapter; import io.ebean.event.changelog.ChangeLogFilter; +import io.ebean.plugin.DeployBeanDescriptorMeta; import io.ebean.text.PathProperties; +import io.ebean.util.AnnotationUtil; import io.ebean.util.SplitName; import io.ebeaninternal.api.ConcurrencyMode; import io.ebeaninternal.server.core.CacheOptions; @@ -45,19 +48,28 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Describes Beans including their deployment information. */ -public class DeployBeanDescriptor { +public class DeployBeanDescriptor implements DeployBeanDescriptorMeta { private static final Map EMPTY_NAMED_QUERY = new HashMap<>(); private static final Map EMPTY_RAW_MAP = new HashMap<>(); + /** + * Returns true, if the table is managed (i.e. an existing m2m relation). + */ + public boolean isTableManaged(String tableName) { + return manager.isTableManaged(tableName); + } + private static class PropOrder implements Comparator { @Override @@ -153,6 +165,10 @@ public int compare(DeployBeanProperty o1, DeployBeanProperty o2) { private DeployBeanProperty idProperty; private TableJoin primaryKeyJoin; + private final Set> interfaces = new HashSet<>(); + + private Integer overridePriority; + /** * Construct the BeanDescriptor. */ @@ -192,7 +208,7 @@ public TableJoin getPrimaryKeyJoin() { /** * Return the DeployBeanInfo for the given bean class. */ - DeployBeanInfo getDeploy(Class cls) { + public DeployBeanInfo getDeploy(Class cls) { return manager.deploy(cls); } @@ -620,6 +636,7 @@ public String[] getDependentTables() { * Return the base table. Only properties mapped to the base table are by * default persisted. */ + @Override public String getBaseTable() { return baseTable; } @@ -697,6 +714,7 @@ public Collection properties() { /** * Get a BeanProperty by its name. */ + @Override public DeployBeanProperty getBeanProperty(String propName) { return propMap.get(propName); } @@ -816,6 +834,7 @@ public String toString() { /** * Return a collection of all BeanProperty deployment information. */ + @Override public Collection propertiesAll() { return propMap.values(); } @@ -885,6 +904,7 @@ public String getSinglePrimaryKeyColumn() { /** * Return the BeanProperty that is the Id. */ + @Override public DeployBeanProperty idProperty() { if (idProperty != null) { return idProperty; @@ -966,7 +986,7 @@ public List propertiesBase() { * Check the mapping for class inheritance */ public void checkInheritanceMapping() { - if (inheritInfo == null) { + if (inheritInfo == null && overridePriority == null) { checkInheritance(getBeanType()); } } @@ -1129,4 +1149,55 @@ private String getDeployWord(String expression) { } } + /** + * Adds a concrete interface, that will be registered as alias for a concrete implementation. + */ + public void addInterface(Class iface) { + if (!iface.isAssignableFrom(beanType)) { + throw new ClassCastException("Cannot cast " + iface.getName() + " to " + beanType.getName()); + } + interfaces.add(iface); + } + + /** + * Returns the interfaces. + */ + public Set> getInterfaces() { + return interfaces; + } + + /** + * Sets the override priority. + */ + public void setOverridePriority(Integer overridePriority) { + this.overridePriority = overridePriority; + } + + /** + * returns the override priority, or null if this is a base entity. + */ + public Integer getOverridePriority() { + return overridePriority; + } + + /** + * Returns the base bean type, if {@link EntityOverride} was used. + */ + public Class getBaseBeanType() { + Class base = getBeanType(); + while (AnnotationUtil.has(base, EntityOverride.class)) { + base = base.getSuperclass(); + } + return base; + } + + @Override + public String getDiscriminatorColumn() { + return inheritInfo == null ? null : inheritInfo.getDiscriminatorColumn(); + } + + @Override + public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class propertyType) { + return getDeploy(propertyType).getDescriptor(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 2507387ab7..4f571c2285 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -1,11 +1,23 @@ package io.ebeaninternal.server.deploy.meta; -import io.ebean.annotation.*; +import io.ebean.annotation.DocCode; +import io.ebean.annotation.DocProperty; +import io.ebean.annotation.DocSortable; +import io.ebean.annotation.Formula; +import io.ebean.annotation.MutationDetection; +import io.ebean.annotation.Platform; +import io.ebean.annotation.SoftDelete; +import io.ebean.annotation.WhenCreated; +import io.ebean.annotation.WhenModified; +import io.ebean.annotation.Where; +import io.ebean.annotation.WhoCreated; +import io.ebean.annotation.WhoModified; import io.ebean.config.ScalarTypeConverter; import io.ebean.config.dbplatform.DbDefaultValue; import io.ebean.config.dbplatform.DbEncrypt; import io.ebean.config.dbplatform.DbEncryptFunction; import io.ebean.core.type.ScalarType; +import io.ebean.plugin.DeployBeanPropertyMeta; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.deploy.BeanProperty; @@ -35,7 +47,7 @@ * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ -public class DeployBeanProperty { +public class DeployBeanProperty implements DeployBeanPropertyMeta { private static final int ID_ORDER = 1000000; private static final int UNIDIRECTIONAL_ORDER = 100000; @@ -404,6 +416,7 @@ public void setOwningType(Class owningType) { this.owningType = owningType; } + @Override public Class getOwningType() { return owningType; } @@ -432,6 +445,7 @@ public void setSetter(BeanPropertySetter setter) { /** * Return the name of the property. */ + @Override public String getName() { return name; } @@ -446,6 +460,7 @@ public void setName(String name) { /** * Return the bean Field associated with this property. */ + @Override public Field getField() { return field; } @@ -549,6 +564,7 @@ public String getSqlFormulaJoin() { /** * The property is based on a formula. */ + @Override public void setSqlFormula(String formulaSelect, String formulaJoin) { this.sqlFormulaSelect = formulaSelect; this.sqlFormulaJoin = formulaJoin.isEmpty() ? null : formulaJoin; @@ -652,6 +668,7 @@ public String getElPlaceHolder() { /** * The database column name this is mapped to. */ + @Override public String getDbColumn() { if (sqlFormulaSelect != null) { return sqlFormulaSelect; @@ -845,6 +862,7 @@ public void setTransient() { /** * Return the property type. */ + @Override public Class getPropertyType() { return propertyType; } @@ -1123,7 +1141,7 @@ public Where getMetaAnnotationWhere(Platform platform) { private boolean matchPlatform(Platform[] platforms, Platform match) { for (Platform platform : platforms) { - if (platform == match) { + if (platform == match || platform == match.base()) { return true; } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java index 3461be3bd8..123972cb0d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.deploy.meta; +import io.ebean.plugin.DeployBeanPropertyAssocMeta; import io.ebeaninternal.server.deploy.BeanCascadeInfo; import io.ebeaninternal.server.deploy.BeanTable; import io.ebeaninternal.server.deploy.PropertyForeignKey; @@ -7,7 +8,7 @@ /** * Abstract base for properties mapped to an associated bean, list, set or map. */ -public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty { +public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty implements DeployBeanPropertyAssocMeta { /** * The type of the joined bean. @@ -129,6 +130,7 @@ public PropertyForeignKey getForeignKey() { * this 'master' bean. *

*/ + @Override public String getMappedBy() { return mappedBy; } @@ -173,4 +175,9 @@ public void setFetchPreference(int fetchPreference) { public void setTargetType(Class targetType) { this.targetType = (Class)targetType; } + + @Override + public String getBaseTable() { + return getBeanTable().getBaseTable(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java index c201308e2d..45f61b77ee 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java @@ -2,6 +2,7 @@ import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.IntersectionFactoryHelp; import io.ebeaninternal.server.deploy.ManyType; import io.ebeaninternal.server.deploy.TableJoin; import io.ebeaninternal.server.type.TypeReflectHelper; @@ -32,6 +33,12 @@ public class DeployBeanPropertyAssocMany extends DeployBeanPropertyAssoc { * Join for manyToMany intersection table. */ private DeployTableJoin intersectionJoin; + + /** + * Factory to create intersection beans (instead of rows). For managed intersections. + */ + private IntersectionFactoryHelp intersectionFactory; + /** * For ManyToMany this is the Inverse join used to build reference queries. */ @@ -113,6 +120,10 @@ public TableJoin createIntersectionTableJoin() { } } + public boolean isTableManaged() { + return intersectionJoin != null && desc.isTableManaged(intersectionJoin.getTable()); + } + /** * Create the immutable version of the inverse join. */ @@ -149,6 +160,20 @@ public void setInverseJoin(DeployTableJoin inverseJoin) { this.inverseJoin = inverseJoin; } + /** + * Return the intersection factory. + */ + public IntersectionFactoryHelp getIntersectionFactory() { + return intersectionFactory; + } + + /** + * Sets the intersection factory. + */ + public void setIntersectionFactory(IntersectionFactoryHelp intersectionFactory) { + this.intersectionFactory = intersectionFactory; + } + /** * Return the order by clause used to order the fetching of the data for * this list, set or map. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java index 0d29c3ba41..586164ffd0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -4,15 +4,13 @@ import io.ebean.annotation.FetchPreference; import io.ebean.annotation.HistoryExclude; import io.ebean.annotation.Where; +import io.ebean.annotation.ext.IntersectionFactory; import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebean.config.NamingConvention; import io.ebean.config.TableName; import io.ebean.core.type.ScalarType; import io.ebean.util.CamelCaseHelper; -import io.ebeaninternal.server.deploy.BeanDescriptorManager; -import io.ebeaninternal.server.deploy.BeanProperty; -import io.ebeaninternal.server.deploy.BeanTable; -import io.ebeaninternal.server.deploy.PropertyForeignKey; +import io.ebeaninternal.server.deploy.*; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; @@ -128,6 +126,11 @@ private void read(DeployBeanPropertyAssocMany prop) { prop.getTableJoin().addJoinColumn(util, true, joinColumns, beanTable); } + IntersectionFactory intersectionFactory = get(prop, IntersectionFactory.class); + if (intersectionFactory != null) { + readIntersectionFactory(prop, intersectionFactory); + } + JoinTable joinTable = get(prop, JoinTable.class); if (joinTable != null) { if (prop.isManyToMany()) { @@ -165,6 +168,12 @@ private void read(DeployBeanPropertyAssocMany prop) { } } + private void readIntersectionFactory(DeployBeanPropertyAssocMany prop, IntersectionFactory factory) { + Class leftSide = descriptor.getBeanType(); + Class rightSide = prop.getPropertyType(); + prop.setIntersectionFactory(new IntersectionFactoryHelp(factory.value(), leftSide, rightSide, factory.factoryMethod())); + } + private void checkSelfManyToMany(DeployBeanPropertyAssocMany prop) { if (prop.getTargetType().equals(descriptor.getBeanType())) { throw new IllegalStateException("@ManyToMany mapping for " + prop.getFullBeanName() + " requires explicit @JoinTable with joinColumns & inverseJoinColumns. Refer issue #2157"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java index 598962fcfd..7424f4d1e1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -122,7 +122,7 @@ private void readAssocOne(DeployBeanPropertyAssocOne prop) { } } - prop.setJoinType(prop.isNullable()); + prop.setJoinType(prop.isNullable() || prop.getForeignKey() != null && prop.getForeignKey().isNoConstraint()); if (!prop.getTableJoin().hasJoinColumns() && beanTable != null) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java index 662b73be80..68087e4261 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java @@ -2,7 +2,10 @@ import io.ebean.annotation.Index; import io.ebean.annotation.*; +import io.ebean.annotation.ext.EntityImplements; +import io.ebean.annotation.ext.EntityOverride; import io.ebean.config.TableName; +import io.ebean.util.AnnotationUtil; import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.server.deploy.BeanDescriptor.EntityType; import io.ebeaninternal.server.deploy.IndexDefinition; @@ -12,6 +15,7 @@ import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import javax.persistence.*; +import java.util.Set; import static io.ebean.util.AnnotationUtil.typeGet; import static java.lang.System.Logger.Level.ERROR; @@ -68,7 +72,7 @@ public void parse() { */ private void setTableName() { if (descriptor.isBaseTableType()) { - Class beanType = descriptor.getBeanType(); + Class beanType = descriptor.getBaseBeanType(); InheritInfo inheritInfo = descriptor.getInheritInfo(); if (inheritInfo != null) { beanType = inheritInfo.getRoot().getType(); @@ -184,6 +188,18 @@ private void read(Class cls) { for (NamedQuery namedQuery : annotationClassNamedQuery(cls)) { descriptor.addNamedQuery(namedQuery.name(), namedQuery.query()); } + + Set entityImplements = AnnotationUtil.typeGetAll(cls, EntityImplements.class); + for (EntityImplements ann : entityImplements) { + for (Class iface : ann.value()) { + descriptor.addInterface(iface); + } + } + + EntityOverride entityOverride = AnnotationUtil.typeGet(cls, EntityOverride.class); + if (entityOverride != null) { + descriptor.setOverridePriority(entityOverride.priority()); + } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1RndIdGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1RndIdGenerator.java index 21e34a8b3a..adbf5e0824 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1RndIdGenerator.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1RndIdGenerator.java @@ -106,7 +106,7 @@ public UUID nextId(Transaction t) { delta = current - last; if (delta < -10000 * 20000) { - log.log(INFO, "Clock skew of {} ms detected", delta / -10000); + log.log(INFO, "Clock skew of {0} ms detected", delta / -10000); // The clock was adjusted back about 2 seconds, or we were generating a lot of ids too fast // if so, we try to set the current as last and also increment the clockSeq. lock.lock(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java index 39e03eb95e..f8a3574cad 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java @@ -506,6 +506,9 @@ private boolean isIncludeProperty(BeanProperty prop) { return true; if (currentIncludeProps != null) { // explicitly controlled by pathProperties + if (prop.isId() && currentIncludeProps.contains("${identifier}")) { + return true; + } return currentIncludeProps.contains(prop.name()); } else { // include only loaded properties 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 f78e11ea83..7a27c3946b 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 @@ -915,7 +915,7 @@ void deleteManyIntersection(EntityBean bean, BeanPropertyAssocMany many, SpiT private SpiSqlUpdate deleteAllIntersection(EntityBean bean, BeanPropertyAssocMany many, boolean publish) { IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish); - return intRow.createDeleteChildren(server); + return intRow.createDeleteChildren(server, many.extraWhere()); } /** @@ -939,7 +939,10 @@ private void deleteAssocMany(PersistRequestBean request) { for (BeanPropertyAssocOne prop : expOnes) { // for soft delete check cascade type also supports soft delete if (deleteMode.isHard() || prop.isTargetSoftDelete()) { - if (request.isLoadedProperty(prop)) { + if (prop.isPrimaryKeyExport()) { + // we can delete by id, neither if property loaded or not + delete(prop.targetDescriptor(), prop.descriptor().id(parentBean), null, t, deleteMode); + } else if (request.isLoadedProperty(prop)) { Object detailBean = prop.getValue(parentBean); if (detailBean != null) { deleteRecurse((EntityBean) detailBean, t, deleteMode); @@ -1009,7 +1012,7 @@ void deleteManyDetails(SpiTransaction t, BeanDescriptor desc, EntityBean pare if (targetDesc.isDeleteByStatement()) { // Just delete all the children with one statement IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds); - SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode); + SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode, many.extraWhere()); executeSqlUpdate(sqlDelete, t); } else { 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 5b6858e784..7752245510 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 @@ -6,18 +6,10 @@ import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.api.SpiSqlUpdate; import io.ebeaninternal.server.core.PersistRequestBean; -import io.ebeaninternal.server.deploy.BeanCollectionUtil; -import io.ebeaninternal.server.deploy.BeanDescriptor; -import io.ebeaninternal.server.deploy.BeanProperty; -import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; -import io.ebeaninternal.server.deploy.IntersectionRow; +import io.ebeaninternal.server.deploy.*; import javax.persistence.PersistenceException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero; @@ -96,6 +88,22 @@ private boolean isSaveIntersection() { // OneToMany JoinTable return true; } + if (many.isTableManaged()) { + List tables = new ArrayList<>(3); + tables.add(many.descriptor().baseTable()); + tables.add(many.targetDescriptor().baseTable()); + tables.add(many.intersectionTableJoin().getTable()); + // put all tables in a deterministic order + tables.sort(Comparator.naturalOrder()); + + if (transaction.isSaveAssocManyIntersection(String.join("-", tables), many.descriptor().rootName())) { + // notify others, that we do save this transaction + transaction.isSaveAssocManyIntersection(many.intersectionTableJoin().getTable(), many.descriptor().rootName()); + return true; + } else { + return false; + } + } return transaction.isSaveAssocManyIntersection(many.intersectionTableJoin().getTable(), many.descriptor().rootName()); } @@ -299,14 +307,16 @@ private void saveAssocManyIntersection(boolean queue) { } transaction.depth(+1); + boolean needsFlush = false; if (deletions != null && !deletions.isEmpty()) { for (Object other : deletions) { EntityBean otherDelete = (EntityBean) other; // the object from the 'other' side of the ManyToMany // build a intersection row for 'delete' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish); - SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD); + SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD, many.extraWhere()); persister.executeOrQueue(sqlDelete, transaction, queue); + needsFlush = true; } } if (additions != null && !additions.isEmpty()) { @@ -322,6 +332,21 @@ private void saveAssocManyIntersection(boolean queue) { } else { if (!many.hasImportedId(otherBean)) { throw new PersistenceException("ManyToMany bean " + otherBean + " does not have an Id value."); + } else if (many.getIntersectionFactory() != null) { + // build a intersection bean for 'insert' + // They need to be executed very late and would normally go to Queue#2, but we do not have + // a SpiSqlUpdate for now. + if (needsFlush) { + transaction.flushBatchOnCascade(); + } + if (queue) { + transaction.depth(+100); + } + Object intersectionBean = many.getIntersectionFactory().invoke(parentBean, otherBean); + persister.saveRecurse((EntityBean) intersectionBean, transaction, parentBean, request.flags()); + if (queue) { + transaction.depth(-100); + } } else { // build a intersection row for 'insert' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlBeanPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlBeanPersister.java index 4a4abe17d5..1b71bc1a09 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlBeanPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlBeanPersister.java @@ -70,7 +70,8 @@ private int execute(PersistRequestBean request, PersistHandler handler) { } } catch (SQLException e) { // log the error to the transaction log - String msg = "Error: " + StringHelper.removeNewLines(e.getMessage()); + String msg = "Error[" + StringHelper.removeNewLines(e.getMessage()) + "] " + handler; + if (request.transaction().isLogSummary()) { request.transaction().logSummary(msg); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java index 58ec931fc5..97c483fc51 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java @@ -263,4 +263,14 @@ PreparedStatement getPstmtBatch(SpiTransaction t, String sql, PersistRequestBean return stmt; } + @Override + public String toString() { + if (sql == null) { + return "not yet initialized"; + } else if (bindLog == null || bindLog.length() == 0) { + return sql; + } else { + return Str.add(sql, " -- bind(", bindLog.toString(), ")"); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java new file mode 100644 index 0000000000..d6cb3f4dd6 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java @@ -0,0 +1,54 @@ + +package io.ebeaninternal.server.query; + +import io.ebeaninternal.api.CoreLog; +import io.ebeaninternal.api.SpiDbQueryPlan; +import io.ebeaninternal.api.SpiQueryPlan; +import io.ebeaninternal.server.bind.capture.BindCapture; + +import java.sql.*; +import java.util.Random; + +import static java.lang.System.Logger.Level.WARNING; + +/** + * A QueryPlanLogger for DB2. + *

+ * To use query plan capturing, you have to install the explain tables with + * SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ). + * To do this in a repeatable script, you may use this statement: + * + *

+ * BEGIN
+ * IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN
+ *    call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA );
+ * END IF;
+ * END
+ * 
+ * + * @author Roland Praml, FOCONIS AG + */ +public final class QueryPlanLoggerDb2 extends QueryPlanLogger { + + private Random rnd = new Random(); + + @Override + public SpiDbQueryPlan collectPlan(Connection conn, SpiQueryPlan plan, BindCapture bind) { + try (Statement stmt = conn.createStatement()) { + int queryNo = rnd.nextInt(Integer.MAX_VALUE); + try (PreparedStatement explainStmt = conn + .prepareStatement("EXPLAIN PLAN SET QUERYNO=" + queryNo + " FOR " + plan.sql())) { + bind.prepare(explainStmt, conn); + explainStmt.execute(); + } + + try (ResultSet rset = stmt.executeQuery("select * from EXPLAIN_STATEMENT where QUERYNO=" + queryNo)) { + return readQueryPlan(plan, bind, rset); + } + } catch (SQLException e) { + CoreLog.log.log(WARNING, "Could not log query plan", e); + return null; + } + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 412c74fc85..a38cfc51b4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -414,7 +414,7 @@ private void addProperty(SqlTreeProperties selectProps, STreeType desc, OrmQuery // make sure we only included the base/embedded bean once if (!selectProps.containsProperty(baseName)) { - STreeProperty p = desc.findPropertyWithDynamic(baseName, null); + STreeProperty p = desc.findProperty(baseName); if (p == null) { // maybe dynamic formula with schema prefix p = desc.findPropertyWithDynamic(propName, null); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java index 9fae2e21c8..4b8408c27a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java @@ -885,6 +885,9 @@ public final void postCommit() { public final void preCommit() { internalBatchFlush(); firePreCommit(); + // we must flush the batch queue again, because the callback can + // modify current transaction + internalBatchFlush(); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index c60386f30a..fdf77e8293 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -56,7 +56,7 @@ public final class DefaultTypeManager implements TypeManager { private final DefaultTypeFactory extraTypeFactory; - private final ScalarType fileType = new ScalarTypeFile(); + private final ScalarType fileType; private final ScalarType hstoreType = new ScalarTypePostgresHstore(); private final JsonConfig.DateTime jsonDateTime; @@ -94,6 +94,7 @@ public DefaultTypeManager(DatabaseConfig config, BootupClasses bootupClasses) { this.arrayTypeSetFactory = arrayTypeSetFactory(config.getDatabasePlatform()); this.offlineMigrationGeneration = DbOffline.isGenerateMigration(); this.defaultEnumType = config.getDefaultEnumType(); + this.fileType = new ScalarTypeFile(config.getTempFileProvider()); ServiceLoader mappers = ServiceLoader.load(ScalarJsonMapper.class); jsonMapper = mappers.findFirst().orElse(null); @@ -111,7 +112,10 @@ public DefaultTypeManager(DatabaseConfig config, BootupClasses bootupClasses) { } private void loadGeoTypeBinder(DatabaseConfig config) { - final GeoTypeProvider provider = ServiceUtil.service(GeoTypeProvider.class); + GeoTypeProvider provider = config.getServiceObject(GeoTypeProvider.class); + if (provider == null) { + provider = ServiceUtil.service(GeoTypeProvider.class); + } if (provider != null) { geoTypeBinder = provider.createBinder(config); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCalendar.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCalendar.java index 8ac56e5176..0331296cb5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCalendar.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeCalendar.java @@ -61,7 +61,7 @@ public Calendar convertFromInstant(Instant ts) { @Override protected String toJsonNanos(Calendar value) { - return String.valueOf(value.getTime()); + return String.valueOf(value.getTime().getTime()); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java index 5e4ad5df80..5dd6ff96e9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeFile.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import io.ebean.config.TempFileProvider; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -20,26 +21,22 @@ */ final class ScalarTypeFile extends ScalarTypeBase { - private final String prefix; - private final String suffix; - private final File directory; + private final TempFileProvider tempFileProvider; private final int bufferSize; /** - * Construct with reasonable defaults of Blob and 8096 buffer size. + * Construct with reasonable defaults of Blob and 8192 buffer size. */ - ScalarTypeFile() { - this(Types.LONGVARBINARY, "db-", null, null, 8096); + ScalarTypeFile(TempFileProvider tempFileProvider) { + this(Types.LONGVARBINARY, tempFileProvider, 8192); } /** * Create the ScalarTypeFile. */ - ScalarTypeFile(int jdbcType, String prefix, String suffix, File directory, int bufferSize) { + ScalarTypeFile(int jdbcType, TempFileProvider tempFileProvider, int bufferSize) { super(File.class, false, jdbcType); - this.prefix = prefix; - this.suffix = suffix; - this.directory = directory; + this.tempFileProvider = tempFileProvider; this.bufferSize = bufferSize; } @@ -66,7 +63,7 @@ public File read(DataReader reader) throws SQLException { } try { // stream from db into our temp file - File tempFile = File.createTempFile(prefix, suffix, directory); + File tempFile = tempFileProvider.createTempFile(); OutputStream os = getOutputStream(tempFile); pump(is, os); return tempFile; @@ -109,7 +106,7 @@ public void jsonWrite(JsonGenerator writer, File value) throws IOException { @Override public File jsonRead(JsonParser parser) throws IOException { - File tempFile = File.createTempFile(prefix, suffix, directory); + File tempFile = tempFileProvider.createTempFile(); try (OutputStream os = getOutputStream(tempFile)) { parser.readBinaryValue(os); os.flush(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java index 3de0184e1f..b5b523c149 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java @@ -11,6 +11,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.format.DateTimeParseException; /** * ScalarType for java.sql.Timestamp. @@ -63,7 +64,11 @@ public String formatValue(LocalDateTime value) { @Override public LocalDateTime parse(String value) { - return LocalDateTime.parse(value); + try { + return LocalDateTime.parse(value); + } catch (DateTimeParseException pe) { + return super.parse(value); + } } @Override diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java index 623402fb71..27aebb9d79 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import io.ebean.config.JsonConfig; + +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.StringWriter; @@ -123,4 +125,20 @@ public void isoJsonFormatParse() { LocalDateTime value = typeIso.fromJsonISO8601(asJson); assertThat(localDateTime).isEqualToIgnoringNanos(value); } + + @Test + @Disabled("Does not work @github due different timezone") + // Expecting actual: + // 2018-02-03T03:05:06 (java.time.LocalDateTime) + // to have same year, month, day, hour, minute and second as: + // 2018-02-03T04:05:06 (java.time.LocalDateTime) + public void testParseEbean11() { + + ScalarTypeLocalDateTime typeDefault = new ScalarTypeLocalDateTime(JsonConfig.DateTime.ISO8601); + + LocalDateTime fromMillis = typeDefault.parse("1517627106000"); + LocalDateTime fromIso= typeDefault.parse("2018-02-03T04:05:06"); + + assertThat(fromMillis).isEqualToIgnoringNanos(fromIso); + } } diff --git a/ebean-csv-reader/pom.xml b/ebean-csv-reader/pom.xml index fce50e8e76..dde65303a1 100644 --- a/ebean-csv-reader/pom.xml +++ b/ebean-csv-reader/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-csv-reader @@ -14,21 +14,21 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-csv-reader/src/test/java/io/ebean/csv/reader/CsvReaderTest.java b/ebean-csv-reader/src/test/java/io/ebean/csv/reader/CsvReaderTest.java index 8a871a62da..6504c219e5 100644 --- a/ebean-csv-reader/src/test/java/io/ebean/csv/reader/CsvReaderTest.java +++ b/ebean-csv-reader/src/test/java/io/ebean/csv/reader/CsvReaderTest.java @@ -11,10 +11,11 @@ import java.net.URL; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.Locale; class CsvReaderTest { - final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy"); + final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy", Locale.ENGLISH); @Test void test() throws Exception { diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index ec4a234564..2291d4810b 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean ddl generation @@ -28,14 +28,14 @@ io.ebean ebean-core-type - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided @@ -58,7 +58,7 @@ io.ebean ebean-platform-all - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java new file mode 100644 index 0000000000..1839cf4917 --- /dev/null +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java @@ -0,0 +1,56 @@ +package io.ebeaninternal.dbmigration; + +import java.io.IOException; + +import io.ebean.plugin.Plugin; +import io.ebean.plugin.SpiServer; + +/** + * Plugin to generate db-migration scripts automatically. + * @author Roland Praml, FOCONIS AG + */ +public class DbMigrationPlugin implements Plugin { + + private DefaultDbMigration dbMigration; + + private static String lastMigration; + private static String lastInit; + + @Override + public void configure(SpiServer server) { + dbMigration = new DefaultDbMigration(); + dbMigration.setServer(server); + } + + @Override + public void online(boolean online) { + try { + lastInit = null; + lastMigration = null; + if (dbMigration.generate) { + String tmp = lastMigration = dbMigration.generateMigration(); + if (tmp == null) { + return; + } + } + if (dbMigration.generateInit) { + lastInit = dbMigration.generateInitMigration(); + } + } catch (IOException e) { + throw new RuntimeException("Error while generating migration", e); + } + } + + @Override + public void shutdown() { + dbMigration = null; + } + + public static String getLastInit() { + return lastInit; + } + + public static String getLastMigration() { + return lastMigration; + } +} diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java index 7567ad4cf4..67c3b8f9a2 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java @@ -63,11 +63,10 @@ public DdlGenerator(SpiEbeanServer server) { if (!config.getTenantMode().isDdlEnabled() && config.isDdlRun()) { log.log(WARNING, "DDL can't be run on startup with TenantMode " + config.getTenantMode()); this.runDdl = false; - this.useMigrationStoredProcedures = false; } else { this.runDdl = config.isDdlRun(); - this.useMigrationStoredProcedures = config.getDatabasePlatform().useMigrationStoredProcedures(); } + this.useMigrationStoredProcedures = config.getDatabasePlatform() != null && config.getDatabasePlatform().useMigrationStoredProcedures(); this.scriptTransform = createScriptTransform(config); this.baseDir = initBaseDir(); } @@ -85,7 +84,7 @@ private File initBaseDir() { @Override public void execute(boolean online) { generateDdl(); - if (online) { + if (online && runDdl) { runDdl(); } } @@ -105,16 +104,15 @@ protected void generateDdl() { /** * Run the DDL drop and DDL create scripts if properties have been set. */ - protected void runDdl() { - if (runDdl) { - Connection connection = null; - try { - connection = obtainConnection(); - runDdlWith(connection); - } finally { - JdbcClose.rollback(connection); - JdbcClose.close(connection); - } + @Override + public void runDdl() { + Connection connection = null; + try { + connection = obtainConnection(); + runDdlWith(connection); + } finally { + JdbcClose.rollback(connection); + JdbcClose.close(connection); } } diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java index a96957d0ab..73857a05ee 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java @@ -1,6 +1,7 @@ package io.ebeaninternal.dbmigration; import io.avaje.applog.AppLog; +import io.avaje.classpath.scanner.core.Location; import io.ebean.DB; import io.ebean.Database; import io.ebean.annotation.Platform; @@ -12,6 +13,7 @@ import io.ebean.config.dbplatform.DatabasePlatformProvider; import io.ebean.dbmigration.DbMigration; import io.ebean.util.IOUtils; +import io.ebean.util.StringHelper; import io.ebeaninternal.api.DbOffline; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions; @@ -26,10 +28,7 @@ import java.io.File; import java.io.IOException; import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.ServiceLoader; +import java.util.*; import static io.ebeaninternal.api.PlatformMatch.matchPlatform; import static java.lang.System.Logger.Level.*; @@ -61,8 +60,8 @@ public class DefaultDbMigration implements DbMigration { private static final String initialVersion = "1.0"; private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY"; - private final List platformProviders = new ArrayList<>(); - protected final boolean online; + private List platformProviders = new ArrayList<>(); + protected boolean online; private boolean logToSystemOut = true; protected SpiEbeanServer server; protected String pathToResources = "src/main/resources"; @@ -77,8 +76,10 @@ public class DefaultDbMigration implements DbMigration { protected List platforms = new ArrayList<>(); protected DatabaseConfig databaseConfig; protected DbConstraintNaming constraintNaming; + @Deprecated protected Boolean strictMode; - protected Boolean includeGeneratedFileComment; + protected boolean includeGeneratedFileComment; + @Deprecated protected String header; protected String applyPrefix = ""; protected String version; @@ -88,6 +89,9 @@ public class DefaultDbMigration implements DbMigration { private int lockTimeoutSeconds; protected boolean includeBuiltInPartitioning = true; protected boolean includeIndex; + protected boolean generate = false; + protected boolean generateInit = false; + private boolean keepLastInit = true; /** * Create for offline migration generation. @@ -123,12 +127,66 @@ public void setServerConfig(DatabaseConfig config) { if (constraintNaming == null) { this.constraintNaming = databaseConfig.getConstraintNaming(); } + if (databasePlatform == null) { + this.databasePlatform = databaseConfig.getDatabasePlatform(); + } Properties properties = config.getProperties(); if (properties != null) { - PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, null); + PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, config.getClassLoadConfig()); migrationPath = props.get("migration.migrationPath", migrationPath); migrationInitPath = props.get("migration.migrationInitPath", migrationInitPath); pathToResources = props.get("migration.pathToResources", pathToResources); + addForeignKeySkipCheck = props.getBoolean("migration.addForeignKeySkipCheck", addForeignKeySkipCheck); + applyPrefix = props.get("migration.applyPrefix",applyPrefix); + databasePlatform = props.createInstance(DatabasePlatform.class, "migration.databasePlatform", databasePlatform); + generatePendingDrop = props.get("migration.generatePendingDrop", generatePendingDrop); + includeBuiltInPartitioning = props.getBoolean("migration.includeBuiltInPartitioning", includeBuiltInPartitioning); + includeGeneratedFileComment = props.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment); + includeIndex = props.getBoolean("migration.includeIndex", includeIndex); + lockTimeoutSeconds = props.getInt("migration.lockTimeoutSeconds", lockTimeoutSeconds); + logToSystemOut = props.getBoolean("migration.logToSystemOut", logToSystemOut); + modelPath = props.get("migration.modelPath", modelPath); + modelSuffix = props.get("migration.modelSuffix", modelSuffix); + name = props.get("migration.name", name); + online = props.getBoolean("migration.online", online); + vanillaPlatform = props.getBoolean("migration.vanillaPlatform", vanillaPlatform); + version = props.get("migration.version", version); + generate = props.getBoolean("migration.generate", generate); + generateInit = props.getBoolean("migration.generateInit", generateInit); + // header & strictMode must be configured at DatabaseConfig level + parsePlatforms(props, config); + } + } + + protected void parsePlatforms(PropertiesWrapper props, DatabaseConfig config) { + String platforms = props.get("migration.platforms"); + if (platforms == null || platforms.isEmpty()) { + return; + } + String[] tmp = StringHelper.splitNames(platforms); + for (String plat : tmp) { + DatabasePlatform dbPlatform; + String platformName = plat; + String platformPrefix = null; + int pos = plat.indexOf('='); + if (pos != -1) { + platformName = plat.substring(0, pos); + platformPrefix = plat.substring(pos + 1); + } + + if (platformName.indexOf('.') == -1) { + // parse platform as enum value + Platform platform = Enum.valueOf(Platform.class, platformName.toUpperCase()); + dbPlatform = platform(platform); + } else { + // parse platform as class + dbPlatform = (DatabasePlatform) config.getClassLoadConfig().newInstance(platformName); + } + if (platformPrefix == null) { + platformPrefix = dbPlatform.platform().name().toLowerCase(); + } + + addDatabasePlatform(dbPlatform, platformPrefix); } } @@ -319,7 +377,18 @@ private String generateMigrationFor(boolean initMigration) throws IOException { } String pendingVersion = generatePendingDrop(); - if (pendingVersion != null) { + if ("auto".equals(pendingVersion)) { + StringJoiner sj = new StringJoiner(","); + String diff = generateDiff(request); + if (diff != null) { + sj.add(diff); + request = createRequest(initMigration); + } + for (String pendingDrop : request.getPendingDrops()) { + sj.add(generatePendingDrop(request, pendingDrop)); + } + return sj.length() == 0 ? null : sj.toString(); + } else if (pendingVersion != null) { return generatePendingDrop(request, pendingVersion); } else { return generateDiff(request); @@ -376,6 +445,7 @@ private void configurePlatforms() { private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, boolean tablePartitioning) throws IOException { if (dbPlatform != null) { if (tablePartitioning && includeBuiltInPartitioning) { + generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltinTablePartitioning(), false); } // skip built-in migration stored procedures based on isUseMigrationStoredProcedures @@ -384,6 +454,7 @@ private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, bo } } + private void generateExtraDdlFor(File migrationDir, DatabasePlatform dbPlatform, ExtraDdl extraDdl, boolean checkSkip) throws IOException { if (extraDdl != null) { List ddlScript = extraDdl.getDdlScript(); @@ -554,7 +625,7 @@ private String generateMigration(Request request, Migration dbMigration, String return null; } else { if (!platforms.isEmpty()) { - writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir); + writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir, request.initMigration && keepLastInit); } else if (databasePlatform != null) { // writer needs the current model to provide table/column details for @@ -634,12 +705,17 @@ private String toUnderScore(String name) { /** * Write any extra platform ddl. */ - private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { + private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath, boolean clear) throws IOException { DdlOptions options = new DdlOptions(addForeignKeySkipCheck); for (Pair pair : platforms) { DdlWrite writer = new DdlWrite(new MConfiguration(), currentModel.read(), options); PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); File subPath = platformWriter.subPath(writePath, pair.prefix); + if (clear) { + for (File existing : subPath.listFiles()) { + existing.delete(); + } + } platformWriter.processMigration(dbMigration, writer, subPath, fullVersion); } } @@ -657,7 +733,7 @@ private boolean writeMigrationXml(Migration dbMigration, File resourcePath, Stri if (file.exists()) { return false; } - String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null; + String comment = includeGeneratedFileComment ? GENERATED_COMMENT : null; MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); xmlWriter.write(dbMigration, file); return true; @@ -675,6 +751,7 @@ private void setDefaults() { databasePlatform = server.databasePlatform(); } if (databaseConfig != null) { + // FIXME: Copy header and StrictMode to databaseConfig if (strictMode != null) { databaseConfig.setDdlStrictMode(strictMode); } @@ -749,15 +826,20 @@ public File migrationDirectory() { * Return the file path to write the xml and sql to. */ File migrationDirectory(boolean initMigration) { - // path to src/main/resources in typical maven project - File resourceRootDir = new File(pathToResources); - if (!resourceRootDir.exists()) { - String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); - throw new UnknownResourcePathException(msg); - } - String resourcePath = migrationPath(initMigration); + Location resourcePath = migrationPath(initMigration); // expect to be a path to something like - src/main/resources/dbmigration - File path = new File(resourceRootDir, resourcePath); + File path; + if (resourcePath.isClassPath()) { + // path to src/main/resources in typical maven project + File resourceRootDir = new File(pathToResources); + if (!resourceRootDir.exists()) { + String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); + throw new UnknownResourcePathException(msg); + } + path = new File(resourceRootDir, resourcePath.path()); + } else { + path = new File(resourcePath.path()); + } if (!path.exists()) { if (!path.mkdirs()) { logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath()); @@ -766,8 +848,9 @@ File migrationDirectory(boolean initMigration) { return path; } - private String migrationPath(boolean initMigration) { - return initMigration ? migrationInitPath : migrationPath; + private Location migrationPath(boolean initMigration) { + // remove classpath: or filesystem: prefix + return new Location(initMigration ? migrationInitPath : migrationPath); } /** diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/BaseTableDdl.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/BaseTableDdl.java index e9fafccc41..9d81397150 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/BaseTableDdl.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/BaseTableDdl.java @@ -142,7 +142,7 @@ private String translate(String ddl, String tableName, String columnName, String private void handleStrictError(String tableName, String columnName) { if (strictMode) { - String message = "DB Migration of non-null column with no default value specified for: " + tableName + "." + columnName+" Use @DbDefault to specify a default value or specify dbMigration.setStrictMode(false)"; + String message = "DB Migration of non-null column with no default value specified for: " + tableName + "." + columnName+" Use @DbDefault to specify a default value or disable strict mode for migration"; throw new IllegalArgumentException(message); } } diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/extraddl/model/ExtraDdlXmlReader.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/extraddl/model/ExtraDdlXmlReader.java index 3ff9034d18..350c584e8c 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/extraddl/model/ExtraDdlXmlReader.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/extraddl/model/ExtraDdlXmlReader.java @@ -42,7 +42,7 @@ private static String buildExtra(Platform platform, boolean drops, ExtraDdl read StringBuilder sb = new StringBuilder(300); for (DdlScript script : read.getDdlScript()) { if (script.isDrop() == drops && matchPlatform(platform, script.getPlatforms())) { - logger.log(DEBUG, "include script {}", script.getName()); + logger.log(DEBUG, "include script {0}", script.getName()); String value = script.getValue(); sb.append(value); if (value.lastIndexOf(';') == -1) { diff --git a/ebean-ddl-generator/src/main/java/module-info.java b/ebean-ddl-generator/src/main/java/module-info.java index 1a7ef332e4..c5b6af08b2 100644 --- a/ebean-ddl-generator/src/main/java/module-info.java +++ b/ebean-ddl-generator/src/main/java/module-info.java @@ -1,5 +1,6 @@ module io.ebean.ddl.generator { + uses io.ebean.plugin.Plugin; exports io.ebean.dbmigration; provides io.ebean.dbmigration.DbMigration with io.ebeaninternal.dbmigration.DefaultDbMigration; diff --git a/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin new file mode 100644 index 0000000000..83ec94df48 --- /dev/null +++ b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin @@ -0,0 +1 @@ +io.ebeaninternal.dbmigration.DbMigrationPlugin diff --git a/ebean-ddl-generator/src/test/resources/application-test.properties b/ebean-ddl-generator/src/test/resources/application-test.properties index dfcb052ab0..af973bb3a5 100644 --- a/ebean-ddl-generator/src/test/resources/application-test.properties +++ b/ebean-ddl-generator/src/test/resources/application-test.properties @@ -5,32 +5,12 @@ datasource.default=h2 datasource.h2.username=sa datasource.h2.password= -datasource.h2.url=jdbc:h2:mem:h2AutoTune +datasource.h2.url=jdbc:h2:mem:h2AutoTune;NON_KEYWORDS=KEY,VALUE + +datasource.db2.username=migtest +datasource.db2.password=migtest +datasource.db2.url=jdbc:db2://localhost:50005/migtest datasource.pg.username=sa datasource.pg.password= datasource.pg.url=jdbc:h2:mem:h2AutoTune - -# parameters for migration test -datasource.migrationtest.username=SA -datasource.migrationtest.password=SA -datasource.migrationtest.url=jdbc:h2:mem:migration -ebean.migrationtest.applyPrefix=V -ebean.migrationtest.ddl.generate=false -ebean.migrationtest.ddl.run=false -ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest -ebean.migrationtest.migration.appName=migrationtest -ebean.migrationtest.migration.migrationPath=dbmigration/migrationtest -ebean.migrationtest.migration.strict=true - -# parameters for migration test -datasource.migrationtest-history.username=SA -datasource.migrationtest-history.password=SA -datasource.migrationtest-history.url=jdbc:h2:mem:migration -ebean.migrationtest-history.applyPrefix=V -ebean.migrationtest-history.ddl.generate=false -ebean.migrationtest-history.ddl.run=false -ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest -ebean.migrationtest-history.migration.appName=migrationtest-history -ebean.migrationtest-history.migration.migrationPath=dbmigration/migrationtest-history -ebean.migrationtest-history.migration.strict=true diff --git a/ebean-externalmapping-api/pom.xml b/ebean-externalmapping-api/pom.xml index 90849e8418..6480c67d9f 100644 --- a/ebean-externalmapping-api/pom.xml +++ b/ebean-externalmapping-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean external mapping api diff --git a/ebean-externalmapping-xml/pom.xml b/ebean-externalmapping-xml/pom.xml index 4ad095b631..f6ad1507f4 100644 --- a/ebean-externalmapping-xml/pom.xml +++ b/ebean-externalmapping-xml/pom.xml @@ -4,12 +4,12 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT - scm:git:git@github.com:ebean-orm/ebean.git - HEAD + scm:git:git@github.com:FOCONIS/ebean.git + ebean-parent-13.6.4-FOC1 ebean external mapping xml @@ -21,7 +21,7 @@ io.ebean ebean-externalmapping-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -54,21 +54,21 @@ io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-ddl-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-jackson-jsonnode/pom.xml b/ebean-jackson-jsonnode/pom.xml index bb9959ba1f..93905497b1 100644 --- a/ebean-jackson-jsonnode/pom.xml +++ b/ebean-jackson-jsonnode/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-jackson-jsonnode @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index 24e4f274b4..0e41387e38 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT 4.0.0 @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided diff --git a/ebean-joda-time/pom.xml b/ebean-joda-time/pom.xml index f1196b1409..e7332c4fe3 100644 --- a/ebean-joda-time/pom.xml +++ b/ebean-joda-time/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT 4.0.0 @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided diff --git a/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaDateMidnight.java b/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaDateMidnight.java index e2520906da..61f71869b1 100644 --- a/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaDateMidnight.java +++ b/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaDateMidnight.java @@ -23,7 +23,7 @@ final class ScalarTypeJodaDateMidnight extends ScalarTypeBaseDate @Override protected String toIsoFormat(DateMidnight value) { - return value.toString(); + return value.toLocalDate().toString(); } @Override diff --git a/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaLocalDateTime.java b/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaLocalDateTime.java index 3b0ec373b5..73b3c6e23b 100644 --- a/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaLocalDateTime.java +++ b/ebean-joda-time/src/main/java/io/ebean/joda/time/ScalarTypeJodaLocalDateTime.java @@ -27,6 +27,11 @@ protected String toJsonNanos(LocalDateTime value) { protected String toJsonISO8601(LocalDateTime value) { return value.toString(); } + + @Override + protected LocalDateTime fromJsonISO8601(String value) { + return LocalDateTime.parse(value); + } @Override public long convertToMillis(LocalDateTime value) { diff --git a/ebean-kotlin/pom.xml b/ebean-kotlin/pom.xml index 1d6f391b48..9db4f9ac34 100644 --- a/ebean-kotlin/pom.xml +++ b/ebean-kotlin/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-kotlin @@ -28,7 +26,7 @@ io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided @@ -50,7 +48,7 @@ io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-postgis/pom.xml b/ebean-postgis/pom.xml index 4e6abbfad0..6082a55217 100644 --- a/ebean-postgis/pom.xml +++ b/ebean-postgis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean postgis @@ -22,14 +22,14 @@ io.ebean ebean-platform-postgres - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided @@ -65,7 +65,7 @@ io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index 7470185859..8262aad872 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean querybean @@ -17,7 +17,7 @@ io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided @@ -63,21 +63,21 @@ io.ebean ebean-ddl-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean querybean-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index 5fc9f258d2..5df930f7c0 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-redis @@ -22,35 +22,35 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean querybean-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index 86f127f967..59b0218287 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean test @@ -28,20 +28,20 @@ io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT provided io.ebean ebean-ddl-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -105,28 +105,28 @@ io.ebean ebean-joda-time - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-jackson-jsonnode - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-jackson-mapper - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-platform-all - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/ebean-test/src/main/java/module-info.java b/ebean-test/src/main/java/module-info.java index 428c3b3f4a..ed64a809eb 100644 --- a/ebean-test/src/main/java/module-info.java +++ b/ebean-test/src/main/java/module-info.java @@ -1,5 +1,5 @@ - -module io.ebean.test { +// module must be open, so tests will pass +open module io.ebean.test { exports io.ebean.test; exports io.ebean.test.config; diff --git a/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java b/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java new file mode 100644 index 0000000000..950fbc57dc --- /dev/null +++ b/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java @@ -0,0 +1,154 @@ +package io.ebean.test.config; + + +import io.ebean.Database; +import io.ebean.DatabaseFactory; +import io.ebean.annotation.Platform; +import io.ebean.config.DatabaseConfig; +import io.ebean.datasource.DataSourceAlert; +import io.ebean.datasource.DataSourceInitialiseException; +import io.ebean.xtest.ForPlatform; + +import io.ebean.xtest.base.PlatformCondition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.tests.model.basic.EBasicVer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import javax.persistence.PersistenceException; +import javax.sql.DataSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(PlatformCondition.class) +public class TestServerOffline { + + @Test + @ForPlatform({Platform.H2}) + public void testOffline_default() throws SQLException { + + String url = "jdbc:h2:mem:testoffline1"; + try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) { + Properties props = props(url); + DatabaseConfig config = config(props); + + assertThatThrownBy(() -> DatabaseFactory.create(config)) + .isInstanceOf(DataSourceInitialiseException.class); + } + + } + + private static class LazyDatasourceInitializer implements DataSourceAlert { + + public Database server; + + private boolean initialized; + + @Override + public void dataSourceUp(DataSource dataSource) { + if (!initialized) { + initDatabase(); + } + } + + public synchronized void initDatabase() { + if (!initialized) { + server.runDdl(); + initialized = true; + } + } + + @Override + public void dataSourceDown(DataSource dataSource, SQLException reason) {} + + @Override + public void dataSourceWarning(DataSource dataSource, String msg) {} + + } + + @Test + @ForPlatform({Platform.H2}) + public void testOffline_recovery() throws SQLException { + + String url = "jdbc:h2:mem:testoffline3"; + try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) { + + Properties props = props(url); + + // to bring up ebean without a database, we must disable various things + // that happen on startup + props.setProperty("datasource.h2_offline.failOnStart", "false"); + props.setProperty("ebean.h2_offline.skipDataSourceCheck", "true"); + props.setProperty("ebean.h2_offline.ddl.run", "false"); + DatabaseConfig config = config(props); + + LazyDatasourceInitializer alert = new LazyDatasourceInitializer() ; + config.getDataSourceConfig().setAlert(alert); + config.getDataSourceConfig().setHeartbeatFreqSecs(1); + + Database h2Offline = DatabaseFactory.create(config); + alert.server = h2Offline; + assertThat(h2Offline).isNotNull(); + // DB is online now in offline mode + + // Accessing the DB will throw a PE + assertThatThrownBy(() -> alert.initDatabase()) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Failed to obtain connection to run DDL"); + + assertThatThrownBy(() -> h2Offline.find(EBasicVer.class).findCount()).isInstanceOf(PersistenceException.class); + + // so - reset the password so that the server can reconnect + try (Statement stmt = bootup.createStatement()) { + stmt.execute("alter user sa set password 'sa'"); + } + + assertThat(alert.initialized).isFalse(); + + // next access to ebean should bring DS online + h2Offline.find(EBasicVer.class).findCount(); + assertThat(alert.initialized).isTrue(); + + // check if server is working (ie ddl was run) + EBasicVer bean = new EBasicVer("foo"); + h2Offline.save(bean); + assertThat(h2Offline.find(EBasicVer.class).findCount()).isEqualTo(1); + h2Offline.delete(bean); + } + } + + private Properties props(String url) { + + Properties props = new Properties(); + + props.setProperty("datasource.h2_offline.username", "sa"); + props.setProperty("datasource.h2_offline.password", "sa"); + props.setProperty("datasource.h2_offline.url", url); + props.setProperty("datasource.h2_offline.driver", "org.h2.Driver"); + + props.setProperty("ebean.h2_offline.databasePlatformName", "h2"); + props.setProperty("ebean.h2_offline.ddl.extra", "false"); + + props.setProperty("ebean.h2_offline.ddl.generate", "true"); + props.setProperty("ebean.h2_offline.ddl.run", "true"); + + return props; + } + + private DatabaseConfig config(Properties props) { + DatabaseConfig config = new DatabaseConfig(); + config.setName("h2_offline"); + config.loadFromProperties(props); + config.setDefaultServer(false); + config.setRegister(false); + config.getClasses().add(EBasicVer.class); + return config; + } + +} diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java index 0dcc4afc28..2915657281 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java @@ -49,6 +49,39 @@ public void create_new_server_with_multi_tenancy_db() { + /** + * Tests using multi tenancy per database + */ + @Test + public void create_new_server_with_multi_tenancy_db_with_master() { + + String tenant = "customer"; + CurrentTenantProvider tenantProvider = Mockito.mock(CurrentTenantProvider.class); + Mockito.doReturn(tenant).when(tenantProvider).currentId(); + + TenantDataSourceProvider dataSourceProvider = Mockito.mock(TenantDataSourceProvider.class); + + DatabaseConfig config = new DatabaseConfig(); + + config.setName("h2"); + config.loadFromProperties(); + config.setRegister(false); + config.setDefaultServer(false); + config.setDdlGenerate(false); + config.setDdlRun(false); + + config.setTenantMode(TenantMode.DB_WITH_MASTER); + config.setCurrentTenantProvider(tenantProvider); + config.setTenantDataSourceProvider(dataSourceProvider); + + Mockito.doReturn(config.getDataSource()).when(dataSourceProvider).dataSource(tenant); + + config.setDatabasePlatform(new PostgresPlatform()); + + final Database database = DatabaseFactory.create(config); + database.shutdown(); + } + /** * Tests using multi tenancy per schema */ diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_ServerConfigStart_Test.java b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_ServerConfigStart_Test.java index e48034d9cd..1135963311 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_ServerConfigStart_Test.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_ServerConfigStart_Test.java @@ -4,13 +4,53 @@ import io.ebean.DatabaseFactory; import io.ebean.config.DatabaseConfig; import io.ebean.event.ServerConfigStartup; +import io.ebeaninternal.api.SpiLogger; +import io.ebeaninternal.api.SpiLoggerFactory; import org.junit.jupiter.api.Test; import org.tests.model.basic.UTDetail; +import java.util.HashSet; +import java.util.Set; + import static org.assertj.core.api.Assertions.assertThat; public class EbeanServerFactory_ServerConfigStart_Test { + + /** + * Demo, how to intercept logging for a certain database. + * In conjunction to ServiceLoader, you can add different loggers to different databases + */ + private static class MySpiLoggerFactory implements SpiLoggerFactory { + Set loggers = new HashSet<>(); + + @Override + public SpiLogger create(String name) { + loggers.add(name); + // just return a dummy here + return new SpiLogger() { + @Override + public boolean isDebug() { + return false; + } + + @Override + public boolean isTrace() { + return false; + } + + @Override + public void debug(String msg) { + } + + @Override + public void trace(String msg) { + } + }; + + } + } + @Test public void test() throws InterruptedException { @@ -30,9 +70,13 @@ public void test() throws InterruptedException { // act - register an instance OnStartup onStartup = new OnStartup(); config.addServerConfigStartup(onStartup); + MySpiLoggerFactory loggerFactory = new MySpiLoggerFactory(); + config.putServiceObject(SpiLoggerFactory.class, loggerFactory); Database db = DatabaseFactory.create(config); + assertThat(loggerFactory.loggers).containsExactlyInAnyOrder("io.ebean.SQL", "io.ebean.SUM", "io.ebean.TXN"); + assertThat(onStartup.calledWithConfig).isSameAs(config); assertThat(OnStartupViaClass.calledWithConfig).isSameAs(config); diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/ServerStartTest.java b/ebean-test/src/test/java/io/ebean/xtest/base/ServerStartTest.java new file mode 100644 index 0000000000..b0dfe787b9 --- /dev/null +++ b/ebean-test/src/test/java/io/ebean/xtest/base/ServerStartTest.java @@ -0,0 +1,15 @@ +package io.ebean.xtest.base; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import io.ebean.DatabaseFactory; + +public class ServerStartTest { + + @Test + @Disabled("run manually") + void testServerStartAndMigrateDb2() throws Exception { + DatabaseFactory.create("db2-migration").shutdown(); + } +} diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java index 4f5471c62c..df9850d57f 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java @@ -79,13 +79,11 @@ public static void main(String[] args) throws IOException { List pendingDrops = migration.getPendingDrops(); assertThat(pendingDrops).contains("1.1"); - //System.setProperty("ddl.migration.pendingDropsFor", "1.1"); migration.setGeneratePendingDrop("1.1"); assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); assertThatThrownBy(()->migration.generateMigration()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("No 'pendingDrops'"); // subsequent call - System.clearProperty("ddl.migration.pendingDropsFor"); server.shutdown(); logger.info("end"); diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java index df70742162..40706eb8b3 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java @@ -4,6 +4,8 @@ import io.ebean.DatabaseFactory; import io.ebean.annotation.Platform; import io.ebean.config.DatabaseConfig; +import io.ebeaninternal.api.DbOffline; + import io.ebean.dbmigration.DbMigration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -17,7 +19,6 @@ import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** @@ -96,7 +97,7 @@ public static void run(String pathToResources) throws IOException { config.getProperties().put("ebean.hana.generateUniqueDdl", "true"); // need to generate unique statements to prevent them from being filtered out as duplicates by the DdlRunner config.setPackages(Arrays.asList("misc.migration.v1_0")); - Database server = DatabaseFactory.create(config); + Database server = createServer(config); migration.setServer(server); // then we generate migration scripts for v1_0 @@ -107,43 +108,28 @@ public static void run(String pathToResources) throws IOException { // and now for v1_1 config.setPackages(Arrays.asList("misc.migration.v1_1")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.1"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - - System.setProperty("ddl.migration.pendingDropsFor", "1.1"); - assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); - - assertThatThrownBy(()->migration.generateMigration()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.1,1.2__dropsFor_1.1"); assertThat(migration.generateMigration()).isNull(); // subsequent call // and now for v1_2 with config.setPackages(Arrays.asList("misc.migration.v1_2")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.3"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - System.setProperty("ddl.migration.pendingDropsFor", "1.3"); - assertThat(migration.generateMigration()).isEqualTo("1.4__dropsFor_1.3"); - assertThatThrownBy(migration::generateMigration) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.3,1.4__dropsFor_1.3"); assertThat(migration.generateMigration()).isNull(); // subsequent call server.shutdown(); logger.info("end"); } + private static Database createServer(DatabaseConfig config) { + DbOffline.setGenerateMigration(); + Database server = DatabaseFactory.create(config); + DbOffline.reset(); + return server; + } + } diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationTest.java index 9a3cff37c5..4e85418f85 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationTest.java @@ -97,6 +97,10 @@ public void testRunMigration() throws IOException, SQLException { runScript("I__create_tablespaces.sql"); } + if(isDb2()) { + runScript("I__create_tablespaces.sql"); + } + if (!isOracle()) { // oracle.getMetaData is too slow. So skip this test assertThat(getTables()).doesNotContain("migtest_QuOtEd"); 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 3fa7f84b8a..a7ac708037 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 @@ -278,6 +278,11 @@ public int saveAll(Object... beans) throws OptimisticLockException { return 0; } + @Override + public void visitSave(Object start, PersistVisitor visitor) { + + } + @Override public boolean delete(Object bean) throws OptimisticLockException { return false; @@ -627,4 +632,9 @@ public void loadBeanL2(EntityBeanIntercept ebi) { public void loadBean(EntityBeanIntercept ebi) { } + + @Override + public void runDdl() { + + } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/server/transaction/JdbcTransactionTest.java b/ebean-test/src/test/java/io/ebean/xtest/internal/server/transaction/JdbcTransactionTest.java index ce9e420b89..b306fef945 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/server/transaction/JdbcTransactionTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/server/transaction/JdbcTransactionTest.java @@ -105,4 +105,25 @@ public void preCommit() { assertThat(postCommitCallCount.get()).isEqualTo(2); // postcommit executed twice } } + + @Test + public void testFlushInCallback() { + try (Transaction transaction = DB.beginTransaction()) { + transaction.setBatchMode(true); + DB.currentTransaction().register( + new TransactionCallbackAdapter() { + + @Override + public void preCommit() { + EBasic basic = new EBasic("binner1"); + DB.save(basic); + } + } + ); + EBasic basic = new EBasic("bouter1"); + DB.save(basic); + transaction.commit(); // transaction will fail if recursive post-commit is failing + } + assertThat(DB.find(EBasic.class).where().eq("name", "binner1").exists()).isTrue(); + } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/text/PathPropertiesTests.java b/ebean-test/src/test/java/io/ebean/xtest/text/PathPropertiesTests.java index 379b5bd927..b4723dc66b 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/text/PathPropertiesTests.java +++ b/ebean-test/src/test/java/io/ebean/xtest/text/PathPropertiesTests.java @@ -1,5 +1,6 @@ package io.ebean.xtest.text; +import io.ebean.CountDistinctOrder; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Query; @@ -10,6 +11,9 @@ import org.slf4j.LoggerFactory; import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; +import org.tests.model.composite.Model; +import org.tests.model.composite.ModelSubEntity; +import org.tests.model.composite.RCustomer; import java.util.List; @@ -48,4 +52,54 @@ void test_withAllPropsQuery() { assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.name, t0.smallnote, t0.anniversary, t0.cretime, t0.updtime, t0.version, t0.shipping_address_id, t1.id, t1.line_1 from o_customer t0 left join o_address t1 on t1.id = t0.billing_address_id;"); } + @Test + void test_withConcat() { + + LoggedSql.start(); + Query query = DB.find(Customer.class) + .select("concat(name,billingAddress.line1)"); + //.fetch("billingAddress", "concat(line1, line2, t0.name)"); + // + query.findSingleAttributeList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.name, t0.smallnote, t0.anniversary, t0.cretime, t0.updtime, t0.version, t0.shipping_address_id, t1.id, t1.line_1 from o_customer t0 left join o_address t1 on t1.id = t0.billing_address_id;"); + } + + @Test + void test_withEmbedededId() { + ModelSubEntity entity1 = new ModelSubEntity(); + DB.save(entity1); + ModelSubEntity entity2 = new ModelSubEntity(); + DB.save(entity2); + ModelSubEntity entity3 = new ModelSubEntity(); + DB.save(entity3); + + Model model1 = new Model(); + model1.setFrom(entity1); + model1.setTo(entity2); + model1.setDescription("model 1"); + DB.save(model1); + + Model model2 = new Model(); + model2.setFrom(entity3); + model2.setTo(entity2); + model2.setDescription("model 2"); + DB.save(model2); + + Query defaultQuery = DB.find(Model.class); + PathProperties root = new PathProperties(); + root.addToPath(null,"id.fromId"); + //root.addToPath("id"","fromId"); so sah mein erster Versuch aus + LoggedSql.start(); + Query query = defaultQuery.copy(); + query.apply(root).where().isNotNull("id.fromId"); + query.setDistinct(true).setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC); + query.setMaxRows(1); + List returnList = query.findSingleAttributeList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select distinct r1.attribute_, count(*) from (select distinct t0.from_id, t0.to_id, null as attribute_ from model t0 where t0.from_id is not null) r1 group by r1.attribute_ order by count(*) desc, r1.attribute_ limit 1;"); + } + } diff --git a/ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java b/ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java new file mode 100644 index 0000000000..0d61a13f8a --- /dev/null +++ b/ebean-test/src/test/java/org/tests/basic/TPersistVisitor.java @@ -0,0 +1,69 @@ +package org.tests.basic; + +import java.util.Collection; + +import io.ebean.DB; +import io.ebean.PersistVisitor; +import io.ebean.bean.EntityBean; +import io.ebean.plugin.Property; +/** + * Sample persist visitor that converts the visited beans in a XML-like structure + */ +public class TPersistVisitor implements PersistVisitor { + + private final StringBuilder sb; + private final String indent; + private final String tag; + private boolean empty = true; + + public TPersistVisitor() { + this(new StringBuilder(), "", "root"); + } + private TPersistVisitor(StringBuilder sb, String indent, String tag) { + this.sb = sb; + this.indent = indent; + this.tag = tag; + this.sb.append(indent).append('<').append(tag); + } + + TPersistVisitor newVisitor(String tag) { + if (empty) { + sb.append(">\n"); + empty = false; + } + return new TPersistVisitor(sb, indent + " ", tag); + } + + @Override + public void visitEnd() { + if (empty) { + sb.append("/>\n"); + } else { + this.sb.append(indent).append("\n"); + } + } + + TPersistVisitor attr(String attr, Object value) { + sb.append(' ').append(attr).append('=').append('\'').append(value).append('\''); + return this; + } + + public TPersistVisitor visitBean(EntityBean bean) { + return newVisitor("bean").attr("type", bean.getClass().getSimpleName()).attr("newOrDirty", DB.beanState(bean).isNewOrDirty()); + } + + @Override + public PersistVisitor visitProperty(Property prop) { + return newVisitor("property").attr("name", prop.name()); + } + + @Override + public PersistVisitor visitCollection(Collection collection) { + return newVisitor("collection").attr("size", collection.size()); + } + + @Override + public String toString() { + return sb.toString(); + } + } \ No newline at end of file diff --git a/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java b/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java index f3dc340dc0..13533f8b4a 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java +++ b/ebean-test/src/test/java/org/tests/basic/TestM2MCascadeOne.java @@ -1,12 +1,14 @@ package org.tests.basic; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Query; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.basic.MRole; import org.tests.model.basic.MUser; +import static org.assertj.core.api.Assertions.assertThat; + public class TestM2MCascadeOne extends BaseTestCase { @Test @@ -30,8 +32,43 @@ public void test() { u1.addRole(r0); u1.addRole(r1); + TPersistVisitor tv = new TPersistVisitor(); + DB.visitSave(u1, tv); + assertThat(tv.toString()).isEqualTo("\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"); + + DB.save(u1); + u1 = DB.find(MUser.class, u.getUserid()); + + tv = new TPersistVisitor(); + DB.visitSave(u1, tv); + // collection is unloaded + assertThat(tv.toString()).isEqualTo("\n" + + " \n" + + "\n"); + + r1 = DB.find(MRole.class, r1.getRoleid()); + tv = new TPersistVisitor(); + r1.getUsers().add(new MUser()); + + DB.visitSave(r1, tv); + assertThat(tv.toString()).isEqualTo("\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"); } @Test diff --git a/ebean-test/src/test/java/org/tests/basic/TestManyOneInterface.java b/ebean-test/src/test/java/org/tests/basic/TestManyOneInterface.java index c8a374302f..f4b35ae275 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestManyOneInterface.java +++ b/ebean-test/src/test/java/org/tests/basic/TestManyOneInterface.java @@ -5,27 +5,51 @@ import org.junit.jupiter.api.Test; import org.tests.model.basic.ResetBasicData; import org.tests.model.interfaces.Address; +import org.tests.model.interfaces.ExtPerson1and2; import org.tests.model.interfaces.IAddress; +import org.tests.model.interfaces.IExtPerson1; +import org.tests.model.interfaces.IExtPerson2; import org.tests.model.interfaces.IPerson; import org.tests.model.interfaces.Person; +import static org.assertj.core.api.Assertions.assertThat; + public class TestManyOneInterface extends BaseTestCase { @Test - public void test() { - + public void testEntityOverrideEntityImplements() { ResetBasicData.reset(); - IAddress a = new Address("hello"); + IAddress a = DB.getDefault().createEntityBean(IAddress.class); + assertThat(a).isInstanceOf(Address.class); - IPerson p = new Person(); + IPerson p = DB.getDefault().createEntityBean(Person.class); + assertThat(p).isInstanceOf(ExtPerson1and2.class); + p = DB.getDefault().pluginApi().createEntityBean(IPerson.class); + assertThat(p).isInstanceOf(ExtPerson1and2.class); p.setDefaultAddress(a); - DB.save(a); + IAddress ea1 = DB.getDefault().createEntityBean(IAddress.class); + IAddress ea2 = DB.getDefault().createEntityBean(IAddress.class); + + p.getExtraAddresses().add(ea1); + p.getExtraAddresses().add(ea2); + DB.save(p); - //Assert.assertTrue(); + IAddress a2 = DB.find(IAddress.class, a.getOid()); + IPerson p2 = DB.find(IPerson.class, p.getOid()); + + assertThat(a2).isInstanceOf(Address.class); + assertThat(p2).isNotNull().isInstanceOf(ExtPerson1and2.class); + assertThat(p2.getDefaultAddress()).isInstanceOf(Address.class); + + // some more checks + IExtPerson1 pe1 = DB.getDefault().pluginApi().createEntityBean(IExtPerson1.class); + IExtPerson2 pe2 = DB.getDefault().pluginApi().createEntityBean(IExtPerson2.class); + assertThat(pe1).isInstanceOf(ExtPerson1and2.class); + assertThat(pe2).isInstanceOf(ExtPerson1and2.class); } } diff --git a/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java b/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java index feff29a687..6e254ea6c7 100644 --- a/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java +++ b/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java @@ -91,12 +91,12 @@ public void findSingleAttributeList() { // ensure that findCount & findSingleAttribute use different // slots in cache. If not a "Cannot cast List to int" should happen. int count = DB - .find(EColAB.class) - .setUseQueryCache(true) - .select("columnA") - .where() - .eq("columnB", "SingleAttribute") - .findCount(); + .find(EColAB.class) + .setUseQueryCache(true) + .select("columnA") + .where() + .eq("columnB", "SingleAttribute") + .findCount(); assertThat(count).isEqualTo(2); } @@ -305,7 +305,7 @@ public void testReadOnlyFind() { assertSame(list, list2B); List list3 = DB.find(Customer.class).setUseQueryCache(true).setReadOnly(false).where() - .ilike("name", "Rob").findList(); + .ilike("name", "Rob").findList(); assertNotSame(list, list3); BeanCollection bc3 = (BeanCollection) list3; @@ -349,10 +349,10 @@ public void findIds() { // and now, ensure that we hit the database LoggedSql.start(); colA_second = DB.find(EColAB.class) - .setUseQueryCache(CacheMode.PUT) - .where() - .eq("columnB", "someId") - .findIds(); + .setUseQueryCache(CacheMode.PUT) + .where() + .eq("columnB", "someId") + .findIds(); sql = LoggedSql.stop(); assertThat(sql).hasSize(1); @@ -361,15 +361,15 @@ public void findIds() { @Test public void findCountDifferentQueriesBit() { DB.getDefault().pluginApi().cacheManager().clearAll(); - differentFindCount(q->q.bitwiseAny("id",1), q->q.bitwiseAny("id",0)); - differentFindCount(q->q.bitwiseAll("id",1), q->q.bitwiseAll("id",0)); + differentFindCount(q -> q.bitwiseAny("id", 1), q -> q.bitwiseAny("id", 0)); + differentFindCount(q -> q.bitwiseAll("id", 1), q -> q.bitwiseAll("id", 0)); // differentFindCount(q->q.bitwiseNot("id",1), q->q.bitwiseNot("id",0)); NOT 1 == AND 1 = 0 - differentFindCount(q->q.bitwiseAnd("id",1, 0), q->q.bitwiseAnd("id",1, 1)); + differentFindCount(q -> q.bitwiseAnd("id", 1, 0), q -> q.bitwiseAnd("id", 1, 1)); - differentFindCount(q->q.bitwiseAnd("id",2, 0), q->q.bitwiseAnd("id",4, 0)); - differentFindCount(q->q.bitwiseAnd("id",2, 1), q->q.bitwiseAnd("id",4, 1)); + differentFindCount(q -> q.bitwiseAnd("id", 2, 0), q -> q.bitwiseAnd("id", 4, 0)); + differentFindCount(q -> q.bitwiseAnd("id", 2, 1), q -> q.bitwiseAnd("id", 4, 1)); // Will produce hash collision - differentFindCount(q->q.bitwiseAnd("id",10, 0), q->q.bitwiseAnd("id",0, 928210)); + differentFindCount(q -> q.bitwiseAnd("id", 10, 0), q -> q.bitwiseAnd("id", 0, 928210)); } diff --git a/ebean-test/src/test/java/org/tests/insert/TestInsertDataIntegrityException.java b/ebean-test/src/test/java/org/tests/insert/TestInsertDataIntegrityException.java index 35f0bf271e..cd5e0415aa 100644 --- a/ebean-test/src/test/java/org/tests/insert/TestInsertDataIntegrityException.java +++ b/ebean-test/src/test/java/org/tests/insert/TestInsertDataIntegrityException.java @@ -5,11 +5,13 @@ import io.ebean.DataIntegrityException; import io.ebean.xtest.IgnorePlatform; import io.ebean.annotation.Platform; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; import org.tests.model.basic.ResetBasicData; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; public class TestInsertDataIntegrityException extends BaseTestCase { @@ -26,7 +28,10 @@ public void insert_invalidForeignKey() { Order order = new Order(); order.setStatus(Order.Status.NEW); order.setCustomer(invalidCustomer); + assertThatThrownBy(() -> DB.save(order)) + .isInstanceOf(DataIntegrityException.class) + .hasMessageContaining("insert into o_order") + .hasMessageContaining("900000"); - assertThrows(DataIntegrityException.class, () -> DB.save(order)); } } diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java b/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java index 27434601ef..0e6c7d60fb 100644 --- a/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java +++ b/ebean-test/src/test/java/org/tests/json/TestDbJson_List.java @@ -2,19 +2,33 @@ import io.ebean.xtest.BaseTestCase; import io.ebean.DB; +import io.ebean.Database; +import io.ebean.DatabaseFactory; +import io.ebean.ValuePair; import io.ebean.xtest.ForPlatform; +import io.ebean.annotation.MutationDetection; import io.ebean.annotation.Platform; +import io.ebean.config.DatabaseConfig; import io.ebean.test.LoggedSql; import io.ebean.text.TextException; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.json.EBasicJsonList; import org.tests.model.json.PlainBean; import javax.persistence.PersistenceException; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class TestDbJson_List extends BaseTestCase { @@ -231,4 +245,43 @@ public void testNullToEmpty() { assertThat(bean.getTags()).isEmpty(); assertThat(bean.getBeanMap()).isEmpty(); } + + @Test + @ForPlatform(Platform.H2) + @Disabled("breaks everything") + public void testDirtyValues() { + DatabaseConfig config = new DatabaseConfig(); + config.loadFromProperties(); + config.setDefaultServer(true); + config.setRegister(true); + config.setDdlRun(false); + config.setJsonMutationDetection(MutationDetection.SOURCE); + Database db = DatabaseFactory.create(config); + try { + assertThat(db).isNotNull(); + + EBasicJsonList bean = new EBasicJsonList(); + bean.getTags().add("aa"); + bean.getTags().add("bb"); + + db.save(bean); + bean = db.find(EBasicJsonList.class, bean.getId()); + + bean.getTags().add("cc"); + final Map dirtyValues = db.beanState(bean).dirtyValues(); + assertThat(dirtyValues).containsOnlyKeys("tags"); + + final ValuePair diff = dirtyValues.get("tags"); + assertThat(diff.getOldValue()).isInstanceOf(List.class).asList() + .containsExactly("aa", "bb"); + assertThat(diff.getNewValue()).isInstanceOf(List.class).asList() + .containsExactly("aa", "bb", "cc"); + } finally { + if (db != null) { + db.shutdown(); + } + } + + + } } diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java index 8f36be0cbc..280b4221cb 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java @@ -1,6 +1,7 @@ package org.tests.lazyforeignkeys; import io.ebean.annotation.DbForeignKey; +import io.ebean.annotation.NotNull; import org.tests.model.basic.Cat; import javax.persistence.*; @@ -29,6 +30,12 @@ public class MainEntityRelation { @DbForeignKey(noConstraint = true) private Cat cat; + @ManyToOne + @NotNull + @JoinColumn(name = "cat2_id") + @DbForeignKey(noConstraint = true) + private Cat cat2; + private String attr1; public MainEntity getEntity1() { @@ -55,6 +62,14 @@ public void setCat(Cat cat) { this.cat = cat; } + public Cat getCat2() { + return cat2; + } + + public void setCat2(Cat cat2) { + this.cat2 = cat2; + } + public String getAttr1() { return attr1; } diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java index f88f148297..771862a953 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java @@ -10,9 +10,11 @@ import org.junit.jupiter.api.Test; import org.tests.model.basic.Cat; +import javax.persistence.EntityNotFoundException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; public class TestLazyForeignKeys extends BaseTestCase { @@ -32,6 +34,10 @@ public void prepare() { rel1.setEntity1(e1); rel1.setEntity2(e2); + + Cat cat = new Cat(); + cat.setId(4711L); + rel1.setCat2(cat); DB.save(rel1); } @@ -57,13 +63,25 @@ public void testFindOne() throws Exception { List sql = LoggedSql.stop(); assertThat(sql).hasSize(3); - assertSql(sql.get(0)).contains("select t0.id, t0.attr1, t0.id1, t0.id2, t1.species, t0.cat_id from main_entity_relation t0 left join animal t1 on t1.id = t0.cat_id"); + assertSql(sql.get(0)).contains("select t0.id, t0.attr1, t0.id1, t0.id2, t1.species, t0.cat_id, t2.species, t0.cat2_id " + + "from main_entity_relation t0 left join animal t1 on t1.id = t0.cat_id left join animal t2 on t2.id = t0.cat2_id"); if (isSqlServer() || isOracle()) { assertSql(sql.get(1)).contains("select t0.id, t0.attr1, t0.attr2, CASE WHEN t0.id is null THEN 1 ELSE 0 END from main_entity t0"); } else { assertSql(sql.get(1)).contains("select t0.id, t0.attr1, t0.attr2, t0.id is null from main_entity t0"); assertSql(sql.get(2)).contains("select t0.id, t0.attr1, t0.attr2, t0.id is null from main_entity t0"); } + + assertThat(rel1.getCat2().getId()).isEqualTo(4711L); + assertThatThrownBy(() -> rel1.getCat2().getName()).isInstanceOf(EntityNotFoundException.class); + + Cat cat = new Cat(); + cat.setId(4711L); + cat.setName("miau"); + DB.save(cat); + + DB.refresh(rel1); + assertThat(rel1.getCat2().getName()).isEqualTo("miau"); } @Test diff --git a/ebean-test/src/test/java/org/tests/model/basic/MDateTime.java b/ebean-test/src/test/java/org/tests/model/basic/MDateTime.java new file mode 100644 index 0000000000..010d378603 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/MDateTime.java @@ -0,0 +1,88 @@ +package org.tests.model.basic; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; +import java.util.Calendar; + +import javax.annotation.Nullable; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class MDateTime { + + @Id + private Integer id; + + @Nullable + private LocalTime localTime; + + @Nullable + private LocalDateTime localDateTime; + + @Nullable + private LocalDate localDate; + + @Nullable + private OffsetDateTime offsetDateTime; + + @Nullable + private ZonedDateTime zonedDateTime; + + @Nullable + private YearMonth propYearMonth; + + @Nullable + private MonthDay propMonthDay; + + @Nullable + private Year propYear; + + @Nullable + private Instant propInstant; + + @Nullable + private Calendar propCalendar; + + @Nullable + private Timestamp propTimestamp; + + @Nullable + private java.sql.Date sqlDate; + + @Nullable + private java.sql.Time sqlTime; + + @Nullable + private java.util.Date utilDate; + + @Nullable + private org.joda.time.DateTime jodaDateTime; + + @Nullable + private org.joda.time.LocalDateTime jodaLocalDateTime; + + @Nullable + private org.joda.time.LocalDate jodaLocalDate; + + @Nullable + private org.joda.time.LocalTime jodaLocalTime; + + @Nullable + private org.joda.time.DateMidnight jodaDateMidnight; + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/MRole.java b/ebean-test/src/test/java/org/tests/model/basic/MRole.java index c66a1db607..47ded509d9 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/MRole.java +++ b/ebean-test/src/test/java/org/tests/model/basic/MRole.java @@ -12,7 +12,7 @@ public class MRole { String roleName; - @ManyToMany(cascade = CascadeType.ALL) + @ManyToMany List users; public MRole() { diff --git a/ebean-test/src/test/java/org/tests/model/composite/Model.java b/ebean-test/src/test/java/org/tests/model/composite/Model.java new file mode 100644 index 0000000000..84904cf261 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/composite/Model.java @@ -0,0 +1,62 @@ +package org.tests.model.composite; + +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.ManyToOne; + +/* + * Licensed Materials - Property of FOCONIS AG + * (C) Copyright FOCONIS AG. + * @author + */ +@Entity +public class Model { + + @EmbeddedId + private ModelKey id; + + private String description; + + @ManyToOne + private ModelSubEntity from; + + @ManyToOne + private ModelSubEntity to; + + public Model() { + + } + + public ModelKey getId() { + return id; + } + + public void setId(ModelKey id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public ModelSubEntity getFrom() { + return from; + } + + public void setFrom(ModelSubEntity from) { + this.from = from; + } + + public ModelSubEntity getTo() { + return to; + } + + public void setTo(ModelSubEntity to) { + this.to = to; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/composite/ModelKey.java b/ebean-test/src/test/java/org/tests/model/composite/ModelKey.java new file mode 100644 index 0000000000..696041158f --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/composite/ModelKey.java @@ -0,0 +1,54 @@ +package org.tests.model.composite; + +import javax.persistence.Embeddable; +import java.util.UUID; + +/* + * Licensed Materials - Property of FOCONIS AG + * (C) Copyright FOCONIS AG. + * @author + */ +@Embeddable +public class ModelKey { + + @io.ebean.annotation.NotNull + private UUID fromId; + + @io.ebean.annotation.NotNull + private UUID toId; + + public UUID getFromId() { + return fromId; + } + + public void setFromId(UUID fromId) { + this.fromId = fromId; + } + + public UUID getToId() { + return toId; + } + + public void setToId(UUID toId) { + this.toId = toId; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hash = 3; + + return hash; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/composite/ModelSubEntity.java b/ebean-test/src/test/java/org/tests/model/composite/ModelSubEntity.java new file mode 100644 index 0000000000..a1785bd4f5 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/composite/ModelSubEntity.java @@ -0,0 +1,27 @@ +package org.tests.model.composite; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import java.util.UUID; + +/* + * Licensed Materials - Property of FOCONIS AG + * (C) Copyright FOCONIS AG. + * @author + */ +@Entity +public class ModelSubEntity { + @Id + @GeneratedValue + private UUID id; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/Address.java b/ebean-test/src/test/java/org/tests/model/interfaces/Address.java index 052ef63f87..454616feac 100644 --- a/ebean-test/src/test/java/org/tests/model/interfaces/Address.java +++ b/ebean-test/src/test/java/org/tests/model/interfaces/Address.java @@ -1,10 +1,14 @@ package org.tests.model.interfaces; +import io.ebean.annotation.ext.EntityImplements; + import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.ManyToOne; import javax.persistence.Version; @Entity +@EntityImplements(IAddress.class) public class Address implements IAddress { @Id @@ -15,6 +19,9 @@ public class Address implements IAddress { private String street; + @ManyToOne + private Person extraAddress; + public Address(String street) { this.street = street; } diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson1.java b/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson1.java new file mode 100644 index 0000000000..dc9fac9e09 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson1.java @@ -0,0 +1,25 @@ +package org.tests.model.interfaces; + +import io.ebean.annotation.ext.EntityImplements; +import io.ebean.annotation.ext.EntityOverride; + +import javax.persistence.Entity; + +@Entity() +@EntityImplements(IExtPerson1.class) +@EntityOverride(priority = 30) +public class ExtPerson1 extends Person implements IExtPerson1 { + + private int myField1; + + @Override + public int getMyField1() { + return myField1; + } + + @Override + public void setMyField1(int myField1) { + this.myField1 = myField1; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson1and2.java b/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson1and2.java new file mode 100644 index 0000000000..a6d2661fd8 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson1and2.java @@ -0,0 +1,26 @@ +package org.tests.model.interfaces; + +import io.ebean.annotation.ext.EntityImplements; +import io.ebean.annotation.ext.EntityOverride; + +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity() +@Table(name = "person") +@EntityImplements(IExtPerson2.class) +@EntityOverride(priority = -30) +public class ExtPerson1and2 extends ExtPerson1 implements IExtPerson2 { + + private int myField2; + + @Override + public int getMyField2() { + return myField2; + } + + @Override + public void setMyField2(int myField2) { + this.myField2 = myField2; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson2.java b/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson2.java new file mode 100644 index 0000000000..670f73ad36 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/interfaces/ExtPerson2.java @@ -0,0 +1,27 @@ +package org.tests.model.interfaces; + +import io.ebean.annotation.ext.EntityImplements; +import io.ebean.annotation.ext.EntityOverride; + +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@EntityOverride(priority = 20) +@EntityImplements(IExtPerson2.class) +@Table(name = "person") +public class ExtPerson2 extends Person implements IExtPerson2 { + + private int myField2; + + @Override + public int getMyField2() { + return myField2; + } + + @Override + public void setMyField2(int myField2) { + this.myField2 = myField2; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/IAddress.java b/ebean-test/src/test/java/org/tests/model/interfaces/IAddress.java index 78b6d20050..9097d23d76 100644 --- a/ebean-test/src/test/java/org/tests/model/interfaces/IAddress.java +++ b/ebean-test/src/test/java/org/tests/model/interfaces/IAddress.java @@ -1,6 +1,9 @@ package org.tests.model.interfaces; public interface IAddress { + + long getOid(); + String getStreet(); void setStreet(String s); diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/IExtPerson1.java b/ebean-test/src/test/java/org/tests/model/interfaces/IExtPerson1.java new file mode 100644 index 0000000000..b81a5266d6 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/interfaces/IExtPerson1.java @@ -0,0 +1,7 @@ +package org.tests.model.interfaces; + +public interface IExtPerson1 extends IPerson { + int getMyField1(); + + void setMyField1(int myField1); +} diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/IExtPerson2.java b/ebean-test/src/test/java/org/tests/model/interfaces/IExtPerson2.java new file mode 100644 index 0000000000..49bf601339 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/interfaces/IExtPerson2.java @@ -0,0 +1,7 @@ +package org.tests.model.interfaces; + +public interface IExtPerson2 extends IPerson { + int getMyField2(); + + void setMyField2(int myField2); +} diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/IPerson.java b/ebean-test/src/test/java/org/tests/model/interfaces/IPerson.java index 0d07ecc6b1..3c1366185d 100644 --- a/ebean-test/src/test/java/org/tests/model/interfaces/IPerson.java +++ b/ebean-test/src/test/java/org/tests/model/interfaces/IPerson.java @@ -1,7 +1,16 @@ package org.tests.model.interfaces; +import java.util.List; + public interface IPerson { + + long getOid(); + IAddress getDefaultAddress(); void setDefaultAddress(IAddress address); + + List getExtraAddresses(); + + List getAddressLinks(); } diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/Person.java b/ebean-test/src/test/java/org/tests/model/interfaces/Person.java index 5edbb68c7c..a2e5e8dafd 100644 --- a/ebean-test/src/test/java/org/tests/model/interfaces/Person.java +++ b/ebean-test/src/test/java/org/tests/model/interfaces/Person.java @@ -1,11 +1,19 @@ package org.tests.model.interfaces; +import io.ebean.annotation.ext.EntityImplements; + +import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Version; +import java.util.ArrayList; +import java.util.List; @Entity +@EntityImplements(IPerson.class) public class Person implements IPerson { @Id private long oid; @@ -13,9 +21,15 @@ public class Person implements IPerson { @Version private int version; - @ManyToOne(targetEntity = Address.class) + @ManyToOne(cascade = CascadeType.PERSIST) private IAddress defaultAddress; + @OneToMany(cascade = CascadeType.PERSIST, orphanRemoval = true) + private List extraAddresses = new ArrayList<>(); + + @ManyToMany(cascade = CascadeType.PERSIST) + private List addressLinks = new ArrayList<>(); + @Override public IAddress getDefaultAddress() { return defaultAddress; @@ -42,4 +56,14 @@ public void setVersion(int version) { this.version = version; } + @Override + public List getExtraAddresses() { + return extraAddresses; + } + + @Override + public List getAddressLinks() { + return addressLinks; + } + } diff --git a/ebean-test/src/test/java/org/tests/model/interfaces/TestTargetEntity.java b/ebean-test/src/test/java/org/tests/model/interfaces/TestTargetEntity.java index aa7030ef1d..f2ed05c573 100644 --- a/ebean-test/src/test/java/org/tests/model/interfaces/TestTargetEntity.java +++ b/ebean-test/src/test/java/org/tests/model/interfaces/TestTargetEntity.java @@ -30,7 +30,7 @@ public void test() { private Person setup() { Address address = new Address("street"); DB.save(address); - Person person = new Person(); + Person person = DB.getDefault().createEntityBean(Person.class); person.setDefaultAddress(address); DB.save(person); return person; diff --git a/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java b/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java index 28e7636a49..c78b3a2cc7 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java @@ -20,6 +20,20 @@ public class MnyEdge { @ManyToOne private MnyNode to; + public MnyEdge() { + } + + public MnyEdge(Object from, Object to) { + this.from = (MnyNode) from; + this.to = (MnyNode) to; + this.id = this.from.id * 10000 + this.to.id; + this.flags = this.from.id + this.to.id; + } + + public static MnyEdge createReverseRelation(Object to, MnyNode from) { + return new MnyEdge(from, to); + } + private int flags; public Integer getId() { diff --git a/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java b/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java index fabb580403..71444320fe 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java @@ -3,6 +3,7 @@ import io.ebean.annotation.Identity; import io.ebean.annotation.Platform; import io.ebean.annotation.Where; +import io.ebean.annotation.ext.IntersectionFactory; import javax.persistence.*; import java.util.List; @@ -16,16 +17,19 @@ public class MnyNode { String name; - @ManyToMany + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "mny_edge", joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id")) + @IntersectionFactory(MnyEdge.class) + @Where(clause = "${mta}.flags != 12345 and '${dbTableName}' = 'mny_node'") List allRelations; - @ManyToMany + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "mny_edge", joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) + @IntersectionFactory(value =MnyEdge.class, factoryMethod = "createReverseRelation") List allReverseRelations; @ManyToMany diff --git a/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java b/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java index d9ac37ffcc..2557d35e72 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java @@ -1,8 +1,8 @@ package org.tests.model.m2m; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -14,10 +14,78 @@ * Tests M2M with complex where queries. * * @author Roland Praml, FOCONIS AG - * */ public class TestM2MWithWhere extends BaseTestCase { + @Test + public void testModify() throws Exception { + + MnyNode node1 = new MnyNode(); + node1.setName("node1"); + node1.setId(111); + MnyNode node2 = new MnyNode(); + node2.setName("node2"); + node2.setId(222); + MnyNode node3 = new MnyNode(); + node3.setName("node3"); + node3.setId(333); + MnyNode node4 = new MnyNode(); + node4.setName("node4"); + node4.setId(444); + + node1.getAllReverseRelations().add(node2); + node1.getAllRelations().add(node2); + node2.getAllRelations().add(node3); + node3.getAllRelations().add(node4); + DB.save(node1); + DB.save(node1); + + DB.refresh(node2); + DB.refresh(node3); + assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node1, node3); + assertThat(node3.getAllReverseRelations()).containsExactlyInAnyOrder(node2); + + DB.refresh(node1); + node1.getAllReverseRelations().clear(); + System.out.println("Clearing"); + DB.save(node1); + DB.refresh(node2); + assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node3); + + node2.getAllRelations().clear(); + node2.getAllRelations().add(node3); + LoggedSql.start(); + DB.save(node2); + LoggedSql.stop().forEach(System.out::println); + + } + + @Test + public void testAccessAndModify() throws Exception { + createTestData(); + + MnyNode node = DB.find(MnyNode.class, 1); + node.setName("fooBarBaz"); + MnyNode removed = node.getAllRelations().remove(0); + + LoggedSql.start(); + DB.save(node); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update mny_node set name=? where id=?; -- bind(fooBarBaz"); + assertThat(sql.get(1)).contains("delete from mny_edge where from_id = ? and to_id = ? and mny_edge.flags != 12345 and 'mny_node' = 'mny_node'"); + assertThat(sql.get(2)).contains("-- bind"); + + node.getAllRelations().add(removed); + LoggedSql.start(); + DB.save(node); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into mny_edge (id, flags, from_id, to_id) values (?,?,?,?)"); + assertThat(sql.get(1)).contains("-- bind"); + + } + @Test public void testQuery() throws Exception { createTestData(); @@ -70,9 +138,9 @@ public void testGetter() throws Exception { // prefetch everything LoggedSql.start(); node = DB.find(MnyNode.class) - .fetch("bit1Relations","*") - .fetch("bit1ReverseRelations","*") - .where().idEq(3).findOne(); + .fetch("bit1Relations", "*") + .fetch("bit1ReverseRelations", "*") + .where().idEq(3).findOne(); sqls = LoggedSql.stop(); assertThat(sqls).hasSize(2); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java index d9a6944765..d7b5d39f62 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java @@ -10,7 +10,7 @@ public class OtoBMaster { String name; - @OneToOne(cascade = CascadeType.ALL, mappedBy = "master", fetch = FetchType.LAZY) + @OneToOne(cascade = CascadeType.ALL, mappedBy = "master", fetch = FetchType.LAZY, optional = false) OtoBChild child; public Long getId() { diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java index 36d7104c2d..afc224ab45 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java @@ -1,9 +1,6 @@ package org.tests.model.onetoone; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.OneToOne; -import javax.persistence.Version; +import javax.persistence.*; import java.util.UUID; @Entity @@ -17,7 +14,7 @@ public class OtoUBPrime { /** * Master side of bi-directional PrimaryJoinColumn. */ - @OneToOne(mappedBy = "prime") + @OneToOne(mappedBy = "prime", optional = false) OtoUBPrimeExtra extra; @Version diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java index df14dee017..ef8532a15f 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java @@ -14,7 +14,7 @@ public class OtoUBPrimeExtra { /** * Child side of bi-directional PrimaryJoinColumn. */ - @OneToOne + @OneToOne(optional = false) @PrimaryKeyJoinColumn OtoUBPrime prime; diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java index e4371304fa..f8ad34b0fd 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java @@ -13,15 +13,27 @@ public class OtoUPrime { String name; + /** * Effectively Ebean automatically sets Cascade PERSIST and mapped by for PrimaryKeyJoinColumn. - * This OneToOne is optional so left join to extra. + * This OneToOne is not optional so use inner join to extra (unless DbForeignkey(noConstraint = true) is set) + * Note: Violating the contract (Storing OtoUPrime without extra) may cause problems: + * - due the inner join, you might not get results from the query + * - you might get a "Beah has been deleted" if lazy load occurs on 'extra' */ - @OneToOne + @OneToOne(orphanRemoval = true, optional = false) @PrimaryKeyJoinColumn + // enforcing left join - without 'noConstraint = true', an inner join is used @DbForeignKey(noConstraint = true) OtoUPrimeExtra extra; + /** + * This OneToOne is optional so left join to extra. + * Setting FetchType.LAZY will NOT add the left join by default to the query. + */ + @OneToOne(mappedBy = "prime", fetch = FetchType.LAZY, orphanRemoval = true, optional = true) + OtoUPrimeOptionalExtra optionalExtra; + @Version Long version; @@ -65,4 +77,12 @@ public Long getVersion() { public void setVersion(Long version) { this.version = version; } + + public OtoUPrimeOptionalExtra getOptionalExtra() { + return optionalExtra; + } + + public void setOptionalExtra(OtoUPrimeOptionalExtra optionalExtra) { + this.optionalExtra = optionalExtra; + } } diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java index edd72b0ae3..311a4f6dbe 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java @@ -1,8 +1,8 @@ package org.tests.model.onetoone; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Version; +import io.ebean.annotation.Formula; + +import javax.persistence.*; import java.util.UUID; @Entity @@ -22,7 +22,7 @@ public OtoUPrimeExtra(String extra) { @Override public String toString() { - return "exId:"+ eid +" "+extra; + return "exId:" + eid + " " + extra; } public UUID getEid() { @@ -48,4 +48,5 @@ public Long getVersion() { public void setVersion(Long version) { this.version = version; } + } diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java new file mode 100644 index 0000000000..9b69a3e7c7 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java @@ -0,0 +1,52 @@ +package org.tests.model.onetoone; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.UUID; + +@Entity +public class OtoUPrimeExtraWithConstraint { + + @Id + UUID eid; + + String extra; + + @Version + Long version; + + public OtoUPrimeExtraWithConstraint(String extra) { + this.extra = extra; + } + + @Override + public String toString() { + return "exId:" + eid + " " + extra; + } + + public UUID getEid() { + return eid; + } + + public void setEid(UUID eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java new file mode 100644 index 0000000000..7ec0d0f1e3 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java @@ -0,0 +1,61 @@ +package org.tests.model.onetoone; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +public class OtoUPrimeOptionalExtra { + + @Id + UUID eid; + + String extra; + + @OneToOne(optional = false) + @PrimaryKeyJoinColumn + private OtoUPrime prime; + + @Version + Long version; + + public OtoUPrimeOptionalExtra(String extra) { + this.extra = extra; + } + + @Override + public String toString() { + return "exId:" + eid + " " + extra; + } + + public UUID getEid() { + return eid; + } + + public void setEid(UUID eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public OtoUPrime getPrime() { + return prime; + } + + public void setPrime(OtoUPrime prime) { + this.prime = prime; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java new file mode 100644 index 0000000000..29babe5685 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java @@ -0,0 +1,63 @@ +package org.tests.model.onetoone; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +public class OtoUPrimeWithConstraint { + + @Id + UUID pid; + + String name; + + @OneToOne(orphanRemoval = true, optional = false) + // @DbForeignKey(noConstraint = true) see OtoUPrime + @PrimaryKeyJoinColumn + OtoUPrimeExtraWithConstraint extra; + + @Version + Long version; + + public OtoUPrimeWithConstraint(String name) { + this.name = name; + } + + @Override + public String toString() { + return "id:" + pid + " name:" + name + " extra:" + extra; + } + + public UUID getPid() { + return pid; + } + + public void setPid(UUID pid) { + this.pid = pid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OtoUPrimeExtraWithConstraint getExtra() { + return extra; + } + + public void setExtra(OtoUPrimeExtraWithConstraint extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java index 122e298e93..1aa765092e 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java @@ -34,7 +34,7 @@ public void findWithLazyOneToOne() { String sql = sqlOf(query); assertThat(sql).contains("select t0.id, t0.name from oto_bmaster t0 where t0.id "); - assertThat(sql).doesNotContain("left join oto_bchild"); + assertThat(sql).doesNotContain("join oto_bchild"); assertThat(one).isNotNull(); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java index 81e8905ed7..63dd38bdde 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java @@ -45,7 +45,7 @@ public void insertUpdateDelete() { OtoUBPrime oneWith = queryWithFetch.findOne(); assertThat(oneWith).isNotNull(); - assertThat(sqlOf(queryWithFetch, 10)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_ubprime t0 left join oto_ubprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + assertThat(sqlOf(queryWithFetch, 10)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_ubprime t0 join oto_ubprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") .as("we join to oto_prime_extra"); assertThat(oneWith.getExtra().getExtra()).isEqualTo("v" + desc); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java index 39cbfa7309..3a9a4d6df2 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java @@ -1,14 +1,21 @@ package org.tests.model.onetoone; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Query; +import io.ebean.plugin.Property; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import javax.persistence.EntityNotFoundException; +import javax.persistence.PersistenceException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; public class TestOneToOnePrimaryKeyJoinOptional extends BaseTestCase { @@ -21,23 +28,120 @@ private OtoUPrime insert(String desc) { return prime; } - @Test - public void insertWithoutExtra() { + @BeforeEach + void prepare() { + OtoUPrime p1Single = new OtoUPrime("Prime without optional"); + p1Single.setExtra(new OtoUPrimeExtra("Non optional prime required")); + DB.save(p1Single); + + OtoUPrimeExtra p2 = new OtoUPrimeExtra("SinglePrimeExtra"); + try { + DB.save(p2); + fail("PrimExtra cannot exist without Prime"); + } catch (PersistenceException pe) { + + } String desc = "" + System.currentTimeMillis(); OtoUPrime p1 = new OtoUPrime("u" + desc); + p1.setExtra(new OtoUPrimeExtra("u" + desc)); + p1.setOptionalExtra(new OtoUPrimeOptionalExtra("This one has also an optional")); DB.save(p1); + } - Query query = DB.find(OtoUPrime.class) - .setId(p1.getPid()) - .fetch("extra", "eid"); + @AfterEach + void cleanup() { + DB.find(OtoUPrime.class).delete(); + assertThat(DB.find(OtoUPrimeExtra.class).findList()).isEmpty(); + assertThat(DB.find(OtoUPrimeOptionalExtra.class).findList()).isEmpty(); + } - OtoUPrime found = query.findOne(); + public void doTest1(boolean extraFetch, boolean optionalFetch) { + + // Query for "fetch" case - extra bean joined by left join + + Query query1 = DB.find(OtoUPrime.class); + if (extraFetch) { + query1.fetch("extra"); + } + if (optionalFetch) { + query1.fetch("optionalExtra"); + } + List primes = query1.findList(); + if (extraFetch && optionalFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.version, " + + "t1.eid, t1.extra, t1.version, " + + "t2.eid, t2.extra, t2.version, t2.eid " + + "from oto_uprime t0 " + + "left join oto_uprime_extra t1 on t1.eid = t0.pid " + // left join on non-optional, because DbForeignKey(noConstraint=true) is set + "left join oto_uprime_optional_extra t2 on t2.eid = t0.pid"); // left join on optional + } else if (extraFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid"); + } else if (optionalFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.pid, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_uprime t0 left join oto_uprime_optional_extra t1 on t1.eid = t0.pid"); + + } else { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.pid, t0.version from oto_uprime t0"); + } - if (found.getExtra() != null) { - found.getExtra().getExtra(); // fails here, because getExtra should be null + List versions = new ArrayList<>(); + for (OtoUPrime prime : primes) { + if (prime.getOptionalExtra() != null) { + versions.add(prime.getOptionalExtra().getVersion()); + } } - assertThat(found.getExtra()).isNull(); + assertThat(primes).hasSize(2); + assertThat(versions).containsExactly(1L); + } + + public void doTest2(boolean withFetch) { + + Query query2 = DB.find(OtoUPrimeOptionalExtra.class); + if (withFetch) { + query2.fetch("prime"); + } + List extraPrimes = query2.findList(); + if (withFetch) { + assertThat(query2.getGeneratedSql()).isEqualTo("select t0.eid, t0.extra, t0.version, t1.pid, t1.name, t1.pid, t1.version from oto_uprime_optional_extra t0 join oto_uprime t1 on t1.pid = t0.eid"); + } else { + assertThat(query2.getGeneratedSql()).isEqualTo("select t0.eid, t0.extra, t0.version, t0.eid from oto_uprime_optional_extra t0"); + } + List versions = new ArrayList<>(); + for (OtoUPrimeOptionalExtra extraPrime : extraPrimes) { + versions.add(extraPrime.getPrime().getVersion()); + } + assertThat(extraPrimes).hasSize(1); + assertThat(versions).containsExactly(1L); + } + + @Test + void testWithExtraFetch1() { + doTest1(true, false); + } + + @Test + void testWithOptionalFetch1() { + doTest1(false, true); + } + + @Test + void testWithBothFetch1() { + doTest1(true, true); + } + + @Test + void testWithoutFetch1() { + doTest1(false, false); + } + + @Test + void testWithFetch2() { + doTest2(true); + } + + @Test + void testWithoutFetch2() { + doTest2(false); } @Test @@ -54,7 +158,7 @@ public void insertUpdateDelete() { OtoUPrime found = query.findOne(); assertThat(found).isNotNull(); - assertThat(sqlOf(query, 4)).contains("select t0.pid, t0.name, t0.version, t0.pid from oto_uprime t0 where t0.pid = ?") + assertThat(sqlOf(query, 4)).contains("select t0.pid, t0.name, t0.pid, t0.version from oto_uprime t0 where t0.pid = ?") .as("we don't join to oto_uprime_extra"); assertThat(found.getName()).isEqualTo("u" + desc); @@ -66,7 +170,8 @@ public void insertUpdateDelete() { OtoUPrime oneWith = queryWithFetch.findOne(); assertThat(oneWith).isNotNull(); - assertThat(sqlOf(queryWithFetch, 6)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + assertThat(sqlOf(queryWithFetch, 6)) + .contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") .as("we join to oto_prime_extra"); @@ -98,8 +203,77 @@ private void thenDelete(OtoUPrime found) { DB.delete(bean); List sql = LoggedSql.stop(); - assertThat(sql).hasSize(2); + + assertThat(sql).hasSize(3); assertSql(sql.get(0)).contains("delete from oto_uprime_extra where"); - assertSql(sql.get(1)).contains("delete from oto_uprime where"); + assertSql(sql.get(1)).contains("delete from oto_uprime_optional_extra where"); + assertSql(sql.get(2)).contains("delete from oto_uprime where"); + } + + @Test + void testDdl() { + Collection props = DB.getDefault().pluginApi().beanType(OtoUPrime.class).allProperties(); + + for (Property prop : props) { + System.out.println(prop); + } + } + + @Test + void testContractViolation1() { + + OtoUPrime p1 = new OtoUPrime("Prime having no extra"); + // extra is "optional=false" - and this is a violating of the contract + DB.save(p1); + + Query query = DB.find(OtoUPrime.class).setId(p1.pid); + + OtoUPrime found1 = query.findOne(); + assertThat(query.getGeneratedSql()).doesNotContain("join"); + + assertThat(found1.getExtra()).isNotNull(); + assertThatThrownBy(() -> found1.getExtra().getVersion()).isInstanceOf(EntityNotFoundException.class); + + query.fetch("extra"); + OtoUPrime found2 = query.findOne(); + // Note: We use "left join" here, because 'DbForeignKey(noConstraint=true)' is set oh the property + // if this annotation is not preset, an inner join would be used and 'found2' would be 'null' then + assertThat(query.getGeneratedSql()).contains("from oto_uprime t0 left join oto_uprime_extra"); + assertThat(found2.getExtra()).isNull(); + + } + + @Test + void testContractViolation2() { + + OtoUPrimeExtraWithConstraint p1Const = new OtoUPrimeExtraWithConstraint("test"); + try { + // a foreign key prevents from saving + DB.save(p1Const); + fail("PrimExtra cannot exist without Prime"); + } catch (PersistenceException pe) { + // OK + } + + OtoUPrimeWithConstraint p1 = new OtoUPrimeWithConstraint("Prime having no extra"); + // extra is "optional=false" - and this is a violating of the contract + // Note there is no real foreign key in the database, that would prevent saving this entity + DB.save(p1); + + Query query = DB.find(OtoUPrimeWithConstraint.class).setId(p1.pid); + + OtoUPrimeWithConstraint found1 = query.findOne(); + assertThat(query.getGeneratedSql()).doesNotContain("join"); + + assertThat(found1.getExtra()).isNotNull(); + assertThatThrownBy(() -> found1.getExtra().getVersion()).isInstanceOf(EntityNotFoundException.class); + + query.fetch("extra"); + OtoUPrimeWithConstraint found2 = query.findOne(); + // Note: We use "left join" here, because 'DbForeignKey(noConstraint=true)' is set oh the property + // if this annotation is not preset, an inner join would be used and 'found2' would be 'null' then + assertThat(query.getGeneratedSql()).contains("from oto_uprime_with_constraint t0 join oto_uprime_extra_with_constraint"); + assertThat(found2).isNull(); + } } diff --git a/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java b/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java new file mode 100644 index 0000000000..3f392aed06 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java @@ -0,0 +1,61 @@ +package org.tests.model.tevent; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.ebean.annotation.Formula; +import io.ebean.config.dbplatform.DatabasePlatform; +import io.ebean.plugin.CustomDeployParser; +import io.ebean.plugin.DeployBeanDescriptorMeta; +import io.ebean.plugin.DeployBeanPropertyMeta; +import io.ebean.util.AnnotationUtil; +import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; + +/** + * Custom Annotation parser which parses @Count annotation + * + * @author Roland Praml, FOCONIS AG + */ +public class CustomFormulaAnnotationParser implements CustomDeployParser { + + private int counter; + + + @Target(FIELD) + @Retention(RUNTIME) + @Formula(select="TODO", join = "TODO") // meta-formula + public @interface Count { + String value(); + } + + + + @Override + public void parse(final DeployBeanDescriptorMeta descriptor, final DatabasePlatform databasePlatform) { + for (DeployBeanPropertyMeta prop : descriptor.propertiesAll()) { + readField(descriptor, prop); + } + } + + private void readField(DeployBeanDescriptorMeta descriptor, DeployBeanPropertyMeta prop) { + Count countAnnot = AnnotationUtil.get(prop.getField(), Count.class); + if (countAnnot != null) { + // @Count found, so build the (complex) count formula + DeployBeanPropertyAssocMany countProp = (DeployBeanPropertyAssocMany) descriptor.getBeanProperty(countAnnot.value()); + counter++; + String tmpTable = "f"+counter; + String sqlSelect = "coalesce(" + tmpTable + ".child_count, 0)"; + String parentId = countProp.getMappedBy() + "_id"; + String tableName = countProp.getBeanTable().getBaseTable(); + String sqlJoin = "left join (select " + parentId +", count(*) as child_count from " + tableName + " GROUP BY " + parentId + " )" + + " " + tmpTable + " on " + tmpTable + "." +parentId + " = ${ta}." + descriptor.idProperty().getDbColumn(); + prop.setSqlFormula(sqlSelect, sqlJoin); +// prop.setSqlFormula("f1.child_count", +// "join (select parent_id, count(*) as child_count from child_entity GROUP BY parent_id) f1 on f1.parent_id = ${ta}.id"); + } + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java b/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java index bcbad8ef2e..678ca9677c 100644 --- a/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java +++ b/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java @@ -42,6 +42,11 @@ public enum Status { @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) List logs; + @CustomFormulaAnnotationParser.Count("logs") + //@Formula(select = "f1.child_count", + //join = "left join (select event_id, count(*) as child_count from tevent_many GROUP BY event_id ) as f1 on f1.event_id = ${ta}.id") + Long customFormula; + public TEventOne(String name, Status status) { this.name = name; this.status = status; @@ -64,6 +69,10 @@ public Long getCount() { return count; } + public Long getCustomFormula() { + return customFormula; + } + public BigDecimal getTotalUnits() { return totalUnits; } diff --git a/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java b/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java index 8636d48d6f..9dde14d07f 100644 --- a/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java +++ b/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java @@ -49,7 +49,7 @@ public void testBaseSelect() { List list = query.findList(); String sql = sqlOf(query, 5); - assertThat(sql).contains("select t0.id, t0.name, t0.status, t0.version, t0.event_id from tevent_one t0"); + assertThat(sql).contains("select t0.id, t0.name, t0.status, coalesce(f1.child_count, 0), t0.version, t0.event_id from tevent_one t0"); for (TEventOne eventOne : list) { // lazy loading on Aggregation properties diff --git a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java index db02e5edaa..b1d380ba6a 100644 --- a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java +++ b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java @@ -489,7 +489,20 @@ void distinctWithOrderByPkWithId() { Query query = DB.find(Contact.class) .setDistinct(true) .select("customer.id") - .order().desc("customer.id"); + .order().desc("concat(customer.id, customer.billingAddress)"); + + query.findSingleAttributeList(); + + assertThat(sqlOf(query)).contains("select distinct t0.customer_id from contact t0 order by t0.customer_id desc"); + } + + @Test + void distinctWithOrderByPkWithId2() { + ResetBasicData.reset(); + + Query query = DB.find(Contact.class) + .setDistinct(true) + .select("concat(customer.id, customer.billingAddress)"); query.findSingleAttributeList(); @@ -825,19 +838,24 @@ void setup() { e3.setAttr1("a1"); DB.save(e3); + Cat cat = new Cat(); + cat.setId(4711L); MainEntityRelation rel = new MainEntityRelation(); rel.setEntity1(e1); rel.setEntity2(e1); + rel.setCat2(cat); DB.save(rel); rel = new MainEntityRelation(); rel.setEntity1(e2); rel.setEntity2(e2); + rel.setCat2(cat); DB.save(rel); rel = new MainEntityRelation(); rel.setEntity1(e3); rel.setEntity2(e3); + rel.setCat2(cat); DB.save(rel); } diff --git a/ebean-test/src/test/java/org/tests/rawsql/TestRawSqlNamedParams.java b/ebean-test/src/test/java/org/tests/rawsql/TestRawSqlNamedParams.java index 17cf86356e..9f37c010ff 100644 --- a/ebean-test/src/test/java/org/tests/rawsql/TestRawSqlNamedParams.java +++ b/ebean-test/src/test/java/org/tests/rawsql/TestRawSqlNamedParams.java @@ -44,9 +44,12 @@ public void testMySqlColonEquals() throws SQLException { Transaction transaction = DB.beginTransaction(); + System.out.println(transaction.connection().getMetaData().getDriverName()); try { - if ("MariaDB connector/J".equals(transaction.connection().getMetaData().getDriverName())) { - return; // MariaDb only supports callable statements in the form "? = call function x(?)" + if ("MariaDB Connector/J".equals(transaction.connection().getMetaData().getDriverName())) { + // CHECKME: Should we report/fail if we are running mysql-tests with mariadb driver + // BTW: This happens with java 8 - the drivers in drivermanager are in different order + return; // MariaDB Connector/J only supports callable statements in the form "? = call function x(?)" } CallableSql callableSql = DB.createCallableSql("set @total = 0"); DB.getDefault().execute(callableSql); diff --git a/ebean-test/src/test/resources/ebean.properties b/ebean-test/src/test/resources/ebean.properties index 9dec823eed..e4eefd3c51 100644 --- a/ebean-test/src/test/resources/ebean.properties +++ b/ebean-test/src/test/resources/ebean.properties @@ -196,6 +196,7 @@ datasource.hana.username=EBEAN_TEST datasource.hana.password=Eb3an_test datasource.hana.url=jdbc:sap://hxehost:39013/?databaseName=HXE #datasource.hana.driver=com.sap.db.jdbc.Driver +# # parameters for migration test datasource.migrationtest.username=SA @@ -207,6 +208,21 @@ ebean.migrationtest.ddl.run=false ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest ebean.migrationtest.migration.appName=migrationtest ebean.migrationtest.migration.migrationPath=migrationtest/dbmigration +ebean.migrationtest.migration.migrationInitPath=migrationtest/dbinit +ebean.migrationtest.migration.strict=true +ebean.migrationtest.migration.generate=true +ebean.migrationtest.migration.run=false +ebean.migrationtest.migration.includeIndex=true +ebean.migrationtest.migration.generateInit=true +ebean.migrationtest.migration.generatePendingDrop=auto +ebean.migrationtest.migration.platforms=db2luw,h2,hsqldb,mysql,mysql55,mariadb,postgres,oracle,sqlite,sqlserver17,hana,yugabyte +#migration.migrationtest.db2luw.prefix=db2 +#migration.migrationtest.sqlserver17.prefix=sqlserver +dbmigration.platform.mariadb.useMigrationStoredProcedures=true +dbmigration.platform.mysql.useMigrationStoredProcedures=true + + + ebean.migrationtest.migration.strict=true # enable stored procedures f dbmigration.platform.mariadb.useMigrationStoredProcedures=true @@ -222,4 +238,17 @@ ebean.migrationtest-history.ddl.run=false ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest ebean.migrationtest-history.migration.appName=migrationtest-history ebean.migrationtest-history.migration.migrationPath=migrationtest-history/dbmigration +ebean.migrationtest-history.migration.migrationInitPath=migrationtest-history/dbinit ebean.migrationtest-history.migration.strict=true + +# ServerStartTest - can we run the migrations and do we find the correct ones! +datasource.db2-migration.username=unit +datasource.db2-migration.password=test +datasource.db2-migration.url=jdbc:db2://127.0.0.1:50000/unit +ebean.db2-migration.ddl.generate=false +ebean.db2-migration.ddl.run=false +ebean.db2-migration.migration.run=true +ebean.db2-migration.databasePlatformName=db2luw +# workaround for https://github.com/ebean-orm/ebean-migration/issues/102 +ebean.db2-migration.migration.migrationPath=migrationtest/dbmigration/db2 +ebean.db2-migration.migration.migrationInitPath=migrationtest/dbinit/db2 \ No newline at end of file diff --git a/ebean-test/src/test/resources/extra-ddl.xml b/ebean-test/src/test/resources/extra-ddl.xml index 130de7fac6..44d49d1589 100644 --- a/ebean-test/src/test/resources/extra-ddl.xml +++ b/ebean-test/src/test/resources/extra-ddl.xml @@ -64,6 +64,15 @@ create index ix_ebasic_jmjb_gin2 on ebasic_json_map_json_b using gin(content jsonb_path_ops); + +delimiter $$ +BEGIN +IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN +call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ); +END IF; +END;$$ + + delimiter $$ BEGIN @@ -101,4 +110,5 @@ END IF; END $$ + diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/R__db2_explain_tables.sql b/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/R__db2_explain_tables.sql new file mode 100644 index 0000000000..b2539a5ea9 --- /dev/null +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/R__db2_explain_tables.sql @@ -0,0 +1,8 @@ + +delimiter $$ +BEGIN +IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN +call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ); +END IF; +END;$$ + \ No newline at end of file diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/idx_db2.migrations b/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/idx_db2.migrations index 16c7b2cec2..39e65b2d4b 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/idx_db2.migrations +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/db2fori/idx_db2.migrations @@ -3,5 +3,6 @@ -1187336846, 1.2__dropsFor_1.1.sql -150875853, 1.3.sql 946163478, 1.4__dropsFor_1.3.sql +-133543359, R__db2_explain_tables.sql 561281075, R__order_views.sql diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/R__db2_explain_tables.sql b/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/R__db2_explain_tables.sql new file mode 100644 index 0000000000..b2539a5ea9 --- /dev/null +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/R__db2_explain_tables.sql @@ -0,0 +1,8 @@ + +delimiter $$ +BEGIN +IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN +call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ); +END IF; +END;$$ + \ No newline at end of file diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/idx_db2.migrations b/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/idx_db2.migrations index 146c2c1d8a..a0380e2e7c 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/idx_db2.migrations +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/db2luw/idx_db2.migrations @@ -4,5 +4,6 @@ -1187336846, 1.2__dropsFor_1.1.sql 1976888196, 1.3.sql 946163478, 1.4__dropsFor_1.3.sql +-133543359, R__db2_explain_tables.sql 561281075, R__order_views.sql diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/R__db2_explain_tables.sql b/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/R__db2_explain_tables.sql new file mode 100644 index 0000000000..b2539a5ea9 --- /dev/null +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/R__db2_explain_tables.sql @@ -0,0 +1,8 @@ + +delimiter $$ +BEGIN +IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN +call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ); +END IF; +END;$$ + \ No newline at end of file diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/idx_db2.migrations b/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/idx_db2.migrations index 2d8e70ad25..382351279d 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/idx_db2.migrations +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/db2zos/idx_db2.migrations @@ -3,5 +3,6 @@ -1187336846, 1.2__dropsFor_1.1.sql 1976888196, 1.3.sql 946163478, 1.4__dropsFor_1.3.sql +-133543359, R__db2_explain_tables.sql 561281075, R__order_views.sql diff --git a/ebean-test/testconfig/ebean-oracle.properties b/ebean-test/testconfig/ebean-oracle.properties index 32b3af2491..24764943b2 100644 --- a/ebean-test/testconfig/ebean-oracle.properties +++ b/ebean-test/testconfig/ebean-oracle.properties @@ -1,3 +1,4 @@ ebean.test.platform=oracle ebean.test.dbName=test_eb +ebean.test.dbPassword=test datasource.default=oracle diff --git a/ebean-test/testplatforms.sh b/ebean-test/testplatforms.sh new file mode 100644 index 0000000000..1e19717c0e --- /dev/null +++ b/ebean-test/testplatforms.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# A small script, to run a certain test on all platforms +# invoke with ./testplatforms.sh -Dtest=DbMigrationTest +# Hint: in case of DbMigrationTest, you may disable ddl.run temporary + +# default H2 platform +set -e +mvn test "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-mysql.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-mariadb.properties "$@" +mvn surefire:test -Dprops.file=testconfig/ebean-mariadb-10.3.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-sqlserver17.properties "$@" +mvn surefire:test -Dprops.file=testconfig/ebean-sqlserver19.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-postgres.properties "$@" + +#mvn surefire:test -Dprops.file=testconfig/ebean-oracle.properties "$@" + +#mvn surefire:test -Dprops.file=testconfig/ebean-sqlite.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-hana.properties "$@" + +mvn surefire:test -Dprops.file=testconfig/ebean-db2.properties "$@" + +## Test ignored +## mvn surefire:test -Dprops.file=testconfig/ebean-yugabyte.properties "$@" + +## Scripts are not correct +## mvn surefire:test -Dprops.file=testconfig/ebean-cockroach.properties "$@" + +## Transactions are not supported +## mvn surefire:test -Dprops.file=testconfig/ebean-clickhouse.properties "$@" + +## I cannot start nuodb +## mvn surefire:test -Dprops.file=testconfig/ebean-nuodb.properties.properties "$@" + + diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml index 550421666c..e8b80acb42 100644 --- a/kotlin-querybean-generator/pom.xml +++ b/kotlin-querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT kotlin querybean generator @@ -29,7 +29,7 @@ io.ebean ebean-querybean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test @@ -43,7 +43,7 @@ io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test @@ -64,14 +64,14 @@ io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-ddl-generator - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/platforms/all/pom.xml b/platforms/all/pom.xml index b656bddd39..a7234f5dec 100644 --- a/platforms/all/pom.xml +++ b/platforms/all/pom.xml @@ -4,7 +4,7 @@ platforms io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-platform-all @@ -14,67 +14,67 @@ io.ebean ebean-platform-h2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-clickhouse - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-db2 - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-hana - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-hsqldb - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-mysql - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-mariadb - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-nuodb - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-oracle - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-postgres - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-sqlanywhere - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-sqlite - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT io.ebean ebean-platform-sqlserver - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/platforms/clickhouse/pom.xml b/platforms/clickhouse/pom.xml index ab5caf43ed..585078042f 100644 --- a/platforms/clickhouse/pom.xml +++ b/platforms/clickhouse/pom.xml @@ -4,7 +4,7 @@ platforms io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-platform-clickhouse @@ -14,7 +14,7 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/platforms/db2/pom.xml b/platforms/db2/pom.xml index af0b33ad1e..2822888a7e 100644 --- a/platforms/db2/pom.xml +++ b/platforms/db2/pom.xml @@ -4,7 +4,7 @@ platforms io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-platform-db2 @@ -14,7 +14,7 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT diff --git a/platforms/h2/pom.xml b/platforms/h2/pom.xml index d43c650cf4..12ef49081e 100644 --- a/platforms/h2/pom.xml +++ b/platforms/h2/pom.xml @@ -4,7 +4,7 @@ platforms io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT ebean-platform-h2 @@ -14,7 +14,7 @@ io.ebean ebean-api - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT + + + foconis-release + FOCONIS Release Repository + https://mvnrepo.foconis.de/repository/release/ + + + foconis-snapshot + FOCONIS Snapshot Repository + https://mvnrepo.foconis.de/repository/snapshot/ + + diff --git a/querybean-generator/pom.xml b/querybean-generator/pom.xml index 97a4023e54..94328c5355 100644 --- a/querybean-generator/pom.xml +++ b/querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT querybean generator diff --git a/release.md b/release.md new file mode 100644 index 0000000000..423bdb9ba6 --- /dev/null +++ b/release.md @@ -0,0 +1,18 @@ +## Release command + +We @foconis use this command to release. + + mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.6.0-FOC2-SNAPSHOT + mvn release:prepare release:perform -Darguments="-Dgpg.skip -DskipTests" -PfoconisRelease + + # RELEASE klappt nun, sollte es failen, ist wie folgt vorzugehen: + # um bei einen Fehler zu release ist dann ins target/checkout Verzeichnis zu gehen und + mvn clean source:jar install org.apache.maven.plugins:maven-deploy-plugin:deploy -DskipTests + # auszuführen. Aber auch das failed bei Kotlin. + + # nach dem Release müssen die Versionen in ebean-kotlin/pom.xml, tests/test-java16/pom.xml und tests/test-kotlin/pom.xml manuell angepasst werden + +generate Java classes from .xsd: + + export JAVA_TOOL_OPTIONS="-Duser.language=en -Duser.country=US -Dfile.encoding=UTF-8" + /c/Program\ Files/Java/jdk1.8.0_201/bin/xjc.exe src/main/resources/ebean-dbmigration-1.0.xsd -d src/main/java -p io.ebeaninternal.dbmigration.migration diff --git a/tests/pom.xml b/tests/pom.xml index ac7678298b..ca3d3d123e 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -5,7 +5,7 @@ org.avaje java8-oss 3.3 - + io.ebean diff --git a/tests/test-java16/pom.xml b/tests/test-java16/pom.xml index ad13be4672..a102ebcf7f 100644 --- a/tests/test-java16/pom.xml +++ b/tests/test-java16/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 io.ebean @@ -15,7 +14,7 @@ io.ebean ebean - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT @@ -27,7 +26,7 @@ io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test diff --git a/tests/test-kotlin/pom.xml b/tests/test-kotlin/pom.xml index 1d6caa2052..a65f752d10 100644 --- a/tests/test-kotlin/pom.xml +++ b/tests/test-kotlin/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 org.avaje java11-oss 3.7 - + test-kotlin @@ -35,14 +33,14 @@ io.ebean ebean-test - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test io.ebean ebean-core - 13.10.1-SNAPSHOT + 13.10.1-FOC4-SNAPSHOT test @@ -71,6 +69,11 @@ src/test/kotlin + + org.sonatype.plugins + nexus-staging-maven-plugin + false + org.jetbrains.kotlin kotlin-maven-plugin @@ -107,5 +110,4 @@ -