realm / realm-kotlin

Kotlin Multiplatform and Android SDK for the Realm Mobile Database: Build Better Apps Faster.
Apache License 2.0
880 stars 52 forks source link

RealmList.remove(RealmObject) does not work #1712

Closed RileyGB closed 2 months ago

RileyGB commented 3 months ago

How frequently does the bug occur?

Always

Description

If I have two simple RealmObjects, where one is the parent and the other is a list of child objects within the parent, when I call parent.children.remove(child), the child is not removed from the RealmList.

It appears that Collections.removeAt is overridden in ManagedRealmList, whereas Collections.remove is not.

Stacktrace & log output

No response

Can you reproduce the bug?

Yes

Reproduction Steps

Here's models and tests that demonstrate the issue:

Models

class ParentModel : RealmObject {
    @PrimaryKey
    var id: String = ""
    var children: RealmList<ChildModel> = realmListOf()
    var strings: RealmList<String> = realmListOf()
}

class ChildModel : RealmObject {
    @PrimaryKey
    var id: String = ""
}

Tests

Note: RealmInstrumentedTest is internal to my project, it basically just opens Realm with our RealmConfiguration and initializes myTestRealm.

Of note here is that the assertion in Test RealmList object remove fails.

class RealmListTest : RealmInstrumentedTest() {
    @Test
    fun `Test RealmList model removeAt`() {
        myTestRealm?.let { realm ->
            realm.writeBlocking {
                val child = ChildModel().apply {
                    id = "a"
                }
                val parent = ParentModel().apply {
                    id = "1"
                    children = realmListOf(child)
                }

                copyToRealm(parent)
            }

            realm.writeBlocking {
                val parent = realm.query<ParentModel>("id = $0", "1").first().find()!!
                val child = parent.children.first()
                val index = parent.children.indexOfFirstOrNull { it.id == child.id } ?: -1

                val removedChild = findLatest(parent)!!.children.removeAt(index)

                assertNotNull(child) // returns true
                assertNotNull(removedChild) // returns true <--- removeAt works as expected
            }
        }
    }

    @Test
    fun `Test RealmList object remove`() {
        myTestRealm?.let { realm ->
            realm.writeBlocking {
                val child = ChildModel().apply {
                    id = "a"
                }
                val parent = ParentModel().apply {
                    id = "1"
                    children = realmListOf(child)
                }

                copyToRealm(parent)
            }

            realm.writeBlocking {
                val parent = realm.query<ParentModel>("id = $0", "1").first().find()!!
                val child = parent.children.first()

                val removedChild = findLatest(parent)!!.children.remove(child)

                assertThat(removedChild, `is`(true)) // returns false <--- remove(RealmObject) does not work as expected, this is the problem
            }
        }
    }

    @Test
    fun `Test RealmList string remove`() {
        myTestRealm?.let { realm ->
            realm.writeBlocking {
                val parent = ParentModel().apply {
                    id = "1"
                    strings = realmListOf("a")
                }

                copyToRealm(parent)
            }

            realm.writeBlocking {
                val parent = realm.query<ParentModel>("id = $0", "1").first().find()!!
                val string = parent.strings.first()

                val removedString = findLatest(parent)!!.strings.remove(string)

                assertThat(removedString, `is`(true)) // true <--- remove(String) works as expected
            }
        }
    }
}

Version

1.8.0

What Atlas App Services are you using?

Local Database only

Are you using encryption?

Yes

Platform OS and version(s)

All

Build environment

Android Studio version: Iguana 2023.2.1 Android Build Tools version: 7.4.2 Gradle version: 7.5

sync-by-unito[bot] commented 3 months ago

➤ PM Bot commented:

Jira ticket: RKOTLIN-1063

kneth commented 2 months ago

There is a difference between removing an object from a list and delete the object: https://www.mongodb.com/docs/atlas/device-sdks/sdk/kotlin/realm-database/crud/delete/#remove-elements-from-a-realmlist

RileyGB commented 2 months ago

There is a difference between removing an object from a list and delete the object: https://www.mongodb.com/docs/atlas/device-sdks/sdk/kotlin/realm-database/crud/delete/#remove-elements-from-a-realmlist

Hi Kneth, thanks for the reply!

Are you referencing this?

Realm collection instances that contain objects only store references to those objects. You can remove one or more referenced objects from a collection without deleting the objects themselves. The objects that you remove from a collection remain in the realm until you manually delete them. Alternatively, deleting a Realm object from a realm also deletes that object from any collection instances that contain the object.

If it is intentional that .removeAt deletes the object, but .remove does not, I would find that to be pretty unintuitive. Every other list in Kotlin removes an item from a list when .remove is called (so long as a the item exists within the list).

The docs are pretty unclear or wrong here if you look at the examples provided below:

To remove one element from the list, pass the element to list.remove().

    // Remove the first pond in the list
    val removeFirstPond = forestPonds.first()
    forestPonds.remove(removeFirstPond)
    assertEquals(4, forestPonds.size)

According to my example above, this assertion would fail. Presumably because RealmInterop.realm_list_erase is not called via RealmList.remove as it is not overridden in ManagedRealmList.

c-villain commented 2 months ago

Any changes for this issue?

rorbech commented 2 months ago

Hi @c-villain. I think the issue here is that you are actually looking up the child in an outdated context.

With

realm.writeBlocking {
    val parent = realm.query<ParentModel>("id = $0", "1").first().find()!!
    val child = parent.children.first()

    val removedChild = findLatest(parent)!!.children.remove(child)

    assertThat(removedChild, `is`(true)) // returns false <--- remove(RealmObject) does not work as expected, this is the problem
}

The instances parent and child are queried from your realm instead of the MutableRealm-receiver of the block argument or writeBlocking. This will try to remove an outdated (or not most recent) version of the object from the list. This will not match and hence will not be removed.

To achieve your intent you will have to either ensure that you are removing an up-to-date reference with:

val removedChild = findLatest(parent)!!.children.remove(findLatests(child)!!) // Added findLatest around child

Alternatively you could just operate solely on objects from the MutableRealm with

realm.writeBlocking { // this: MutableRealm ->
    // using this (MutableRealm) instead of global frozen `realm`
    val parent = this.query<ParentModel>("id = $0", "1").first().find()!! 
    val child = parent.children.first()

    val removedChild = parent.children.remove(child)

    assertThat(removedChild, `is`(true))
}
rorbech commented 2 months ago

I have created #1723 to improve the APIs and/or throw in these case, so will close this issue for now. If the advised details in https://github.com/realm/realm-kotlin/issues/1712#issuecomment-2058989283 is not fixing your issue, please leave a note and we can reinvestigate.