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个回答

9

您需要在后台执行请求。 一种简单的方式是使用Executors

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

6
你如何返回结果? - CanCoder

8

Room 数据库不允许您在主线程中执行数据库 IO 操作(后台操作),除非您在数据库构建器中使用 allowMainThreadQueries()。但这是一种不好的方法。


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

在 Repository 的方法前添加 suspend 关键字。

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

在你的viewmodel类中,首先需要使用协程分发器IO执行数据库操作,以从room数据库中获取数据。然后使用协程分发器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中观察ViewModel的数据来使用它的数据。

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


1
很棒的方法!帮助解决了我遇到的问题。非常感谢。 - Sayed Sadat

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
你可以在主线程上允许数据库访问,但只限于调试目的,不应在生产环境中这样做。 原因在此。 注意:除非您在构建器上调用了allowMainThreadQueries(),否则Room不支持在主线程上访问数据库,因为它可能会锁定UI很长时间。异步查询——返回LiveData或Flowable实例的查询——不受此规则的限制,因为它们会在需要时在后台线程上异步运行查询。

5

对于快速查询,您可以允许在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);
        }
    }

3
如果您更喜欢使用 异步任务
  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
您可以使用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();
}

这就是为什么我们使用 Callable/Future,因为 Android 不允许查询在主线程上运行。正如上面的问题所问。 - beginner
2
我的意思是,尽管你的答案中的代码在后台线程中执行查询,但主线程被阻塞并等待查询完成。因此,最终它并不比“allowMainThreadQueries()”好多少。在这两种情况下,主线程仍然被阻塞。 - eugeneek
@eugeneek 为什么“主线程在查询完成时被阻塞并等待”? - Dinh Quang Tuan
@DinhQuangTuan,这是因为Future.get()方法的工作方式是:"必要时等待计算完成,然后检索其结果"。 - eugeneek
@eugeneek 那么在这种情况下我们应该使用什么? - Dinh Quang Tuan
@DinhQuangTuan,不要使用这种方法,看看本帖中其他得到赞同的答案(例如协程)。 - eugeneek

3

这个错误信息是非常具体和准确的,它告诉你不要在主线程上访问数据库,因为这可能会长时间锁定用户界面。如果想避免在主线程上访问数据库,可以阅读AsyncTask来开始学习相关知识。

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

看起来你在运行单元测试时遇到了问题,你有几种选择来解决:

  1. 直接在开发机上运行测试,而不是在 Android 设备(或模拟器)上运行。这对于那些以数据库为中心的测试非常有效,因为它们不关心是否在设备上运行。

  2. 使用注释@RunWith(AndroidJUnit4.class)在 Android 设备上运行测试,但不在带有 UI 的活动中运行。更多详情,请参考此教程


我理解您的观点,我的假设是,在尝试通过JUnit测试任何数据库操作时,相同的观点都是有效的。但是,在https://developer.android.com/topic/libraries/architecture/room.html中,样本测试方法writeUserAndReadInList未在后台线程上调用insert查询。我有什么遗漏吗?请建议。 - Devarshi
抱歉,我错过了这个测试有问题的事实。我会编辑我的答案,添加更多信息。 - Dale Wilson

3

更新:当我尝试在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。

1
在流的末尾添加 Dispatchers.IO,如下所示:
flow { ... }.flowOn(Dispatchers.IO)

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