diff --git a/README.md b/README.md index b43bd0b63..14c8be025 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ +## Ebean ORM for Java & Kotlin + +**Multiple abstraction levels**: Ebean provides multiple levels of query abstraction [ORM Queries, mixed with SQL](https://ebean.io/docs/intro/queries/orm-query), [DTO Queries](https://ebean.io/docs/intro/queries/dto-query), [SqlQuery and JDBC](https://ebean.io/docs/intro/queries/sql-query). +Work at the highest level of abstraction and drop down levels as needed. + +**Database migrations**: Built in [DB migration](https://ebean.io/docs/db-migrations/) generation and running. Support for "rebase" migrations as well as repeatable, init and 'normal' migrations. + +**Awesome SQL**: Ebean produces SQL that you would hand craft yourself. Use great SQL, never generate SQL cartesian product, always honor relational limit/offset. + +**Automated query tuning**: For ORM queries Ebean can profile the object graph being used and either [automatically tune the query](https://ebean.io/docs/query/background/autotune). + +**Docker test containers**: [Docker test containers](https://ebean.io/docs/testing/) for all the supported databases. Get 100% test coverage on all the features of the database we use. + +**Type safe queries**: We can build queries using type safe [query beans](https://ebean.io/docs/query/query-beans). IDE auto-complete when writing queries, compile time checking and it's FUN. + +**Performance isn't optional**: Optimise queries to only fetch what we need (partial objects). Automatically avoid N+1 via a smart load context. + +#### Benefits of ORM + +* Automatically avoid N+1 +* L2 caching to reduce database load +* Queries mixing database and L2 cache +* Automatically tune ORM queries +* Elasticsearch for search or L3 cache + +## Actions + [![Build](https://github.com/ebean-orm/ebean/actions/workflows/build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/build.yml) [![Maven Central : ebean](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ebean-orm/ebean/blob/master/LICENSE) @@ -15,6 +42,8 @@ #### Builds against EA (Early Access) versions of Java (19, Loom, panama etc) + + [![ebean EA](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml) [![datasource EA](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml) [![migration EA](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml) diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index ef5b9f081..097d918c6 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -27,7 +27,13 @@ io.avaje avaje-config - 2.0 + 2.2 + + + + io.avaje + avaje-applog-slf4j + 1.0 - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5 diff --git a/ebean-kotlin/pom.xml b/ebean-kotlin/pom.xml index 7d71caa1a..23524c3f8 100644 --- a/ebean-kotlin/pom.xml +++ b/ebean-kotlin/pom.xml @@ -90,7 +90,7 @@ true - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5 diff --git a/ebean-postgis/pom.xml b/ebean-postgis/pom.xml index 2c68b0745..ca7ade150 100644 --- a/ebean-postgis/pom.xml +++ b/ebean-postgis/pom.xml @@ -104,7 +104,7 @@ true - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5 diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index 3e6d0033a..9d5b91b01 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -35,7 +35,7 @@ io.avaje avaje-lang - 1.0 + 1.1 @@ -107,7 +107,7 @@ true - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5 diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java b/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java index cc107a7bb..39340ddf8 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java @@ -9,10 +9,9 @@ import java.lang.annotation.Target; * Used to denote a query bean that has already been enhanced. *

* Used by the agent to detect already enhanced type query beans to skip enhancement processing. - *

