it-swarm.com.de

Android Room - einfache Auswahlabfrage - Zugriff auf die Datenbank im Haupt-Thread nicht möglich

Ich probiere ein Beispiel mit Room Persistence Library . Ich habe eine Entität erstellt:

@Entity
public class Agent {
    @PrimaryKey
    public String guid;
    public String name;
    public String email;
    public String password;
    public String phone;
    public String licence;
}

Eine DAO-Klasse erstellt:

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

Die Datenbankklasse wurde erstellt:

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

Freiliegende Datenbank mit der folgenden Unterklasse in 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()
    }
}

Funktion unterhalb meiner Funktion implementiert:

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

Bei der Ausführung der obigen Methode stürzt sie leider mit der folgenden Ablaufverfolgung ab:

    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) 

Anscheinend hängt dieses Problem mit der Ausführung der Datenbankoperation auf dem Hauptthread zusammen. Der im obigen Link bereitgestellte Beispieltestcode wird jedoch nicht in einem separaten Thread ausgeführt:

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

Fehlt mir hier etwas? Wie kann ich es ohne Absturz ausführen lassen? Bitte vorschlagen.

63
Devarshi

Der Datenbankzugriff auf den Haupt-Thread, der die Benutzeroberfläche sperrt, ist der Fehler, wie Dale sagte.

Erstellen Sie eine statische verschachtelte Klasse (um einen Speicherverlust zu verhindern) in der Activity, die AsyncTask erweitert.

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

Oder Sie können eine letzte Klasse in einer eigenen Datei erstellen.

Führen Sie es dann in der signUpAction (View view) -Methode aus:

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

In einigen Fällen möchten Sie möglicherweise auch einen Verweis auf die AgentAsyncTask in Ihrer Aktivität halten, damit Sie sie abbrechen können, wenn die Aktivität zerstört wird. Sie müssten jedoch alle Transaktionen selbst unterbrechen.

Auch Ihre Frage zum Testbeispiel von Google ..... Auf dieser Webseite heißt es:

Der empfohlene Ansatz zum Testen der Datenbankimplementierung lautet Schreiben eines JUnit-Tests, der auf einem Android-Gerät ausgeführt wird. Weil diese Tests erfordern keine Erstellung einer Aktivität, sie sollten schneller zu .__ werden. Ausführen als Ihre UI-Tests.

Keine Aktivität, keine Benutzeroberfläche.

--BEARBEITEN--

Für Leute, die sich fragen ... Sie haben andere Möglichkeiten ... Ich empfehle einen Blick in die neuen ViewModel- und LiveData-Komponenten. LiveData funktioniert hervorragend mit Room . https://developer.Android.com/topic/libraries/architecture/livedata.html

Eine weitere Option ist der RxJava/RxAndroid. Leistungsfähiger, aber komplexer als LiveData . https://github.com/ReactiveX/RxJava

41
mcastro

Es wird nicht empfohlen, aber Sie können mit allowMainThreadQueries() auf die Datenbank im Haupt-Thread zugreifen.

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

Für alle RxJava oder RxAndroid oder RxKotlin Liebhaber

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

Direkter Code mit Kotlin Coroutines

AsyncTask ist wirklich klobig. Kotlin Coroutines ist eine sauberere Alternative (im Wesentlichen nur synchroner Code mit ein paar zusätzlichen Schlüsselwörtern).

private fun myFun() {
    launch {
        val query = async(Dispatchers.IO) { // Async stuff
            MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license)
        }

        val agentsCount = query.await()
        // do UI stuff
    }
}

Um asynchron von einer Aktivität zu verwenden, benötigen Sie ein CoroutineScope. Sie können Ihre Aktivität folgendermaßen verwenden:

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

   // ...rest of class
}

Das suspend-Schlüsselwort stellt sicher, dass async-Methoden nur aus asynchronen Blöcken aufgerufen werden. Dies kann jedoch (wie von @Robin angemerkt) nicht gut mit mit Raum kommentierten Methoden gespielt werden.

// Wrap API to use suspend (probably not worth it)
public suspend fun agentsCount(...): Int = agentsCountPrivate(...)

@Query("SELECT ...")
protected abstract fun agentsCountPrivate(...): Int
21
AjahnCharles

Sie können es nicht im Haupt-Thread ausführen, stattdessen Handler, Async oder Arbeitsthreads. Einen Beispielcode finden Sie hier und lesen Sie den Artikel über die Raumbibliothek hier: Android's 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();
    }
});

Wenn Sie es im Haupt-Thread ausführen möchten, ist dies nicht der bevorzugte Weg.

Sie können diese Methode verwenden, um den Haupt-Thread Room.inMemoryDatabaseBuilder() zu erreichen. 

17
Rizvan

Mit der Jetbrains Anko-Bibliothek können Sie die Methode doAsync {..} zum automatischen Ausführen von Datenbankaufrufen verwenden. Dies kümmert sich um das Problem der Ausführlichkeit, das Sie mit der Antwort von mcastro gehabt zu haben schienen. 

Verwendungsbeispiel: 

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

Ich verwende dies häufig für Einfügungen und Aktualisierungen, jedoch für ausgewählte Abfragen, die ich mit dem RX-Workflow empfehle. 

8
Arsala Bangash

Eine elegante Lösung von RxJava/Kotlin ist die Verwendung von Completable.fromCallable , wodurch Sie ein Observable erhalten, das keinen Wert zurückgibt, sondern in einem anderen Thread beobachtet und abonniert werden kann.

public Completable insert(Event event) {
    return Completable.fromCallable(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
            return database.eventDao().insert(event)
        }
    }
}

Oder in Kotlin:

