web-dev-qa-db-ja.com

Androidルーム - 単純な選択クエリ - メインスレッドでデータベースにアクセスできない

私は 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) 

この問題はメインスレッドでの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));
    }

ここに何か足りないの?クラッシュせずに実行させるにはどうすればいいですか?提案してください。

70
Devarshi

Daleが言ったように、UIをロックしているメインスレッド上のデータベースアクセスはエラーです。

AsyncTaskを拡張するActivityに静的なネストクラスを作成します(メモリリークを防ぐため)。

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への参照を保持して、アクティビティが破棄されたときにキャンセルできるようにすることもできます。しかし、あなたは自分でトランザクションを中断しなければならないでしょう。

また、グーグルのテスト例についてのあなたの質問...彼らはそのウェブページで述べています:

データベース実装をテストするための推奨されるアプローチは、Androidデバイス上で動作するJUnitテストを書くことです。これらのテストはアクティビティを作成する必要がないので、UIテストより実行が速いはずです。

アクティビティなし、UIなし.

- 編集 -

疑問に思う人のために...あなたには他の選択肢があります。新しいViewModelコンポーネントとLiveDataコンポーネントを調べてみることをお勧めします。 LiveDataはRoomとうまく連携します。 https://developer.Android.com/topic/libraries/architecture/livedata.html

別の選択肢はRxJava/RxAndroidです。 LiveDataよりも強力ですが複雑です。 https://github.com/ReactiveX/RxJava

- 編集2 -

多くの人がこの答えに出くわすかもしれないので...今日最も一般的に言えば、最良の選択肢はKotlin Coroutinesです。 Roomは直接サポートしています(現在はベータ版)。 https://kotlinlang.org/docs/reference/coroutines-overview.htmlhttps:// developer。 Android.com/jetpack/androidx/releases/room#2.1.0-beta01

45
mcastro

お勧めしませんが、allowMainThreadQueries()を使用してメインスレッドでデータベースにアクセスできます。

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.Java, "MyDatabase").allowMainThreadQueries().build()
91
mpolat

すべての RxJava または RxAndroid または RxKotlin がそこに恋人たち

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

コトリンコルーチン(クリア&コンサイス)

AsyncTaskは本当に不格好です。 Kotlinコルーチンは、よりクリーンな代替方法です(基本的には同期コードと2つのキーワードだけです)。

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としての活動

アクティビティからの非同期を使用するには、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ルーム&一時停止

2019年5月8日:Room 2.1でsuspendhttps://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
24
AjahnCharles

メインスレッドで実行することはできません。代わりに、ハンドラ、非同期スレッド、または作業スレッドを使用してください。サンプルコードはこちらから入手でき、ここでルームライブラリに関する記事を読むことができます。 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();
    }
});

あなたがメインスレッドでそれを実行したいなら、これは好ましい方法ではありません。

このメソッドを使ってメインスレッドで実行することができますRoom.inMemoryDatabaseBuilder()

18
Rizvan

Jetbrains Ankoライブラリでは、doAsync {..}メソッドを使用して自動的にデータベース呼び出しを実行できます。これは、mcastroの答えにあったと思われる冗長性の問題を解決します。

使用例

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

私はこれを挿入と更新に頻繁に使用しますが、RXワークフローを使用して推奨する選択クエリには使用します。

8
Arsala Bangash

洗練されたRxJava/Kotlinの解決策は Completable.fromCallable を使うことです。糸。

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(...)
5
dcr24

エラーメッセージ

メインスレッド上のデータベースにアクセスすることはできません。長期間にわたってUIをロックする可能性があるためです。

かなり説明的で正確です。問題は、メインスレッドでデータベースにアクセスすることをどのように避けるべきかということです。これは大きなトピックですが、始めるには AsyncTaskについて(こちらをクリック) を読んでください。

-----編集----------

ユニットテストを実行すると問題が発生しているようです。これを修正するにはいくつかの選択肢があります。

  1. Androidデバイス(またはエミュレータ)ではなく、開発用マシンで直接テストを実行してください。これはデータベース中心であり、それらがデバイス上で実行されているかどうかを実際には気にしないテストに有効です。

  2. Androidデバイス上でテストを実行するにはアノテーション@RunWith(AndroidJUnit4.class)を使用しますが、UIのあるアクティビティでは使用しません。これについての詳細は、このチュートリアルで見つけることができます

3
Dale Wilson

AsyncTaskで実行するのが簡単なラムダ

 AsyncTask.execute(() -> //run your query here );
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
Hitesh Sahu

バックグラウンドでリクエストを実行する必要があります。簡単な方法は エグゼキュータ を使うことです。

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

それを解決するためにこのコードを使うことができます。

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()を自分のコードに置き換えることができます。

2
Mostafa Rostami

更新: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は必要ありません。

2
live-love

データベース操作は別のスレッドで実行するだけです。こんな感じ(コトリン):

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

素早い問い合わせのために、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);
        }
    }
2
Vihaan Verma

メインスレッドでデータベースアクセスを許可することができますが、デバッグ目的でのみ、本番環境では行わないでください。

これが理由です

注:ビルダーでallowMainThreadQueries()を呼び出さない限り、Roomはメインスレッドでのデータベースアクセスをサポートしません。これは、UIを長期間ロックする可能性があるためです。非同期クエリ(LiveDataまたはFlowableのインスタンスを返すクエリ)は、必要に応じてバックグラウンドスレッドで非同期にクエリを実行するため、この規則から除外されます。

1
Nilesh Rathore

あなたはFutureとCallableを使うことができます。そのため、長いasynctaskを書く必要はなく、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();
}
1
beginner

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()を呼び出し、必要なデータを渡します。

メインスレッドとバックグラウンドスレッド間の双方向通信を行うためには、メインスレッドから別のハンドラを使用してメッセージを送信することで詳細を確認できます 双方向通信

0
has19