*/ @Target({ ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) public @interface AlreadyEnhancedMarker { } diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/Generated.java b/ebean-querybean/src/main/java/io/ebean/typequery/Generated.java index 918e0d1b4..92036e7eb 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/Generated.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/Generated.java @@ -11,7 +11,7 @@ import java.lang.annotation.Target; * This is code generated by the query bean generator (annotation processor). */ @Target({ ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) public @interface Generated { /** diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java index 3883afe66..e6b2590e8 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java @@ -247,4 +247,15 @@ public abstract class PBaseValueEqual extends TQPropertyBase { public final R isIn(Query subQuery) { return in(subQuery); } + + /** + * Is NOT in the result of a subquery. + * + * @param subQuery values provided by a subQuery + * @return the root query bean instance + */ + public final R notIn(Query subQuery) { + expr().notIn(_name, subQuery); + return _root; + } } diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java index 566ce3f27..7f8621fb0 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -852,9 +852,11 @@ public abstract class TQRootBean { } /** - * When set to true all the beans from this query are loaded into the bean - * cache. + * Deprecated migrate to setBeanCacheMode() or setUseCache(). + *

+ * When set to true all the beans from this query are loaded into the bean cache. */ + @Deprecated public R setLoadBeanCache(boolean loadBeanCache) { query.setLoadBeanCache(loadBeanCache); return root; diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java index d0a538ee5..10a7c7e58 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java @@ -9,10 +9,9 @@ import java.lang.annotation.Target; * Used to denote a type query bean. *

* These are typically generated beans used to build queries using type safe query criteria. - *

*/ @Target({ ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) public @interface TypeQueryBean { /** diff --git a/ebean-querybean/src/test/resources/ebean.mf b/ebean-querybean/src/test/resources/ebean.mf index 5c97580de..90beca56f 100644 --- a/ebean-querybean/src/test/resources/ebean.mf +++ b/ebean-querybean/src/test/resources/ebean.mf @@ -1,2 +1,4 @@ entity-packages: org.example.domain querybean-packages: org.example.domain,org.querytest +debug: 0 +synthetic: false diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index d469881b0..122d21d26 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -80,7 +80,7 @@ true - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5 diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index 056704984..717175d86 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -266,7 +266,7 @@ true - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5 diff --git a/ebean-test/src/main/java/io/ebean/test/config/AutoConfigureForTesting.java b/ebean-test/src/main/java/io/ebean/test/config/AutoConfigureForTesting.java index c751732cf..fbe48318d 100644 --- a/ebean-test/src/main/java/io/ebean/test/config/AutoConfigureForTesting.java +++ b/ebean-test/src/main/java/io/ebean/test/config/AutoConfigureForTesting.java @@ -5,6 +5,7 @@ import io.ebean.config.DatabaseConfig; import io.ebean.datasource.DataSourceConfig; import io.ebean.test.config.platform.PlatformAutoConfig; import io.ebean.test.config.provider.ProviderAutoConfig; +import io.ebean.test.containers.DockerHost; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,11 +30,17 @@ public class AutoConfigureForTesting implements AutoConfigure { @Override public void preConfigure(DatabaseConfig config) { + Properties properties = config.getProperties(); + if (properties != null) { + // trigger determination of docker.host system property if not already done + // and re-evaluate properties in case there is use of ${docker.host} in jdbc url etc + DockerHost.host(); + io.avaje.config.Config.asConfiguration().evalModify(properties); + } if (!config.isDefaultServer()) { log.info("skip automatic testing config on non-default server name:{} register:{}", config.getName(), config.isRegister()); return; } - Properties properties = config.getProperties(); if (isExtraServer(config, properties)) { setupExtraDataSourceIfNecessary(config); return; diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java b/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java index 4dd36034a..44207a393 100644 --- a/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java @@ -2,6 +2,7 @@ package io.ebean.test.config.platform; import io.ebean.config.DatabaseConfig; import io.ebean.datasource.DataSourceConfig; +import io.ebean.test.containers.DockerHost; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +39,6 @@ class Config { private final DatabaseConfig config; private boolean containerDropCreate; private final Properties dockerProperties = new Properties(); - private final DockerHost dockerHost = new DockerHost(); Config(String db, String platform, String databaseName, DatabaseConfig config) { this.db = db; @@ -241,8 +241,7 @@ class Config { } String host() { - String explicitDockerHost = getKey("dockerHost", null); - return getKey("host", dockerHost.dockerHost(explicitDockerHost)); + return getKey("host", getKey("dockerHost", DockerHost.host())); } /** @@ -385,10 +384,6 @@ class Config { } private void initDockerProperties() { - if (dockerHost.runningInDocker()) { - // tell ebean-docker-test we are not using localhost (for jdbc DB setup commands) - dockerProperties.setProperty(dockerKey("host"), dockerHost.dockerHost()); - } dockerProperties.setProperty(dockerKey("port"), String.valueOf(port)); dockerProperties.setProperty(dockerKey("dbName"), databaseName); if (schema != null) { diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/DockerHost.java b/ebean-test/src/main/java/io/ebean/test/config/platform/DockerHost.java deleted file mode 100644 index 5e3f0ce94..000000000 --- a/ebean-test/src/main/java/io/ebean/test/config/platform/DockerHost.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.ebean.test.config.platform; - -import java.io.File; -import java.util.Locale; - -/** - * Helper to detect if running inside docker and determine host name for that case. - */ -class DockerHost { - - private final boolean runningInDocker; - private String dockerHost; - - DockerHost() { - runningInDocker = initInDocker(); - } - - boolean runningInDocker() { - return runningInDocker; - } - - String dockerHost() { - return dockerHost; - } - - String dockerHost(String explicitHost) { - if (!runningInDocker) { - return "localhost"; - } - dockerHost = explicitHost != null ? explicitHost : defaultDockerHost(); - return dockerHost; - } - - /** - * Return true if running inside a docker container (we are using docker in docker). - */ - boolean initInDocker() { - return new File("/.dockerenv").exists(); - } - - /** - * Return the default host name to use when running in docker. - *

- * Can instead be explicitly specified via ebean.test.dockerHost. - */ - String defaultDockerHost() { - String os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); - if (os.contains("mac") || os.contains("darwin") || os.contains("win")) { - return "host.docker.internal"; - } else { - return "172.17.0.1"; - } - } - -} diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/RedisSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/RedisSetup.java index 0fd933d26..5dc46ade5 100644 --- a/ebean-test/src/main/java/io/ebean/test/config/platform/RedisSetup.java +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/RedisSetup.java @@ -1,5 +1,6 @@ package io.ebean.test.config.platform; +import io.ebean.test.containers.DockerHost; import io.ebean.test.containers.RedisContainer; import java.util.Properties; @@ -10,11 +11,8 @@ class RedisSetup { String version = properties.getProperty("ebean.test.redis"); version = properties.getProperty("ebean.test.redis.version", version); if (version != null) { - DockerHost dockerHost = new DockerHost(); - if (dockerHost.runningInDocker()) { - String host = dockerHost.dockerHost(properties.getProperty("ebean.test.dockerHost")); - properties.setProperty("redis.host", host); - } + String host = properties.getProperty("ebean.test.dockerHost", DockerHost.host()); + properties.setProperty("redis.host", host); RedisContainer.builder(version) .properties(properties) .build() diff --git a/ebean-test/src/test/java/io/ebean/test/config/platform/DockerHostTest.java b/ebean-test/src/test/java/io/ebean/test/config/platform/DockerHostTest.java deleted file mode 100644 index 0be8a5d43..000000000 --- a/ebean-test/src/test/java/io/ebean/test/config/platform/DockerHostTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.ebean.test.config.platform; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class DockerHostTest { - - @Test - void runningInDocker_when_false_alwaysUseLocalhost() { - DockerHost dockerHost = new DockerHost(); - assertFalse(dockerHost.runningInDocker()); - assertEquals("localhost", dockerHost.dockerHost("foo")); - } - - @Test - void runningInDocker_when_true_useExplicit() { - TDDockerHost dockerHost = new TDDockerHost(); - assertTrue(dockerHost.runningInDocker()); - - assertEquals("my-host", dockerHost.dockerHost("my-host")); - } - - @Disabled - @Test - void runningInDocker_when_trueAndLinux_useDefault() { - TDDockerHost dockerHost = new TDDockerHost(); - assertTrue(dockerHost.runningInDocker()); - - assertEquals("172.17.0.1", dockerHost.dockerHost(null)); - } - - @Test - void runningInDocker_when_windowsDefault() { - TDDockerHost dockerHost = new TDDockerHost(); - assertTrue(dockerHost.runningInDocker()); - - String origName = System.getProperty("os.name"); - System.setProperty("os.name", "win"); - try { - assertEquals("host.docker.internal",dockerHost.defaultDockerHost()); - assertEquals("host.docker.internal", dockerHost.dockerHost(null)); - } finally { - System.setProperty("os.name", origName); - } - } - - @Test - void runningInDocker_when_macDefault() { - TDDockerHost dockerHost = new TDDockerHost(); - assertTrue(dockerHost.runningInDocker()); - - String origName = System.getProperty("os.name"); - System.setProperty("os.name", "mac"); - try { - assertEquals("host.docker.internal",dockerHost.defaultDockerHost()); - assertEquals("host.docker.internal", dockerHost.dockerHost(null)); - } finally { - System.setProperty("os.name", origName); - } - } - - - @Test - void runningInDocker_when_linuxDefault() { - TDDockerHost dockerHost = new TDDockerHost(); - assertTrue(dockerHost.runningInDocker()); - - String origName = System.getProperty("os.name"); - System.setProperty("os.name", "linux"); - try { - assertEquals("172.17.0.1",dockerHost.defaultDockerHost()); - assertEquals("172.17.0.1", dockerHost.dockerHost(null)); - } finally { - System.setProperty("os.name", origName); - } - } - - static class TDDockerHost extends DockerHost { - - @Override - boolean initInDocker() { - return true; - } - } -} diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQuery2Test.java b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQuery2Test.java index 8b86e635c..5c62ac653 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQuery2Test.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQuery2Test.java @@ -24,12 +24,36 @@ public class DtoQuery2Test extends BaseTestCase { private static final Logger log = LoggerFactory.getLogger(DtoQuery2Test.class); @Test - public void dto_findList_constructorMatch() { + void dto_findList_fluidAccessors() { + ResetBasicData.reset(); + List list = server().findDto(DCustFluidAccessors.class, "select id, name from o_customer").findList(); + + assertThat(list).isNotEmpty(); + for (DCustFluidAccessors cust: list) { + assertThat(cust.id()).isNotNull(); + assertThat(cust.name()).isNotNull(); + } + } + + @Test + void dto_findList_plainAccessors() { + ResetBasicData.reset(); + + List list = server().findDto(DCustPlainAccessors.class, "select id, name from o_customer").findList(); + + assertThat(list).isNotEmpty(); + for (DCustPlainAccessors cust: list) { + assertThat(cust.id()).isNotNull(); + assertThat(cust.name()).isNotNull(); + } + } + + @Test + void dto_findList_constructorMatch() { ResetBasicData.reset(); DtoQuery dtoQuery = server().findDto(DCust.class, "select id, name from o_customer"); - List list = dtoQuery.findList(); log.info(list.toString()); @@ -37,7 +61,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findIterator_closeWithResources() { + void dto_findIterator_closeWithResources() { ResetBasicData.reset(); int counter = 0; @@ -55,7 +79,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findIterator() { + void dto_findIterator() { ResetBasicData.reset(); final int expectedCount = server().find(Customer.class).findCount(); @@ -80,7 +104,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findStream() { + void dto_findStream() { ResetBasicData.reset(); final int expectedCount = server().find(Customer.class).findCount(); @@ -104,8 +128,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findEach_constructorMatch() { - + void dto_findEach_constructorMatch() { ResetBasicData.reset(); LoggedSql.start(); @@ -118,8 +141,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findEachWhile_constructorMatch() { - + void dto_findEachWhile_constructorMatch() { ResetBasicData.reset(); LoggedSql.start(); @@ -135,8 +157,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findOneEmpty() { - + void dto_findOneEmpty() { ResetBasicData.reset(); Optional rob = server().findDto(DCust.class, "select id, name from o_customer where name = :name") @@ -153,8 +174,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findOne() { - + void dto_findOne() { ResetBasicData.reset(); DCust fiona = server().findDto(DCust.class, "select id, name from o_customer where name = :name") @@ -172,8 +192,7 @@ public class DtoQuery2Test extends BaseTestCase { @Test - public void dto_queryPlanHits() { - + void dto_queryPlanHits() { ResetBasicData.reset(); resetAllMetrics(); @@ -191,7 +210,7 @@ public class DtoQuery2Test extends BaseTestCase { } // collect without reset - BasicMetricVisitor basic = new BasicMetricVisitor(false, true, true, true); + BasicMetricVisitor basic = new BasicMetricVisitor("db", false, true, true, true); server().metaInfo().visitMetrics(basic); List stats = basic.queryMetrics(); @@ -218,8 +237,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findList_relaxedMode() { - + void dto_findList_relaxedMode() { ResetBasicData.reset(); List list = server().findDto(DCust2.class, "select id, '42' as something_we_cannot_map, name from o_customer") @@ -231,8 +249,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findList_relaxedMode_defaultConstructor() { - + void dto_findList_relaxedMode_defaultConstructor() { ResetBasicData.reset(); List list = server().findDto(DCust2.class, "select id, '42' as something_we_cannot_map, name from o_customer") @@ -244,8 +261,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findList_constructorPlusMatch() { - + void dto_findList_constructorPlusMatch() { ResetBasicData.reset(); String sql = "select c.id, c.name, count(o.id) as totalOrders\n" + @@ -263,8 +279,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto_findList_setters() { - + void dto_findList_setters() { ResetBasicData.reset(); DtoQuery dtoQuery = server().findDto(DCust2.class, "select id, name from o_customer"); @@ -275,8 +290,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto3_findList_constructorMatch() { - + void dto3_findList_constructorMatch() { ResetBasicData.reset(); List robs = server().findDto(DCust3.class, "select id, name, 42 as totalOrders from o_customer where name like ?") @@ -290,8 +304,7 @@ public class DtoQuery2Test extends BaseTestCase { } @Test - public void dto3_findList_settersMatch() { - + void dto3_findList_settersMatch() { ResetBasicData.reset(); List robs = server().findDto(DCust3.class, "select id, name from o_customer where name = :name") @@ -305,9 +318,7 @@ public class DtoQuery2Test extends BaseTestCase { public static class DCust { final Integer id; - final String name; - int totalOrders; public DCust(Integer id, String name) { @@ -340,7 +351,6 @@ public class DtoQuery2Test extends BaseTestCase { public static class DCust2 { Integer id; - String name; @Override @@ -368,9 +378,7 @@ public class DtoQuery2Test extends BaseTestCase { public static class DCust3 { Integer id; - String name; - int totalOrders; public DCust3() { @@ -411,4 +419,60 @@ public class DtoQuery2Test extends BaseTestCase { this.name = name; } } + + public static class DCustFluidAccessors { + + Integer id; + String name; + + @Override + public String toString() { + return "id:" + id + " name:" + name; + } + + public Integer id() { + return id; + } + + public DCustFluidAccessors id(Integer id) { + this.id = id; + return this; + } + + public String name() { + return name; + } + + public DCustFluidAccessors name(String name) { + this.name = name; + return this; + } + } + + public static class DCustPlainAccessors { + + Integer id; + String name; + + @Override + public String toString() { + return "id:" + id + " name:" + name; + } + + public Integer id() { + return id; + } + + public void id(Integer id) { + this.id = id; + } + + public String name() { + return name; + } + + public void name(String name) { + this.name = name; + } + } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryTest.java b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryTest.java index 4717fabf4..a97dbc39a 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryTest.java @@ -298,7 +298,7 @@ public class DtoQueryTest extends BaseTestCase { } // collect without reset - BasicMetricVisitor basic = new BasicMetricVisitor(false, true, true, true); + BasicMetricVisitor basic = new BasicMetricVisitor("db", false, true, true, true); server().metaInfo().visitMetrics(basic); List stats = basic.queryMetrics(); @@ -322,6 +322,8 @@ public class DtoQueryTest extends BaseTestCase { log.info("stats " + stats); + String asJson = server().metaInfo().metricsAsJson(metric2).withHash(false).withNewLine(false).json(); + assertThat(asJson).contains("dto.DCust_basic2"); } @Test diff --git a/ebean-test/src/test/java/org/tests/basic/TestLoadBeanCache.java b/ebean-test/src/test/java/org/tests/basic/TestLoadBeanCache.java index 249a2d0b3..3448e1c2c 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestLoadBeanCache.java +++ b/ebean-test/src/test/java/org/tests/basic/TestLoadBeanCache.java @@ -1,8 +1,10 @@ package org.tests.basic; +import io.ebean.Query; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.test.LoggedSql; +import io.ebeaninternal.api.SpiQuery; import org.junit.jupiter.api.Test; import org.tests.model.basic.Country; import org.tests.model.basic.Customer; @@ -16,6 +18,14 @@ import static org.junit.jupiter.api.Assertions.assertSame; class TestLoadBeanCache extends BaseTestCase { + @Test + void loadBeanCache_false() { + Query query = DB.find(Country.class).setLoadBeanCache(false); + + SpiQuery spiQuery = (SpiQuery) query; + assertThat(spiQuery.isBeanCachePut()).isFalse(); + } + @Test void testLoad() { diff --git a/ebean-test/src/test/java/org/tests/basic/type/TestExtraScalarTypes.java b/ebean-test/src/test/java/org/tests/basic/type/TestExtraScalarTypes.java index ff8806b25..62d3a0a59 100644 --- a/ebean-test/src/test/java/org/tests/basic/type/TestExtraScalarTypes.java +++ b/ebean-test/src/test/java/org/tests/basic/type/TestExtraScalarTypes.java @@ -16,8 +16,26 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; class TestExtraScalarTypes extends BaseTestCase { @Test - void test() { + void insertLargeTimezone() { + TimeZone tz = TimeZone.getTimeZone("America/Argentina/ComodRivadavia"); + var bean = new ESomeType(); + bean.setTimeZone(tz); + DB.save(bean); + var found = DB.find(ESomeType.class, bean.getId()); + assert found != null; + assertThat(found.getTimeZone()).isEqualTo(tz); + + var findByTimezone = DB.find(ESomeType.class).where().eq("timeZone", tz).findList(); + assertThat(findByTimezone).hasSize(1); + assertThat(findByTimezone.get(0).getId()).isEqualTo(found.getId()); + assertThat(findByTimezone.get(0).getTimeZone()).isEqualTo(tz); + + DB.delete(found); + } + + @Test + void test() { Locale locale = Locale.ENGLISH; Currency currency = Currency.getInstance(Locale.US); TimeZone tz = TimeZone.getDefault(); diff --git a/ebean-test/src/test/java/org/tests/o2m/OMVertexOther.java b/ebean-test/src/test/java/org/tests/o2m/OMVertexOther.java index 237e4aa01..95ec6d101 100644 --- a/ebean-test/src/test/java/org/tests/o2m/OMVertexOther.java +++ b/ebean-test/src/test/java/org/tests/o2m/OMVertexOther.java @@ -11,8 +11,25 @@ public class OMVertexOther { private UUID id; private final String name; + private String other; public OMVertexOther(String name) { this.name = name; } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public String getOther() { + return other; + } + + public void setOther(String other) { + this.other = other; + } } diff --git a/ebean-test/src/test/java/org/tests/o2m/TestOneToManyDuplicateInTxn.java b/ebean-test/src/test/java/org/tests/o2m/TestOneToManyDuplicateInTxn.java new file mode 100644 index 000000000..5575cd061 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/TestOneToManyDuplicateInTxn.java @@ -0,0 +1,68 @@ +package org.tests.o2m; + +import io.ebean.CacheMode; +import io.ebean.DB; +import io.ebean.Transaction; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestOneToManyDuplicateInTxn { + + @Test + void findTwice() { + OMVertex master = new OMVertex(UUID.randomUUID()); + OMVertexOther child = new OMVertexOther("child"); + master.getRelated().add(child); + DB.save(master); + + try (Transaction txn = DB.beginTransaction()) { + OMVertex first = DB.find(OMVertex.class) + .setDisableLazyLoading(true) + .fetch("related") + .where().eq("id", master.getId()) + .findOne(); + + assertThat(first.getRelated()).hasSize(1); + + OMVertex second = DB.find(OMVertex.class) + //.setLoadBeanCache(true) + .setBeanCacheMode(CacheMode.PUT) // force query to hit database + .setDisableLazyLoading(true) + .fetch("related") + .where().eq("id", master.getId()) + .findOne(); + + assertThat(second.getRelated()).hasSize(1); + } + } + + @Test + void findTwice_partial() { + OMVertex master = new OMVertex(UUID.randomUUID()); + OMVertexOther child = new OMVertexOther("child"); + master.getRelated().add(child); + DB.save(master); + + try (Transaction txn = DB.beginTransaction()) { + OMVertex first = DB.find(OMVertex.class) + .setDisableLazyLoading(true) + .fetch("related", "name") // load a partially loaded bean + .where().eq("id", master.getId()) + .findOne(); + + assertThat(first.getRelated()).hasSize(1); + + OMVertex second = DB.find(OMVertex.class) + .setBeanCacheMode(CacheMode.PUT) // force query to hit database + .setDisableLazyLoading(true) + .fetch("related") + .where().eq("id", master.getId()) + .findOne(); + + assertThat(second.getRelated()).hasSize(1); + } + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/TestOneToManyStatelessUpdateResultsInSoftDelete.java b/ebean-test/src/test/java/org/tests/o2m/TestOneToManyStatelessUpdateResultsInSoftDelete.java new file mode 100644 index 000000000..e6e86e5dd --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/TestOneToManyStatelessUpdateResultsInSoftDelete.java @@ -0,0 +1,238 @@ +package org.tests.o2m; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.ebean.DB; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; +import org.tests.o2m.dm.GoodsEntity; +import org.tests.o2m.dm.PersonEntity; +import org.tests.o2m.dm.WorkflowEntity; +import org.tests.o2m.dm.WorkflowOperationEntity; + +import java.io.StringWriter; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase { + @Test + void testStatelessUpdateShouldntDelete() { + LoggedSql.start(); + var goods = new GoodsEntity(); + var workflow = new WorkflowEntity(); + var operation1 = new WorkflowOperationEntity(); + goods.setWorkflowEntity(workflow); + workflow.setOperations(List.of(operation1)); + + DB.save(goods); + LoggedSql.collect(); + + // statelessly add another operation to the workflow and save goods + var goodsStateless = new GoodsEntity(); + goodsStateless.setId(goods.getId()); + var workflowStateless = new WorkflowEntity(); + workflowStateless.setId(workflow.getId()); + var operation1Stateless = new WorkflowOperationEntity(); + operation1Stateless.setId(operation1.getId()); + var operation2 = new WorkflowOperationEntity(); + goodsStateless.setWorkflowEntity(workflowStateless); + workflowStateless.setOperations(List.of(operation1Stateless, operation2)); + + // With the fix the SQL is now: + /* + txn[1002] update workflow_entity set when_modified=? where id=? + txn[1002] -- bind(2022-07-04 11:10:54.446,1) + txn[1002] update workflow_operation_entity set deleted=true where workflow_id = ? and not ( id in (?) ) + txn[1002] -- bind(1, Array[1]={1}) + txn[1002] insert into workflow_operation_entity (name, version, when_created, when_modified, deleted, workflow_id) values (?,?,?,?,?,?) + txn[1002] -- bind(null,1,2022-07-04 11:10:54.458,2022-07-04 11:10:54.458,false,1) + txn[1002] update goods_entity set when_modified=?, workflow_entity_id=? where id=?; -- bind(2022-07-04 11:10:54.446,1,1) + */ + + /* + - this update generates following statements + 1 txn[] delete from workflow_operation_entity where workflow_id=? + 2 txn[] -- bind(1) + 3 txn[] update workflow_entity set when_modified=? where id=? + 4 txn[] -- bind(2022-06-29 15:43:55.573,1) + 5 txn[] update workflow_operation_entity set deleted=true where workflow_id = ? and not ( id in (?) ) + 6 txn[] -- bind(1, Array[1]={1}) + 7 txn[] insert into workflow_operation_entity (name, version, when_created, when_modified, deleted, workflow_id) values (?,?,?,?,?,?) + 8 txn[] -- bind(null,1,2022-06-29 15:43:55.584,2022-06-29 15:43:55.584,false,1) + 9 txn[] update goods_entity set when_modified=?, workflow_entity_id=? where id=?; -- bind(2022-06-29 15:43:55.573,1,1) + + - number 1 is wrong + - no delete should be issued + - even if it was issued, it should have been soft delete + - the DB.update will throw exception if there is one-to-many relation on workflow_operation_entity + - it would still be referenced from other table + */ + DB.update(goodsStateless); + var updateSql = LoggedSql.stop(); + //updateSql.forEach(System.out::println); + var dbGoodsAfterUpdate = DB.find(GoodsEntity.class, goods.getId()); + assertThat(dbGoodsAfterUpdate.getWorkflowEntity().getOperations()).hasSize(2); + assertThat(dbGoodsAfterUpdate.getWorkflowEntity().getOperations()).extracting("id").contains(operation1.getId(), operation2.getId()); + updateSql.forEach(sql -> assertThat(sql).doesNotContain("delete from workflow_entity")); + } + + // same as previous but DB.update throws exception + @Test + void testStatelessUpdateShouldntDeleteThrows() { + LoggedSql.start(); + var goods = new GoodsEntity(); + goods.setName("ver1"); + var workflow = new WorkflowEntity(); + workflow.setRevision("ver1"); + var operation1 = new WorkflowOperationEntity(); + operation1.setName("ver1"); + goods.setWorkflowEntity(workflow); + workflow.setOperations(List.of(operation1)); + + DB.save(goods); + + List createSql = LoggedSql.stop(); + LoggedSql.start(); + + // statelessly add another operation to the workflow and save goods + var goodsStateless = new GoodsEntity(); + goodsStateless.setId(goods.getId()); + goodsStateless.setName("ver2"); + var workflowStateless = new WorkflowEntity(); + workflowStateless.setRevision("ver2"); + workflowStateless.setId(workflow.getId()); + var operation1Stateless = new WorkflowOperationEntity(); + operation1Stateless.setName("ver2"); + operation1Stateless.setId(operation1.getId()); + var operation2 = new WorkflowOperationEntity(); + operation2.setName("ver2"); + goodsStateless.setWorkflowEntity(workflowStateless); + workflowStateless.setOperations(List.of(operation1Stateless, operation2)); + + + // throws + DB.update(goodsStateless); + var updateSql = LoggedSql.stop(); + updateSql.forEach(System.out::println); + var dbGoodsAfterUpdate = DB.find(GoodsEntity.class, goods.getId()); + assertThat(dbGoodsAfterUpdate.getWorkflowEntity().getOperations()).hasSize(2); + assertThat(dbGoodsAfterUpdate.getWorkflowEntity().getOperations().get(0).getId()).isEqualTo(operation1.getId()); + assertThat(dbGoodsAfterUpdate.getWorkflowEntity().getOperations().get(1).getId()).isEqualTo(operation2.getId()); + updateSql.forEach(sql -> assertThat(sql).doesNotContain("delete from workflow_entity")); + } + + @Test + void duplicateKeyWorkflowEntityInsertInsteadOfUpdate() { + var goods = new GoodsEntity(); + goods.setName("ver1"); + var workflow = new WorkflowEntity(); + workflow.setRevision("ver1"); + var operation1 = new WorkflowOperationEntity(); + operation1.setName("ver1"); + goods.setWorkflowEntity(workflow); + workflow.setOperations(List.of(operation1)); + + DB.save(goods); + + // delete operation + var goodsAfterInsert = DB.find(GoodsEntity.class, goods.getId()); + assertThat(goodsAfterInsert.getWorkflowEntity().getOperations()).hasSize(1); + goodsAfterInsert.getWorkflowEntity().setOperations(List.of()); + + DB.save(goodsAfterInsert); + assertThat(goodsAfterInsert.getWorkflowEntity().getOperations()).isEmpty(); + assertThat(DB.find(GoodsEntity.class, goods.getId()).getWorkflowEntity().getOperations()).isEmpty(); + + // statelessly add new WorkflowOperationEntity + var goodsStateless = new GoodsEntity(); + goodsStateless.setId(goods.getId()); + + var workflowStateless = new WorkflowEntity(); + workflowStateless.setId(workflow.getId()); + goodsStateless.setWorkflowEntity(workflowStateless); + + var operation2 = new WorkflowOperationEntity(); + workflowStateless.setOperations(List.of(operation2)); + + // Using save() throws io.ebean.DuplicateKeyException: Error when batch flush on sql: insert into workflow_entity ... + // Must be an update() and not save() for this to be a "stateless update" + DB.update(goodsStateless); + + var ops = workflow.getOperations(); + // shouldn't contain deleted operations + assertThat(ops).hasSize(1); + assertThat(goodsStateless.getWorkflowEntity().getOperations().get(0).getId()).isNotEqualTo(operation1.getId()); + } + + @Test + void softDeleteIncludedInQuery() throws Exception { + var defaultPerson = new PersonEntity(); + defaultPerson.setName("test"); + DB.save(defaultPerson); + + // create GoodsEntity with 1 WorkflowOperationEntity + var goods = new GoodsEntity(); + goods.setCreatedBy(defaultPerson); + goods.setName("ver1"); + var workflow = new WorkflowEntity(); + workflow.setRevision("ver1"); + var operation1 = new WorkflowOperationEntity(); + operation1.setName("ver1"); + goods.setWorkflowEntity(workflow); + workflow.setOperations(List.of(operation1)); + + DB.save(goods); + + // statelessly delete WorkflowOperationEntity + var goodsStateless = new GoodsEntity(); + goodsStateless.setId(goods.getId()); + var workflowStateless = new WorkflowEntity(); + workflowStateless.setId(workflow.getId()); + goodsStateless.setWorkflowEntity(workflowStateless); + workflowStateless.setOperations(List.of()); + + LoggedSql.start(); + DB.update(goodsStateless); + // uncommenting this lines makes the test pass + //assertThat(goodsStateless.getWorkflowEntity().getOperations().size()).isEqualTo(0); + + var sql = LoggedSql.stop(); + sql.forEach(System.out::println); + + System.out.println("BEFORE TRY"); + LoggedSql.start(); + + try (var writer = new StringWriter()) { + var mapper = new ObjectMapper(); + mapper.writeValue(writer, goodsStateless); + sql = LoggedSql.stop(); + sql.forEach(System.out::println); + /* + select t0.id, t0.name, t0.workflow_entity_id, t0.version, t0.when_created, t0.when_modified from goods_entity t0 where t0.id = ?; --bind(4, ) --micros(161) + select t0.id, t0.name, t0.version, t0.when_created, t0.when_modified, t0.created_by, t0.updated_by, t0.workflow_entity_id from goods_entity t0 where t0.id = ?; --bind(4, ) --micros(525) + select t0.id, t0.name, t0.version, t0.when_created, t0.when_modified from person_entity t0 where t0.id = ?; --bind(1, ) --micros(325) + ! is this even issue? - select does not check if workflow_entity is deleted + select t0.id, t0.revision, t0.version, t0.when_created, t0.when_modified from workflow_entity t0 where t0.id = ?; --bind(1, ) --micros(439) + select t0.id, t0.revision, t0.version, t0.when_created, t0.when_modified, t0.created_by, t0.updated_by from workflow_entity t0 where t0.id = ?; --bind(1, ) --micros(332) + + select t0.id, t0.name, t0.version, t0.when_created, t0.when_modified from person_entity t0 where t0.id = ?; --bind(1, ) --micros(197) + select t0.workflow_id, t0.id, t0.position, t0.name, t0.workflow_id, t0.version, t0.when_created, t0.when_modified, t0.deleted from workflow_operation_entity t0 where (t0.workflow_id) in (?) order by t0.workflow_id, t0.position; --bind(Array[1]={1}) --micros(2776) + select t0.id, t0.position, t0.name, t0.version, t0.when_created, t0.when_modified, t0.deleted, t0.workflow_id, t0.created_by, t0.updated_by from workflow_operation_entity t0 where t0.id = ?; --bind(1, ) --micros(415) + + !! ignores soft delete + also to note - when the DM extends BaseDomain instead of HistoryColumns, this bug does not happen + (presumably since @WhoCreated Person createdBy is lazy loaded, when it is eagerly loaded, this bug does not occur) + select t0.workflow_id, t0.id, t0.position, t0.name, t0.workflow_id, t0.version, t0.when_created, t0.when_modified, t0.deleted from workflow_operation_entity t0 where (t0.workflow_id) in (?) order by t0.workflow_id, t0.position; --bind(Array[1]={4}) --micros(585) + + select t0.id, t0.position, t0.name, t0.version, t0.when_created, t0.when_modified, t0.deleted, t0.created_by, t0.updated_by, t0.workflow_id from workflow_operation_entity t0 where t0.id = ?; --bind(7, ) --micros(532) + */ + + writer.flush(); + var serialized = writer.toString(); + System.out.println(serialized); + var readGoods = mapper.readValue(writer.toString(), GoodsEntity.class); + assertThat(readGoods.getWorkflowEntity().getOperations()).hasSize(0); + } + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/dm/GoodsEntity.java b/ebean-test/src/test/java/org/tests/o2m/dm/GoodsEntity.java new file mode 100644 index 000000000..ab29d5435 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/dm/GoodsEntity.java @@ -0,0 +1,29 @@ +package org.tests.o2m.dm; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToOne; + +@Entity +public class GoodsEntity extends HistoryColumns { + private String name; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + private WorkflowEntity workflowEntity; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public WorkflowEntity getWorkflowEntity() { + return workflowEntity; + } + + public void setWorkflowEntity(WorkflowEntity workflowEntity) { + this.workflowEntity = workflowEntity; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/dm/HistoryColumns.java b/ebean-test/src/test/java/org/tests/o2m/dm/HistoryColumns.java new file mode 100644 index 000000000..31d8f127e --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/dm/HistoryColumns.java @@ -0,0 +1,36 @@ +package org.tests.o2m.dm; + +import org.tests.model.draftable.BaseDomain; + +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; + +@MappedSuperclass +public class HistoryColumns extends BaseDomain { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by") + private PersonEntity createdBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "updated_by") + private PersonEntity updatedBy; + + + public PersonEntity getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(PersonEntity createdBy) { + this.createdBy = createdBy; + } + + public PersonEntity getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(PersonEntity updatedBy) { + this.updatedBy = updatedBy; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/dm/PersonEntity.java b/ebean-test/src/test/java/org/tests/o2m/dm/PersonEntity.java new file mode 100644 index 000000000..6b71ab238 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/dm/PersonEntity.java @@ -0,0 +1,26 @@ +package org.tests.o2m.dm; + +import org.tests.model.draftable.BaseDomain; + +import javax.persistence.Entity; + +@Entity +public class PersonEntity extends BaseDomain { + + public PersonEntity() { + } + + public PersonEntity(Long id) { + this.setId(id); + } + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/dm/WorkflowEntity.java b/ebean-test/src/test/java/org/tests/o2m/dm/WorkflowEntity.java new file mode 100644 index 000000000..1cb1bfa1a --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/dm/WorkflowEntity.java @@ -0,0 +1,46 @@ +package org.tests.o2m.dm; + +import io.ebean.annotation.SoftDelete; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class WorkflowEntity extends HistoryColumns { + private String revision; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "workflow_id") + private List operations = new ArrayList<>(); + + @SoftDelete + private boolean deleted; + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public String getRevision() { + return revision; + } + + public void setRevision(String revision) { + this.revision = revision; + } + + public List getOperations() { + return operations; + } + + public void setOperations(List operations) { + this.operations = operations; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/dm/WorkflowOperationEntity.java b/ebean-test/src/test/java/org/tests/o2m/dm/WorkflowOperationEntity.java new file mode 100644 index 000000000..325d0b811 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/dm/WorkflowOperationEntity.java @@ -0,0 +1,44 @@ +package org.tests.o2m.dm; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.ebean.annotation.SoftDelete; + +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +@Entity +public class WorkflowOperationEntity extends HistoryColumns { + private String name; + + @ManyToOne + @JoinColumn(name = "workflow_id") + @JsonIgnore + private WorkflowEntity workflowEntity; + @SoftDelete + private boolean deleted; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public WorkflowEntity getWorkflowEntity() { + return workflowEntity; + } + + public void setWorkflowEntity(WorkflowEntity workflowEntity) { + this.workflowEntity = workflowEntity; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/recurse/RMItem.java b/ebean-test/src/test/java/org/tests/o2m/recurse/RMItem.java new file mode 100644 index 000000000..a08a0a58f --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/recurse/RMItem.java @@ -0,0 +1,51 @@ +package org.tests.o2m.recurse; + +import javax.persistence.*; +import java.util.List; + +@Entity +public class RMItem { + + @Id + private long itemId; + + @ManyToOne + @JoinColumn(name = "item_group_id") + private RMItem itemGroup; + + private String name; + + @OneToMany(mappedBy = "itemGroup") + private List subItems; + + public RMItem() { + } + + public RMItem(String name) { + this.name = name; + } + + public Long getItemId() { + return itemId; + } + + public void setItemId(Long itemId) { + this.itemId = itemId; + } + + public RMItem getItemGroup() { + return itemGroup; + } + + public void setItemGroup(RMItem itemGroup) { + this.itemGroup = itemGroup; + } + + public List getSubItems() { + return subItems; + } + + public void setSubItems(List subItems) { + this.subItems = subItems; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/recurse/RMItemHolder.java b/ebean-test/src/test/java/org/tests/o2m/recurse/RMItemHolder.java new file mode 100644 index 000000000..aa77c3e73 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/recurse/RMItemHolder.java @@ -0,0 +1,80 @@ +package org.tests.o2m.recurse; + +import io.ebean.Model; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Version; + +@Entity +public class RMItemHolder extends Model { + + @Id + long id; + String name; + String notes; + @ManyToOne + //@JoinColumn(name = "item_a_id") + private RMItem itemA; + @ManyToOne + //@JoinColumn(name = "item_b_id") + private RMItem itemB; + @Version + long version; + + public RMItemHolder(String name) { + this.name = name; + } + + public RMItemHolder() { + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + public RMItem getItemA() { + return itemA; + } + + public void setItemA(RMItem itemA) { + this.itemA = itemA; + } + + public RMItem getItemB() { + return itemB; + } + + public void setItemB(RMItem itemB) { + this.itemB = itemB; + } +} diff --git a/ebean-test/src/test/java/org/tests/o2m/recurse/TestFetchOneToManySameTypeTwoPaths.java b/ebean-test/src/test/java/org/tests/o2m/recurse/TestFetchOneToManySameTypeTwoPaths.java new file mode 100644 index 000000000..34df0ac88 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/o2m/recurse/TestFetchOneToManySameTypeTwoPaths.java @@ -0,0 +1,133 @@ +package org.tests.o2m.recurse; + +import io.ebean.DB; +import io.ebean.Database; +import io.ebean.FetchConfig; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestFetchOneToManySameTypeTwoPaths { + + @Test + void testSubItemListFetch_ofQuery() { + Database server = DB.getDefault(); + + RMItem itemA = new RMItem("a"); + server.save(itemA); + RMItem itemB = new RMItem("b"); + server.save(itemB); + + for (int i=0; i<2; i++) { + RMItem subItem = new RMItem(); + subItem.setItemGroup(itemA); + server.save(subItem); + } + + for (int i=0; i<3; i++) { + RMItem subItem = new RMItem(); + subItem.setItemGroup(itemB); + server.save(subItem); + } + + RMItemHolder customer = new RMItemHolder(); + customer.setItemA(itemA); + customer.setItemB(itemB); + server.save(customer); + + // This is OK + { + RMItemHolder requestedCustomer = server.find(RMItemHolder.class) + .setDisableLazyLoading(true) + .fetch("itemA.subItems", FetchConfig.ofQuery()) + .fetch("itemB.subItems", FetchConfig.ofQuery()) + .where() + .eq("id", customer.getId()) + .findOne(); + assertEquals(2, requestedCustomer.getItemA().getSubItems().size()); + assertEquals(3, requestedCustomer.getItemB().getSubItems().size()); + } + } + + @Test + void testSubItemListFetch_itemAFirst() { + Database server = DB.getDefault(); + + RMItem itemA = new RMItem("aa"); + server.save(itemA); + RMItem itemB = new RMItem("bb"); + server.save(itemB); + + for (int i=0; i<2; i++) { + RMItem subItem = new RMItem(); + subItem.setItemGroup(itemA); + server.save(subItem); + } + + for (int i=0; i<3; i++) { + RMItem subItem = new RMItem(); + subItem.setItemGroup(itemB); + server.save(subItem); + } + + RMItemHolder customer = new RMItemHolder(); + customer.setItemA(itemA); + customer.setItemB(itemB); + server.save(customer); + + // This fails because requestedCustomer.getItemB().getSubItems() is not loaded + { + RMItemHolder requestedCustomer = server.find(RMItemHolder.class) + .setDisableLazyLoading(true) + .fetch("itemA.subItems") + .fetch("itemB.subItems") + .where() + .eq("id", customer.getId()) + .findOne(); + assertEquals(2, requestedCustomer.getItemA().getSubItems().size()); + assertEquals(3, requestedCustomer.getItemB().getSubItems().size()); + } + } + + @Test + void testSubItemListFetch_itemBFirst() { + Database server = DB.getDefault(); + + RMItem itemA = new RMItem("a"); + server.save(itemA); + RMItem itemB = new RMItem("b"); + server.save(itemB); + + for (int i=0; i<2; i++) { + RMItem subItem = new RMItem(); + subItem.setItemGroup(itemA); + server.save(subItem); + } + + for (int i=0; i<5; i++) { + RMItem subItem = new RMItem(); + subItem.setItemGroup(itemB); + server.save(subItem); + } + + RMItemHolder customer = new RMItemHolder(); + customer.setItemA(itemA); + customer.setItemB(itemB); + server.save(customer); + + // This fails because requestedCustomer.getItemA().getSubItems() is not loaded + { + RMItemHolder requestedCustomer = server.find(RMItemHolder.class) + .setDisableLazyLoading(true) + .fetch("itemB.subItems") + .fetch("itemA.subItems") + .where() + .eq("id", customer.getId()) + .findOne(); + assertEquals(2, requestedCustomer.getItemA().getSubItems().size()); + assertEquals(5, requestedCustomer.getItemB().getSubItems().size()); + System.out.println("here"); + } + } + +} diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/1.0__initial.sql b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/1.0__initial.sql index 25f82c76d..1fc2b0f70 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/1.0__initial.sql +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/1.0__initial.sql @@ -14,9 +14,9 @@ create table migtest_ckey_detail ( create table migtest_ckey_parent ( one_key integer not null, + version integer not null, two_key varchar(127) not null, name varchar(255), - version integer not null, constraint pk_migtest_ckey_parent primary key (one_key,two_key) ); @@ -56,10 +56,6 @@ create table migtest_fk_set_null ( create table migtest_e_basic ( id integer generated by default as identity not null, - status varchar(1), - status2 varchar(1) default 'N' not null, - name varchar(127), - description varchar(127), description_file bytea, json_list json, a_lob varchar(255) default 'X' not null, @@ -67,13 +63,17 @@ create table migtest_e_basic ( old_boolean boolean default false not null, old_boolean2 boolean, eref_id integer, + user_id integer not null, + status varchar(1), + status2 varchar(1) default 'N' not null, + name varchar(127), + description varchar(127), indextest1 varchar(127), indextest2 varchar(127), indextest3 varchar(127), indextest4 varchar(127), indextest5 varchar(127), indextest6 varchar(127), - user_id integer not null, constraint ck_migtest_e_basic_status check ( status in ('N','A','I')), constraint ck_migtest_e_basic_status2 check ( status2 in ('N','A','I')), constraint uq_migtest_e_basic_indextest2 unique (indextest2), diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/idx_postgres.migrations b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/idx_postgres.migrations index 7f9a10350..4639f4b3b 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/idx_postgres.migrations +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres/idx_postgres.migrations @@ -1,4 +1,4 @@ -1014378814, 1.0__initial.sql +2094204208, 1.0__initial.sql -2047324426, 1.1.sql -261111052, 1.2__dropsFor_1.1.sql 1335621453, 1.3.sql diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/1.0__initial.sql b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/1.0__initial.sql index e319a8737..a9869f95b 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/1.0__initial.sql +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/1.0__initial.sql @@ -14,9 +14,9 @@ create table migtest_ckey_detail ( create table migtest_ckey_parent ( one_key integer not null, + version integer not null, two_key varchar(127) not null, name varchar(255), - version integer not null, constraint pk_migtest_ckey_parent primary key (one_key,two_key) ); @@ -56,10 +56,6 @@ create table migtest_fk_set_null ( create table migtest_e_basic ( id serial not null, - status varchar(1), - status2 varchar(1) default 'N' not null, - name varchar(127), - description varchar(127), description_file bytea, json_list json, a_lob varchar(255) default 'X' not null, @@ -67,13 +63,17 @@ create table migtest_e_basic ( old_boolean boolean default false not null, old_boolean2 boolean, eref_id integer, + user_id integer not null, + status varchar(1), + status2 varchar(1) default 'N' not null, + name varchar(127), + description varchar(127), indextest1 varchar(127), indextest2 varchar(127), indextest3 varchar(127), indextest4 varchar(127), indextest5 varchar(127), indextest6 varchar(127), - user_id integer not null, constraint ck_migtest_e_basic_status check ( status in ('N','A','I')), constraint ck_migtest_e_basic_status2 check ( status2 in ('N','A','I')), constraint uq_migtest_e_basic_indextest2 unique (indextest2), diff --git a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/idx_postgres.migrations b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/idx_postgres.migrations index 3ed7fcf1d..fd06b52c1 100644 --- a/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/idx_postgres.migrations +++ b/ebean-test/src/test/resources/migrationtest/dbmigration/postgres9/idx_postgres.migrations @@ -1,4 +1,4 @@ -382349675, 1.0__initial.sql +-231429368, 1.0__initial.sql -606251140, 1.1.sql -261111052, 1.2__dropsFor_1.1.sql -893728811, 1.3.sql diff --git a/pom.xml b/pom.xml index 807aaf9b6..01ead27fd 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.avaje java11-oss - 3.8 + 3.9 io.ebean @@ -41,13 +41,13 @@ false 2.13.2 2.1.212 - 2.0 + 2.2 1.2 13.6.0 - 6.1 - 8.0 - 13.6.4 - 13.6.4 + 6.2 + 8.2 + 13.6.5 + 13.6.5 false diff --git a/tests/test-java16/pom.xml b/tests/test-java16/pom.xml index a2aca2bd1..2748f7921 100644 --- a/tests/test-java16/pom.xml +++ b/tests/test-java16/pom.xml @@ -52,7 +52,7 @@ io.ebean querybean-generator - 13.6.4-FOC3-SNAPSHOT + 13.6.6-SNAPSHOT @@ -65,7 +65,7 @@ true - io.ebean.tile:enhancement:13.6.0 + io.ebean.tile:enhancement:13.6.5