Android Room-simple select query-nie można uzyskać dostępu do bazy danych w głównym wątku

Próbuję próbki z Room Persistence Library . Stworzyłem byt:

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

Utworzono klasę 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);
}

Utworzono klasę bazy danych:

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

Odsłonięta baza danych używając poniższej podklasy w Kotlinie:

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

Zaimplementowana poniżej funkcja w mojej aktywności:

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

Niestety przy wykonaniu powyższej metody wywala się z poniższym śladem stosu:

    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) 

Wygląda na to, że problem związany jest z wykonaniem operacji db na main nić. Jednak przykładowy kod testowy podany w powyższym linku nie działa w oddzielnym wątku:

@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));
    }
Czy coś mi umknęło? Jak mogę go wykonać bez awarii? Proszę sugerować.
Author: Devarshi, 2017-05-24

14 answers

Dostęp do bazy danych w głównym wątku blokowanie interfejsu użytkownika jest błędem, jak powiedział Dale.

Utwórz statycznie zagnieżdżoną klasę (aby zapobiec wyciekowi pamięci) w aktywności rozszerzającej 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();
        }
    }
}

Lub możesz utworzyć końcową klasę na własnym pliku.

Następnie wykonaj go w metodzie signUpAction (View view):

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

W niektórych przypadkach możesz również chcieć trzymać odniesienie do AgentAsyncTask w swojej aktywności, aby móc ją anulować, gdy aktywność zostanie zniszczona. Ale ty byś musisz sam przerwać wszelkie transakcje.

Również twoje pytanie dotyczące przykładu testowego Google... Stwierdzają na tej stronie:

Zalecanym podejściem do testowania implementacji bazy danych jest pisanie testu JUnit, który działa na urządzeniu z Androidem. Ponieważ te testy nie wymagają tworzenia aktywności, powinny być szybsze do wykonaj niż testy interfejsu użytkownika.

Brak aktywności, brak interfejsu użytkownika.

--EDIT --

Dla osób zastanawiających się... Masz inne opcje. Polecam przyjrzeć się nowym komponentom ViewModel i LiveData. LiveData świetnie współgra z pokojem. https://developer.android.com/topic/libraries/architecture/livedata.html

Inną opcją jest RxJava / RxAndroid. Potężniejsze, ale bardziej złożone niż LiveData. https://github.com/ReactiveX/RxJava

 35
Author: mcastro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-02 19:55:31

Nie jest to zalecane, ale możesz uzyskać dostęp do bazy danych w głównym wątku za pomocą allowMainThreadQueries()

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()
 52
Author: mpolat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-11 16:33:44

Dla wszystkich RxJava lub RxAndroid lub rxkotlin

Observable.just(db)
          .subscribeOn(Schedulers.io())
          .subscribe { db -> // database operation }
 33
Author: Samuel Robert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-12 04:29:44

Prosty kod wykorzystujący Koroutiny Kotlina

AsyncTask jest naprawdę niezgrabny. Kotlin coroutines jest czystszą alternatywą (zasadniczo tylko twój synchroniczny kod z kilkoma dodatkowymi słowami kluczowymi).

gradle.właściwości:

# Coroutines opt-in
kotlin.coroutines=enable

UI thread (non-blocking):

private fun myFun() {
    launch(UI) {
        val query = async(CommonPool) { // Async stuff
            MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license)
        }

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

Słowo kluczowe suspend zapewnia, że metody asynchroniczne są wywoływane tylko z bloków asynchronicznych, jednak (jak zauważył @ Robin )nie pasuje to do pokoju z adnotacją metody.

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

@Query("SELECT ...")
protected abstract fun agentsCountPrivate(...): Int
 14
Author: AjahnCharles,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-09 03:25:28

Nie można go uruchomić w głównym wątku zamiast tego użyć programów obsługi, asynchronicznych lub roboczych wątków . Przykładowy kod jest dostępny tutaj i przeczytaj artykuł o room library tutaj: Android ' s 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();
    }
});

Jeśli chcesz uruchomić go na głównym wątku, który nie jest preferowany sposób .

Możesz użyć tej metody, aby osiągnąć na głównym wątku Room.inMemoryDatabaseBuilder()

 9
