Android Room - 简单选择查询 - 无法访问主线程上的数据库

问题描述 投票:67回答:18

我正在尝试使用Room Persistence Library的样本。我创建了一个实体:

@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类:

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

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

在我的活动中实现了以下功能:

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

我在这里错过任何东西吗?如何让它在没有崩溃的情况下执行?请建议。

android crash kotlin android-studio-3.0 android-room
18个回答
46
投票

Dale说,锁定UI的主线程上的数据库访问是错误的。

在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视图)方法中执行它:

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

在某些情况下,您可能还希望在活动中保留对AgentAsyncTask的引用,以便在销毁活动时取消它。但你必须自己打断任何交易。

另外,您关于Google测试示例的问题......他们在该网页中说明:

测试数据库实现的推荐方法是编写在Android设备上运行的JUnit测试。因为这些测试不需要创建活动,所以它们的执行速度应该比UI测试快。

哦,我。

- 编辑 -

对于想知道的人......你有其他选择。我建议您查看新的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。 Room现在直接支持它(目前处于测试阶段)。 https://kotlinlang.org/docs/reference/coroutines-overview.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01


3
投票

如果您对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();

3
投票

您必须在后台执行请求。一个简单的方法可能是使用Executors

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

3
投票

只需在单独的线程中执行数据库操作即可。像这样(Kotlin):

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

2
投票

对于快速查询,您可以允许在UI线程上执行它。

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

2
投票

更新:当我尝试使用DAO中的@RawQuery和SupportSQLiteQuery构建查询时,我也收到了此消息。

@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。


2
投票

您只需使用此代码即可解决此问题:

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

或者在lambda中,您可以使用以下代码:

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

您可以用自己的代码替换appDb.daoAccess().someJobes();


1
投票

您可以在主线程上允许数据库访问,但仅用于调试目的,您不应该在生产中执行此操作。

Here is the reason.

注意:除非您在构建器上调用allowMainThreadQueries(),否则Room不支持主线程上的数据库访问,因为它可能会长时间锁定UI。异步查询 - 返回LiveData或Flowable实例的查询 - 不受此规则的约束,因为它们在需要时在后台线程上异步运行查询。


1
投票

您可以使用Future和Callable。因此,您不需要编写长的asynctask,并且可以在不添加allowMainThreadQueries()的情况下执行查询。

我的dao查询: -

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

91
投票

不推荐,但您可以使用allowMainThreadQueries()访问主线程上的数据库

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

38
投票

对于那里的所有RxJavaRxAndroidRxKotlin爱好者

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

26
投票

Kotlin Coroutines (Clear & Concise)

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
}

Bonus: Android Room & Suspend

201-May-2019:2.1房间现在支持suspendhttps://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

18
投票

您无法在主线程上运行它,而是使用处理程序,异步或工作线程。这里有一个示例代码,您可以在这里阅读文章: 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();
    }
});

如果你想在主线程上运行它,这不是首选方式。

您可以使用此方法在主线程Room.inMemoryDatabaseBuilder()上实现


9
投票

使用Jetbrains Anko库,您可以使用doAsync {..}方法自动执行数据库调用。这照顾了你似乎对mcastro的回答所带来的冗长问题。

用法示例:

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

我经常使用它进行插入和更新,但对于选择的查询,我建议使用RX工作流程。


5
投票

优雅的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)
        }
    }
}

或者在Kotlin:

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

你可以像往常一样观察和订阅:

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

5
投票

使用lambda,它易于使用AsyncTask运行

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

3
投票

错误消息,

无法访问主线程上的数据库,因为它可能会长时间锁定UI。

非常具有描述性和准确性。问题是如何避免在主线程上访问数据库。这是一个很大的话题,但要开始,请阅读AsyncTask (click here)

- - -编辑 - - - - -

我看到你在进行单元测试时遇到问题。你有几个选择来解决这个问题:

  1. 直接在开发计算机上运行测试,而不是在Android设备(或模拟器)上运行。这适用于以数据库为中心的测试,并不关心它们是否在设备上运行。
  2. 使用注释@RunWith(AndroidJUnit4.class)在Android设备上运行测试,但不在具有UI的活动中运行。有关这方面的更多细节可以在in this tutorial找到
© www.soinside.com 2019 - 2024. All rights reserved.