fun insert(event: Event) : Completable = Completable.fromCallable {
    database.eventDao().insert(event)
}

Sie können das beobachten und abonnieren wie gewohnt:

dataManager.insert(event)
    .subscribeOn(scheduler)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...)
5
dcr24

Die Fehlermeldung, 

Kann nicht auf die Datenbank im Haupt-Thread zugreifen, da die Benutzeroberfläche möglicherweise für längere Zeit gesperrt wird.

Ist ziemlich beschreibend und genau. Die Frage ist, wie Sie den Zugriff auf die Datenbank im Hauptthread vermeiden sollten. Dies ist ein riesiges Thema, aber lesen Sie zu AsyncTask (hier klicken)

-----BEARBEITEN----------

Ich sehe, dass Sie Probleme haben, wenn Sie einen Komponententest ausführen. Sie haben mehrere Möglichkeiten, dies zu beheben:

  1. Führen Sie den Test direkt auf dem Entwicklungscomputer und nicht auf einem Android-Gerät (oder Emulator) aus. Dies funktioniert für Tests, die datenbankorientiert sind und sich nicht wirklich darum kümmern, ob sie auf einem Gerät ausgeführt werden.

  2. Verwenden Sie die Annotation@RunWith(AndroidJUnit4.class), um den Test auf dem Android-Gerät auszuführen, jedoch nicht in einer Aktivität mit einer Benutzeroberfläche . Weitere Informationen hierzu finden Sie in diesem Tutorial .

3
Dale Wilson

Mit Lambda ist es einfach, mit AsyncTask zu laufen

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

Wenn Sie mit Async-Task besser vertraut sind :

  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

Sie müssen die Anfrage im Hintergrund ausführen ..__ Eine einfache Möglichkeit wäre die Verwendung eines Executors :

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

Sie können diesen Code einfach verwenden, um ihn zu lösen: 

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

Oder in Lambda können Sie diesen Code verwenden:

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

Sie können appDb.daoAccess().someJobes() durch Ihren eigenen Code ersetzen.

2
Mostafa Rostami

Update: Ich habe diese Nachricht auch erhalten, als ich versuchte, eine Abfrage mit @RawQuery und SupportSQLiteQuery im DAO zu erstellen.

@Transaction
public LiveData<List<MyEntity>> getList(MySettings mySettings) {
    //return getMyList(); -->this is ok

    return getMyList(new SimpleSQLiteQuery("select * from mytable")); --> this is an error

Lösung: Erstellen Sie die Abfrage im ViewModel und übergeben Sie sie an das DAO.

public MyViewModel(Application application) {
...
        list = Transformations.switchMap(searchParams, params -> {

            StringBuilder sql;
            sql = new StringBuilder("select  ... ");

            return appDatabase.rawDao().getList(new SimpleSQLiteQuery(sql.toString()));

        });
    }

Oder...

Sie sollten nicht direkt auf die Datenbank im Haupt-Thread zugreifen. Beispiel:

 public void add(MyEntity item) {
     appDatabase.myDao().add(item); 
 }

Sie sollten AsyncTask zum Aktualisieren, Hinzufügen und Löschen von Vorgängen verwenden. 

Beispiel:

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

    }
}

Wenn Sie LiveData für ausgewählte Vorgänge verwenden, benötigen Sie keine AsyncTask.

2
live-love

Führen Sie die Datenbankoperationen einfach in einem separaten Thread aus. Wie dieses (Kotlin):

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

Sie können den Datenbankzugriff auf den Haupt-Thread zulassen. Dies ist jedoch nur zu Debugging-Zwecken der Fall. Sie sollten dies nicht für die Produktion tun.

Hier ist der Grund.

Hinweis: Room unterstützt keinen Datenbankzugriff im Hauptthread, es sei denn, Sie haben allowMainThreadQueries () im Builder aufgerufen, da die Benutzeroberfläche möglicherweise für einen längeren Zeitraum gesperrt wird. Asynchrone Abfragen - Abfragen, die Instanzen von LiveData oder Flowable zurückgeben - sind von dieser Regel ausgenommen, da sie die Abfrage bei Bedarf asynchron in einem Hintergrundthread ausführen.

1
Nilesh Rathore

Sie können Future und Callable verwenden. Sie müssen also keine lange asynctask schreiben und können Ihre Abfragen ausführen, ohne allowMainThreadQueries () hinzuzufügen.

Meine Dao-Abfrage: -

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

Meine Repository-Methode: -

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

Für schnelle Abfragen können Sie Platz für die Ausführung im UI-Thread zulassen.

AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),
        AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build();

In meinem Fall musste ich herausfinden, ob der angeklickte Benutzer in der Liste in der Datenbank vorhanden ist oder nicht. Wenn nicht, erstellen Sie den Benutzer und starten Sie eine andere Aktivität

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

sie können den gesamten Datenbankzugriff mit HandlerThread in einem anderen Thread sequentiell abwickeln. Überprüfen Sie dies auf weitere Informationen mithilfe von handlerThread

im Grunde werden Sie eine Klasse erstellen, die handlerThread erweitert und onLooperPrepared wie folgt überschreibt

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



}

und dann fügen Sie der Aktivitätsklasse den folgenden Code hinzu

MyHandlerThread myHandlerThread = new MyHandlerThread();
myHandlerThread.start();

um mit dem Thread zu kommunizieren, rufen Sie myHandlerThread.sendMessage() auf und übergeben die gewünschten Daten

um eine bidirektionale Kommunikation zwischen dem Hauptthread und dem Hintergrundthread zu erhalten, können Sie einen anderen Handler aus dem Hauptthread verwenden und ihm Nachrichten senden Weitere Informationen finden Sie hier Zweiwege-Kommunikation

0
has19