Android Room - простой запрос выбора - не удается получить доступ к базе данных в основной теме

Я пытаюсь создать образец с Библиотека сохранения пространства. Я создал объект:

@Entity
public class Agent {
    @PrimaryKey
    public String guid;
    public String name;
    public String email;
    public String password;
    public String phone;
    public String licence;
}

Создан класс DAO:

@Dao
public interface AgentDao {
    @Query("SELECT COUNT(*) FROM Agent where email = :email OR phone = :phone OR licence = :licence")
    int agentsCount(String email, String phone, String licence);

    @Insert
    void insertAgent(Agent agent);
}

Создан класс базы данных:

@Database(entities = {Agent.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract AgentDao agentDao();
}

Открытая база данных, использующая ниже подкласс в Котлине:

class MyApp : Application() {

    companion object DatabaseSetup {
        var database: AppDatabase? = null
    }

    override fun onCreate() {
        super.onCreate()
        MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").build()
    }
}

Реализована ниже функция в моей деятельности:

void signUpAction(View view) {
        String email = editTextEmail.getText().toString();
        String phone = editTextPhone.getText().toString();
        String license = editTextLicence.getText().toString();

        AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao();
        //1: Check if agent already exists
        int agentsCount = agentDao.agentsCount(email, phone, license);
        if (agentsCount > 0) {
            //2: If it already exists then prompt user
            Toast.makeText(this, "Agent already exists!", Toast.LENGTH_LONG).show();
        }
        else {
            Toast.makeText(this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();
            onBackPressed();
        }
    }

К сожалению, при выполнении вышеуказанного метода он выходит из строя с трассировкой ниже стека:

    FATAL EXCEPTION: main
 Process: com.example.me.MyApp, PID: 31592
java.lang.IllegalStateException: Could not execute method for android:onClick
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:293)
    at android.view.View.performClick(View.java:5612)
    at android.view.View$PerformClick.run(View.java:22288)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6123)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757)
 Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Method.invoke(Native Method)
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
    at android.view.View.performClick(View.java:5612) 
    at android.view.View$PerformClick.run(View.java:22288) 
    at android.os.Handler.handleCallback(Handler.java:751) 
    at android.os.Handler.dispatchMessage(Handler.java:95) 
    at android.os.Looper.loop(Looper.java:154) 
    at android.app.ActivityThread.main(ActivityThread.java:6123) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 
 Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.
    at android.arch.persistence.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:137)
    at android.arch.persistence.room.RoomDatabase.query(RoomDatabase.java:165)
    at com.example.me.MyApp.RoomDb.Dao.AgentDao_Impl.agentsCount(AgentDao_Impl.java:94)
    at com.example.me.MyApp.View.SignUpActivity.signUpAction(SignUpActivity.java:58)
    at java.lang.reflect.Method.invoke(Native Method) 
    at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) 
    at android.view.View.performClick(View.java:5612) 
    at android.view.View$PerformClick.run(View.java:22288) 
    at android.os.Handler.handleCallback(Handler.java:751) 
    at android.os.Handler.dispatchMessage(Handler.java:95) 
    at android.os.Looper.loop(Looper.java:154) 
    at android.app.ActivityThread.main(ActivityThread.java:6123) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:757) 

Похоже, эта проблема связана с выполнением операции db в основном потоке. Однако примерный тестовый код, приведенный в ссылке выше, не запускается в отдельном потоке:

@Test
    public void writeUserAndReadInList() throws Exception {
        User user = TestUtil.createUser(3);
        user.setName("george");
        mUserDao.insert(user);
        List<User> byName = mUserDao.findUsersByName("george");
        assertThat(byName.get(0), equalTo(user));
    }

Я что-то пропустил? Как я могу выполнить его без сбоев? Пожалуйста, предложите.

Ответ 1

Как сказал Дейл, доступ к базе данных в главном потоке, блокирующем пользовательский интерфейс, является ошибкой.

Создайте статический вложенный класс (для предотвращения утечки памяти) в вашем Activity, расширяющем AsyncTask.

private static class AgentAsyncTask extends AsyncTask<Void, Void, Integer> {

    //Prevent leak
    private WeakReference<Activity> weakActivity;
    private String email;
    private String phone;
    private String license;

    public AgentAsyncTask(Activity activity, String email, String phone, String license) {
        weakActivity = new WeakReference<>(activity);
        this.email = email;
        this.phone = phone;
        this.license = license;
    }

    @Override
    protected Integer doInBackground(Void... params) {
        AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao();
        return agentDao.agentsCount(email, phone, license);
    }

