Search code examples
androidrealmrealm-mobile-platform

Realm doesn't find existing items


I'm using Realm 3.4.0 and having one object that should be a singleton. The database is synced.

Here is a simplified version of the code: test if that object exist, add it if it doesn't exist. What is the correct way to do that? (copyToRealmOrUpdate shouldn't be needed or is there any other reason why the instance becomes null?)

@PrimaryKey
public long id = 1;

public static PlannerManager getInstance(Realm realm) {
    PlannerManager ourInstance = null;
    if (instanceLock == null)
        instanceLock = new ReentrantLock();
    try {
        instanceLock.lock();
        realm.refresh(); // Force getting all data from online database
        ourInstance = realm.where(PlannerManager.class).findFirst();

            if (ourInstance == null) { // The item doesn't exist
                realm.beginTransaction();
                ourInstance = realm.copyToRealm(new PlannerManager()); // Crashes sometimes with the error that an object with primary ID already exists
                realm.commitTransaction();
            }
        } finally {
          instanceLock.unlock();
        }
        return ourInstance;
    }

Relevant part of the stacktrace

2:9.446 Primary key value already exists: 1 .
(/Users/cm/Realm/realm-java/realm/realm-library/src/main/cpp/io_realm_internal_OsObject.cpp:189) io.realm.exceptions.RealmPrimaryKeyConstraintException: Primary key value already exists: 1 .
(/Users/cm/Realm/realm-java/realm/realm-library/src/main/cpp/io_realm_internal_OsObject.cpp:189)
    at io.realm.internal.OsObject.nativeCreateNewObjectWithLongPrimaryKey(Native Method)
    at io.realm.internal.OsObject.createWithPrimaryKey(OsObject.java:198)
    at io.realm.Realm.createObjectInternal(Realm.java:1052)
    at io.realm.PlannerManagerRealmProxy.copy(PlannerManagerRealmProxy.java:1279)
    at io.realm.PlannerManagerRealmProxy.copyOrUpdate(PlannerManagerRealmProxy.java:1268)
    at io.realm.DefaultRealmModuleMediator.copyOrUpdate(DefaultRealmModuleMediator.java:438)
    at io.realm.Realm.copyOrUpdate(Realm.java:1660)
    at io.realm.Realm.copyToRealm(Realm.java:1072)
    at com.myapp.internal.PlannerManager.getInstance(PlannerManager.java:857)

Thanks!


Solution

  • Your logic is actually slightly wrong. By doing the query outside the transaction, the background sync thread might put data into Realm between you do the query and begin the transaction. Transactions will always move the Realm to the latest version, so calling refresh() is also not needed. Your logic should be something like:

        realm.beginTransaction();
        ourInstance = realm.where(PlannerManager.class).findFirst();
        if (ourInstance == null) {
          ourInstance = realm.createObject(PlannerManager.class, 1);
          realm.commitTransaction();
        } else {
          realm.cancelTransaction();
        }
    

    Note, that using realm.copyToRealm() will cause changes from other devices to be overridden, so for initial data like this, it is safer to use createObject as changes to individual fields will then merge correctly. Using copyToRealm() is the same as actually setting all fields to the initial value.

    E.g if you have two devices A and B that are both offline:

    1. A starts app and creates the default PlannerManager.
    2. A modifies a field in PlannerManager.
    3. B starts the app, but since A is offline, it doesn't know that PlannerManager is already created, so it also creates the default PlannerManager.
    4. A and B both go online.
    5. Due to how Realm uses "last-write-wins", B will now override all changes done by A, since using copyToRealm is the equivalent of setting all fields manually.

    Using Realm.createObject() uses a special "default" instruction for all fields, that automatically loses to any explicit set like the one used when using normal Java setters (and which copyToRealm uses).