How to handel no results with Android Room and RxJava 2?
Asked Answered
M

6

21

I have database with table contact and I want to check if there is contact with some phone number.

@Query("SELECT * FROM contact WHERE phone_number = :number")
Flowable<Contact> findByPhoneNumber(int number);

I have RxJava 2 Composite disposable with statement from above to check if there is contact with phone number.

disposable.add(Db.with(context).getContactsDao().findByPhoneNumber(phoneNumber)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeWith(new DisposableSubscriber<Contact>() {
                @Override
                public void onNext(final Contact contact) {
                    Log.d("TAG", "phone number fined");
                    Conversation conversation;
                    if(contact != null){
                        conversation = Db.with(context).getConversationsDao().findBySender(contact.getContactId());
                        if(conversation != null){
                            conversation.setUpdatedAt(Utils.getDateAndTimeNow());
                            saveConversation(contact, conversation, context, text, phoneNumber, false);
                        } else {
                            conversation = getConversation(contact, contact.getPhoneNumber());
                            saveConversation(contact, conversation, context, text, phoneNumber, true);
                        }
                    } else {
                        conversation = Db.with(context).getConversationsDao().findByPhone(phoneNumber);
                        if(conversation != null){
                            conversation.setUpdatedAt(Utils.getDateAndTimeNow());
                            saveConversation(contact, conversation, context, text, phoneNumber, false);
                        } else {
                            conversation = getConversation(contact, phoneNumber);
                            saveConversation(contact, conversation, context, text, phoneNumber, true);
                        }
                    }
                }

                @Override
                public void onError(Throwable t) {
                    Log.d("TAG", "find phone number throwable");
                    Toast.makeText(context, t.getLocalizedMessage(), Toast.LENGTH_LONG).show();
                }

                @Override
                public void onComplete() {
                    Log.d("TAG", "onComplete");
                }
            }));

This is working fine if query can find contact with required phone number, but if there is result, it nothing happens.

Here are two test cases that I wrote and they work fine:

@RunWith(AndroidJUnit4.class)
public class ContactsTest {

    private AppDatabase db;

    @Rule
    public InstantTaskExecutorRule instantTaskExecutorRule =
            new InstantTaskExecutorRule();

    @Before
    public void initDb() throws Exception {
        db = Room.inMemoryDatabaseBuilder(
                InstrumentationRegistry.getContext(),
                AppDatabase.class)
                // allowing main thread queries, just for testing
                .allowMainThreadQueries()
                .build();
    }

    @After
    public void close(){
        db.close();
    }

    @Test
    public void insertAndFindTest(){
        final Contact contact = new Contact();
        contact.setName("Test");
        contact.setPhoneNumber(555);
        db.contactsDao()
                .insert(contact);

        db.contactsDao().findByPhoneNumber(contact.getPhoneNumber())
                .test()
                .assertValue(new Predicate<Contact>() {
                    @Override
                    public boolean test(@NonNull Contact savedContact) throws Exception {
                        if(savedContact.getPhoneNumber() == contact.getPhoneNumber()){
                            return true;
                        }
                        return false;
                    }
                });
    }

    @Test
    public void findNoValues(){
        db.contactsDao().findByPhoneNumber(333)
                .test()
                .assertNoValues();
    }

}

How I can solve this?

Morry answered 6/7, 2017 at 9:41 Comment(1)
your check contact != null is useless, rxjava2 don't allow nulls. Also, do you have typo in your question? I can't understand you third sentenseSleep
G
38

As said here, you can use Maybe or Single for this case:

Maybe

@Query("SELECT * FROM Users WHERE id = :userId")
Maybe<User> getUserById(String userId);

Here’s what happens:

  • When there is no user in the database and the query returns no rows, Maybe will complete.
  • When there is a user in the database, Maybe will trigger onSuccess and it will complete.
  • If the user is updated after Maybe was completed, nothing happens.

Single

@Query("SELECT * FROM Users WHERE id = :userId")
Single<User> getUserById(String userId);

Here are some scenarios:

  • When there is no user in the database and the query returns no rows, Single will trigger onError(EmptyResultSetException.class)
  • When there is a user in the database, Single will trigger onSuccess.
  • If the user is updated after Single.onComplete was called, nothing happens, since the stream was completed.

It was added in version 1.0.0-alpha5.

