Realm-java: unexpected behaviour for Collection Change Listener

Created on 11 Apr 2017  路  13Comments  路  Source: realm/realm-java

Expected Results

No Change Event or something.

Actual Results

Change Event occured and it has wrong ChangeSet

Steps & Code to Reproduce

  1. Create Realm Model like the following.
public class RealmPerson extends RealmObject {

    @Index
    private int id;
    @Required
    private String name;

}

2.Add initial items and subscribe change event

        realm.beginTransaction();
        try {
            realm.delete(RealmPerson.class);
            for (int i = 0; i < 10000; i++) {
                RealmPerson test = new RealmPerson();
                test.setId(i);
                test.setName("name" + i);
                realm.copyToRealm(test);
            }
            realm.commitTransaction();
        } catch (Throwable t) {
            realm.cancelTransaction();
            throw t;
        }

        partialPesons = realm.where(RealmPerson.class).greaterThanOrEqualTo("id", 8000).findAll();
        partialPesons.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<RealmPerson>>() {

            @Override
            public void onChange(RealmResults<RealmPerson> collection, OrderedCollectionChangeSet changeSet) {
                Log.i("partialPersons", "ttt");
                dumpChangSet(changeSet);
            }

        });

and dumpChangeSet is something like the followings

    private static void dumpChangSet(OrderedCollectionChangeSet changeSet) {
        for (int index : changeSet.getDeletions())
            Log.i("deletion", String.valueOf(index));
        for (int index : changeSet.getInsertions())
            Log.i("insertion", String.valueOf(index));
        for (int index : changeSet.getChanges())
            Log.i("modification", String.valueOf(index));
    }

3.Delete items

        realm.beginTransaction();
        try {
            RealmResults<RealmPerson> targets = realm.where(RealmPerson.class).equalTo("id", 3000).findAll();
            targets.deleteAllFromRealm();
            realm.commitTransaction();
        } catch (Throwable t) {
            realm.cancelTransaction();
            throw t;
        }

4.
I deleted item with ID of 3000 that partialPesons does'nt contain
but change event occured and it's ChageSet has wrong index.

you are releaseing new functinon without basic test??

Version of Realm and tooling

Realm version(s): 3.1.1

Realm sync feature enabled: no

Android Studio version: 2.3

Which Android version and device: ?

T-Doc

Most helpful comment

oh so that's why my example was working, I used findAllSorted() instead of findAll().

Wow I'm terrible at reproducing bugs if I fix them without noticing 馃槄

All 13 comments

Your results is built from:

partialPesons = realm.where(RealmPerson.class).greaterThanOrEqualTo("id", 8000).findAll();

which includes all RealmPerson whose id is >= 8000

but what you deleted is id == 3000 which is not a part of the results.
And I think that is why the listener is not called.

it's reverse.

Actual result is that the linster is called.

Expected Results is that change event does not occur.

Sorry for my poor English.

OK, let me try that :)

I've just tried it (although it is fine if Beender also tries it) but

04-11 14:44:38.433 3127-3127/com.zhuinden.realmbreak I/MainActivity: DELETING [Aardwolf]

OrderedRealmChangeListener defined as

words = realm.where(Word.class).not().beginsWith(WordFields.WORD, "A").findAllSorted(WordFields.WORD, Sort.ASCENDING);

was not called


