diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java index be515b634..522aff847 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java @@ -148,4 +148,10 @@ public interface DbSqlContext { * Add extra joins *IF* required to support inheritance discriminator in projection. */ void flushExtraJoins(); + + /** + * Return true if the last join was added and false means the join was suppressed + * as it was already added to the query. + */ + boolean joinAdded(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java index 9488ad118..7e5df0f3f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -38,6 +38,7 @@ final class DefaultDbSqlContext implements DbSqlContext { private final CQueryDraftSupport draftSupport; private final CQueryHistorySupport historySupport; private final boolean historyQuery; + private boolean joinSuppressed; DefaultDbSqlContext(SqlTreeAlias alias, String columnAliasPrefix, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport, String fromForUpdate) { @@ -50,6 +51,11 @@ final class DefaultDbSqlContext implements DbSqlContext { this.fromForUpdate = fromForUpdate; } + @Override + public boolean joinAdded() { + return !joinSuppressed; + } + @Override public boolean isIncludeSoftDelete() { return alias.isIncludeSoftDelete(); @@ -118,9 +124,10 @@ final class DefaultDbSqlContext implements DbSqlContext { } String joinKey = table + "-" + a1 + "-" + a2; if (tableJoins.contains(joinKey)) { + joinSuppressed = true; return; } - + joinSuppressed = false; tableJoins.add(joinKey); sb.append(' ').append(type); boolean addAsOfOnClause = false; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java index 9b8806406..6c5a2ead1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java @@ -10,6 +10,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import static io.ebeaninternal.api.SpiQuery.TemporalMode.SOFT_DELETED; + /** * The purpose is to add an extra join to the query. *

@@ -145,7 +147,7 @@ final class SqlTreeNodeExtraJoin implements SqlTreeNode { assocBeanProperty.appendFrom(ctx, joinType, null); } joinType = assocBeanProperty.addJoin(joinType, prefix, ctx); - if (!oneToOneExported && assocBeanProperty.isTargetSoftDelete() && temporalMode != SpiQuery.TemporalMode.SOFT_DELETED) { + if (temporalMode != SOFT_DELETED && ctx.joinAdded() && assocBeanProperty.isTargetSoftDelete()) { ctx.append(" and ").append(assocBeanProperty.softDeletePredicate(ctx.tableAlias(prefix))); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java index 325f58cea..ada05c4c9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java @@ -88,14 +88,14 @@ final class SqlTreeNodeManyWhereJoin implements SqlTreeNode { if (nodeBeanProp instanceof STreePropertyAssocOne) { nodeBeanProp.addJoin(joinType, parentAlias, alias, ctx); - if (softDelete) { + if (softDelete && ctx.joinAdded()) { ctx.append(" and ").append(target.softDeletePredicate(alias)); } } else { STreePropertyAssocMany manyProp = (STreePropertyAssocMany) nodeBeanProp; if (!manyProp.hasJoinTable()) { manyProp.addJoin(joinType, parentAlias, alias, ctx); - if (softDelete) { + if (softDelete && ctx.joinAdded()) { ctx.append(" and ").append(target.softDeletePredicate(alias)); } } else { diff --git a/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java b/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java index e48d2be8e..e18f2983e 100644 --- a/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java +++ b/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java @@ -331,7 +331,6 @@ public class TestMergeCustomer extends BaseTestCase { assertThat(sql.get(17)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?"); assertSqlBind(sql, 18, 21); assertThat(sql.get(23)).contains("update mcontact_message set title=?, subject=?, notes=?, version=?, contact_id=? where id=? and version=?"); - assertSqlBind(sql, 24, 29); } } diff --git a/ebean-test/src/test/java/org/tests/model/softdelete/ESoftDelOneBOwner.java b/ebean-test/src/test/java/org/tests/model/softdelete/ESoftDelOneBOwner.java new file mode 100644 index 000000000..e2303d559 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/softdelete/ESoftDelOneBOwner.java @@ -0,0 +1,59 @@ +package org.tests.model.softdelete; + +import io.ebean.annotation.SoftDelete; +import jakarta.persistence.*; + +@SuppressWarnings("unused") +@Entity +public class ESoftDelOneBOwner { + + @Id + long id; + + String name; + + @ManyToOne + ESoftDelOneB oneb; + + @SoftDelete + boolean deleted; + + @Version + long version; + + public ESoftDelOneBOwner(String name) { + this.name = name; + } + + 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 ESoftDelOneB oneb() { + return oneb; + } + + public void setOneb(ESoftDelOneB oneb) { + this.oneb = oneb; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } +} diff --git a/ebean-test/src/test/java/org/tests/softdelete/TestSoftDeleteOtoImported.java b/ebean-test/src/test/java/org/tests/softdelete/TestSoftDeleteOtoImported.java new file mode 100644 index 000000000..f420f2bb2 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/softdelete/TestSoftDeleteOtoImported.java @@ -0,0 +1,47 @@ +package org.tests.softdelete; + +import io.ebean.DB; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; +import org.tests.model.softdelete.ESoftDelOneA; +import org.tests.model.softdelete.ESoftDelOneB; +import org.tests.model.softdelete.ESoftDelOneBOwner; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestSoftDeleteOtoImported extends BaseTestCase { + + @Test + void extraJoinToOtoImported_expect_softDeletePredicate() { + ESoftDelOneB b = new ESoftDelOneB("xbImported"); + DB.save(b); + + ESoftDelOneA a = new ESoftDelOneA("xaImported"); + a.setOneb(b); + DB.save(a); + + ESoftDelOneBOwner co = new ESoftDelOneBOwner("xoImport"); + co.setOneb(b); + DB.save(co); + + LoggedSql.start(); + var listResult = DB.find(ESoftDelOneBOwner.class) + .where() + .eq("oneb.onea.name", "xaImported") + .findList(); + + var countResult = DB.find(ESoftDelOneBOwner.class) + .where() + .eq("oneb.onea.name", "xaImported") + .findCount(); + + var sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + + assertThat(listResult).hasSize(1); + assertThat(countResult).isEqualTo(1); + assertThat(sql.get(0)).contains("and t2.deleted = "); + assertThat(sql.get(1)).contains("and t2.deleted = "); + } +}