Как справиться с отсутствием результатов с помощью Android Room и RxJava 2?

У меня есть база данных с табличным контактом, и я хочу проверить, есть ли контакт с каким-либо номером телефона.

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

У меня есть одноразовый RxJava 2 Composite с заявлением сверху, чтобы проверить, есть ли контакт с номером телефона.

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");
                }
            }));

Это нормально работает, если запрос может найти контакт с требуемым номером телефона, но если есть результат, ничего не происходит.

Вот два тестовых примера, которые я написал, и они отлично работают:

@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();
    }

}

Как я могу это решить?


person Zookey    schedule 06.07.2017    source источник
comment
ваш контрольный контакт != null бесполезен, rxjava2 не допускает нулей. Кроме того, у вас есть опечатка в вашем вопросе? Я не могу понять тебя, третье предложение   -  person orium    schedule 17.11.2017


Ответы (4)


Как сказано здесь, в этом случае вы можете использовать Maybe или Single:

Может быть

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

Вот что происходит:

  • Когда в базе данных нет пользователя и запрос не возвращает строк, Maybe завершится.
  • Когда в базе данных есть пользователь, Maybe сработает при успехе и завершится.
  • Если пользователь обновляется после завершения Maybe, ничего не происходит.

Одинокий

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

Вот несколько сценариев:

  • Когда в базе данных нет пользователя и запрос не возвращает строк, Single сработает onError(EmptyResultSetException.class)
  • Когда в базе данных есть пользователь, Single сработает при успехе.
  • Если пользователь обновляется после вызова Single.onComplete, ничего не происходит, так как поток был завершен.

Он был добавлен в версии 1.0.0-alpha5.

person J-rooft    schedule 23.07.2017
comment
Есть ли способ преобразовать такой сингл в Single‹Boolean›, где Boolean = есть ли такая строка в таблице? - person Evgenii Vorobei; 16.07.2019
comment
Важное примечание: если Single‹T› содержит аргумент типа коллекции (например, Single‹List‹Song››), то это исключение (EmptyResultSetException.class) не генерируется, вместо этого генерируется пустая коллекция. документация - person bitvale; 02.02.2020
comment
Одно важное уточнение относительно Maybe: когда в базе данных нет пользователя и запрос не возвращает строк, Maybe будет завершен. Когда в базе данных есть пользователь, Maybe сработает при успехе и завершится. Если есть пользователь, он не будет завершен, поскольку onComplete вызывается только в том случае, если Maybe пуст. - person kcrimi; 30.04.2020

Если вы хотите использовать свою сущность только один раз, достаточно Single или Maybe. Но если вы хотите наблюдать, обновляется ли ваш запрос, вы можете использовать Flowable и обернуть свой объект в List, поэтому, когда результатов нет, вы получите пустой список, а когда после этого база данных будет обновлена, вы получите другое событие с вашим результатом в списке.

Код

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

Я считаю, что это полезно в некоторых сценариях. Недостатком является то, что вам нужно получить доступ к объекту, например resultList.get(0)

person Wiktor Wardzichowski    schedule 14.04.2018
comment
Это была отличная идея, и она идеально подошла для моего варианта использования. К вашему сведению, еще одним преимуществом этого метода является то, что вы можете проверить, пуст ли список, и если это так, вернуть значение по умолчанию в нисходящем потоке. - person Mackalester; 08.03.2021

Когда вы используете FlowableLiveData тоже) в качестве возвращаемого значения в вашем классе Dao, ваш запрос никогда не перестанет выдавать данные, поскольку Room отслеживает таблицы на предмет изменений данных. Цитирование официальной документации:

Кроме того, если ответ представляет собой наблюдаемый тип данных, например Flowable или LiveData, Room отслеживает все таблицы, на которые есть ссылки в запросе, на предмет признания их недействительными.

Не знаю, как лучше поступить в такой ситуации, но мне помог старый добрый оператор .timeout(). Пожалуйста, взгляните на следующий тест и следите за комментариями:

@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);
}
person pelotasplus    schedule 12.07.2017

Я думаю, вы также можете использовать обертку с Single. Нравиться:

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

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

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

И используйте его как:

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

Где 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");
                }
            });
}

Не забудьте использовать поток ввода-вывода.

person Djek-Grif    schedule 08.05.2018