From bbe799913bc467bf55d0db8441ae19709f7891c6 Mon Sep 17 00:00:00 2001 From: patrickspensieri Date: Tue, 13 Sep 2022 13:59:56 -0400 Subject: [PATCH 1/5] Reproduce possible regression with lazy loading soft-deleted relations This commit demonstrates - Deeply relations that are soft-deleted not loaded if lazy loading - But they are loaded when eager loading is used via .fetch() --- pom.xml | 17 ++-- .../java/org/example/domain/Customer.java | 61 -------------- .../java/org/example/domain/Environment.java | 34 ++++++++ .../org/example/domain/ServiceAccount.java | 43 ++++++++++ .../java/org/example/domain/UsageRaw.java | 38 +++++++++ .../dbmigration/1.1_create_tables.xml | 25 ++++++ src/test/java/main/MainDbMigration.java | 2 +- .../org/example/domain/CustomerQueryTest.java | 19 ----- .../java/org/example/domain/CustomerTest.java | 78 ------------------ .../LazyLoadingSoftDeletedReferencesTest.java | 80 +++++++++++++++++++ src/test/resources/application-test.yaml | 14 ++-- src/test/resources/logback-test.xml | 4 +- 12 files changed, 244 insertions(+), 171 deletions(-) delete mode 100644 src/main/java/org/example/domain/Customer.java create mode 100644 src/main/java/org/example/domain/Environment.java create mode 100644 src/main/java/org/example/domain/ServiceAccount.java create mode 100644 src/main/java/org/example/domain/UsageRaw.java create mode 100644 src/main/resources/dbmigration/1.1_create_tables.xml delete mode 100644 src/test/java/org/example/domain/CustomerQueryTest.java delete mode 100644 src/test/java/org/example/domain/CustomerTest.java create mode 100644 src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java diff --git a/pom.xml b/pom.xml index cd1b866..4761517 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.example - example-mininal + minimal-bug-reproduction 1.1-SNAPSHOT @@ -14,6 +14,7 @@ 3.8.1 3.0.0-M5 3.0.0-M5 + 13.8.0 @@ -27,14 +28,20 @@ io.ebean ebean - 12.11.1 + ${ebean-version} + + + + mysql + mysql-connector-java + 8.0.28 io.ebean querybean-generator - 12.11.1 + ${ebean-version} provided @@ -43,7 +50,7 @@ io.ebean ebean-test - 12.11.1 + ${ebean-version} test @@ -75,7 +82,7 @@ org.avaje.tile:java-compile:11 - io.ebean.tile:enhancement:12.11.1 + io.ebean.tile:enhancement:${ebean-version} diff --git a/src/main/java/org/example/domain/Customer.java b/src/main/java/org/example/domain/Customer.java deleted file mode 100644 index 0d3da1f..0000000 --- a/src/main/java/org/example/domain/Customer.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.example.domain; - -import io.ebean.Model; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Version; - -@Entity -public class Customer extends Model { - - @Id - Long id; - - String name; - - String notes; - - @Version - Long version; - - public Customer(String name) { - this.name = name; - } - - public Customer() { - } - - 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; - } - -} diff --git a/src/main/java/org/example/domain/Environment.java b/src/main/java/org/example/domain/Environment.java new file mode 100644 index 0000000..f02cd34 --- /dev/null +++ b/src/main/java/org/example/domain/Environment.java @@ -0,0 +1,34 @@ +package org.example.domain; + +import java.util.UUID; + +import javax.persistence.Entity; +import javax.persistence.Id; + +import io.ebean.Model; +import io.ebean.annotation.SoftDelete; + +@Entity +public class Environment extends Model { + @Id + private UUID id; + + @SoftDelete + private boolean deleted; + + private String name; + + public Environment(String name, boolean deleted) { + this.id = UUID.randomUUID(); + this.name = name; + this.deleted = deleted; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/example/domain/ServiceAccount.java b/src/main/java/org/example/domain/ServiceAccount.java new file mode 100644 index 0000000..8c35500 --- /dev/null +++ b/src/main/java/org/example/domain/ServiceAccount.java @@ -0,0 +1,43 @@ +package org.example.domain; + +import java.util.UUID; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; + +import io.ebean.Model; +import io.ebean.annotation.SoftDelete; + +@Entity +public class ServiceAccount extends Model { + @Id + private UUID id; + + @OneToOne + private Environment environment; + + @SoftDelete + private boolean deleted; + + private String name; + + public ServiceAccount(String name, Environment environment, boolean deleted) { + this.id = UUID.randomUUID(); + this.name = name; + this.environment = environment; + this.deleted = deleted; + } + + public UUID getId() { + return id; + } + + public Environment getEnvironment() { + return environment; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/example/domain/UsageRaw.java b/src/main/java/org/example/domain/UsageRaw.java new file mode 100644 index 0000000..608a989 --- /dev/null +++ b/src/main/java/org/example/domain/UsageRaw.java @@ -0,0 +1,38 @@ +package org.example.domain; + +import java.util.UUID; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +import io.ebean.Model; + +@Entity +public class UsageRaw extends Model { + @Id + private UUID id; + + @ManyToOne + private ServiceAccount serviceAccount; + + private String name; + + public UsageRaw(String name, ServiceAccount serviceAccount) { + this.id = UUID.randomUUID(); + this.name = name; + this.serviceAccount = serviceAccount; + } + + public UUID getId() { + return id; + } + + public ServiceAccount getServiceAccount() { + return serviceAccount; + } + + public String getName() { + return name; + } +} diff --git a/src/main/resources/dbmigration/1.1_create_tables.xml b/src/main/resources/dbmigration/1.1_create_tables.xml new file mode 100644 index 0000000..37fe40f --- /dev/null +++ b/src/main/resources/dbmigration/1.1_create_tables.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/main/MainDbMigration.java b/src/test/java/main/MainDbMigration.java index 428b246..0db1f00 100644 --- a/src/test/java/main/MainDbMigration.java +++ b/src/test/java/main/MainDbMigration.java @@ -18,7 +18,7 @@ public static void main(String[] args) throws Exception { //System.setProperty("ddl.migration.pendingDropsFor", "1.1"); DbMigration dbMigration = DbMigration.create(); - dbMigration.setPlatform(Platform.POSTGRES); + dbMigration.setPlatform(Platform.MYSQL); // generate the migration ddl and xml dbMigration.generateMigration(); } diff --git a/src/test/java/org/example/domain/CustomerQueryTest.java b/src/test/java/org/example/domain/CustomerQueryTest.java deleted file mode 100644 index 6524ab1..0000000 --- a/src/test/java/org/example/domain/CustomerQueryTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.example.domain; - -import io.ebean.DB; -import org.example.domain.query.QCustomer; -import org.junit.jupiter.api.Test; - -public class CustomerQueryTest { - - @Test - public void findAll() { - - DB.find(Customer.class) - .findList(); - - new QCustomer() - .id.greaterOrEqualTo(1L) - .findList(); - } -} diff --git a/src/test/java/org/example/domain/CustomerTest.java b/src/test/java/org/example/domain/CustomerTest.java deleted file mode 100644 index 29e66e4..0000000 --- a/src/test/java/org/example/domain/CustomerTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.example.domain; - -import io.ebean.DB; -import io.ebean.Database; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - - -/** - * When running tests in the IDE install the "Enhancement plugin". - *

- * http://ebean-orm.github.io/docs/setup/enhancement#ide - */ -public class CustomerTest { - - - /** - * Get the "default database" and save(). - */ - @Test - public void insert_via_database() { - - Customer rob = new Customer("Rob"); - - Database server = DB.getDefault(); - server.save(rob); - - assertNotNull(rob.getId()); - } - - /** - * Use the Ebean singleton (effectively using the "default server"). - */ - @Test - public void insert_via_model() { - - Customer jim = new Customer("Jim"); - jim.save(); - - assertNotNull(jim.getId()); - } - - - /** - * Find and then update. - */ - @Test - public void updateRob() { - - Customer newBob = new Customer("Bob"); - newBob.save(); - - Customer bob = DB.find(Customer.class) - .where().eq("name", "Bob") - .findOne(); - - bob.setNotes("Doing an update"); - bob.save(); - } - - /** - * Execute an update without a prior query. - */ - @Test - public void statelessUpdate() { - - Customer newMob = new Customer("Mob"); - newMob.save(); - - Customer upd = new Customer(); - upd.setId(newMob.getId()); - upd.setNotes("Update without a fetch"); - - upd.update(); - } - -} diff --git a/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java b/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java new file mode 100644 index 0000000..85f734e --- /dev/null +++ b/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java @@ -0,0 +1,80 @@ +package org.example.domain; + +import static java.util.Objects.nonNull; + +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import io.ebean.DB; +import io.ebean.Database; + +public class LazyLoadingSoftDeletedReferencesTest { + + @BeforeAll + private static void setup() { + insertRows(true); + insertRows(false); + } + + @Test + public void lazy_loading_does_not_fetch_soft_deleted_references() { + /* given: existing usage + [ + { id: 1, account: { name: 'usage-1', deleted: false, environment: { name: 'env-1', deleted: false } }, + { id: 2, account: { name: 'usage-2', deleted: true, environment: { name: 'env-2', deleted: true } }, + ] */ + Database server = DB.getDefault(); + + // when: lazily fetching + List lazyUsage = server.find(UsageRaw.class) + .setIncludeSoftDeletes() + .findList(); + // then: soft-deleted environments not loaded + assert(lazyUsage.size() == 2); + assert(lazyUsage.stream().allMatch(accountLoaded())); + assert(lazyUsage.stream().filter(environmentLoaded()).count() == 1); + + // when: eagerly fetching + List eagerUsage = server.find(UsageRaw.class) + .setIncludeSoftDeletes() + .fetch("serviceAccount.environment") + .findList(); + // then: soft-deleted environments are loaded + assert(eagerUsage.size() == 2); + assert(eagerUsage.stream().allMatch(accountLoaded())); + assert(eagerUsage.stream().allMatch(environmentLoaded())); + } + + private static Predicate accountLoaded() { + return usage -> { + boolean loaded = nonNull(usage.getServiceAccount()) + && nonNull(usage.getServiceAccount().getName()); + // put breakpoint here to see post-load state + return loaded; + }; + } + + private static Predicate environmentLoaded() { + return usage -> { + boolean loaded = nonNull(usage.getServiceAccount()) + && nonNull(usage.getServiceAccount().getEnvironment()) + && nonNull(usage.getServiceAccount().getEnvironment().getName()); + // put breakpoint here to see post-load state + return loaded; + }; + } + + private static void insertRows(boolean softDeleted) { + Environment environment = new Environment("environment", softDeleted); + ServiceAccount account = new ServiceAccount("account", environment, softDeleted); + UsageRaw usage = new UsageRaw("usage", account); + + Database server = DB.getDefault(); + server.save(environment); + server.save(account); + server.save(usage); + } +} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 49adf3b..95558eb 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,7 +1,11 @@ ebean: test: -# useDocker: false -# shutdown: stop # stop | remove - platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite - ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate - dbName: myapp + useDocker: true + platform: mysql # h2, postgres, mysql, oracle, sqlserver + dbName: test + mysql: + username: cloudmc + password: cloudmc + url: jdbc:mysql://localhost:3306/test + collation: utf8mb4_unicode_ci + characterSet: utf8mb4 diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index e16eb74..0c9a6e1 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -28,7 +28,7 @@ - + @@ -45,4 +45,4 @@ - \ No newline at end of file + From 9b1d055a009512967d9b36e12f892537c5d1429a Mon Sep 17 00:00:00 2001 From: patrickspensieri Date: Tue, 13 Sep 2022 15:28:42 -0400 Subject: [PATCH 2/5] Add missing deleted column to serviceAccounts table --- src/main/resources/dbmigration/1.1_create_tables.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/dbmigration/1.1_create_tables.xml b/src/main/resources/dbmigration/1.1_create_tables.xml index 37fe40f..a4857e1 100644 --- a/src/main/resources/dbmigration/1.1_create_tables.xml +++ b/src/main/resources/dbmigration/1.1_create_tables.xml @@ -13,6 +13,7 @@ + From cb416316079ee140aa2c5e5d50a87e60d09c6aef Mon Sep 17 00:00:00 2001 From: patrickspensieri Date: Thu, 15 Sep 2022 08:12:57 -0400 Subject: [PATCH 3/5] Remove username/pw for local test db --- src/test/resources/application-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 95558eb..fc4c72e 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -4,8 +4,8 @@ ebean: platform: mysql # h2, postgres, mysql, oracle, sqlserver dbName: test mysql: - username: cloudmc - password: cloudmc + username: your-username + password: your-password url: jdbc:mysql://localhost:3306/test collation: utf8mb4_unicode_ci characterSet: utf8mb4 From 6614a996496f09b66202ae77efecffab2d1fd170 Mon Sep 17 00:00:00 2001 From: patrickspensieri Date: Tue, 20 Sep 2022 11:13:51 -0400 Subject: [PATCH 4/5] Demonstrate behaviour with multiple fetchLazy paths --- pom.xml | 2 + .../org/example/domain/ServiceAccount.java | 4 +- .../java/org/example/domain/UsageRaw.java | 3 +- .../LazyLoadingSoftDeletedReferencesTest.java | 92 ++++++++++++++++--- 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 4761517..b1370d1 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ 3.0.0-M5 3.0.0-M5 13.8.0 + + diff --git a/src/main/java/org/example/domain/ServiceAccount.java b/src/main/java/org/example/domain/ServiceAccount.java index 8c35500..f6755b7 100644 --- a/src/main/java/org/example/domain/ServiceAccount.java +++ b/src/main/java/org/example/domain/ServiceAccount.java @@ -2,8 +2,10 @@ import java.util.UUID; +import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.ManyToOne; import javax.persistence.OneToOne; import io.ebean.Model; @@ -14,7 +16,7 @@ public class ServiceAccount extends Model { @Id private UUID id; - @OneToOne + @ManyToOne(cascade = CascadeType.REMOVE) private Environment environment; @SoftDelete diff --git a/src/main/java/org/example/domain/UsageRaw.java b/src/main/java/org/example/domain/UsageRaw.java index 608a989..9e362e0 100644 --- a/src/main/java/org/example/domain/UsageRaw.java +++ b/src/main/java/org/example/domain/UsageRaw.java @@ -2,6 +2,7 @@ import java.util.UUID; +import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -13,7 +14,7 @@ public class UsageRaw extends Model { @Id private UUID id; - @ManyToOne + @ManyToOne(cascade = CascadeType.REMOVE) private ServiceAccount serviceAccount; private String name; diff --git a/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java b/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java index 85f734e..552e914 100644 --- a/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java +++ b/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java @@ -5,44 +5,110 @@ import java.util.List; import java.util.function.Predicate; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.ebean.DB; import io.ebean.Database; public class LazyLoadingSoftDeletedReferencesTest { - - @BeforeAll - private static void setup() { + @BeforeEach + private void setup() { insertRows(true); insertRows(false); } + @AfterEach + private void cleanup() { + Database server = DB.getDefault(); + server.deleteAll(server.find(UsageRaw.class).findList()); + } + + /** + * Demonstrates that default fetching behaves differently as of 13.6.6. + * + * Also demonstrates workaround, using fetchLazy() which yields desired results. + */ @Test - public void lazy_loading_does_not_fetch_soft_deleted_references() { - /* given: existing usage + public void setIncludeSoftDeletes_does_not_fetch_deep_relations_that_are_softdeleted() { + // given: existing usage + /* [ { id: 1, account: { name: 'usage-1', deleted: false, environment: { name: 'env-1', deleted: false } }, { id: 2, account: { name: 'usage-2', deleted: true, environment: { name: 'env-2', deleted: true } }, ] */ Database server = DB.getDefault(); - // when: lazily fetching - List lazyUsage = server.find(UsageRaw.class) + // demonstrates new behaviour in 13.6.6 + // when: selecting without fetch variations + List usage = server.find(UsageRaw.class) .setIncludeSoftDeletes() .findList(); // then: soft-deleted environments not loaded + assert(usage.size() == 2); + assert(usage.stream().allMatch(accountLoaded())); + assert(usage.stream().filter(environmentLoaded()).count() == 1); + + // demonstrates workaround, use fetchLazy() + // when: lazy fetching + List lazyUsage = server.find(UsageRaw.class) + .setIncludeSoftDeletes() + .fetchLazy("serviceAccount.environment") + .findList(); + // then: soft-deleted environments are loaded assert(lazyUsage.size() == 2); assert(lazyUsage.stream().allMatch(accountLoaded())); - assert(lazyUsage.stream().filter(environmentLoaded()).count() == 1); + assert(lazyUsage.stream().allMatch(environmentLoaded())); + } + + /** + * Demonstrates that .fetchLazy() with multiple paths behaves differently in 13.6.6 + * and does not behave the same as .fetch(). + * + * Also demonstrates workaround that yield desired results. + */ + @Test + public void fetchLazy_with_multiple_paths_does_not_fetch_deep_relations_that_are_softdeleted() { + // given: existing usage + /* + [ + { id: 1, account: { name: 'usage-1', deleted: false, environment: { name: 'env-1', deleted: false } }, + { id: 2, account: { name: 'usage-2', deleted: true, environment: { name: 'env-2', deleted: true } }, + ] */ + Database server = DB.getDefault(); - // when: eagerly fetching + // demonstrates new behaviour in 13.6.6 + // when: lazy fetching serviceAccount AND serviceAccount.environment + List lazyUsageMultiplePaths = server.find(UsageRaw.class) + .setIncludeSoftDeletes() + .fetchLazy("serviceAccount") + .fetchLazy("serviceAccount.environment") + .findList(); + // then: soft-deleted environments are NOT loaded (not the case before 13.6.6) + assert(lazyUsageMultiplePaths.size() == 2); + assert(lazyUsageMultiplePaths.stream().allMatch(accountLoaded())); + assert(lazyUsageMultiplePaths.stream().filter(environmentLoaded()).count() == 1); + + // demonstrates workaround, use single path, all required properties are loaded + // when: lazy fetching serviceAccount.environment + List lazyUsageSinglePath = server.find(UsageRaw.class) + .setIncludeSoftDeletes() + .fetchLazy("serviceAccount.environment") + .findList(); + // then: soft-deleted environments loaded + assert(lazyUsageSinglePath.size() == 2); + assert(lazyUsageSinglePath.stream().allMatch(accountLoaded())); + assert(lazyUsageSinglePath.stream().allMatch(environmentLoaded())); + + // demonstrates that eager fetching works when multiple paths are used + // when: eagerly fetching serviceAccount AND serviceAccount.environment List eagerUsage = server.find(UsageRaw.class) .setIncludeSoftDeletes() + .fetch("serviceAccount") .fetch("serviceAccount.environment") .findList(); - // then: soft-deleted environments are loaded + // then: soft-deleted environments loaded assert(eagerUsage.size() == 2); assert(eagerUsage.stream().allMatch(accountLoaded())); assert(eagerUsage.stream().allMatch(environmentLoaded())); @@ -50,8 +116,7 @@ public void lazy_loading_does_not_fetch_soft_deleted_references() { private static Predicate accountLoaded() { return usage -> { - boolean loaded = nonNull(usage.getServiceAccount()) - && nonNull(usage.getServiceAccount().getName()); + boolean loaded = nonNull(usage.getServiceAccount().getName()); // put breakpoint here to see post-load state return loaded; }; @@ -60,7 +125,6 @@ private static Predicate accountLoaded() { private static Predicate environmentLoaded() { return usage -> { boolean loaded = nonNull(usage.getServiceAccount()) - && nonNull(usage.getServiceAccount().getEnvironment()) && nonNull(usage.getServiceAccount().getEnvironment().getName()); // put breakpoint here to see post-load state return loaded; From c224828ce834b82b9999de07d4cf07a12e1fcc61 Mon Sep 17 00:00:00 2001 From: patrickspensieri Date: Thu, 29 Sep 2022 11:49:21 -0400 Subject: [PATCH 5/5] Document other workaround, using fetch for nested path --- .../LazyLoadingSoftDeletedReferencesTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java b/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java index 552e914..6b1c261 100644 --- a/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java +++ b/src/test/java/org/example/domain/LazyLoadingSoftDeletedReferencesTest.java @@ -68,6 +68,7 @@ public void setIncludeSoftDeletes_does_not_fetch_deep_relations_that_are_softdel * * Also demonstrates workaround that yield desired results. */ + @Test public void fetchLazy_with_multiple_paths_does_not_fetch_deep_relations_that_are_softdeleted() { // given: existing usage @@ -101,6 +102,18 @@ public void fetchLazy_with_multiple_paths_does_not_fetch_deep_relations_that_are assert(lazyUsageSinglePath.stream().allMatch(accountLoaded())); assert(lazyUsageSinglePath.stream().allMatch(environmentLoaded())); + // demonstrates other workaround, use fetch() for environment, all required properties are loaded + // when: lazy fetching serviceAccount.environment + List lazyUsageMultiplePathsWithFetch = server.find(UsageRaw.class) + .setIncludeSoftDeletes() + .fetchLazy("serviceAccount") + .fetch("serviceAccount.environment") + .findList(); + // then: soft-deleted environments loaded + assert(lazyUsageMultiplePathsWithFetch.size() == 2); + assert(lazyUsageMultiplePathsWithFetch.stream().allMatch(accountLoaded())); + assert(lazyUsageMultiplePathsWithFetch.stream().allMatch(environmentLoaded())); + // demonstrates that eager fetching works when multiple paths are used // when: eagerly fetching serviceAccount AND serviceAccount.environment List eagerUsage = server.find(UsageRaw.class)