diff --git a/realm-annotations/src/main/java/io/realm/annotations/StrongRelationship.java b/realm-annotations/src/main/java/io/realm/annotations/StrongRelationship.java new file mode 100644 index 0000000000..4089d98835 --- /dev/null +++ b/realm-annotations/src/main/java/io/realm/annotations/StrongRelationship.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to describe the relationship between two Realm model classes as Strong. + * If this annotation is not used the relationship is implicitly Weak. + *

+ * This annotation can only be applied to fields that reference other Realm model classes either + * using a differet reference or using a RealmList. It cannot be used on RealmLists containing + * primitive types nor on fields marked with {@link LinkingObjects}. The annotation processor will + * throw an error in that case. + *

+ * The relationship is determined pr. field, i.e. it is possible for an object A to have both a Weak + * and Strong relationship with object B if A contains multiple fields that reference B. + *

+ * A strong relationship between a parent object A and a child object B means B will automatically + * be deleted if A no longer holds a reference to B or if A is deleted itself. This is also known as + * cascading deletes. + *

+ * It is possible for multiple different classes to have a strong reference to the same object. + * In that case the child object is only deleted automatically when the last strong parent reference + * is removed or all parent objects are deleted. + *

+ * It is possible to create child objects (B) with no parents (A), these will only be deleted if + * an parent actually existed at some point, e.g. If A references B, then it is possible to + * create object B first and then later add the reference from A to B. B will only be + * automatically deleted when A either explicitly removes its reference or is deleted itself. + *

+ * Warning:
+ * Some special corner cases exists for cyclic object graphs that are strongly connected: + *

+ *