    @Override
    protected void onPostExecute(Integer agentsCount) {
        Activity activity = weakActivity.get();
        if(activity == null) {
            return;
        }

        if (agentsCount > 0) {
            //2: If it already exists then prompt user
            Toast.makeText(activity, "Agent already exists!", Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(activity, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();
            activity.onBackPressed();
        }
    }
}

Или вы можете создать окончательный класс в своем собственном файле.

Затем выполните его в методе signUpAction (View view):

new AgentAsyncTask(this, email, phone, license).execute();

В некоторых случаях вы можете также захотеть сохранить ссылку на AgentAsyncTask в своей активности, чтобы вы могли отменить ее, когда активность будет уничтожена. Но вам придется прервать любые транзакции самостоятельно.

Кроме того, ваш вопрос о тестовом примере Google... Они указывают на этой веб-странице:

Рекомендуемый подход для тестирования реализации вашей базы данных - написание теста JUnit, который выполняется на устройстве Android. Поскольку эти тесты не требуют создания действия, они должны выполняться быстрее, чем ваши тесты пользовательского интерфейса.

Нет активности, нет пользовательского интерфейса.

--EDIT -

Для людей, интересующихся... У вас есть другие варианты. Я рекомендую взглянуть на новые компоненты ViewModel и LiveData. LiveData прекрасно работает с Room. https://developer.android.com/topic/libraries/architecture/livedata.html

Другой вариант - RxJava/RxAndroid. Более мощный, но более сложный, чем LiveData. https://github.com/ReactiveX/RxJava

--EDIT 2--

Так как многие люди могут встретить этот ответ... На сегодняшний день лучший вариант - это KOTLIN COROUTINES. https://kotlinlang.org/docs/reference/coroutines-overview.html

Ответ 2

Не рекомендуется, но вы можете получить доступ к базе данных в основном потоке с помощью allowMainThreadQueries()

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()

Ответ 3

Для всех RxJava или RxAndroid или RxKotlin любители там

Observable.just(db)
          .subscribeOn(Schedulers.io())
          .subscribe { db -> // database operation }

Ответ 4

Котлин сопрограммы (ясно и кратко)

AsyncTask действительно неуклюжий. Сопрограммы Kotlin - более чистая альтернатива (по сути, просто синхронный код плюс пара ключевых слов).

private fun myFun() {
    launch { // coroutine on Main
        val query = async(Dispatchers.IO) { // coroutine on IO
            MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license)
        }

        val agentsCount = query.await()
        // do UI stuff
    }
}

И это оно !!

Бонус: активность как CoroutineScope

Чтобы использовать асинхронность из Activity, вам нужен CoroutineScope. Вы можете использовать свою активность следующим образом:

class LoadDataActivity : AppCompatActivity(), CoroutineScope {

    private val job by lazy { Job() }

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onDestroy() {
        super.onDestroy()
        job.cancel() // cancels all coroutines under this scope
    }

    // ...rest of class
}

Бонус: Android Room & Suspend

8 мая 2019 года. Комната 2.1 теперь поддерживает suspend (https://youtu.be/Qxj2eBmXLHg?t=1662).

Ключевое слово suspend гарантирует, что асинхронные методы вызываются только из асинхронных блоков, однако (как отмечает @Robin) это не очень хорошо работает с аннотированными методами Room (<2.1).

// Wrap API to use suspend (probably not worth it)
public suspend fun agentsCount(...): Int = agentsCountPrivate(...)

@Query("SELECT ...")
protected abstract fun agentsCountPrivate(...): Int

Ответ 5

Вы не можете запустить его в основном потоке, вместо этого используйте обработчики, асинхронные или рабочие потоки. Пример кода доступен здесь и прочитайте статью о библиотеке комнаты здесь: Android Room Library

/**
 *  Insert and get data using Database Async way
 */
AsyncTask.execute(new Runnable() {
    @Override
    public void run() {
        // Insert Data
        AppDatabase.getInstance(context).userDao().insert(new User(1,"James","Mathew"));

        // Get Data
        AppDatabase.getInstance(context).userDao().getAllUsers();
    }
});

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

Вы можете использовать этот метод для достижения в основном потоке Room.inMemoryDatabaseBuilder()

Ответ 6

С помощью библиотеки Jetbrains Anko вы можете использовать метод doAsync {..} для автоматического выполнения вызовов базы данных. Это позаботится о проблеме многословия, которую вы, казалось, имели с ответом mcastro.

Пример использования:

    doAsync { 
        Application.database.myDAO().insertUser(user) 
    }

Я использую это часто для вставок и обновлений, однако для выбранных запросов я рекомендую использовать рабочий процесс RX.

Ответ 7

Элегантное решение RxJava/Kotlin - это использование Completable.fromCallable, который даст вам Observable, который не возвращает значение, но может наблюдаться и подписываться в другом потоке.

public Completable insert(Event event) {
    return Completable.fromCallable(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
            return database.eventDao().insert(event)
        }
    }
}

Или в Котлине:

fun insert(event: Event) : Completable = Completable.fromCallable {
    database.eventDao().insert(event)
}

Вы можете наблюдать и подписаться как обычно:

dataManager.insert(event)
    .subscribeOn(scheduler)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...)

Ответ 8

Сообщение об ошибке,

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

Является довольно описательным и точным. Вопрос в том, как следует избегать доступа к базе данных в основном потоке. Это огромная тема, но для начала прочитайте AsyncTask (нажмите здесь)

----- EDIT ----------

Я вижу, что у вас возникают проблемы при запуске unit test. У вас есть несколько вариантов, чтобы исправить это:

