Android Room - simple select query - Cannot access database on the main thread

匿名 (未验证) 提交于 2019-12-03 01:58:03

问题:

I am trying a sample with Room Persistence Library. I created an Entity:

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

Created a DAO class:

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

Created the Database class:

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

Exposed database using below subclass in Kotlin:

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

Implemented below function in my activity:

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

Unfortunately on execution of above method it crashes with below stack trace:

Seems like that problem is related to execution of db operation on main thread. However the sample test code provided in above link does not run on a separate thread:

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

Am I missing anything over here? How can I make it execute without crash? Please suggest.

回答1:

Database access on main thread locking the UI is the error, like Dale said.

Something like the following code should work inside the signUpAction(View view) method...

final AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao(); new AsyncTask() {             @Override             protected Integer doInBackground(Void... params) {                 return agentDao.agentsCount(email, phone, license);             }              @Override             protected void onPostExecute(Integer agentsCount) {                 if (agentsCount > 0) {                     //2: If it already exists then prompt user                     Toast.makeText(Activity.this, "Agent already exists!", Toast.LENGTH_LONG).show();                 }                 else {                     Toast.makeText(Activity.this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();                     onBackPressed();                 }             }         }.execute(); 

*Replace Activity.this with [your activity].this

Or creating a new class extending AsyncTask would be cleaner.

Also, your question about the Google's test example... They state in that web page:

The recommended approach for testing your database implementation is writing a JUnit test that runs on an Android device. Because these tests don't require creating an activity, they should be faster to execute than your UI tests.

No Activity, no UI.

--EDIT--

For people wondering... You have other options. I recommend taking a look into the new ViewModel and LiveData components. LiveData works great with Room. https://developer.android.com/topic/libraries/architecture/livedata.html

Another option is the RxJava/RxAndroid. More powerful but more complex than LiveData. https://github.com/ReactiveX/RxJava



回答2:

It's not recommended but you can access to database on main thread with allowMainThreadQueries()

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


回答3:

For all the RxJava or RxAndroid or RxKotlin lovers out there

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


回答4:

Straight-forward code using Kotlin Coroutines

AsyncTask is really clunky. Kotlin coroutines is a cleaner alternative (essentially just your synchronous code with a couple of extra keywords).

gradle.properties:

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

The suspend keyword ensures async methods are only called from within async blocks, however (as noted by @Robin) this doesn't play nicely with Room annotated methods.

// Wrap API to use suspend (probably not worth it) public suspend fun agentsCount(...): Int = agentsCountPrivate(...)  @Query("SELECT ...") protected abstract fun agentsCountPrivate(...): Int 


回答5:

With the Jetbrains Anko library, you can use the doAsync{..} method to automatically execute database calls. This takes care of the verbosity problem you seemed to have been having with mcastro's answer.

Example usage:

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

I use this frequently for inserts and updates, however for select queries I reccommend using the RX workflow.



回答6:

The error message,

Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.

Is quite descriptive and accurate. The question is how should you avoid accessing the database on the main thread. That is a huge topic, but to get started, read about AsyncTask (click here)

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

I see you are having problems when you run a unit test. You have a couple of choices to fix this:

  1. Run the test directly on the development machine rather than on an Android device (or emulator). This works for tests that are database-centric and don't really care whether they are running on a device.

  2. Use the annotation @RunWith(AndroidJUnit4.class) to run the test on the android device, but not in an activity with a UI. More details about this can be found in this tutorial



回答7:

You cannot run it on main thread instead use handlers, async or working threads . A sample code is available here and read article over room library here : 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();     } }); 

If you want to run it on main thread which is not preferred way .

You can use this method to achieve on main thread Room.inMemoryDatabaseBuilder()



回答8:

For quick queries you can allow room to execute it on UI thread.

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

In my case I had to figure out of the clicked user in list exists in database or not. If not then create the user and start another activity

       @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 users = new ArrayList();             users.add(user);             List 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);         }     } 


回答9:

An elegant RxJava/Kotlin solution is to use Completable.fromCallable, which will give you an Observable which does not return a value, but can observed and subscribed on a different thread.

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

Or in Kotlin:

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

You can the observe and subscribe as you would usually:

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


回答10:

You can allow database access on the main thread but only for debugging purpose, you shouldn't do this on production.

Here is the reason.

Note: Room doesn't support database access on the main thread unless you've called allowMainThreadQueries() on the builder because it might lock the UI for a long period of time. Asynchronous queries―queries that return instances of LiveData or Flowable―are exempt from this rule because they asynchronously run the query on a background thread when needed.



回答11:

You can't access the database directly on the main thread, for example:

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

You have to extend AsyncTask for update, add, and delete in the ViewModel.

Example:

public class MyViewModel extends AndroidViewModel {      private LiveData> list;      private AppDatabase appDatabase;      public MyViewModel(Application application) {         super(application);          appDatabase = AppDatabase.getDatabase(this.getApplication());         list = appDatabase.myDao().getItems();     }      public LiveData> getItems() {         return list;     }      public void delete(Obj item) {         new deleteAsyncTask(appDatabase).execute(item);     }      private static class deleteAsyncTask extends AsyncTask {          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 {          private AppDatabase db;          addAsyncTask(AppDatabase appDatabase) {             db = appDatabase;         }          @Override         protected Void doInBackground(final MyEntity... params) {             db.myDao().add((params[0]));             return null;         }      } } 


回答12:

If you are more comfortable with Async task:

  new AsyncTask() {                 @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(); 


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!