Author: Rizvan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-30 17:49:36

Z biblioteką Jetbrains Anko możesz użyć doasync{..} metoda automatycznego wykonywania wywołań bazy danych. To rozwiązuje problem gadatliwości, który wydaje ci się mieć z odpowiedzią mcastro.

Przykładowe użycie:

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

Używam tego często do wstawiania i aktualizacji, jednak dla zapytań select polecam korzystanie z obiegu pracy RX.

 8
Author: Arsala Bangash,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-07 04:18:06

Eleganckim rozwiązaniem RxJava / Kotlin jest użycie Completable.fromCallable, co da Ci obserwowalny, który nie zwraca wartości, ale może być obserwowany i subskrybowany w innym wątku.

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

Możesz obserwować i subskrybować, jak zwykle:

dataManager.insert(event)
    .subscribeOn(scheduler)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...)
 4
Author: dcr24,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-02 20:52:22

Musisz wykonać żądanie w tle. Prostym sposobem może być użycie Executors :

Executors.newSingleThreadExecutor().execute { 
   yourDb.yourDao.yourRequest() //Replace this by your request
}
 4
Author: Phil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-11 12:50:07

Komunikat o błędzie,

Nie można uzyskać dostępu do bazy danych w głównym wątku, ponieważ może potencjalnie zablokować interfejs użytkownika na długi czas.

Jest dość opisowy i dokładny. Pytanie brzmi, jak uniknąć dostępu do bazy danych w głównym wątku. To ogromny temat, ale aby zacząć, przeczytaj o AsyncTask (kliknij tutaj)

-----edytuj----------

Widzę, że masz problemy podczas testów jednostkowych. Masz kilka wybór, aby to naprawić:
  1. Uruchom test bezpośrednio na maszynie deweloperskiej, a nie na urządzeniu z Androidem (lub emulatorze). Działa to w przypadku testów, które są skoncentrowane na bazie danych i tak naprawdę nie dbają o to, czy są uruchomione na urządzeniu.

  2. Użyj adnotacji @RunWith(AndroidJUnit4.class) aby uruchomić test na urządzeniu z Androidem, ale nie w aktywności z interfejsem użytkownika. Więcej szczegółów na ten temat można znaleźć w tym samouczku

 3
Author: Dale Wilson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-24 20:31:10

Jeśli wolisz asynchroniczne Zadanie :

  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();
 2
Author: Hitesh Sahu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-09 05:24:49

Dla szybkich zapytań możesz zezwolić pokojowi na wykonanie go w wątku UI.

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

W moim przypadku musiałem dowiedzieć się, że kliknięty użytkownik na liście istnieje w bazie danych lub nie. Jeśli nie, Utwórz użytkownika i rozpocznij inną aktywność

       @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);
        }
    }
 1
Author: Vihaan Verma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-07 00:50:23

Możesz zezwolić na dostęp do bazy danych w głównym wątku, ale tylko w celu debugowania, nie powinieneś tego robić podczas produkcji.

Oto powód.

Uwaga: pokój nie obsługuje dostępu do bazy danych w głównym wątku, chyba że wywołałeś allowmainthreadqueries () w konstruktorze, ponieważ może to zablokować interfejs użytkownika na długi okres czasu. Asynchronous queries-zapytania zwracające instancje LiveData lub Flowable - są zwolnione z tej reguły, ponieważ asynchronicznie uruchamiają zapytanie w razie potrzeby na wątku tła.

 1
Author: Nilesh Rathore,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-12 13:21:22

Nie możesz uzyskać dostępu do bazy danych bezpośrednio w głównym wątku, na przykład:

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

Musisz rozszerzyć AsyncTask dla aktualizacji, dodawania i usuwania w ViewModel.

Przykład:

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

    }
}
 1
Author: live-love,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-05 01:14:30

Możesz użyć Future I Callable. Tak więc nie będziesz musiał pisać długich asynctask i możesz wykonywać swoje zapytania bez dodawania allowMainThreadQueries ().

Moje zapytanie dao: -

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

Moja metoda repozytorium:-

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();
}
 1
Author: beginner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-07 06:43:42