Android Room - простой запрос выбора - не удается получить доступ к базе данных в основной теме
Я пытаюсь создать образец с Библиотека сохранения пространства.
Я создал объект:
@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(entities = {Agent.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AgentDao agentDao();
}
Открытая база данных, использующая ниже подкласс в Котлине:
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));
}
Я что-то пропустил? Как я могу выполнить его без сбоев? Пожалуйста, предложите.
Ответы
Ответ 1
Как сказал Дейл, доступ к базе данных в главном потоке, блокирующем пользовательский интерфейс, является ошибкой.
Создайте статический вложенный класс (для предотвращения утечки памяти) в вашем 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();
}
}
}
Или вы можете создать окончательный класс в своем собственном файле.
Затем выполните его в методе signUpAction (View view):
new AgentAsyncTask(this, email, phone, license).execute();
В некоторых случаях вы можете также захотеть сохранить ссылку на AgentAsyncTask в своей активности, чтобы вы могли отменить ее, когда активность будет уничтожена. Но вам придется прервать любые транзакции самостоятельно.
Кроме того, ваш вопрос о тестовом примере Google... Они указывают на этой веб-странице:
Рекомендуемый подход для тестирования реализации вашей базы данных - написание теста JUnit, который выполняется на устройстве Android. Поскольку эти тесты не требуют создания действия, они должны выполняться быстрее, чем ваши тесты пользовательского интерфейса.
Нет активности, нет пользовательского интерфейса.
--EDIT -
Для людей, интересующихся... У вас есть другие варианты. Я рекомендую взглянуть на новые компоненты ViewModel и LiveData. LiveData прекрасно работает с Room. https://developer.android.com/topic/libraries/architecture/livedata.html
Другой вариант - RxJava/RxAndroid. Более мощный, но более сложный, чем LiveData. https://github.com/ReactiveX/RxJava
--EDIT 2--
Так как многие люди могут встретить этот ответ... На сегодняшний день лучший вариант - это KOTLIN COROUTINES. https://kotlinlang.org/docs/reference/coroutines-overview.html
Ответ 2
Не рекомендуется, но вы можете получить доступ к базе данных в основном потоке с помощью allowMainThreadQueries()
MyApp.database = Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()
Ответ 3
Для всех RxJava или RxAndroid или RxKotlin любители там
Observable.just(db)
.subscribeOn(Schedulers.io())
.subscribe { db -> // database operation }
Ответ 4
Котлин сопрограммы (ясно и кратко)
AsyncTask действительно неуклюжий. Сопрограммы Kotlin - более чистая альтернатива (по сути, просто синхронный код плюс пара ключевых слов).
private fun myFun() {
launch { // coroutine on Main
val query = async(Dispatchers.IO) { // coroutine on IO
MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license)
}
val agentsCount = query.await()
// do UI stuff
}
}
И это оно !!
Бонус: активность как CoroutineScope
Чтобы использовать асинхронность из Activity, вам нужен CoroutineScope. Вы можете использовать свою активность следующим образом:
class LoadDataActivity : AppCompatActivity(), CoroutineScope {
private val job by lazy { Job() }
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onDestroy() {
super.onDestroy()
job.cancel() // cancels all coroutines under this scope
}
// ...rest of class
}
Бонус: Android Room & Suspend
8 мая 2019 года. Комната 2.1 теперь поддерживает suspend
(https://youtu.be/Qxj2eBmXLHg?t=1662).
Ключевое слово suspend
гарантирует, что асинхронные методы вызываются только из асинхронных блоков, однако (как отмечает @Robin) это не очень хорошо работает с аннотированными методами Room (<2.1).
// Wrap API to use suspend (probably not worth it)
public suspend fun agentsCount(...): Int = agentsCountPrivate(...)
@Query("SELECT ...")
protected abstract fun agentsCountPrivate(...): Int
Ответ 5
Вы не можете запустить его в основном потоке, вместо этого используйте обработчики, асинхронные или рабочие потоки. Пример кода доступен здесь и прочитайте статью о библиотеке комнаты здесь: 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()
Ответ 6
С помощью библиотеки Jetbrains Anko вы можете использовать метод doAsync {..} для автоматического выполнения вызовов базы данных. Это позаботится о проблеме многословия, которую вы, казалось, имели с ответом mcastro.
Пример использования:
doAsync {
Application.database.myDAO().insertUser(user)
}
Я использую это часто для вставок и обновлений, однако для выбранных запросов я рекомендую использовать рабочий процесс RX.
Ответ 7
Элегантное решение 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)
}
}
}
Или в Котлине:
fun insert(event: Event) : Completable = Completable.fromCallable {
database.eventDao().insert(event)
}
Вы можете наблюдать и подписаться как обычно:
dataManager.insert(event)
.subscribeOn(scheduler)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...)
Ответ 8
Сообщение об ошибке,
Невозможно получить доступ к базе данных в основном потоке, так как это может потенциально заблокировать пользовательский интерфейс в течение длительных периодов времени.
Является довольно описательным и точным. Вопрос в том, как следует избегать доступа к базе данных в основном потоке. Это огромная тема, но для начала прочитайте AsyncTask (нажмите здесь)
----- EDIT ----------
Я вижу, что у вас возникают проблемы при запуске unit test. У вас есть несколько вариантов, чтобы исправить это:
-
Запустите тест непосредственно на машине разработки, а не на устройстве Android (или эмуляторе). Это работает для тестов, ориентированных на базу данных, и им не важно, работают ли они на устройстве.
-
Используйте аннотацию
@RunWith(AndroidJUnit4.class)
для запуска теста на устройстве Android, но не в действии с пользовательским интерфейсом.
Подробнее об этом можно узнать в этом уроке
Ответ 9
Если вам удобнее задача Async:
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();
Ответ 10
Вы должны выполнить запрос в фоновом режиме. Простым способом может быть использование Исполнителей:
Executors.newSingleThreadExecutor().execute {
yourDb.yourDao.yourRequest() //Replace this by your request
}
Ответ 11
С лямбдой легко работать с AsyncTask
AsyncTask.execute(() -> //run your query here );
Ответ 12
Для быстрых запросов вы можете позволить комнате выполнить его в потоке пользовательского интерфейса.
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);
}
}
Ответ 13
Обновление: я также получил это сообщение, когда пытался построить запрос, используя @RawQuery и SupportSQLiteQuery внутри DAO.
@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.
Ответ 14
Просто вы можете использовать этот код для его решения:
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
appDb.daoAccess().someJobes();//replace with your code
}
});
Или в лямбде вы можете использовать этот код:
Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());
Вы можете заменить appDb.daoAccess().someJobes()
своим собственным кодом;
Ответ 15
Просто выполните операции с базой данных в отдельном потоке. Вот так (Котлин):
Thread {
//Do your database´s operations here
}.start()
Ответ 16
Вы можете разрешить доступ к базе данных в основном потоке, но только для целей отладки, вы не должны делать этого при производстве.
Вот причина.
Примечание. Room не поддерживает доступ к базе данных в основном потоке, если вы не вызывали allowMainThreadQueries() в построителе, поскольку он может блокировать пользовательский интерфейс в течение длительного периода времени. Асинхронные запросы-запросы, возвращающие экземпляры LiveData или Flowable, освобождаются от этого правила, потому что они асинхронно запускают запрос в фоновом потоке, когда это необходимо.
Ответ 17
Вы можете использовать Future и Callable. Таким образом, вам не нужно будет писать длинную асинхронную задачу, и вы сможете выполнять запросы без добавления 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();
}
Ответ 18
вы можете обрабатывать весь доступ к базе данных последовательно в другом потоке, используя HandlerThread, проверьте это для получения дополнительной информации, используя handlerThread
в основном, что вы будете делать, это создать класс, который расширяет handlerThread и переопределяет onLooperPrepared, как показано ниже
public class MyHandlerThread extends HandlerThread{
private Handler handler;
public SocketManager(){
super("MyHandlerThread");
}
@Override
protected void onLooperPrepared() {
handler = new Handler(getLooper()) {
@Override
public void handleMessage(android.os.Message msg) {
super.handleMessage(msg);
//handle all your database tasks here
YourObject yourObject = (YourObject)msg.obj;
}};
}
//send message to the background thread
public void sendMessage(YourObject msg){
android.os.Message msg = new android.os.Message();
msg.obj = message;
handler.sendMessage(msg);
}
public void onDestroy(){
quit();
interrupt();
}
}
а затем вы добавляете следующий код в свой класс деятельности
MyHandlerThread myHandlerThread = new MyHandlerThread();
myHandlerThread.start();
для связи с потоком вы вызываете myHandlerThread.sendMessage()
и передаете myHandlerThread.sendMessage()
данные
для того, чтобы иметь двустороннюю связь между главным потоком и фоновый поток можно использовать другой обработчик из главного потока и отправлять сообщения, которые он проверить это для получения дополнительной информации двухсторонней связи