  • Запустите тест непосредственно на машине разработки, а не на устройстве Android (или эмуляторе). Это работает для тестов, ориентированных на базу данных, и им не важно, работают ли они на устройстве.

  • Используйте аннотацию @RunWith(AndroidJUnit4.class) для запуска теста на устройстве Android, но не в действии с пользовательским интерфейсом. Подробнее об этом можно узнать в этом уроке

Ответ 9

Если вам удобнее задача Async:

  new AsyncTask<Void, Void, Integer>() {
                @Override
                protected Integer doInBackground(Void... voids) {
                    return Room.databaseBuilder(getApplicationContext(),
                            AppDatabase.class, DATABASE_NAME)
                            .fallbackToDestructiveMigration()
                            .build()
                            .getRecordingDAO()
                            .getAll()
                            .size();
                }

                @Override
                protected void onPostExecute(Integer integer) {
                    super.onPostExecute(integer);
                    Toast.makeText(HomeActivity.this, "Found " + integer, Toast.LENGTH_LONG).show();
                }
            }.execute();

Ответ 10

Вы должны выполнить запрос в фоновом режиме. Простым способом может быть использование Исполнителей:

Executors.newSingleThreadExecutor().execute { 
   yourDb.yourDao.yourRequest() //Replace this by your request
}

Ответ 11

С лямбдой легко работать с AsyncTask

 AsyncTask.execute(() -> //run your query here );

Ответ 12

Для быстрых запросов вы можете позволить комнате выполнить его в потоке пользовательского интерфейса.

AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),
        AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build();

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

       @Override
        public void onClick(View view) {



            int position = getAdapterPosition();

            User user = new User();
            String name = getName(position);
            user.setName(name);

            AppDatabase appDatabase = DatabaseCreator.getInstance(mContext).getDatabase();
            UserDao userDao = appDatabase.getUserDao();
            ArrayList<User> users = new ArrayList<User>();
            users.add(user);
            List<Long> ids = userDao.insertAll(users);

            Long id = ids.get(0);
            if(id == -1)
            {
                user = userDao.getUser(name);
                user.setId(user.getId());
            }
            else
            {
                user.setId(id);
            }

            Intent intent = new Intent(mContext, ChatActivity.class);
            intent.putExtra(ChatActivity.EXTRAS_USER, Parcels.wrap(user));
            mContext.startActivity(intent);
        }
    }

Ответ 13

Обновление: я также получил это сообщение, когда пытался построить запрос, используя @RawQuery и SupportSQLiteQuery внутри DAO.

