Skip to content

Commit 84e1b4d

Browse files
author
Christian Melchior
committed
Merge branch 'releases' into release/transformer-api
# Conflicts: # CHANGELOG.md # version.txt
2 parents 4ecd4cc + 7219b6a commit 84e1b4d

File tree

9 files changed

+259
-6
lines changed

9 files changed

+259
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ the main release [CHANGELOG.md](https://github.com/realm/realm-java/blob/release
88
* None.
99

1010
### Fixed
11+
* Added support for automatic handling of orphan embedded objects after migrating regular object properties to become embedded objects. (Issue [#7769](https://github.com/realm/realm-java/issues/7769)).
1112
* Unit tests not being executed. (Issue [#7771](https://github.com/realm/realm-java/issues/7771))
1213
* Instrumented unit tests failed to execute because of the Realm dependencies being missing. (Issue [#7736](https://github.com/realm/realm-java/issues/7736))
1314

realm/realm-library/src/androidTest/java/io/realm/RealmMigrationTests.java

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737

3838
import androidx.test.ext.junit.runners.AndroidJUnit4;
3939
import androidx.test.platform.app.InstrumentationRegistry;
40+
41+
import io.realm.annotations.PrimaryKey;
42+
import io.realm.annotations.RealmClass;
4043
import io.realm.entities.AllTypes;
4144
import io.realm.entities.AnnotationTypes;
4245
import io.realm.entities.CatOwner;
@@ -62,6 +65,10 @@
6265
import io.realm.entities.Thread;
6366
import io.realm.entities.embedded.EmbeddedSimpleChild;
6467
import io.realm.entities.embedded.EmbeddedSimpleParent;
68+
import io.realm.entities.migration.HandleBackLinksChild1;
69+
import io.realm.entities.migration.HandleBackLinksChild2;
70+
import io.realm.entities.migration.HandleBackLinksParent1;
71+
import io.realm.entities.migration.HandleBackLinksParent2;
6572
import io.realm.entities.migration.MigrationClassRenamed;
6673
import io.realm.entities.migration.MigrationCore6PKStringIndexedByDefault;
6774
import io.realm.entities.migration.MigrationFieldRenameAndAdd;
@@ -1712,6 +1719,135 @@ public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {
17121719
realm = Realm.getInstance(realmConfig);
17131720
}
17141721

1722+
@Test
1723+
public void migrate_embeddedObject_deleteOrphans() {
1724+
RealmConfiguration config1 = configFactory.createConfigurationBuilder()
1725+
.schema(HandleBackLinksParent1.class, HandleBackLinksChild1.class)
1726+
.schemaVersion(1)
1727+
.build();
1728+
1729+
Realm realm1 = Realm.getInstance(config1);
1730+
realm1.executeTransaction(realm -> {
1731+
HandleBackLinksChild1 child = new HandleBackLinksChild1();
1732+
HandleBackLinksParent1 parent = new HandleBackLinksParent1();
1733+
parent.child = child;
1734+
HandleBackLinksChild1 orphan = new HandleBackLinksChild1();
1735+
realm.insertOrUpdate(parent);
1736+
realm.insertOrUpdate(orphan);
1737+
});
1738+
long parentCountV1 = realm1.where(HandleBackLinksParent1.class)
1739+
.count();
1740+
assertEquals(1L, parentCountV1);
1741+
long childCountV1 = realm1.where(HandleBackLinksChild1.class)
1742+
.count();
1743+
assertEquals(2L, childCountV1);
1744+
realm1.close();
1745+
1746+
RealmMigration migration = (realm, oldVersion, newVersion) -> {
1747+
RealmSchema schema = realm.getSchema();
1748+
if (oldVersion == 1L) {
1749+
RealmObjectSchema parent = schema.get("HandleBackLinksParent1");
1750+
assertNotNull(parent);
1751+
RealmObjectSchema child = schema.get("HandleBackLinksChild1");
1752+
assertNotNull(child);
1753+
try {
1754+
child.setEmbedded(true);
1755+
} catch (Exception e) {
1756+
assertEquals(IllegalStateException.class, e.getClass());
1757+
assertEquals("Cannot convert 'HandleBackLinksChild1' to embedded: at least one object has no incoming links and would be deleted.", e.getMessage());
1758+
}
1759+
child.setEmbedded(true, true);
1760+
1761+
// Rename classes to avoid conflicts with all other tests
1762+
parent.setClassName("HandleBackLinksParent2");
1763+
child.setClassName("HandleBackLinksChild2");
1764+
}
1765+
};
1766+
1767+
// Create schema v2
1768+
RealmConfiguration config2 = configFactory.createConfigurationBuilder()
1769+
.schema(HandleBackLinksParent2.class, HandleBackLinksChild2.class)
1770+
.schemaVersion(2)
1771+
.migration(migration)
1772+
.build();
1773+
1774+
// The orphan child is erased
1775+
Realm realm2 = Realm.getInstance(config2);
1776+
long parentCountV2 = realm2.where(HandleBackLinksParent2.class)
1777+
.count();
1778+
assertEquals(1L, parentCountV2);
1779+
long childCountV2 = realm2.where(HandleBackLinksChild2.class)
1780+
.count();
1781+
assertEquals(1L, childCountV2);
1782+
realm2.close();
1783+
}
1784+
1785+
@Test
1786+
public void migrate_embeddedObject_clonesChildWhenReferencedMoreThanOnce() {
1787+
RealmConfiguration config1 = configFactory.createConfigurationBuilder()
1788+
.schema(HandleBackLinksParent1.class, HandleBackLinksChild1.class)
1789+
.schemaVersion(1)
1790+
.build();
1791+
1792+
Realm realm1 = Realm.getInstance(config1);
1793+
HandleBackLinksChild1 child = new HandleBackLinksChild1();
1794+
HandleBackLinksParent1 parent1 = new HandleBackLinksParent1();
1795+
HandleBackLinksParent1 parent2 = new HandleBackLinksParent1();
1796+
realm1.executeTransaction(realm -> {
1797+
// Copy child and set it as children for both parents.
1798+
// Now the managed child has two parents. This will not be allowed after converting it
1799+
// to an embedded object and will result into the child object being duplicated so that
1800+
// both parents can have a reference to a separate and different embedded object.
1801+
HandleBackLinksChild1 managedChild = realm.copyToRealm(child);
1802+
parent1.id = 1L;
1803+
parent1.child = managedChild;
1804+
realm.insert(parent1);
1805+
parent2.id = 2L;
1806+
parent2.child = managedChild;
1807+
realm.insert(parent2);
1808+
});
1809+
long parentCountV1 = realm1.where(HandleBackLinksParent1.class)
1810+
.count();
1811+
assertEquals(2L, parentCountV1);
1812+
long childCountV1 = realm1.where(HandleBackLinksChild1.class)
1813+
.count();
1814+
assertEquals(1L, childCountV1);
1815+
realm1.close();
1816+
1817+
RealmMigration migration = (realm, oldVersion, newVersion) -> {
1818+
RealmSchema schema = realm.getSchema();
1819+
if (oldVersion == 1L) {
1820+
RealmObjectSchema parentSchema = schema.get("HandleBackLinksParent1");
1821+
assertNotNull(parentSchema);
1822+
RealmObjectSchema childSchema = schema.get("HandleBackLinksChild1");
1823+
assertNotNull(childSchema);
1824+
childSchema.setEmbedded(true, true);
1825+
1826+
// Rename classes to avoid conflicts with all other tests
1827+
parentSchema.setClassName("HandleBackLinksParent2");
1828+
childSchema.setClassName("HandleBackLinksChild2");
1829+
}
1830+
};
1831+
1832+
// Create schema v2
1833+
RealmConfiguration config2 = configFactory.createConfigurationBuilder()
1834+
.schema(HandleBackLinksParent2.class, HandleBackLinksChild2.class)
1835+
.schemaVersion(2)
1836+
.migration(migration)
1837+
.build();
1838+
1839+
// After the migration the only child class is embedded and will not be allowed to have two parents
1840+
// so the child object will be duplicated
1841+
Realm realm2 = Realm.getInstance(config2);
1842+
long parentCountV2 = realm2.where(HandleBackLinksParent2.class)
1843+
.count();
1844+
assertEquals(2L, parentCountV2);
1845+
long childCountV2 = realm2.where(HandleBackLinksChild2.class)
1846+
.count();
1847+
assertEquals(2L, childCountV2);
1848+
realm2.close();
1849+
}
1850+
17151851
// TODO Add unit tests for default nullability
17161852
// TODO Add unit tests for default Indexing for Primary keys
17171853
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.realm.entities.migration;
2+
3+
import io.realm.RealmObject;
4+
5+
// Original child as a regular object
6+
public class HandleBackLinksChild1 extends RealmObject {
7+
8+
public String name;
9+
10+
public HandleBackLinksChild1() {}
11+
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.realm.entities.migration;
2+
3+
import io.realm.RealmObject;
4+
import io.realm.annotations.RealmClass;
5+
6+
// Child, now as an embedded object, respecting table names
7+
@RealmClass(embedded = true)
8+
public class HandleBackLinksChild2 extends RealmObject {
9+
10+
public String name;
11+
12+
public HandleBackLinksChild2() {}
13+
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.realm.entities.migration;
2+
3+
import io.realm.RealmObject;
4+
import io.realm.annotations.PrimaryKey;
5+
6+
// Original parent with a regular object as a child
7+
public class HandleBackLinksParent1 extends RealmObject {
8+
@PrimaryKey
9+
public long id;
10+
11+
public HandleBackLinksChild1 child;
12+
13+
public HandleBackLinksParent1() {}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.realm.entities.migration;
2+
3+
import io.realm.RealmObject;
4+
import io.realm.annotations.PrimaryKey;
5+
6+
// Parent, now having an embedded object as a child, respecting table names
7+
public class HandleBackLinksParent2 extends RealmObject {
8+
@PrimaryKey
9+
public long id;
10+
11+
public HandleBackLinksChild2 child;
12+
13+
public HandleBackLinksParent2() {}
14+
}

realm/realm-library/src/main/cpp/io_realm_internal_Table.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,11 +1120,11 @@ JNIEXPORT jboolean JNICALL Java_io_realm_internal_Table_nativeIsEmbedded(JNIEnv*
11201120
return false;
11211121
}
11221122

1123-
JNIEXPORT jboolean JNICALL Java_io_realm_internal_Table_nativeSetEmbedded(JNIEnv* env, jclass, jlong j_table_ptr, jboolean j_embedded)
1123+
JNIEXPORT jboolean JNICALL Java_io_realm_internal_Table_nativeSetEmbedded(JNIEnv* env, jclass, jlong j_table_ptr, jboolean j_embedded, jboolean j_handle_backlinks)
11241124
{
11251125
try {
11261126
TableRef table = TableRef(TBL_REF(j_table_ptr));
1127-
table->set_table_type(to_bool(j_embedded) ? Table::Type::Embedded : Table::Type::TopLevel);
1127+
table->set_table_type(to_bool(j_embedded) ? Table::Type::Embedded : Table::Type::TopLevel, to_bool(j_handle_backlinks));
11281128
return true;
11291129
}
11301130
CATCH_STD()

realm/realm-library/src/main/java/io/realm/RealmObjectSchema.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,15 +584,66 @@ public boolean isEmbedded() {
584584
* </li>
585585
* </ul>
586586
*
587-
* @throws IllegalStateException if the class could not be converted because it broke some of the Embedded Objects invariants.
587+
* @param embedded If @{code true}, the class type will be turned into an embedded class, and
588+
* must satisfy the constraints defined above. If @{code false}, the class will be turn into
589+
* a normal class. An embeded class can always be turned into a non-embedded one.
590+
* @throws IllegalStateException if the class could not be converted because it broke some of
591+
* the Embedded Objects invariants.
588592
* @see RealmClass#embedded()
589593
*/
590594
public void setEmbedded(boolean embedded) {
595+
setEmbedded(embedded, false);
596+
}
597+
598+
/**
599+
* Converts the class to be embedded or not, while also providing automatic handling of objects
600+
* that break some of the constraints for making the class embedded.
601+
* <p>
602+
* A class can only be marked as embedded if the following invariants are satisfied:
603+
* <ul>
604+
* <li>
605+
* The class is not allowed to have a primary key defined.
606+
* </li>
607+
* <li>
608+
* All existing objects of this type, must have one and exactly one parent object
609+
* already pointing to it. If 0 or more than 1 object has a reference to an object
610+
* about to be marked embedded an {@link IllegalStateException} will be thrown.
611+
* </li>
612+
* </ul>
613+
*
614+
* If some of these constraints are broken you can ask Realm to resolve them automatically using
615+
* the @{code resolveEmbeddedClassConstraints} parameter. Setting this to @{code true} will
616+
* do the following:
617+
* <ul>
618+
* <li>
619+
* An object with 0 parents, i.e. no other objects have a reference to it, will be
620+
* deleted.
621+
* </li>
622+
* <li>
623+
* An object with more than 1 parent, i.e. 2 or more objects have a reference to it,
624+
* will be copied so each copy have exactly one parent.
625+
* </li>
626+
* <li>
627+
* Objects with a primary key defined will still throw an IllegalStateException and
628+
* cannot be converted.
629+
* </li>
630+
* </ul>
631+
*
632+
* @param embedded If @{code true}, the class type will be turned into an embedded class, and
633+
* must satisfy the constraints defined above. If @{code false}, the class will be turn into
634+
* a normal class. An embeded class can always be turned into a non-embedded one.
635+
* @param resolveEmbeddedClassConstraints whether or not to automatically fix broken constraints
636+
* if @{code embedded} was set to true. See above for a full description of what that entails.
637+
* @throws IllegalStateException if the class could not be converted because it broke some of
638+
* the Embedded Objects invariants and these could not be resolved automatically.
639+
* @see RealmClass#embedded()
640+
*/
641+
public void setEmbedded(boolean embedded, boolean resolveEmbeddedClassConstraints) {
591642
if (hasPrimaryKey()) {
592643
throw new IllegalStateException("Embedded classes cannot have primary keys. This class " +
593644
"has a primary key defined so cannot be marked as embedded: " + getClassName());
594645
}
595-
boolean setEmbedded = table.setEmbedded(embedded);
646+
boolean setEmbedded = table.setEmbedded(embedded, resolveEmbeddedClassConstraints);
596647
if (!setEmbedded && embedded) {
597648
throw new IllegalStateException("The class could not be marked as embedded as some " +
598649
"objects of this type break some of the Embedded Objects invariants. In order to convert " +

realm/realm-library/src/main/java/io/realm/internal/Table.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,18 @@ public boolean isEmbedded() {
793793
* some invariant was broken when trying to change the state
794794
*/
795795
public boolean setEmbedded(boolean embedded) {
796-
return nativeSetEmbedded(nativeTableRefPtr, embedded);
796+
return setEmbedded(embedded, false);
797+
}
798+
799+
/**
800+
* Returns true if the state was changed, false if not. If false was returned, it meant
801+
* some invariant was broken when trying to change the state. The {@code handleBackLinks}
802+
* parameter tells Core to automatically handle all unsatisfied invariants for backlinks, e.g.
803+
* children becoming orphan or cloning objects with multiple references during migrations from
804+
* a regular object to an embedded object.
805+
*/
806+
public boolean setEmbedded(boolean embedded, boolean handleBackLinks) {
807+
return nativeSetEmbedded(nativeTableRefPtr, embedded, handleBackLinks);
797808
}
798809

799810
@Nullable
@@ -957,5 +968,5 @@ public static String getTableNameForClass(String name) {
957968

958969
private static native boolean nativeIsEmbedded(long nativeTableRefPtr);
959970

960-
private static native boolean nativeSetEmbedded(long nativeTableRefPtr, boolean isEmbedded);
971+
private static native boolean nativeSetEmbedded(long nativeTableRefPtr, boolean isEmbedded, boolean handleBackLinks);
961972
}

0 commit comments

Comments
 (0)