+ * Example: + * {@code + * public class Person extends RealmObject { + * + * public String name; + * + * // The ContactInfo object is deleted when the person is. + * \@StrongRelationship + * public ContactInfo contactInfo; + * + * // Is implicitly weak. The Dog objects will not be deleted when the person is. + * public RealmList dogs = new RealmList<>(); + * } + * } + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface StrongRelationship { +} \ No newline at end of file diff --git a/realm/realm-annotations-processor/src/main/java/io/realm/processor/ClassMetaData.java b/realm/realm-annotations-processor/src/main/java/io/realm/processor/ClassMetaData.java index 8292873163..3509cf3ce3 100644 --- a/realm/realm-annotations-processor/src/main/java/io/realm/processor/ClassMetaData.java +++ b/realm/realm-annotations-processor/src/main/java/io/realm/processor/ClassMetaData.java @@ -48,6 +48,7 @@ import io.realm.annotations.RealmField; import io.realm.annotations.RealmNamingPolicy; import io.realm.annotations.Required; +import io.realm.annotations.StrongRelationship; import io.realm.processor.nameconverter.NameConverter; @@ -65,7 +66,7 @@ public class ClassMetaData { private final Set backlinks = new HashSet(); private final Set nullableFields = new HashSet(); // Set of fields which can be nullable private final Set nullableValueListFields = new HashSet(); // Set of fields whose elements can be nullable - + private final Set strongReferences = new HashSet<>(); // Set of fields whose elements have strong references private String packageName; // package name for model class. private boolean hasDefaultConstructor; // True if model has a public no-arg constructor. private VariableElement primaryKey; // Reference to field used as primary key, if any. @@ -171,6 +172,10 @@ public Set getBacklinkFields() { return Collections.unmodifiableSet(backlinks); } + public boolean isStrongReference(VariableElement field) { + return strongReferences.contains(field); + } + public String getInternalGetter(String fieldName) { return "realmGet$" + fieldName; } @@ -556,11 +561,20 @@ private boolean categorizeField(Element element) { } } + // Check that @RealmField is used on the correct fields. + if (field.getAnnotation(StrongRelationship.class) != null) { + if (!Utils.isRealmModel(field) && !Utils.isRealmModelList(field)) { + Utils.error("@RealmField is only allowed on fields that reference either other Realm model classes or lists of model classes."); + return false; + } + strongReferences.add(field); + } + if (field.getAnnotation(PrimaryKey.class) != null) { if (!categorizePrimaryKeyField(field)) { return false; } } - // @LinkingObjects cannot be @PrimaryKey or @Index. + // @LinkingObjects cannot be a Strong reference, @PrimaryKey or @Index. if (field.getAnnotation(LinkingObjects.class) != null) { // Do not add backlinks to fields list. return categorizeBacklinkField(field); diff --git a/realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java b/realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java index ef1e771df3..e62dbb697b 100644 --- a/realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java +++ b/realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java @@ -746,15 +746,15 @@ private void emitCreateExpectedObjectSchemaInfo(JavaWriter writer) throws IOExce case OBJECT: { String fieldTypeQualifiedName = Utils.getFieldTypeQualifiedName(field); String internalClassName = Utils.getReferencedTypeInternalClassNameStatement(fieldTypeQualifiedName, classCollection); - writer.emitStatement("builder.addPersistedLinkProperty(\"%s\", RealmFieldType.OBJECT, %s)", - fieldName, internalClassName); + writer.emitStatement("builder.addPersistedLinkProperty(\"%s\", RealmFieldType.OBJECT, %s, %s)", + fieldName, internalClassName, metadata.isStrongReference(field)); break; } case LIST: { String genericTypeQualifiedName = Utils.getGenericTypeQualifiedName(field); String internalClassName = Utils.getReferencedTypeInternalClassNameStatement(genericTypeQualifiedName, classCollection); - writer.emitStatement("builder.addPersistedLinkProperty(\"%s\", RealmFieldType.LIST, %s)", - fieldName, internalClassName); + writer.emitStatement("builder.addPersistedLinkProperty(\"%s\", RealmFieldType.LIST, %s, %s)", + fieldName, internalClassName, metadata.isStrongReference(field)); break; } case INTEGER_LIST: diff --git a/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_AllTypesRealmProxy.java b/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_AllTypesRealmProxy.java index ee1fccac5a..bd23cb2a6c 100644 --- a/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_AllTypesRealmProxy.java +++ b/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_AllTypesRealmProxy.java @@ -840,8 +840,8 @@ private static OsObjectSchemaInfo createExpectedObjectSchemaInfo() { builder.addPersistedProperty("columnDate", RealmFieldType.DATE, !Property.PRIMARY_KEY, !Property.INDEXED, Property.REQUIRED); builder.addPersistedProperty("columnBinary", RealmFieldType.BINARY, !Property.PRIMARY_KEY, !Property.INDEXED, Property.REQUIRED); builder.addPersistedProperty("columnMutableRealmInteger", RealmFieldType.INTEGER, !Property.PRIMARY_KEY, !Property.INDEXED, !Property.REQUIRED); - builder.addPersistedLinkProperty("columnObject", RealmFieldType.OBJECT, "AllTypes"); - builder.addPersistedLinkProperty("columnRealmList", RealmFieldType.LIST, "AllTypes"); + builder.addPersistedLinkProperty("columnObject", RealmFieldType.OBJECT, "AllTypes", false); + builder.addPersistedLinkProperty("columnRealmList", RealmFieldType.LIST, "AllTypes", false); builder.addPersistedValueListProperty("columnStringList", RealmFieldType.STRING_LIST, !Property.REQUIRED); builder.addPersistedValueListProperty("columnBinaryList", RealmFieldType.BINARY_LIST, !Property.REQUIRED); builder.addPersistedValueListProperty("columnBooleanList", RealmFieldType.BOOLEAN_LIST, !Property.REQUIRED); diff --git a/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_NullTypesRealmProxy.java b/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_NullTypesRealmProxy.java index 1e5c65b4a4..ec1968dc51 100644 --- a/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_NullTypesRealmProxy.java +++ b/realm/realm-annotations-processor/src/test/resources/io/realm/some_test_NullTypesRealmProxy.java @@ -1671,7 +1671,7 @@ private static OsObjectSchemaInfo createExpectedObjectSchemaInfo() { builder.addPersistedProperty("fieldDoubleNull", RealmFieldType.DOUBLE, !Property.PRIMARY_KEY, !Property.INDEXED, !Property.REQUIRED); builder.addPersistedProperty("fieldDateNotNull", RealmFieldType.DATE, !Property.PRIMARY_KEY, !Property.INDEXED, Property.REQUIRED); builder.addPersistedProperty("fieldDateNull", RealmFieldType.DATE, !Property.PRIMARY_KEY, !Property.INDEXED, !Property.REQUIRED); - builder.addPersistedLinkProperty("fieldObjectNull", RealmFieldType.OBJECT, "NullTypes"); + builder.addPersistedLinkProperty("fieldObjectNull", RealmFieldType.OBJECT, "NullTypes", false); builder.addPersistedValueListProperty("fieldStringListNotNull", RealmFieldType.STRING_LIST, Property.REQUIRED); builder.addPersistedValueListProperty("fieldStringListNull", RealmFieldType.STRING_LIST, !Property.REQUIRED); builder.addPersistedValueListProperty("fieldBinaryListNotNull", RealmFieldType.BINARY_LIST, Property.REQUIRED); diff --git a/realm/realm-library/src/androidTest/java/io/realm/LinkingObjectsManagedTests.java b/realm/realm-library/src/androidTest/java/io/realm/LinkingObjectsManagedTests.java index ef0e68ffbc..c77fc8dfca 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/LinkingObjectsManagedTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/LinkingObjectsManagedTests.java @@ -599,7 +599,7 @@ public void migration_backlinkedSourceFieldDoesntExist() throws ClassNotFoundExc .build(); OsObjectSchemaInfo sourceSchemaInfo = new OsObjectSchemaInfo.Builder("BacklinksSource", 2, 0) .addPersistedProperty("name", RealmFieldType.STRING, !Property.PRIMARY_KEY, !Property.INDEXED, !Property.REQUIRED) - .addPersistedLinkProperty("child", RealmFieldType.OBJECT, "BacklinksTarget") + .addPersistedLinkProperty("child", RealmFieldType.OBJECT, "BacklinksTarget", false) .build(); Map, OsObjectSchemaInfo> infoMap = new HashMap, OsObjectSchemaInfo>(); @@ -643,7 +643,7 @@ public void migration_backlinkedSourceFieldWrongType() { OsObjectSchemaInfo sourceSchemaInfo = new OsObjectSchemaInfo.Builder("BacklinksSource", 2, 0) .addPersistedProperty("name", RealmFieldType.STRING, !Property.PRIMARY_KEY, !Property.INDEXED, !Property.REQUIRED) .addPersistedLinkProperty("child", RealmFieldType.OBJECT, - "BacklinksSource"/*"BacklinksTarget" is the original value*/) + "BacklinksSource"/*"BacklinksTarget" is the original value*/, false) .build(); Map, OsObjectSchemaInfo> infoMap = new HashMap, OsObjectSchemaInfo>(); diff --git a/realm/realm-library/src/androidTest/java/io/realm/RelationshipTests.java b/realm/realm-library/src/androidTest/java/io/realm/RelationshipTests.java new file mode 100644 index 0000000000..2e57b69ad4 --- /dev/null +++ b/realm/realm-library/src/androidTest/java/io/realm/RelationshipTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm; + + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.realm.entities.RelationshipChild; +import io.realm.entities.RelationshipParent; +import io.realm.rule.TestRealmConfigurationFactory; + +import static org.junit.Assert.assertEquals; + + +/** + * Testing Strong/Weak leaks. This should already be tested in ObjectStore/Core, so just doing + * smoke test here for the most common scenarios. + */ +@RunWith(AndroidJUnit4.class) +public class RelationshipTests { + + @Rule + public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory(); + + private RealmConfiguration realmConfig; + private Realm realm; + + @Before + public void setUp() { + realmConfig = configFactory.createConfiguration(); + realm = Realm.getInstance(realmConfig); + realm.beginTransaction(); + } + + @After + public void tearDown() { + if (realm != null && !realm.isClosed()) { + realm.cancelTransaction(); + realm.close(); + } + } + + @Test + public void cascadeDeleteStrongObjectReference() { + RealmResults children = realm.where(RelationshipChild.class).findAll(); + + // Create Strong relationship + RelationshipChild child = realm.createObject(RelationshipChild.class); + child.name = "Child"; + RelationshipParent parent = realm.createObject(RelationshipParent.class); + parent.strongObjectRef = child; + + // Delete parent + assertEquals(1, children.size()); + parent.deleteFromRealm(); + assertEquals(0, children.size()); + } + + @Test + public void dontCascadeDeleteWeakObjectReference() { + RealmResults children = realm.where(RelationshipChild.class).findAll(); + + // Create Strong relationship + RelationshipChild child = realm.createObject(RelationshipChild.class); + child.name = "Child"; + RelationshipParent parent = realm.createObject(RelationshipParent.class); + parent.weakObjectRef = child; + + // Delete parent + assertEquals(1, children.size()); + parent.deleteFromRealm(); + assertEquals(1, children.size()); + } + + @Test + public void cascadeDeleteStrongListReference() { + int TEST_SIZE = 10; + RealmResults children = realm.where(RelationshipChild.class).findAll(); + + // Create Strong relationship + RelationshipParent parent = realm.createObject(RelationshipParent.class); + for (int i = 0; i < TEST_SIZE; i++) { + parent.strongListRef.add(new RelationshipChild("child " + i)); + } + + // Delete parent + assertEquals(TEST_SIZE, children.size()); + parent.deleteFromRealm(); + assertEquals(0, children.size()); + } + + @Test + public void dontCascadeDeleteWeakListReference() { + int TEST_SIZE = 10; + RealmResults children = realm.where(RelationshipChild.class).findAll(); + + // Create Strong relationship + RelationshipParent parent = realm.createObject(RelationshipParent.class); + for (int i = 0; i < TEST_SIZE; i++) { + parent.weakListRef.add(new RelationshipChild("child " + i)); + } + + // Delete parent + assertEquals(TEST_SIZE, children.size()); + parent.deleteFromRealm(); + assertEquals(TEST_SIZE, children.size()); + } + + @Test + public void cascadeWhenFinalStrongReferenceIsGone() { + RealmResults children = realm.where(RelationshipChild.class).findAll(); + + // Create Strong relationship + RelationshipChild child = realm.createObject(RelationshipChild.class); + child.name = "Child"; + RelationshipParent parent1 = realm.createObject(RelationshipParent.class); + parent1.strongObjectRef = child; + RelationshipParent parent2 = realm.createObject(RelationshipParent.class); + parent2.strongObjectRef = child; + + assertEquals(1, children.size()); + parent1.deleteFromRealm(); + assertEquals(1, children.size()); + parent2.deleteFromRealm(); + assertEquals(0, children.size()); + } + +} \ No newline at end of file diff --git a/realm/realm-library/src/androidTest/java/io/realm/entities/RelationshipChild.java b/realm/realm-library/src/androidTest/java/io/realm/entities/RelationshipChild.java new file mode 100644 index 0000000000..3ee3f9ef7e --- /dev/null +++ b/realm/realm-library/src/androidTest/java/io/realm/entities/RelationshipChild.java @@ -0,0 +1,30 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.entities; + +import io.realm.RealmObject; + +public class RelationshipChild extends RealmObject { + + public String name; + + public RelationshipChild() { + } + + public RelationshipChild(String name) { + this.name = name; + } +} diff --git a/realm/realm-library/src/androidTest/java/io/realm/entities/RelationshipParent.java b/realm/realm-library/src/androidTest/java/io/realm/entities/RelationshipParent.java new file mode 100644 index 0000000000..7ce477a5d8 --- /dev/null +++ b/realm/realm-library/src/androidTest/java/io/realm/entities/RelationshipParent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.entities; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.StrongRelationship; + +public class RelationshipParent extends RealmObject { + + @StrongRelationship + public RelationshipChild strongObjectRef; + + public RelationshipChild weakObjectRef; + + @StrongRelationship + public RealmList strongListRef = new RealmList<>(); + + public RealmList weakListRef = new RealmList<>(); + +} diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_Property.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_Property.cpp index 8362e7c559..c8003a6634 100644 --- a/realm/realm-library/src/main/cpp/io_realm_internal_Property.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_internal_Property.cpp @@ -70,14 +70,16 @@ JNIEXPORT jlong JNICALL Java_io_realm_internal_Property_nativeCreatePersistedPro JNIEXPORT jlong JNICALL Java_io_realm_internal_Property_nativeCreatePersistedLinkProperty(JNIEnv* env, jclass, jstring j_name_str, jint type, - jstring j_target_class_name) + jstring j_target_class_name, + jboolean is_strong_reference) { TR_ENTER() try { JStringAccessor name(env, j_name_str); JStringAccessor link_name(env, j_target_class_name); PropertyType p_type = static_cast(static_cast(type)); - return reinterpret_cast(new Property(name, p_type, link_name)); + Relationship relationship = (is_strong_reference) ? Relationship::Strong : Relationship::Weak; + return reinterpret_cast(new Property(name, p_type, link_name, relationship)); } CATCH_STD() return 0; diff --git a/realm/realm-library/src/main/cpp/jni_util/log.hpp b/realm/realm-library/src/main/cpp/jni_util/log.hpp index db61e5fc7a..a2ab5448f1 100644 --- a/realm/realm-library/src/main/cpp/jni_util/log.hpp +++ b/realm/realm-library/src/main/cpp/jni_util/log.hpp @@ -27,7 +27,6 @@ #include "io_realm_log_LogLevel.h" #include "realm/util/logger.hpp" - #define TR_ENTER() \ if (realm::jni_util::Log::s_level <= realm::jni_util::Log::trace) { \ realm::jni_util::Log::t(" --> %1", __FUNCTION__); \ diff --git a/realm/realm-library/src/main/cpp/object-store b/realm/realm-library/src/main/cpp/object-store index f2a536d29d..ccf2d9b6ff 160000 --- a/realm/realm-library/src/main/cpp/object-store +++ b/realm/realm-library/src/main/cpp/object-store @@ -1 +1 @@ -Subproject commit f2a536d29de48e34e60799a5bf3f36e13806387e +Subproject commit ccf2d9b6ff6d8182da2768d487080536b23c95ba diff --git a/realm/realm-library/src/main/cpp/util.hpp b/realm/realm-library/src/main/cpp/util.hpp index 42071658f0..d92b30da06 100644 --- a/realm/realm-library/src/main/cpp/util.hpp +++ b/realm/realm-library/src/main/cpp/util.hpp @@ -31,8 +31,8 @@ #include #include #include -#include "io_realm_internal_Util.h" +#include "io_realm_internal_Util.h" #include "java_exception_def.hpp" #include "jni_util/log.hpp" #include "jni_util/java_exception_thrower.hpp" diff --git a/realm/realm-library/src/main/java/io/realm/internal/OsObjectSchemaInfo.java b/realm/realm-library/src/main/java/io/realm/internal/OsObjectSchemaInfo.java index cac92e21df..74e2403a08 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/OsObjectSchemaInfo.java +++ b/realm/realm-library/src/main/java/io/realm/internal/OsObjectSchemaInfo.java @@ -90,9 +90,12 @@ public Builder addPersistedValueListProperty(String name, RealmFieldType type, b * {@link RealmFieldType#LIST}. * @return this {@code OsObjectSchemaInfo.Builder}. */ - public Builder addPersistedLinkProperty(String name, RealmFieldType type, String linkedClassName) { + public Builder addPersistedLinkProperty(String name, + RealmFieldType type, + String linkedClassName, + boolean isStrongRelationship) { long propertyPtr = Property.nativeCreatePersistedLinkProperty(name, - Property.convertFromRealmFieldType(type, false), linkedClassName); + Property.convertFromRealmFieldType(type, false), linkedClassName, isStrongRelationship); persistedPropertyPtrArray[persistedPropertyPtrCurPos] = propertyPtr; persistedPropertyPtrCurPos++; return this; diff --git a/realm/realm-library/src/main/java/io/realm/internal/Property.java b/realm/realm-library/src/main/java/io/realm/internal/Property.java index 0bcff85f24..d13d89b98a 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/Property.java +++ b/realm/realm-library/src/main/java/io/realm/internal/Property.java @@ -210,7 +210,7 @@ public long getNativeFinalizerPtr() { static native long nativeCreatePersistedProperty( String name, int type, boolean isPrimary, boolean isIndexed); - static native long nativeCreatePersistedLinkProperty(String name, int type, String linkedToName); + static native long nativeCreatePersistedLinkProperty(String name, int type, String linkedToName, boolean isStrongRelationship); static native long nativeCreateComputedLinkProperty( String name, String sourceClassName, String sourceFieldName);