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

Dev*_*shi 94 crash android kotlin android-studio-3.0 android-room

我正在尝试使用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;
}
Run Code Online (Sandbox Code Playgroud)

创建了一个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);
}
Run Code Online (Sandbox Code Playgroud)

创建了Database类:

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

在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()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

不幸的是,在执行上面的方法时,它崩溃了下面的堆栈跟踪:

    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) 
Run Code Online (Sandbox Code Playgroud)

似乎该问题与主线程上的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));
    }
Run Code Online (Sandbox Code Playgroud)

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

mpo*_*lat 114

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

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()
Run Code Online (Sandbox Code Playgroud)

  • Room不允许访问主线程上的数据库,除非您在构建器上调用了"allowMainThreadQueries()",因为它可能会长时间锁定UI.异步查询(返回`LiveData`或RxJava`Flowable`的查询)不受此规则的约束,因为它们在需要时在后台线程上异步运行查询. (8认同)
  • @JideGuruTheProgrammer不,不应该。在某些情况下,这可能会大大降低您的应用程序运行速度。该操作应异步进行。 (4认同)
  • 谢谢这对迁移非常有用,因为我想测试Room在从Loaders转换为Live Data之前按预期工作 (3认同)
  • @JustinMeiners,这只是不好的做法,只要数据库保持很小,你就可以这样做。 (2认同)

mca*_*tro 51

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

或者您可以在自己的文件上创建最终类.

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

new AgentAsyncTask(this, email, phone, license).execute();
Run Code Online (Sandbox Code Playgroud)

在某些情况下,您可能还希望在活动中保留对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

  • 我是否应该在每次需要访问数据库时进行这个巨大的异步任务(您在代码示例中提出过)?这是十几行代码而不是一行来从db获取一些数据.您还建议创建新类,但这是否意味着我需要为每个插入/选择数据库调用创建新的AsyncTask类? (23认同)
  • 你有其他选择,是的.您可能想要查看新的ViewModel和LiveData组件.使用LiveData时,您不需要AsyncTask,只要有变化,就会通知对象.https://developer.android.com/topic/libraries/architecture/viewmodel.html https://developer.android.com/topic/libraries/architecture/livedata.html还有AndroidRx(虽然它几乎与LiveData一样) )和承诺.使用AsyncTask时,您可以通过这种方式构建体系结构,以便在一个AsyncTask中包含多个操作或将每个操作分开. (5认同)
  • Sh*,我回到编写 SQLite 至少它的样板是明智的并且实际上是必需的。该死的谷歌,该死的谷歌! (2认同)

Sam*_*ert 39

适用于所有RxJavaRxAndroidRxKotlin爱好者

Observable.just(db)
          .subscribeOn(Schedulers.io())
          .subscribe { db -> // database operation }
Run Code Online (Sandbox Code Playgroud)

  • 如果我将此代码放在方法中,如何从数据库操作返回结果? (2认同)

Aja*_*les 37

使用Kotlin协同程序的直接代码

AsyncTask非常笨重.Kotlin协同程序是一个更清洁的替代方案(基本上只是带有几个额外关键字的同步代码).

// 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
    }
}
Run Code Online (Sandbox Code Playgroud)

要从Activity中使用异步,您需要一个CoroutineScope.您可以像这样使用您的活动:

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

// viewModelScope:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha04'
Run Code Online (Sandbox Code Playgroud)

suspend关键字确保异步方法仅从异步块中调用,但是(如@Robin所述)这与使用Room注释的方法不能很好地协作.

// 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
    }
}
Run Code Online (Sandbox Code Playgroud)


Riz*_*van 19

您无法在主线程上运行它,而是使用处理程序,异步或工作线程.这里有一个示例代码,在这里阅读房间图书馆的文章: Android的房间图书馆

/**
 *  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();
    }
});
Run Code Online (Sandbox Code Playgroud)

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

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

  • 这是插入/更新的最简单的解决方案。 (2认同)

Ang*_*spo 14

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

Thread {
   //Do your database´s operations here
}.start()
Run Code Online (Sandbox Code Playgroud)


Mos*_*ami 10

只需您可以使用此代码来解决它:

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

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

Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());
Run Code Online (Sandbox Code Playgroud)

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


小智 9

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

用法示例:

    doAsync { 
        Application.database.myDAO().insertUser(user) 
    }
Run Code Online (Sandbox Code Playgroud)

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


小智 8

使用lambda,可以轻松地通过AsyncTask运行

 AsyncTask.execute(() -> //run your query here );
Run Code Online (Sandbox Code Playgroud)

  • 但是如何使用这种方法得到结果呢? (4认同)
  • 这个很方便,谢谢 顺便说一下,Kotlin 更简单:AsyncTask.execute { } (2认同)

Rez*_*lam 8

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


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

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

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

首先,在您的viewmodel课程中,您需要使用 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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

希望这对您有帮助:)。


Phi*_*hil 7

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

Executors.newSingleThreadExecutor().execute { 
   yourDb.yourDao.yourRequest() //Replace this by your request
}
Run Code Online (Sandbox Code Playgroud)

  • 你如何返回结果? (4认同)

Sas*_*iha 6

由于 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);
                });
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Vih*_*rma 5

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

AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),
        AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build();
Run Code Online (Sandbox Code Playgroud)

在我的情况下,我必须弄清楚列表中的点击用户是否存在于数据库中。如果没有,则创建用户并开始另一个活动

       @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);
        }
    }
Run Code Online (Sandbox Code Playgroud)


dcr*_*r24 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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

或者在Kotlin:

fun insert(event: Event) : Completable = Completable.fromCallable {
    database.eventDao().insert(event)
}
Run Code Online (Sandbox Code Playgroud)

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

dataManager.insert(event)
    .subscribeOn(scheduler)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...)
Run Code Online (Sandbox Code Playgroud)


Nil*_*ore 5

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

这是原因。

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