Programing

Android Room-단순 선택 쿼리-기본 스레드에서 데이터베이스에 액세스 할 수 없음

lottogame 2020. 9. 10. 08:20
반응형

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

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

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

여기에 빠진 게 있나요? 충돌없이 실행하려면 어떻게해야합니까? 제안 해주세요.


Dale이 말한 것처럼 UI를 잠그는 주 스레드에 대한 데이터베이스 액세스는 오류입니다.

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의 테스트 예에 대한 질문 ... 해당 웹 페이지에 다음과 같이 명시되어 있습니다.

데이터베이스 구현을 테스트하는 데 권장되는 접근 방식은 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.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01


권장되지는 않지만 기본 스레드의 데이터베이스에 액세스 할 수 있습니다. allowMainThreadQueries()

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

모든 RxJava 또는 RxAndroid 또는 RxKotlin 애호가를 위해

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

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

종속성 (아치 구성 요소에 대한 코 루틴 범위 추가) :

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

// viewModelScope:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha04'

-업데이트 :
2019 년 5 월 8 일 : 이제 Room 2.1 suspend
에서 2019 년 9 월 13 일 지원 : 아키텍처 구성 요소 범위 를 사용하도록 업데이트되었습니다 .


대신 핸들러, 비동기 또는 작업 스레드를 사용하여 주 스레드에서 실행할 수 없습니다. 여기에서 샘플 코드를 사용할 수 있으며 여기에서 룸 라이브러리에 대한 기사를 읽으십시오. 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()


Jetbrains Anko 라이브러리를 사용하면 doAsync {..} 메서드를 사용하여 데이터베이스 호출을 자동으로 실행할 수 있습니다. 이것은 mcastro의 답변으로 겪었던 것처럼 보였던 자세한 문제를 처리합니다.

사용 예 :

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

삽입 및 업데이트에 자주 사용하지만 일부 쿼리의 경우 RX 워크 플로를 사용하는 것이 좋습니다.


우아한 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(...)

람다를 사용하면 AsyncTask로 쉽게 실행할 수 있습니다.

 AsyncTask.execute(() -> //run your query here );

백그라운드에서 요청을 실행해야합니다. 간단한 방법은 Executors를 사용하는 것입니다 .

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

오류 메시지,

오랫동안 UI를 잠글 수 있으므로 주 스레드의 데이터베이스에 액세스 할 수 없습니다.

매우 설명적이고 정확합니다. 문제는 주 스레드에서 데이터베이스에 액세스하지 않는 방법입니다. 그것은 거대한 주제이지만 시작하려면 AsyncTask 에 대해 읽어보십시오 (여기를 클릭하십시오)

-----편집하다----------

단위 테스트를 실행할 때 문제가있는 것 같습니다. 이 문제를 해결할 수있는 몇 가지 방법이 있습니다.

  1. Android 기기 (또는 에뮬레이터)가 아닌 개발 머신에서 직접 테스트를 실행하세요. 이것은 데이터베이스 중심의 테스트에서 작동하며 장치에서 실행되는지 여부에 실제로 신경 쓰지 않습니다.

  2. 주석 @RunWith(AndroidJUnit4.class)사용하여 Android 기기에서 테스트를 실행하지만 UI가있는 활동에서는 실행하지 않습니다. 이에 대한 자세한 내용은 이 튜토리얼에서 찾을 수 있습니다.


비동기 작업에 더 익숙한 경우 :

  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(new Runnable() {
                    @Override
                    public void run() {
                        appDb.daoAccess().someJobes();//replace with your code
                    }
                });

또는 람다에서 다음 코드를 사용할 수 있습니다.

Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());

appDb.daoAccess().someJobes()자신의 코드로 바꿀 수 있습니다 .


빠른 쿼리를 위해 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);
        }
    }

주 스레드에서 데이터베이스 액세스를 허용 할 수 있지만 디버깅 목적으로 만 프로덕션에서는이 작업을 수행하면 안됩니다.

여기에 이유가 있습니다.

참고 : Room은 오랜 시간 동안 UI를 잠글 수 있으므로 빌더에서 allowMainThreadQueries ()를 호출하지 않는 한 기본 스레드에서 데이터베이스 액세스를 지원하지 않습니다. 비동기 쿼리 (LiveData 또는 Flowable의 인스턴스를 반환하는 쿼리)는 필요할 때 백그라운드 스레드에서 쿼리를 비동기 적으로 실행하므로이 규칙에서 제외됩니다.


업데이트 : 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) :

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

You can use Future and Callable. So you would not be required to write a long asynctask and can perform your queries without adding allowMainThreadQueries().

My dao query:-

@Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();

My repository method:-

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

참고URL : https://stackoverflow.com/questions/44167111/android-room-simple-select-query-cannot-access-database-on-the-main-thread

반응형