From ce403e72a73a70b2532c05d170948ff04faeec7d Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Tue, 13 Nov 2018 08:44:45 +0100 Subject: [PATCH] FIX: improved support for @DbForeignKey(noConstraint = true) (#1527) * FIX: improved support for DbForeignKey(noConstraint = true) * FIX: Tests for mariadb and sqlserver * FIX: SqlServer was still incorrect * FIX: not ommit property for formula * Update BeanDescriptor.java (typo fix) fix typo column (no code change!) --- .../io/ebean/bean/EntityBeanIntercept.java | 2 +- .../io/ebeaninternal/api/LoadBeanRequest.java | 9 +- .../server/deploy/BeanDescriptor.java | 20 +- .../server/query/STreePropertyAssocOne.java | 11 + .../ebeaninternal/server/query/STreeType.java | 8 + .../server/query/SqlTreeBuilder.java | 7 +- .../server/query/SqlTreeNodeBean.java | 8 + .../java/org/tests/model/nofk/EFile2NoFk.java | 60 +++ .../java/org/tests/model/nofk/EFileNoFk.java | 79 ++++ .../java/org/tests/model/nofk/EUserNoFk.java | 44 ++ .../tests/model/nofk/EUserNoFkSoftDel.java | 62 +++ .../java/org/tests/model/nofk/Test2NoFk.java | 230 ++++++++++ .../java/org/tests/model/nofk/TestNoFk.java | 402 ++++++++++++++++++ 13 files changed, 931 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/tests/model/nofk/EFile2NoFk.java create mode 100644 src/test/java/org/tests/model/nofk/EFileNoFk.java create mode 100644 src/test/java/org/tests/model/nofk/EUserNoFk.java create mode 100644 src/test/java/org/tests/model/nofk/EUserNoFkSoftDel.java create mode 100644 src/test/java/org/tests/model/nofk/Test2NoFk.java create mode 100644 src/test/java/org/tests/model/nofk/TestNoFk.java diff --git a/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/src/main/java/io/ebean/bean/EntityBeanIntercept.java index fcd6bb7ad..87b4f3023 100644 --- a/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -829,7 +829,7 @@ public final class EntityBeanIntercept implements Serializable { if (lazyLoadFailure) { // failed when batch lazy loaded by another bean in the batch - throw new EntityNotFoundException("Lazy loading failed on type:" + owner.getClass().getName() + " id:" + ownerId + " - Bean has been deleted"); + throw new EntityNotFoundException("(Lazy) loading failed on type:" + owner.getClass().getName() + " id:" + ownerId + " - Bean has been deleted"); } if (lazyLoadProperty == -1) { diff --git a/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java b/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java index 3642daecd..3a5d9b79e 100644 --- a/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java +++ b/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java @@ -168,13 +168,8 @@ public class LoadBeanRequest extends LoadRequest { // necessary but allow processing to continue until it is accessed by client code Object id = desc.getId(ebi.getOwner()); if (!loadedIds.contains(id)) { - if (desc.isSoftDelete()) { - // assume this is logically deleted (hence not found) - desc.setSoftDeleteValue(ebi.getOwner()); - } else { - logger.info("Lazy loading unsuccessful for type:" + desc.getName() + " id:" + id + " - expecting when bean has been deleted"); - ebi.setLazyLoadFailure(id); - } + // assume this is logically deleted (hence not found) + desc.markAsDeleted(ebi.getOwner()); } } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 60f0ab854..d5f590d8f 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -7,6 +7,7 @@ import io.ebean.SqlUpdate; import io.ebean.Transaction; import io.ebean.ValuePair; import io.ebean.annotation.DocStoreMode; +import io.ebean.annotation.Formula; import io.ebean.bean.BeanCollection; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; @@ -496,7 +497,10 @@ public class BeanDescriptor implements BeanType, STreeType { DeployBeanPropertyLists listHelper = new DeployBeanPropertyLists(owner, this, deploy); this.softDeleteProperty = listHelper.getSoftDeleteProperty(); - this.softDelete = (softDeleteProperty != null); + // if formula is set, the property is virtual only (there is no column in db) the formula must evaluate to true, + // if there is a join to a deleted bean. Example: '@Formula(select = "${ta}.user_id is null")' + // this is required to support markAsDelete on beans that may have no FK constraint. + this.softDelete = (softDeleteProperty != null && !softDeleteProperty.isFormula()); this.idProperty = listHelper.getId(); this.versionProperty = listHelper.getVersionProperty(); this.unmappedJson = listHelper.getUnmappedJson(); @@ -2920,6 +2924,20 @@ public class BeanDescriptor implements BeanType, STreeType { return softDeleteProperty.getSoftDeleteDbPredicate(tableAlias); } + @Override + public void markAsDeleted(EntityBean bean) { + if (softDeleteProperty == null) { + Object id = getId(bean); + logger.info("(Lazy) loading unsuccessful for type:{} id:{} - expecting when bean has been deleted", getName(), id); + bean._ebean_getIntercept().setLazyLoadFailure(id); + } else { + setSoftDeleteValue(bean); + bean._ebean_getIntercept().setLoaded(); + setAllLoaded(bean); + } + } + + /** * Return true if this entity type is draftable. */ diff --git a/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocOne.java index 722a43a02..4ec7bcbc5 100644 --- a/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocOne.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.bean.EntityBean; import io.ebeaninternal.server.type.ScalarType; public interface STreePropertyAssocOne extends STreePropertyAssoc { @@ -13,4 +14,14 @@ public interface STreePropertyAssocOne extends STreePropertyAssoc { * Return the scalar type of the associated id property. */ ScalarType getIdScalarType(); + + /** + * Returns true, if this relation has a foreign key. + */ + boolean hasForeignKey(); + + /** + * Return the property value as an entity bean from the parent. + */ + public EntityBean getValueAsEntityBean(EntityBean parentBean); } diff --git a/src/main/java/io/ebeaninternal/server/query/STreeType.java b/src/main/java/io/ebeaninternal/server/query/STreeType.java index e824272c0..1e4d40e9c 100644 --- a/src/main/java/io/ebeaninternal/server/query/STreeType.java +++ b/src/main/java/io/ebeaninternal/server/query/STreeType.java @@ -132,5 +132,13 @@ public interface STreeType { */ void inheritanceLoad(SqlBeanLoad sqlBeanLoad, STreeProperty property, DbReadContext ctx); + /** + * Mark the bean as deleted by setting the softDelete property to true. + * + * This works also, if there is only a virtual softDelete property computed by a formula. + * + * If there is no softdelete property, it sets the lazyLoadFailure flag in EBI. + */ + void markAsDeleted(EntityBean bean); } diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 0dcf9f773..bf8201a84 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -498,8 +498,11 @@ public final class SqlTreeBuilder { for (STreePropertyAssocOne propertyAssocOne : desc.propsOne()) { //noinspection StatementWithEmptyBody - if (queryProps != null && queryProps.isIncludedBeanJoin(propertyAssocOne.getName())) { - // if it is a joined bean... then don't add the property + if (queryProps != null + && queryProps.isIncludedBeanJoin(propertyAssocOne.getName()) + && propertyAssocOne.hasForeignKey() + && !propertyAssocOne.isFormula()) { + // if it is a joined bean with FK constraint... then don't add the property // as it will have its own entire Node in the SqlTree } else { selectProps.add(propertyAssocOne); diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java index c67d58600..62a77917b 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -266,6 +266,14 @@ class SqlTreeNodeBean implements SqlTreeNode { if (id == null) { // bean must be null... localBean = null; + + // ... but there may exist as reference bean in parent which has to be marked as deleted. + if (parentBean != null && nodeBeanProp instanceof STreePropertyAssocOne) { + contextBean = ((STreePropertyAssocOne)nodeBeanProp).getValueAsEntityBean(parentBean); + if (contextBean != null) { + desc.markAsDeleted(contextBean); + } + } } else if (!temporalVersions) { // check the PersistenceContext to see if the bean already exists contextBean = (EntityBean) localDesc.contextPutIfAbsent(persistenceContext, id, localBean); diff --git a/src/test/java/org/tests/model/nofk/EFile2NoFk.java b/src/test/java/org/tests/model/nofk/EFile2NoFk.java new file mode 100644 index 000000000..aee08e6fb --- /dev/null +++ b/src/test/java/org/tests/model/nofk/EFile2NoFk.java @@ -0,0 +1,60 @@ +package org.tests.model.nofk; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.validation.constraints.Size; + +import io.ebean.annotation.DbForeignKey; +import io.ebean.annotation.Formula; +import io.ebean.annotation.Index; + +@Entity +public class EFile2NoFk { + + @Id + @Size(max = 64) // Note: mysql supports only 767 bytes for index + String fileName; + + @Index + int ownerId; + + // owner without softdelete property - will throw bean has been deleted + @ManyToOne + @Formula(select = "${ta}.Owner_Id") + EUserNoFk owner; + + // owner with softdelete property - will set the property to true + @ManyToOne + @Formula(select = "${ta}.Owner_id") + EUserNoFkSoftDel ownerSoftDel; + + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getOwnerId() { + return ownerId; + } + + public void setOwnerId(int ownerId) { + this.ownerId = ownerId; + } + + public EUserNoFk getOwner() { + return owner; + } + + public EUserNoFkSoftDel getOwnerSoftDel() { + return ownerSoftDel; + } + +} diff --git a/src/test/java/org/tests/model/nofk/EFileNoFk.java b/src/test/java/org/tests/model/nofk/EFileNoFk.java new file mode 100644 index 000000000..fb48a53b0 --- /dev/null +++ b/src/test/java/org/tests/model/nofk/EFileNoFk.java @@ -0,0 +1,79 @@ +package org.tests.model.nofk; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.validation.constraints.Size; + +import io.ebean.annotation.DbForeignKey; + +@Entity +public class EFileNoFk { + + @Id + @Size(max = 64) // Note: mysql supports only 767 bytes for index + String fileName; + + // owner without softdelete property - will throw bean has been deleted + @ManyToOne + @DbForeignKey(noConstraint = true) + EUserNoFk owner; + + // owner with softdelete property - will set the property to true + @ManyToOne + @DbForeignKey(noConstraint = true) + EUserNoFkSoftDel ownerSoftDel; + + // hold also a many-2-many relation for tests. + @ManyToMany + @DbForeignKey(noConstraint = true) + List editors; + + @ManyToMany + @DbForeignKey(noConstraint = true) + List editorsSoftDel; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public EUserNoFk getOwner() { + return owner; + } + + public void setOwner(EUserNoFk owner) { + this.owner = owner; + } + + public EUserNoFkSoftDel getOwnerSoftDel() { + return ownerSoftDel; + } + + public void setOwnerSoftDel(EUserNoFkSoftDel ownerSoftDel) { + this.ownerSoftDel = ownerSoftDel; + } + + public List getEditors() { + return editors; + } + + public void setEditors(List editors) { + this.editors = editors; + } + + public List getEditorsSoftDel() { + return editorsSoftDel; + } + + public void setEditorsSoftDel(List editorsSoftDel) { + this.editorsSoftDel = editorsSoftDel; + } + +} diff --git a/src/test/java/org/tests/model/nofk/EUserNoFk.java b/src/test/java/org/tests/model/nofk/EUserNoFk.java new file mode 100644 index 000000000..8513ad8d5 --- /dev/null +++ b/src/test/java/org/tests/model/nofk/EUserNoFk.java @@ -0,0 +1,44 @@ +package org.tests.model.nofk; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; + +@Entity +public class EUserNoFk { + + @Id + int userId; + + String userName; + + @OneToMany(mappedBy = "owner") + List files; + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public List getFiles() { + return files; + } + + public void setFiles(List files) { + this.files = files; + } + +} diff --git a/src/test/java/org/tests/model/nofk/EUserNoFkSoftDel.java b/src/test/java/org/tests/model/nofk/EUserNoFkSoftDel.java new file mode 100644 index 000000000..792a004af --- /dev/null +++ b/src/test/java/org/tests/model/nofk/EUserNoFkSoftDel.java @@ -0,0 +1,62 @@ +package org.tests.model.nofk; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; + +import io.ebean.annotation.Formula; +import io.ebean.annotation.Platform; +import io.ebean.annotation.SoftDelete; + +@Entity +public class EUserNoFkSoftDel { + + @Id + int userId; + + String userName; + + @SoftDelete + @Formula(select = "${ta}.user_id is null") + @Formula(select = "CASE WHEN ${ta}.user_id is null THEN 1 ELSE 0 END", platforms = Platform.SQLSERVER17) + // evaluates to true in a left join if bean has been deleted. + boolean deleted; + + @OneToMany(mappedBy = "ownerSoftDel") + List files; + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public List getFiles() { + return files; + } + + public void setFiles(List files) { + this.files = files; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + +} diff --git a/src/test/java/org/tests/model/nofk/Test2NoFk.java b/src/test/java/org/tests/model/nofk/Test2NoFk.java new file mode 100644 index 000000000..4e963b4f3 --- /dev/null +++ b/src/test/java/org/tests/model/nofk/Test2NoFk.java @@ -0,0 +1,230 @@ +package org.tests.model.nofk; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import javax.persistence.EntityNotFoundException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +// test that simulates relations with @formula +public class Test2NoFk extends BaseTestCase { + + @Before + public void setup() { + // Reset t + Ebean.find(EFile2NoFk.class).delete(); + Ebean.find(EUserNoFk.class).delete(); + Ebean.find(EUserNoFkSoftDel.class).delete(); + + // There are two user accounts persisted in our database + EUserNoFk root = new EUserNoFk(); + root.setUserId(1); + root.setUserName("root"); + Ebean.save(root); + + EUserNoFk nobody = new EUserNoFk(); + nobody.setUserId(2); + nobody.setUserName("nobody"); + Ebean.save(nobody); + + + // Now build the same with the softDel flag. + EUserNoFkSoftDel rootSoftDel = new EUserNoFkSoftDel(); + rootSoftDel.setUserId(1); + rootSoftDel.setUserName("root"); + Ebean.save(rootSoftDel); + + EUserNoFkSoftDel nobodySoftDel = new EUserNoFkSoftDel(); + nobodySoftDel.setUserId(2); + nobodySoftDel.setUserName("nobody"); + Ebean.save(nobodySoftDel); + + + EFile2NoFk bash = new EFile2NoFk(); + bash.setFileName("bash"); + bash.setOwnerId(1); + + EUserNoFk user501 = new EUserNoFk(); + EUserNoFkSoftDel user501SoftDel = new EUserNoFkSoftDel(); + user501.setUserId(501); + user501SoftDel.setUserId(501); + + Ebean.save(bash); + + // create relation to non existent user + EFile2NoFk cmd = new EFile2NoFk(); + + cmd.setFileName("java"); + cmd.setOwnerId(500); + + Ebean.save(cmd); + + assertThat(Ebean.find(EFile2NoFk.class).findCount()).isEqualTo(2); + assertThat(Ebean.find(EUserNoFk.class).findCount()).isEqualTo(2); + assertThat(Ebean.find(EUserNoFkSoftDel.class).findCount()).isEqualTo(2); + + // We should have this data in the database: + // + // Owner: userId | userName File: fileName | ownerId + // =======+========= =========+======== + // 1 | root bash | 1 (= root) + // 2 | nobody java | 500 (= non existent user account) + // + // Editors: userId | fileName + // =======+========= + // 1 | bash + // 2 | bash + // 501 | bash + } + + @Test + public void testLazyLoadFile() { + List files = Ebean.find(EFile2NoFk.class).findList(); + assertThat(files).hasSize(2); + + EFile2NoFk file1 = files.get(0); + EFile2NoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwner())._ebean_getIntercept(); + assertThat(file1.getOwner().getUserId()).isEqualTo(1); + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + + assertThat(file1.getOwner().getUserName()).isEqualTo("root"); // trigger lazy-load + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); // and expect, that bean is fully loaded + + + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwner())._ebean_getIntercept(); + + assertThat(file2.getOwner().getUserId()).isEqualTo(500); + assertThatThrownBy(()->file2.getOwner().getUserName()) + .isInstanceOf(EntityNotFoundException.class) + .hasMessageContaining("id:500 - Bean has been deleted"); + + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + assertTrue(ownerEbi.isLazyLoadFailure()); + + } + + @Test + public void testEagerLoadFile() { + List files = Ebean.find(EFile2NoFk.class).fetch("owner").findList(); + assertThat(files).hasSize(2); + + EFile2NoFk file1 = files.get(0); + EFile2NoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwner())._ebean_getIntercept(); + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); + + assertThat(file1.getOwner().getUserId()).isEqualTo(1); + assertThat(file1.getOwner().getUserName()).isEqualTo("root"); + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwner())._ebean_getIntercept(); + + assertThat(file2.getOwner().getUserId()).isEqualTo(500); + assertThatThrownBy(()->file2.getOwner().getUserName()) + .isInstanceOf(EntityNotFoundException.class) + .hasMessageContaining("id:500 - Bean has been deleted"); + + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + assertTrue(ownerEbi.isLazyLoadFailure()); + + } + + @Test + public void testLazyLoadFileSoftDel() { + List files = Ebean.find(EFile2NoFk.class).findList(); + assertThat(files).hasSize(2); + + EFile2NoFk file1 = files.get(0); + EFile2NoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwnerSoftDel())._ebean_getIntercept(); + assertThat(file1.getOwnerSoftDel().getUserId()).isEqualTo(1); + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + + + assertThat(file1.getOwnerSoftDel().isDeleted()).isEqualTo(false); // trigger lazy-load + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); // and expect, that bean is fully loaded + + assertThat(file1.getOwnerSoftDel().getUserName()).isEqualTo("root"); + + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwnerSoftDel())._ebean_getIntercept(); + + assertThat(file2.getOwnerSoftDel().getUserId()).isEqualTo(500); + assertThat(file2.getOwnerSoftDel().isDeleted()).isEqualTo(true); + assertThat(file2.getOwnerSoftDel().getUserName()).isNull(); + + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); + + } + + @Test + public void testEagerLoadFileSoftDel() { + List files = Ebean.find(EFile2NoFk.class).fetch("ownerSoftDel").findList(); + assertThat(files).hasSize(2); + + EFile2NoFk file1 = files.get(0); + EFile2NoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwnerSoftDel())._ebean_getIntercept(); + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); + + assertThat(file1.getOwnerSoftDel().getUserId()).isEqualTo(1); + assertThat(file1.getOwnerSoftDel().isDeleted()).isEqualTo(false); + assertThat(file1.getOwnerSoftDel().getUserName()).isEqualTo("root"); + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwnerSoftDel())._ebean_getIntercept(); +// assertFalse(ownerEbi.isReference()); +// assertFalse(ownerEbi.isPartial()); + + assertThat(file2.getOwnerSoftDel().getUserId()).isEqualTo(500); + assertThat(file2.getOwnerSoftDel().isDeleted()).isEqualTo(true); + assertThat(file2.getOwnerSoftDel().getUserName()).isNull(); + + + } + +} diff --git a/src/test/java/org/tests/model/nofk/TestNoFk.java b/src/test/java/org/tests/model/nofk/TestNoFk.java new file mode 100644 index 000000000..9d49f0666 --- /dev/null +++ b/src/test/java/org/tests/model/nofk/TestNoFk.java @@ -0,0 +1,402 @@ +package org.tests.model.nofk; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.SqlRow; +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; +import io.ebean.plugin.BeanType; +import io.ebean.plugin.Property; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + +import javax.persistence.EntityNotFoundException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestNoFk extends BaseTestCase { + + @Before + public void setup() { + // Reset t + Ebean.find(EFileNoFk.class).delete(); + Ebean.find(EUserNoFk.class).delete(); + Ebean.find(EUserNoFkSoftDel.class).delete(); + + Ebean.createSqlUpdate("delete from efile_no_fk_euser_no_fk").execute(); + Ebean.createSqlUpdate("delete from efile_no_fk_euser_no_fk_soft_del").execute(); + + // There are two user accounts persisted in our database + EUserNoFk root = new EUserNoFk(); + root.setUserId(1); + root.setUserName("root"); + Ebean.save(root); + + EUserNoFk nobody = new EUserNoFk(); + nobody.setUserId(2); + nobody.setUserName("nobody"); + Ebean.save(nobody); + + + // Now build the same with the softDel flag. + EUserNoFkSoftDel rootSoftDel = new EUserNoFkSoftDel(); + rootSoftDel.setUserId(1); + rootSoftDel.setUserName("root"); + Ebean.save(rootSoftDel); + + EUserNoFkSoftDel nobodySoftDel = new EUserNoFkSoftDel(); + nobodySoftDel.setUserId(2); + nobodySoftDel.setUserName("nobody"); + Ebean.save(nobodySoftDel); + + + EFileNoFk bash = new EFileNoFk(); + bash.setFileName("bash"); + bash.setOwner(root); + bash.setOwnerSoftDel(rootSoftDel); + + EUserNoFk user501 = new EUserNoFk(); + EUserNoFkSoftDel user501SoftDel = new EUserNoFkSoftDel(); + user501.setUserId(501); + user501SoftDel.setUserId(501); + bash.getEditors().add(root); + bash.getEditors().add(nobody); + bash.getEditors().add(user501); + bash.getEditorsSoftDel().add(rootSoftDel); + bash.getEditorsSoftDel().add(nobodySoftDel); + bash.getEditorsSoftDel().add(user501SoftDel); + + Ebean.save(bash); + + // create relation to non existent user + EFileNoFk cmd = new EFileNoFk(); + + cmd.setFileName("java"); + EUserNoFk user500 = new EUserNoFk(); + user500.setUserId(500); + user500.setUserName("not persisted"); + cmd.setOwner(user500); + + EUserNoFkSoftDel user500SoftDel = new EUserNoFkSoftDel(); + user500SoftDel.setUserId(500); + user500SoftDel.setUserName("not persisted"); + cmd.setOwnerSoftDel(user500SoftDel); + + Ebean.save(cmd); + + + assertThat(Ebean.find(EFileNoFk.class).findCount()).isEqualTo(2); + assertThat(Ebean.find(EUserNoFk.class).findCount()).isEqualTo(2); + assertThat(Ebean.find(EUserNoFkSoftDel.class).findCount()).isEqualTo(2); + + SqlRow row = Ebean.createSqlQuery("select count(*) as cnt from efile_no_fk_euser_no_fk").findOne(); + assertThat(row.getInteger("cnt")).isEqualTo(3); + // We should have this data in the database: + // + // Owner: userId | userName File: fileName | ownerId + // =======+========= =========+======== + // 1 | root bash | 1 (= root) + // 2 | nobody java | 500 (= non existent user account) + // + // Editors: userId | fileName + // =======+========= + // 1 | bash + // 2 | bash + // 501 | bash + } + + @Test + public void testLazyLoadFile() { + List files = Ebean.find(EFileNoFk.class).findList(); + assertThat(files).hasSize(2); + + EFileNoFk file1 = files.get(0); + EFileNoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwner())._ebean_getIntercept(); + assertThat(file1.getOwner().getUserId()).isEqualTo(1); + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + + assertThat(file1.getOwner().getUserName()).isEqualTo("root"); // trigger lazy-load + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); // and expect, that bean is fully loaded + + + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwner())._ebean_getIntercept(); + + assertThat(file2.getOwner().getUserId()).isEqualTo(500); + assertThatThrownBy(()->file2.getOwner().getUserName()) + .isInstanceOf(EntityNotFoundException.class) + .hasMessageContaining("id:500 - Bean has been deleted"); + + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + assertTrue(ownerEbi.isLazyLoadFailure()); + + } + + @Test + public void testEagerLoadFile() { + List files = Ebean.find(EFileNoFk.class).fetch("owner").findList(); + assertThat(files).hasSize(2); + + EFileNoFk file1 = files.get(0); + EFileNoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwner())._ebean_getIntercept(); + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); + + assertThat(file1.getOwner().getUserId()).isEqualTo(1); + assertThat(file1.getOwner().getUserName()).isEqualTo("root"); + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwner())._ebean_getIntercept(); + + assertThat(file2.getOwner().getUserId()).isEqualTo(500); + assertThatThrownBy(()->file2.getOwner().getUserName()) + .isInstanceOf(EntityNotFoundException.class) + .hasMessageContaining("id:500 - Bean has been deleted"); + + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + assertTrue(ownerEbi.isLazyLoadFailure()); + + } + + @Test + public void testLazyLoadOwner() { + List owners = Ebean.find(EUserNoFk.class).findList(); + assertThat(owners).hasSize(2); + + EUserNoFk owner1 = owners.get(0); + EUserNoFk owner2 = owners.get(1); + + assertThat(owner1.getUserId()).isEqualTo(1); + assertThat(owner1.getUserName()).isEqualTo("root"); + assertThat(owner2.getUserId()).isEqualTo(2); + assertThat(owner2.getUserName()).isEqualTo("nobody"); + + assertThat(owner1.getFiles()).hasSize(1); + assertThat(owner2.getFiles()).hasSize(0); + + assertThat(owner1.getFiles().get(0).getFileName()).isEqualTo("bash"); + + } + + @Test + public void testEagerLoadOwner() { + List owners = Ebean.find(EUserNoFk.class) + .fetch("files") + .findList(); + assertThat(owners).hasSize(2); + + EUserNoFk owner1 = owners.get(0); + EUserNoFk owner2 = owners.get(1); + + assertThat(owner1.getUserId()).isEqualTo(1); + assertThat(owner1.getUserName()).isEqualTo("root"); + assertThat(owner2.getUserId()).isEqualTo(2); + assertThat(owner2.getUserName()).isEqualTo("nobody"); + + assertThat(owner1.getFiles()).hasSize(1); + assertThat(owner2.getFiles()).hasSize(0); + + assertThat(owner1.getFiles().get(0).getFileName()).isEqualTo("bash"); + + } + + @Test + public void testLazyLoadFileSoftDel() { + List files = Ebean.find(EFileNoFk.class).findList(); + assertThat(files).hasSize(2); + + EFileNoFk file1 = files.get(0); + EFileNoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwnerSoftDel())._ebean_getIntercept(); + assertThat(file1.getOwnerSoftDel().getUserId()).isEqualTo(1); + assertTrue(ownerEbi.isReference()); + assertTrue(ownerEbi.isPartial()); + + + assertThat(file1.getOwnerSoftDel().isDeleted()).isEqualTo(false); // trigger lazy-load + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); // and expect, that bean is fully loaded + + assertThat(file1.getOwnerSoftDel().getUserName()).isEqualTo("root"); + + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwnerSoftDel())._ebean_getIntercept(); + + assertThat(file2.getOwnerSoftDel().getUserId()).isEqualTo(500); + assertThat(file2.getOwnerSoftDel().isDeleted()).isEqualTo(true); + assertThat(file2.getOwnerSoftDel().getUserName()).isNull(); + + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); + + } + + @Test + public void testEagerLoadFileSoftDel() { + List files = Ebean.find(EFileNoFk.class).fetch("ownerSoftDel").findList(); + assertThat(files).hasSize(2); + + EFileNoFk file1 = files.get(0); + EFileNoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + // File1 is owned by "root" + EntityBeanIntercept ownerEbi = ((EntityBean)file1.getOwnerSoftDel())._ebean_getIntercept(); + assertFalse(ownerEbi.isReference()); + assertFalse(ownerEbi.isPartial()); + + assertThat(file1.getOwnerSoftDel().getUserId()).isEqualTo(1); + assertThat(file1.getOwnerSoftDel().isDeleted()).isEqualTo(false); + assertThat(file1.getOwnerSoftDel().getUserName()).isEqualTo("root"); + + // File2 is owned by user #500, but user account does not exist + ownerEbi = ((EntityBean)file2.getOwnerSoftDel())._ebean_getIntercept(); +// assertFalse(ownerEbi.isReference()); +// assertFalse(ownerEbi.isPartial()); + + assertThat(file2.getOwnerSoftDel().getUserId()).isEqualTo(500); + assertThat(file2.getOwnerSoftDel().isDeleted()).isEqualTo(true); + assertThat(file2.getOwnerSoftDel().getUserName()).isNull(); + + + } + + @Test + public void testLazyLoadOwnerSoftDel() { + List owners = Ebean.find(EUserNoFkSoftDel.class).findList(); + assertThat(owners).hasSize(2); + + EUserNoFkSoftDel owner1 = owners.get(0); + EUserNoFkSoftDel owner2 = owners.get(1); + + assertThat(owner1.getUserId()).isEqualTo(1); + assertThat(owner1.getUserName()).isEqualTo("root"); + assertThat(owner2.getUserId()).isEqualTo(2); + assertThat(owner2.getUserName()).isEqualTo("nobody"); + + assertThat(owner1.getFiles()).hasSize(1); + assertThat(owner2.getFiles()).hasSize(0); + + assertThat(owner1.getFiles().get(0).getFileName()).isEqualTo("bash"); + + } + + @Test + public void testEagerLoadOwnerSoftDel() { + List owners = Ebean.find(EUserNoFkSoftDel.class) + .fetch("files") + .findList(); + assertThat(owners).hasSize(2); + + EUserNoFkSoftDel owner1 = owners.get(0); + EUserNoFkSoftDel owner2 = owners.get(1); + + assertThat(owner1.getUserId()).isEqualTo(1); + assertThat(owner1.getUserName()).isEqualTo("root"); + assertThat(owner2.getUserId()).isEqualTo(2); + assertThat(owner2.getUserName()).isEqualTo("nobody"); + + assertThat(owner1.getFiles()).hasSize(1); + assertThat(owner2.getFiles()).hasSize(0); + + assertThat(owner1.getFiles().get(0).getFileName()).isEqualTo("bash"); + + } + + + @Test + @Ignore("this would be a bonus task :)") + public void testLazyLoadUser500() { + // user 500 does not exist in DB + EUserNoFk owner = Ebean.find(EUserNoFk.class, 500); + assertThat(owner).isNull(); + + // but there are files that are owned by #500 + owner = Ebean.getReference(EUserNoFk.class, 500); + assertThat(owner.getUserId()).isEqualTo(500); + // this does not work yet, because the executed select contains a join: + // select t0.user_id, t1.file_name, t1.owner_user_id + // from eowner_no_fk t0 + // left join efile_no_fk t1 on t1.owner_user_id = t0.user_id + // where t0.user_id = ? order by t0.user_id; --bind(500, ) + // + // expected select (without join, as t0.user_id is already set and not neccessary to query): + // select t1.file_name, t1.owner_user_id + // from efile_no_fk t1 + // where t1.owner_user_id = ?; --bind(500, ) + // + assertThat(owner.getFiles()).hasSize(1); + assertThat(owner.getFiles().get(0).getFileName()).isEqualTo("java"); + + } + + @Test + @Ignore("Bonus Task 2") + public void testM2mLazyLoadFile() { + List files = Ebean.find(EFileNoFk.class).findList(); + assertThat(files).hasSize(2); + + EFileNoFk file1 = files.get(0); + EFileNoFk file2 = files.get(1); + + assertThat(file1.getFileName()).isEqualTo("bash"); + assertThat(file2.getFileName()).isEqualTo("java"); + + + // Currently, the test will fail here, because the generated SQL is: + // + // select int_.efile_no_fk_file_name, t0.user_id, t0.user_name from euser_no_fk t0 + // left join efile_no_fk_euser_no_fk int_ on int_.euser_no_fk_user_id = t0.user_id + // where (int_.efile_no_fk_file_name) in (?, ?, ?, ?, ? ) ; --bind(Array[5]={bash,java,bash,bash,bash}) + // + // So as there is no entry in 'euser_no_fk', we also cannot read the m2m table + assertThat(file1.getEditors()).hasSize(3); + assertThat(file1.getEditorsSoftDel()).hasSize(3); + + assertThat(file2.getEditors()).isEmpty(); + assertThat(file2.getEditorsSoftDel()).isEmpty(); + + + assertThat(file2.getEditors().get(2).getUserId()).isEqualTo(501); + assertThatThrownBy(()->file2.getEditors().get(2).getUserName()) + .isInstanceOf(EntityNotFoundException.class) + .hasMessageContaining("id:501 - Bean has been deleted"); + + assertThat(file2.getEditorsSoftDel().get(2).getUserId()).isEqualTo(501); + assertThat(file2.getEditorsSoftDel().get(2).isDeleted()).isTrue(); + assertThat(file2.getEditorsSoftDel().get(2).getUserName()).isNull(); + + } +}