Giffer answered 23/7, 2017 at 23:42 Comment(3)
Is there any way to convert such single to Single<Boolean> where Boolean = "is there such row at the table?"Pantie
Important note: "If the Single<T> contains a type argument of a collection (e.g. Single<List<Song>>) then this exception (EmptyResultSetException.class) is not thrown an an empty collection is emitted instead." documentationSaltire
One important clarification regarding Maybe -- When there is no user in the database and the query returns no rows, Maybe will complete. When there is a user in the database, Maybe will trigger onSuccess and it will complete. If there is a user it will not "complete" since "onComplete" is only called if the Maybe is emptyCorrinacorrine
E
19

If you want to use your entity only once, Single or Maybe is sufficient. But if you want to observe if your query is updated you can use Flowable and wrap your object in List, so when there is no results you will get empty list, and when after that, database is updated you will get another event with your result in list.

Code

@Query("SELECT * FROM contact WHERE phone_number = :number LIMIT 1")
Flowable<List<Contact>> findByPhoneNumber(int number)

I believe it's usefull in some scenarios. The drawback is that you have to access object like resultList.get(0)

Epigenesis answered 14/4, 2018 at 20:59 Comment(1)
This was a great idea and worked perfect for my use case. FYI another advantage of this method is you can check if the "list" is empty and if so return a default value down streamDelarosa
G
7

When you use Flowable (and LiveData too) as a return value in your Dao class then your query never stops emitting data as Room is monitoring tables for data changes. Quoting official documentation:

Furthermore, if the response is an observable data type, such as Flowable or LiveData, Room watches all tables referenced in the query for invalidation.

Not sure what's the best way of handling such situation but what worked for me was a good old .timeout() operator. Please have a look at the following test and follow comments:

@Test
public void shouldCompleteIfForced() throws InterruptedException {
    // given
    TestScheduler testScheduler = new TestScheduler();

    // when asking db for non existent project
    TestSubscriber<Project> test = projectDao.getProject("non existent project")
            .timeout(4, TimeUnit.SECONDS, testScheduler)
            .test();

    // then hang forever waiting for first emission which will never happen
    // as there is no such project
    test.assertNoValues();
    test.assertNotComplete();
    test.assertNoErrors();

    // when time passes and we trigger timeout() operator
    testScheduler.advanceTimeBy(10, TimeUnit.SECONDS);

    // then finally break stream with TimeoutException error ...
    test.assertError(TimeoutException.class);
}
Gladiatorial answered 12/7, 2017 at 21:54 Comment(0)
T
2

I guess you also could use wrapper with Single. Like:

public class QueryResult<D> {
            public D data;
            public QueryResult() {}

            public QueryResult(D data) {
                this.data = data;
            }

            public boolean isEmpty(){
                return data != null;
            }
 }

And use it like:

public Single<QueryResult<Transaction>> getTransaction(long id) {
            return createSingle(() -> database.getTransactionDao().getTransaction(id))
                    .map(QueryResult::new);
}

Where createAsyncSingle:

protected <T> Single<T> createSingle(final Callable<T> func) {
            return Single.create(emitter -> {
                try {
                    T result = func.call();
                    emitter.onSuccess(result);

                } catch (Exception ex) {
                    Log.e("TAG", "Error of operation with db");
                }
            });
}

Don't forget to use IO thread.

Theona answered 8/5, 2018 at 22:22 Comment(0)
M
1

I personally prefer using java.util.Optional. Your code will look like:

@Query("SELECT * FROM contact WHERE phone_number = :number")
Flowable<Optional<Contact>> findByPhoneNumber(int number);

And then

findByPhoneNumber(number).map(optionalContact -> {
    if (optionalContact.isPresent()) {
        return optionalContact.get();
    } else {
        // no contact present, use default value or something
    }
})
Myoglobin answered 5/10, 2023 at 13:15 Comment(0)
S
0
@Query("SELECT * FROM contact WHERE phone_number = :number")
Flowable<List<Contact>> findByPhoneNumber(int number);

then

Optional<Contact> queryPhone(int number) {
   findByPhoneNumber(number).map { list ->
      if (list.isEmpt()) return Optional.empty() else return Optional.of(list[0])
   }
}

as described here

I find it's strange that this behaviour is no where to be found on official docs though

Sinfonia answered 1/11, 2022 at 20:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.