@Transaction
public LiveData<List<MyEntity>> getList(MySettings mySettings) {
    //return getMyList(); -->this is ok

    return getMyList(new SimpleSQLiteQuery("select * from mytable")); --> this is an error

Решение: создайте запрос внутри ViewModel и передайте его в DAO.

public MyViewModel(Application application) {
...
        list = Transformations.switchMap(searchParams, params -> {

            StringBuilder sql;
            sql = new StringBuilder("select  ... ");

            return appDatabase.rawDao().getList(new SimpleSQLiteQuery(sql.toString()));

        });
    }

Или же...

Вы не должны обращаться к базе данных непосредственно в главном потоке, например:

 public void add(MyEntity item) {
     appDatabase.myDao().add(item); 
 }

Вы должны использовать AsyncTask для операций обновления, добавления и удаления.

Пример:

public class MyViewModel extends AndroidViewModel {

    private LiveData<List<MyEntity>> list;

    private AppDatabase appDatabase;

    public MyViewModel(Application application) {
        super(application);

        appDatabase = AppDatabase.getDatabase(this.getApplication());
        list = appDatabase.myDao().getItems();
    }

    public LiveData<List<MyEntity>> getItems() {
        return list;
    }

    public void delete(Obj item) {
        new deleteAsyncTask(appDatabase).execute(item);
    }

    private static class deleteAsyncTask extends AsyncTask<MyEntity, Void, Void> {

        private AppDatabase db;

        deleteAsyncTask(AppDatabase appDatabase) {
            db = appDatabase;
        }

        @Override
        protected Void doInBackground(final MyEntity... params) {
            db.myDao().delete((params[0]));
            return null;
        }
    }

    public void add(final MyEntity item) {
        new addAsyncTask(appDatabase).execute(item);
    }

    private static class addAsyncTask extends AsyncTask<MyEntity, Void, Void> {

        private AppDatabase db;

        addAsyncTask(AppDatabase appDatabase) {
            db = appDatabase;
        }

        @Override
        protected Void doInBackground(final MyEntity... params) {
            db.myDao().add((params[0]));
            return null;
        }

    }
}

Если вы используете LiveData для операций выбора, вам не нужен AsyncTask.

Ответ 14

Просто вы можете использовать этот код для его решения:

Executors.newSingleThreadExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        appDb.daoAccess().someJobes();//replace with your code
                    }
                });

Или в лямбде вы можете использовать этот код:

Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());

Вы можете заменить appDb.daoAccess().someJobes() своим собственным кодом;

Ответ 15

Просто выполните операции с базой данных в отдельном потоке. Вот так (Котлин):

Thread {
   //Do your database´s operations here
}.start()

Ответ 16

Вы можете разрешить доступ к базе данных в основном потоке, но только для целей отладки, вы не должны делать этого при производстве.

Вот причина.

Примечание. Room не поддерживает доступ к базе данных в основном потоке, если вы не вызывали allowMainThreadQueries() в построителе, поскольку он может блокировать пользовательский интерфейс в течение длительного периода времени. Асинхронные запросы-запросы, возвращающие экземпляры LiveData или Flowable, освобождаются от этого правила, потому что они асинхронно запускают запрос в фоновом потоке, когда это необходимо.

Ответ 17

Вы можете использовать Future и Callable. Таким образом, вам не нужно будет писать длинную асинхронную задачу, и вы сможете выполнять запросы без добавления allowMainThreadQueries().

Мой дао-запрос: -

@Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();

Мой метод хранилища: -

public UserData getDefaultData() throws ExecutionException, InterruptedException {

    Callable<UserData> callable = new Callable<UserData>() {
        @Override
        public UserData call() throws Exception {
            return userDao.getDefaultData();
        }
    };

    Future<UserData> future = Executors.newSingleThreadExecutor().submit(callable);

    return future.get();
}

Ответ 18

вы можете обрабатывать весь доступ к базе данных последовательно в другом потоке, используя HandlerThread, проверьте это для получения дополнительной информации, используя handlerThread

в основном, что вы будете делать, это создать класс, который расширяет handlerThread и переопределяет onLooperPrepared, как показано ниже

public class MyHandlerThread extends HandlerThread{


    private Handler handler;


    public SocketManager(){
        super("MyHandlerThread");
    }

    @Override
    protected void onLooperPrepared() {
        handler = new Handler(getLooper()) {
            @Override
            public void handleMessage(android.os.Message msg) {
                super.handleMessage(msg);
                //handle all your database tasks here 
                YourObject yourObject = (YourObject)msg.obj;

        }};

    }

    //send message to the background thread
    public void sendMessage(YourObject msg){

        android.os.Message msg = new android.os.Message();
        msg.obj = message;
        handler.sendMessage(msg);

    }

    public void onDestroy(){
           quit();
           interrupt();
    }



}

а затем вы добавляете следующий код в свой класс деятельности

MyHandlerThread myHandlerThread = new MyHandlerThread();
myHandlerThread.start();

для связи с потоком вы вызываете myHandlerThread.sendMessage() и передаете myHandlerThread.sendMessage() данные

для того, чтобы иметь двустороннюю связь между главным потоком и фоновый поток можно использовать другой обработчик из главного потока и отправлять сообщения, которые он проверить это для получения дополнительной информации двухсторонней связи