Delete happens locally

    Single.just("").delay(5000, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(s -> {
        if(realm != null) {
            realm.executeTransaction(_realm -> {
                _realm.where(Word.class).equalTo(WordFields.WORD, "Aardwolf").findAll().deleteAllFromRealm();
                Log.i(TAG, "DELETING [Aardwolf]");
            });
        }
    });


Changing condition to

words = realm.where(Word.class).not().beginsWith(WordFields.WORD, "B").findAllSorted(WordFields.WORD, Sort.ASCENDING);

And deleting

    Single.just("").delay(5000, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(s -> {
        if(realm != null) {
            realm.executeTransaction(_realm -> {
                _realm.where(Word.class).equalTo(WordFields.WORD, "Aang").findAll().deleteAllFromRealm();
                Log.i(TAG, "DELETING [Aang]");
            });
        }
    });

Returns log:

04-11 14:46:55.417 5066-5066/com.zhuinden.realmbreak I/MainActivity: DELETING [Aang]
04-11 14:46:55.445 5066-5066/com.zhuinden.realmbreak I/MainActivity: Deletion [0]


Aang was 0th index and it was called only when it was in the RealmResults.

My complete code is

public class MainActivity extends AppCompatActivity {

    private Realm realm;

    private RealmResults<RealmPerson> allPersons;
    private RealmResults<RealmPerson> partialPersons;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Realm.init(this);
        realm = Realm.getDefaultInstance();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_activity, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        return true;
    }

    private static void dumpChangSet(OrderedCollectionChangeSet changeSet) {
        for (int index : changeSet.getDeletions())
            Log.i("deletion", String.valueOf(index));
        for (int index : changeSet.getInsertions())
            Log.i("insertion", String.valueOf(index));
        for (int index : changeSet.getChanges())
            Log.i("modification", String.valueOf(index));
    }

    private void addItems() {
        realm.beginTransaction();
        try {
            realm.delete(RealmPerson.class);
            for (int i = 0; i < 10000; i++) {
                RealmPerson test = new RealmPerson();
                test.setId(i);
                test.setName("name" + i);
                realm.copyToRealm(test);
            }
            realm.commitTransaction();
        } catch (Throwable t) {
            realm.cancelTransaction();
            throw t;
        }
        allPersons = realm.where(RealmPerson.class).findAll();
        allPersons.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<RealmPerson>>() {

            @Override
            public void onChange(RealmResults<RealmPerson> collection, OrderedCollectionChangeSet changeSet) {
                Log.i("allPersons", "ttt");
                dumpChangSet(changeSet);
            }

        });

        partialPersons = realm.where(RealmPerson.class).greaterThanOrEqualTo("id", 8000).findAll();
        partialPersons.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<RealmPerson>>() {

            @Override
            public void onChange(RealmResults<RealmPerson> collection, OrderedCollectionChangeSet changeSet) {
                Log.i("partialPersons", "ttt");
                dumpChangSet(changeSet);
            }

        });
    }

    private void addItem() {
        Log.i("add item", "ttt");
        realm.beginTransaction();
        try {
            RealmPerson test = new RealmPerson();
            test.setId(3000);
            test.setName("name");
            realm.copyToRealm(test);
            realm.commitTransaction();
        } catch (Throwable t) {
            realm.cancelTransaction();
            throw t;
        }
    }

    private void updateItem() {
        Log.i("update item", "ttt");
        realm.beginTransaction();
        try {
            RealmResults<RealmPerson> targets = realm.where(RealmPerson.class).equalTo("id", 3000).findAll();
            for (RealmPerson person : targets) {
                person.setName("ABC");
            }
            realm.commitTransaction();
        } catch (Throwable t) {
            realm.cancelTransaction();
            throw t;
        }
    }

    private void deleteItem() {
        Log.i("delete item", "ttt");
        realm.beginTransaction();
        try {
            RealmResults<RealmPerson> targets = realm.where(RealmPerson.class).equalTo("id", 3000).findAll();
            targets.deleteAllFromRealm();
            realm.commitTransaction();
        } catch (Throwable t) {
            realm.cancelTransaction();
            throw t;
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_add_items:
                addItems();
                return true;
            case R.id.action_add_item:
                addItem();
                return true;
            case R.id.action_update_item:
                updateItem();
                return true;
            case R.id.action_delete_item:
                deleteItem();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

}

Then when I select delete menu.
logs are

04-11 10:50:13.957 3960-3960/realmtest2 I/delete聽item: ttt
04-11 10:50:13.967 3960-3960/realmtest2 I/allPersons: ttt
04-11 10:50:13.967 3960-3960/realmtest2 I/deletion: 3000
04-11 10:50:13.967 3960-3960/realmtest2 I/deletion: 9999 <- ???
04-11 10:50:13.967 3960-3960/realmtest2 I/insertion: 3000 <- ???
04-11 10:50:13.967 3960-3960/realmtest2 I/partialPersons: ttt
04-11 10:50:13.967 3960-3960/realmtest2 I/deletion: 1999 <-- ????
04-11 10:50:13.967 3960-3960/realmtest2 I/insertion: 0 <-- ???

Insertion??

Hey, i can produce your issue, but, it is actually the expected behaviour :)

you can get the desired behavior if you change:

partialPersons = realm.where(RealmPerson.class).greaterThanOrEqualTo("id", 8000).findAll();

to findAllSorted("id").

This is is because of the order of returned results by findAll() is not guaranteed, it could be changed even no elements change in it.

When you delete the id=3000 RealmPerson, internally in realm core, the last element before whose id is 9999 will be moved to the empty slot where the id = 3000 is (to make it faster). Without sorting, findAll()'s results will update to the id = 9999 as the first element. That why you get an insertion at index 0.

So, just sort the results ~

oh so that's why my example was working, I used findAllSorted() instead of findAll().

Wow I'm terrible at reproducing bugs if I fix them without noticing 馃槄

oh. Thx a lot.
I understand what u told then one question comes to my mind.
How about Model's RealmResults??

Let's say I have the following models

public class RealmHouse extends RealmObject {

    private RealmResults<RealmPerson> persons;

    public RealmResults<RealmPerson> getPersons() {
        return persons;
    }
}

Then i also need to sort this like the following for the same behaviours?

        RealmHouse house;
        house.getPersons().sort("id").addChangeListener(..);


hmmm, that is a very good question!
Actually i am not quite sure about that. what is think is yes, you need to sort it otherwise it won't maintain the order, but i will try to confirm it.

one thing about your code above is:

RealmResults sorted = house.getPersons().sort("id");
sorted.addChangeListener(..);

you still need to maintain a strong ref to the sorted, otherwise after it gets GCed, your listener won't be triggered anymore.

you still need to maintain a strong ref to the sorted, otherwise after it gets GCed, your listener won't be triggered anymore.

I see. thx a lot.

and Please document this behaviour somewhere after confirming it.

This was added to the website docs here: https://realm.io/docs/java/latest/#notifications
We should probably also add a comment to all the places where ChangeListeners can be registered:

Something like:

NOTE: Registering a change listener will not prevent the underlying object from being garbage collected. If the object is garbage collected, the change listener will stop being triggered. To avoid this, keep a strong reference for as long as appropriate e.g. in a class variable.

That is definitely a comment you should add to doc

Closed by #4631

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jjorian picture jjorian  路  3Comments

CNyezi picture CNyezi  路  3Comments

harshvishu picture harshvishu  路  3Comments

AAChartModel picture AAChartModel  路  3Comments

wezley98 picture wezley98  路  3Comments