Android房间-简单的选择查询-不能访问数据库在主线程

我正在尝试房间持久化库的样本。 我创建了一个实体:

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

我漏了什么吗?我怎样才能使它不崩溃地执行?请建议。

217987 次浏览

错误信息,

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

描述的很准确。问题是如何避免在主线程上访问数据库。这是一个很大的话题,但要开始,请阅读AsyncTask(点击这里)

——编辑 ----------

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

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

  2. 使用注释 @RunWith(AndroidJUnit4.class) 在android设备上运行测试,而不是在带有UI的活动中。 更多细节可以找到在本教程中

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

——编辑2

因为很多人可能会遇到这个答案… 一般来说,现在最好的选择是Kotlin协程。Room现在直接支持它(目前处于测试阶段)。 https://kotlinlang.org/docs/reference/coroutines-overview.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01 < / p >

——编辑1

对于想知道…你还有其他选择。 我建议查看一下新的ViewModel和LiveData组件。LiveData与Room合作得很好。 https://developer.android.com/topic/libraries/architecture/livedata.html < / p > 另一个选择是RxJava/RxAndroid。比LiveData更强大但更复杂。 https://github.com/ReactiveX/RxJava < / p >

——原来的答案

在您的活动扩展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类自己的文件上创建一个final类。

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

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

在某些情况下,你可能还想在你的活动中保留一个对AgentAsyncTask的引用,这样你就可以在活动被销毁时取消它。但是你必须自己中断任何交易。

此外,你关于谷歌的测试示例的问题… 他们在那个网页上说:

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

没有活动,就没有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();

在我的情况下,我必须找出在列表中单击的用户是否存在于数据库中。如果不是,那么创建用户并启动另一个活动

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

我经常在插入和更新中使用RX工作流,但是对于选择查询,我建议使用RX工作流。

对于所有的RxJavaRxAndroidRxKotlin爱好者

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

一个优雅的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(...)

你不能在主线程上运行它,而是使用处理程序,异步或工作线程。一个示例代码可以在这里获得,并阅读文章在房间库这里:Android的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()上实现

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

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'
< br > < p >——更新: 2019年5月- 08 -: 2.1房间现在支持suspend
2019年9月13日:更新到使用架构组件作用域

如果你更喜欢异步任务:

  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.newSingleThreadExecutor().execute {
yourDb.yourDao.yourRequest() //Replace this by your request
}

你可以使用Future和Callable。所以你不需要写一个很长的asynctask,并且可以在不添加allowMainThreadQueries()的情况下执行你的查询。

我的刀问:-

@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已弃用,我们可以使用executor服务。或者你也可以在LiveData中使用ViewModel,详见其他答案。

对于使用执行者服务,您可以使用如下内容。

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)

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


< p > 推荐方法:
这里我使用了一些来自我当前项目的代码

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

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

viewmodel类中,首先你需要用协程disature IO执行数据库操作,从房间数据库中获取数据。然后用协程disature 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来使用视图模型的数据。

希望这对你有帮助:)。

在协程中用(Dispatchers.IO)调用你的查询,因为我们需要在后台做db操作。

请查看下面的截图: enter image description here < / p >