Android 房间 - 简单 select 查询 - 无法在主线程上访问数据库

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

我正在尝试 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 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);
}

创建数据库class:

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

在 Kotlin 中使用以下子 class 公开数据库:

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

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

不幸的是,在执行上述方法时它崩溃并显示以下堆栈跟踪:

    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) 

这个问题似乎与在主线程上执行数据库操作有关。但是,上面 link 中提供的示例测试代码不会 运行 在单独的线程上:

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

我在这里遗漏了什么吗?我怎样才能让它在不崩溃的情况下执行?请提出建议。

错误信息,

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

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

-----编辑---------

我发现您在 运行 单元测试时遇到了问题。你有几个选择来解决这个问题:

  1. 运行 直接在开发机器上而不是 Android 设备(或模拟器)上进行测试。这适用于以数据库为中心的测试,并不真正关心它们是否在设备上 运行ning。

  2. 使用注解 @RunWith(AndroidJUnit4.class) 运行 在 android 设备上进行测试,但不在带有 UI 的 activity 中进行测试。 有关此的更多详细信息,请参见 in this tutorial

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

--编辑 2--

因为很多人可能会遇到这个答案... 一般来说,现在最好的选择是 Kotlin Coroutines。 Room 现在直接支持它(目前处于测试阶段)。 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 中创建静态嵌套 class(以防止内存泄漏)。

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

或者您可以在自己的文件上创建一个最终 class。

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

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

在某些情况下,您可能还想在 activity 中保留对 AgentAsyncTask 的引用,以便在 Activity 被销毁时取消它。但是您必须自己中断任何交易。

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

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.

没有Activity,没有UI。

不推荐,但您可以使用 allowMainThreadQueries()

在主线程上访问数据库
MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()

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

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

在我的例子中,我必须弄清楚列表中的点击用户是否存在于数据库中。如果没有,则创建用户并启动另一个 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<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);
        }
    }

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

用法示例:

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

我经常将其用于插入和更新,但是对于 select 查询,我建议使用 RX 工作流。

致所有 RxJava or RxAndroid or RxKotlin 爱好者

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

一个优雅的 RxJava/Kotlin 解决方案是使用 Completable.fromCallable,这将为您提供一个没有 return 值但可以在不同线程上观察和订阅的 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(...)

您不能 运行 它在主线程上,而是使用处理程序、异步或工作线程。此处提供示例代码,并在此处阅读房间图书馆的文章: 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()

您可以允许在主线程上访问数据库,但仅用于调试目的,您不应在生产环境中这样做。

Here is the reason.

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

更新:当我尝试在 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 进行 select 操作,则不需要 AsyncTask。

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

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

// 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 日:更新为使用 Architecture components 范围

如果您更喜欢异步任务

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

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

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

你可以使用 Future 和 Callable。所以你不需要写一个很长的异步任务并且可以在不添加 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();
}

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

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

使用 lambda 可以很容易地 运行 使用 AsyncTask

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

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

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

由于不推荐使用 asyncTask,我们可以使用执行器服务。或者您也可以将 ViewModel 与 LiveData 一起使用,如其他答案中所述。

要使用执行器服务,您可以使用如下内容。

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

使用 Main Looper,以便您可以从 onFetchDataSuccess 回调中访问 UI 元素。

在我看来,正确的做法是使用 RxJava 将查询委托给 IO 线程。

我有一个解决我刚刚遇到的等效问题的示例。

((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.VISIBLE);//Always good to set some good feedback
        Completable.fromAction(() -> {
            //Creating view model requires DB access
            homeViewModel = new ViewModelProvider(this, factory).get(HomeViewModel.class);
        }).subscribeOn(Schedulers.io())//The DB access executes on a non-main-thread thread
        .observeOn(AndroidSchedulers.mainThread())//Upon completion of the DB-involved execution, the continuation runs on the main thread
        .subscribe(
                () ->
                {
                    mAdapter = new MyAdapter(homeViewModel.getExams());
                    recyclerView.setAdapter(mAdapter);
                    ((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.INVISIBLE);
                },
                error -> error.printStackTrace()
        );

如果我们想推广解决方案:

((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.VISIBLE);//Always good to set some good feedback
        Completable.fromAction(() -> {
            someTaskThatTakesTooMuchTime();
        }).subscribeOn(Schedulers.io())//The long task executes on a non-main-thread thread
        .observeOn(AndroidSchedulers.mainThread())//Upon completion of the DB-involved execution, the continuation runs on the main thread
        .subscribe(
                () ->
                {
                    taskIWantToDoOnTheMainThreadWhenTheLongTaskIsDone();
                },
                error -> error.printStackTrace()
        );

在数据库文件中添加.allowMainThreadQueries()

@Database(entities = [Country::class], version = 1)
abstract class CountryDatabase: RoomDatabase() {
    abstract fun getCountryDao(): CountryDao
    companion object {
        @Volatile
        private var instance: CountryDatabase? = null
        private val LOCK = Any()

        operator fun invoke(context: Context) = instance ?:
        synchronized(LOCK) {
            instance ?:
            createDatabase(context).also { instance = it }
        }
        private fun createDatabase(context: Context) =
            Room.databaseBuilder(
                context.applicationContext,
                CountryDatabase::class.java,
                "country_db"
            ).allowMainThreadQueries()
             .build()
    }
}

在流程末尾添加 Dispatchers.IO,如下所示:

flow { ... }.flowOn(Dispatchers.IO)

房间数据库不允许您在主线程中执行数据库 IO 操作(后台操作),除非您使用 allowMainThreadQueries() 和数据库构建器。但这是一个糟糕的做法。


推荐方法:
这里我使用了我当前项目中的一些代码。

在存储库中的方法之前添加 suspend 关键字

class FavRepository @Inject constructor(private val dao: WallpaperDao) {
    suspend fun getWallpapers(): List<Wallpaper> =  dao.getWallpapers()
}

在您的 viewmodel class 中,首先您需要使用 Coroutine Dispature IO 执行数据库操作,以便从房间数据库中获取数据。然后使用 Coroutine Dispature MAIN 更新您的值。

@HiltViewModel
class FavViewModel @Inject constructor(repo: FavRepository, @ApplicationContext context: Context) : ViewModel() {
    var favData = MutableLiveData<List<Wallpaper>>()
    init {
        viewModelScope.launch(Dispatchers.IO){
            val favTmpData: List<Wallpaper> = repo.getWallpapers()
            withContext(Dispatchers.Main){
                favData.value = favTmpData
            }
        }
    }
}

现在您可以通过从 Activity/ Fragment 观察来使用视图模型的数据。

希望这对您有所帮助 :) .