Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

addChangeListener is always triggered on update, even when no result found for query #4196

Closed
NeverwinterMoon opened this issue Feb 18, 2017 · 16 comments

Comments

@NeverwinterMoon
Copy link

NeverwinterMoon commented Feb 18, 2017

I have the following query:

query = realm.where(Trip::class.java)
        .equalTo(Trip::isMyJourneySupported.name, true)
        .isEmpty(Trip::notifications.name)
        .findFirstAsync()

query.addChangeListener(blaListener)

Trip is updated constantly and, at first, it doesn't have any notifications (empty RealmList is default value), so the blaListener is triggered. Inside the listener callback notifications are fetched from BE, saved to Realm and linked to Trip. All fine, except that the listener continues calling the blaListener continously even though `isEmpty(Trip::notifications.name) is already not true... Am I missing something?

Oh, when I use findAllAsync() or findAll() instead, it works as expected.

@beeender
Copy link
Contributor

The current behavior of findFirstAsync() is it will try to run the query again until it can find a object match the query condition. But maybe this will be changed in the #4101 . What do you think? I feel the query should only be run once, if it cannot find a object match the query condition, then isLoaded() should return true and isValid() should returns false.
@realm/java

@NeverwinterMoon
Copy link
Author

My understanding of how this would work was this:

  • If queried object does not exist at all (initial state with not data at all), changeListener is called and object.isValid = false (not a single Trip::class.java in my case)

  • If object does exist but query returns true, changeListener is called (isEmpty(Trip::notifications.name) is true in my case) and isValid = true

  • If object does exist but query returns false, changeListener is not called

  • And the query should always be alive in case queried object changes.

What I got was:

  • On init I had not Trips, query called changeListener with isValid = false, isLoaded = true. All good.

  • Then query was true because app added Trips without notifications to Realm. changeListener was called with isValid = true, isLoaded = true. All good. Here is where I fetched Notifications and linked them to Trip...

  • Then the changeListener was called again with the same Trip - but now it had Notifications attached, so query shouldn't have called changeListener at all - condition is not met (isEmpty). isValid = true, isLoaded = true. So, I pretty much got into the dead loop, as this updated Notifications again and that updated Trip...

I checked #4101 and it does indeed look very good and would satisfy my needs. But I still don't understand why currently the falsy query result ends up calling changeListener...

@beeender
Copy link
Contributor

For:

Then the changeListener was called again with the same Trip - but now it had Notifications attached, so query shouldn't have called changeListener at all - condition is not met (isEmpty). isValid = true, isLoaded = true. So, I pretty much got into the dead loop, as this updated Notifications again and that updated Trip...

There is a misunderstanding here:
When the first time findFirstAsync() returns, the query won't be re-run. So the notification will be sent to the same object whenever the object itself changes (although there are false positive notifications reported now, see #3894 and this will be fixed in #4101 )

The current behavior for your example:

  1. on init, no Trips, changeListener with isValid = false, isLoaded = true
  2. after there is one trip match the query, changeListener with isValid = true, isLoaded = true
  3. then query will NOT be rerun since we already found an object matched the query. Instead the notification will be triggered when the object we found in step 2 changes (same, false positive notification will happen like describes above.)

For your case, i think you should just use the findAllAsync() instead. Unlike the findFirstAsync(), it will re-run the query all the time to make sure all the objects found in the change listener matches the query conditions.

@cmelchior
Copy link
Contributor

I think a lot of these misunderstandings would be solved if we adopted the semantics described here: #2748

i.e. instead of now where we stop evaluating the query once a match is found, it should keep evaluating the query, possibly returning different objects (which is fine IMO).

@beeender
Copy link
Contributor

instead of now where we stop evaluating the query once a match is found, it should keep evaluating the query, possibly returning different objects (which is fine IMO).

It makes the implementation impossible:

  • should the notification sent if the RealmObject changes
  • should the notification sent if the query returns a different RealmObject
  • which notification should be sent if the original RealmObject changes and the query returns a different RealmObject?

I think it is either we keep current implementation or remove the findAllAsync(). ()

For the use case described in this issue, user should use findAll()/findAllAsync() instead. Probably the LIMIT #544 will help this, so something like findAll(class, limit=1).

@beeender
Copy link
Contributor

beeender commented Feb 20, 2017

RealmObject.addChangeListener()'s meaning should be restricted to the listener will be call only if any fields in the object changes or the object gets deleted. Changing the row of the RealmObject to a different one will be a really bad idea.

@Zhuinden
Copy link
Contributor

Wait, changeListener is called if the object is deleted?

Are you sure?

@NeverwinterMoon
Copy link
Author

@beeender

the query won't be re-run. So the notification will be sent to the same object whenever the object itself changes

Aha, got it. I did start using findAllAsync() already, after going a bit mad from findFirstAsync() not doing what I expected it to do :) But was curious nonetheless.

It is extremely weird, in my humble opinion, that changeListener keeps on being called on object update that was not described by query the listener was attached to.

@beeender
Copy link
Contributor

@Zhuinden Just found there is no test to ensure that. But from the code, it should be called when the object gets deleted. the listener should be called with isLoaded() == true isValid() = false. Shouldn't it be called in this case?

@cmelchior
Copy link
Contributor

I don't think it is called when an object is deleted. It is being tracked here: #3138

@kneth
Copy link
Contributor

kneth commented Mar 7, 2017

With Realm Java 3.0.0 being released (https://realm.io/news/realm-java-3-0-collection-notifications/), the change listeners have changed. @NeverwinterMoon it might be a good idea to upgrade to 3.0.0 and tell us if you think we have improved the API :-)

@cmelchior
Copy link
Contributor

cmelchior commented Mar 7, 2017

The current behavior of findFirstAsync() is it will try to run the query again until it can find a object match the query condition. But maybe this will be changed in the #4101 . What do you think? I feel the query should only be run once, if it cannot find a object match the query condition, then isLoaded() should return true and isValid() should returns false.

I agree that right now the semantics do seem kinda weird. It is a mix between a live query and a live object.

It should probably:

A) Only evaluate the query once and return the matching live object. Which would be invalid if the query didn't find any objects

B) Be a live query all the time, which means that the object returned would be a sort of wrapper around findAll().first(). Which also means the object could go from obj -> deleted -> obj If it was deleted and then created again with the same key. This was actually the semantics I was arguing for in #2748.

Thinking about this a bit more, I probably lean toward A) ... It would keep the semantics around RealmObject and RealmResults more clear, i.e. RealmObject is a single object in Realm while RealmResults represent the result of a live query.

I can see why you would like a convenience method around findAll().first() though, so perhaps we should consider adding something to support that.

@Zhuinden
Copy link
Contributor

Zhuinden commented Mar 7, 2017

Don't you already have a firstOrDefault?

@NeverwinterMoon
Copy link
Author

@kneth Will try to check out 3.0.0 today-tomorrow and report back on my experience this week. Thanks.

@kneth
Copy link
Contributor

kneth commented Mar 7, 2017

I move the issue to our "Ideas backlog" as it has spawned a lot of discussion.

@NeverwinterMoon Thank you for getting us to think :-)

@beeender
Copy link
Contributor

This has been addressed by #4358 and #4367

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 16, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants