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

231

我正在使用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

看起来问题与在主线程上执行数据库操作有关。然而,上面链接提供的示例测试代码没有在单独的线程上运行:

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

我有什么遗漏吗?如何使其不崩溃并执行?请建议。


2
尽管是针对Kotlin编写的,但这篇文章非常好地解释了潜在的问题! - Peter Lehnhardt
请访问 https://stackoverflow.com/questions/58532832/read-and-write-room-database-synchronically-everywhere - nnyerges
看看这个答案。这个答案对我有用。 https://dev59.com/klUK5IYBdhLWcg3w_z60#51720501 - Somen Tushir
但是当我使用线程、协程或异步时,它需要一些时间,因此我的 if else 条件失败了,因为我正在检查项目是否存在于数据库中,如果存在,则不存储该值,如果不存在,则将该值存储在数据库中。我该如何摆脱这个解决方案? - Menu
24个回答

248

虽然不建议这样做,但是你可以使用allowMainThreadQueries()在主线程上访问数据库。

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

24
除非您在构建器上调用了allowMainThreadQueries(),否则房间不允许在主线程上访问数据库,因为这可能会长时间锁定用户界面。 异步查询(返回LiveData或RxJava Flowable的查询)不受此规则限制,因为它们会在需要时在后台线程上异步运行查询。 - pRaNaY
4
谢谢,这个对迁移非常有用。在从Loaders转换为LiveData之前,我希望测试Room是否按预期工作。 - SammyT
5
不应该这样做。在某些情况下,这可能会严重降低您的应用程序速度。操作应该异步执行。 - Alex
1
@Alex,在主线程上执行查询从来不是一个好的选择。 - Justin Meiners
3
只是不良的做法,只要数据库保持小规模,那么你这么做应该没问题。 - lasec0203
显示剩余6条评论

89

Kotlin 协程(简洁易懂)

AsyncTask 实现复杂笨重,而协程提供了更加清晰的替代方案(只需添加几个关键字即可将同步代码转换为异步)。

// Step 1: add `suspend` to your fun
suspend fun roomFun(...): Int
suspend fun notRoomFun(...) = withContext(Dispatchers.IO) { ... }

// Step 2: launch from coroutine scope
private fun myFun() {
    lifecycleScope.launch { // coroutine on Main
        val queryResult = roomFun(...) // coroutine on IO
        doStuff() // ...back on Main
    }
}

依赖项(为架构组件添加协程作用域):

// lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha04'

// viewModelScope:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha04'

-- 更新:
2019年5月8日: Room 2.1 现在支持 suspend
2019年9月13日: 已更新为使用架构组件作用域


1
您在使用suspend关键字时,是否遇到了@Query abstract suspend fun count()的编译错误?您可以看一下这个类似的问题:https://dev59.com/PVYM5IYBdhLWcg3wjAqw - Robin
@Robin - 是的。我的错误;我正在对一个公共(未注释)DAO方法使用suspend,该方法调用了一个受保护的非suspend @Query函数。当我也在内部的@Query方法中添加了suspend关键字时,它确实无法编译。看起来suspend和Room的巧妙底层功能存在冲突(正如您在其他问题中提到的那样,suspend的编译版本返回了一个续体,而Room不能处理)。 - charles-allen
非常有道理。我将使用协程函数来调用它。 - Robin
1
@Robin - FYI,Room 2.1 版本已经支持暂停操作了 :) - charles-allen
我已经使用了lifecycleScope.launch(Dispatchers.IO){ // 你的代码 },它运行得很好。 - varotariya vajsi
我遇到了同样的问题,尽管我使用了生命周期范围的启动,但问题是我的DAO方法缺少了suspend关键字。 - undefined

77

像 Dale 所说的那样,将数据库访问放在主线程会阻塞用户界面。

--编辑 2--

由于许多人可能会遇到这个问题......现在通常最好的选择是使用 Kotlin 协程。 Room 现在直接支持它(目前在 beta 版本中)。 https://kotlinlang.org/docs/reference/coroutines-overview.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01

--编辑 1--

对于想知道其他选项的人... 我建议看一下新的 ViewModel 和 LiveData 组件。LiveData 与 Room 配合使用效果很好。 https://developer.android.com/topic/libraries/architecture/livedata.html

另一个选择是 RxJava/RxAndroid。比 LiveData 更强大,但也更复杂。 https://github.com/ReactiveX/RxJava

--原始回答--

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

或者您可以在单独的文件中创建一个final类。

然后在signUpAction(View view)方法中执行它:

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

在某些情况下,您可能还希望在活动中保持对AgentAsyncTask的引用,以便在销毁活动时可以取消它。但是,您必须自己中断任何事务。
另外,关于Google的测试示例的问题... 他们在网页中声明:
推荐测试数据库实现的方法是编写在Android设备上运行的JUnit测试。由于这些测试不需要创建活动,因此执行速度应该比UI测试更快。
没有活动,也没有UI。

45
我需要每次访问数据库时都执行你在代码示例中提出的巨大异步任务吗?这是十几行代码,而不是从数据库获取一些数据的一行代码。你还建议创建新类,但这是否意味着我需要为每个插入/选择数据库调用创建新的AsyncTask类? - Piotrek
8
你还有其他选择,没错。你可能想要研究一下新的ViewModel和LiveData组件。 使用LiveData时,你不需要AsyncTask,对象会在每次更改时被通知。 https://developer.android.com/topic/libraries/architecture/viewmodel.html https://developer.android.com/topic/libraries/architecture/livedata.html还有AndroidRx(虽然它基本上做与LiveData相同的事情)和Promises。在使用AsyncTask时,你可以以这样的方式构建架构:将多个操作包括在一个AsyncTask中,或者将每个操作分开。 - mcastro
2
我回去写SQLite了,至少它的样板代码是明智且实际需要的。该死的谷歌,该死的你! - Samuel Owino
1
@SamuelOwino 是的,我后悔“升级”我的应用程序以使用Room。我以前从未遇到过Sqlite的问题,而且它正在进行主线程数据库调用,我想。但是我的数据库只有几个表格和几十条记录,速度非常快,没有问题。但是天哪,他们说Room可以删除“样板”代码?! 微笑。对于简单的应用程序,使用Sqlite及其游标要容易得多。也许对于某些具有数千条记录的复杂应用程序来说更好,但对于小型普通应用程序来说呢?噗。我花了很多时间在弄这个,但它甚至还没有起作用。 - clamum
1
嘿,@clamum,我同意你的看法。我实际上转向了GreenDAO,它很神奇。有很多代码是为您生成的,您不必处理Room的疯狂。您还可以节省一些SQLite代码。我强烈推荐GreenDAO https://greenrobot.org/greendao/。 - Samuel Owino
显示剩余8条评论

53

给所有喜欢RxJavaRxAndroidRxKotlin的人们

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

3
如果我把这段代码放进一个方法里,如何返回数据库操作的结果? - Eggakin Baconwalker
@EggakinBaconwalker 我有一个 override fun getTopScores(): Observable<List<PlayerScore>> { return Observable .fromCallable({ GameApplication.database .playerScoresDao().getTopScores() }) .applySchedulers() },其中 applySchedulers() 只是执行 fun <T> Observable<T>.applySchedulers(): Observable<T> = this.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - noloman
这对IntentService无效。因为IntentService将在其线程完成后完成。 - Umang Kothari
1
@UmangKothari 如果你在 IntentService#onHandleIntent 上,你不会得到异常,因为这个方法在工作线程上执行,所以你不需要任何线程机制来执行 Room 数据库操作。 - Samuel Robert
@SamuelRobert,是的,我同意我的错。我忘了。 - Umang Kothari

36

你不能在主线程上运行它,而应该使用处理程序、异步或工作线程。这里提供了一个示例代码,并阅读有关Room库的文章:Android的Room库

/**
 *  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()


如果我使用这种方法来获取数据(在这种情况下仅使用getAllUsers()),如何从此方法返回数据?如果我在“run”内部放置“return”单词,则会出现错误。 - Eggakin Baconwalker
1
在某个地方创建一个接口方法,然后添加匿名类以从此处获取数据。 - Rizvan
2
这是插入/更新的最简单解决方案。 - Beer Me

27

使用lambda表达式可以轻松地与AsyncTask配合使用

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

2
这很方便,谢谢。顺便说一下,Kotlin更容易:AsyncTask.execute { } - alexrnov
4
但是使用这个方法怎么得到结果呢? - CanCoder

16

将数据库操作放在单独的线程中进行。像这样(Kotlin):

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

不是一个好的方式。你无法控制正在运行的线程,因此如果活动关闭,你无法停止它。 - Steffen Vangsgaard

14

你可以简单地使用这段代码来解决问题:

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


13

使用Jetbrains Anko库,您可以使用doAsync {..}方法自动执行数据库调用。这解决了您似乎在mcastro的答案中遇到的冗长问题。

示例用法:

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

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


10

由于 asyncTask 已被弃用,我们可以使用 executor service。或者您也可以像其他答案中所述那样使用带有 LiveData 的 ViewModel。

要使用 executor service,您可以使用类似下面的内容。

public class DbHelper {

    private final Executor executor = Executors.newSingleThreadExecutor();

    public void fetchData(DataFetchListener dataListener){
        executor.execute(() -> {
                Object object = retrieveAgent(agentId);
                new Handler(Looper.getMainLooper()).post(() -> {
                        dataListener.onFetchDataSuccess(object);
                });
        });
    }
}

使用主循环器,以便您可以从 onFetchDataSuccess 回调中访问UI元素。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接