235 votes

Android Room - simple requête select - Impossible d'accéder à la base de données sur le thread principal

J'essaie un échantillon avec Bibliothèque de persistance des pièces . J'ai créé une Entité :

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

Création d'une classe 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);
}

Création de la classe de base de données :

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

Base de données exposée en utilisant la sous-classe ci-dessous en 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()
    }
}

J'ai implémenté la fonction ci-dessous dans mon activité :

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

Malheureusement, lors de l'exécution de la méthode ci-dessus, il y a un crash avec la trace de pile ci-dessous :

    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) 

Il semble que le problème soit lié à l'exécution d'une opération de base de données sur le fil principal. Cependant, l'exemple de code de test fourni dans le lien ci-dessus ne s'exécute pas sur un thread séparé :

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

Est-ce que je rate quelque chose ici ? Comment puis-je faire en sorte qu'il s'exécute sans se planter ? Veuillez me conseiller.

2 votes

Bien qu'écrit pour Kotlin, cet article explique très bien le problème sous-jacent !

0 votes

Jetez un coup d'œil à stackoverflow.com/questions/58532832/

0 votes

Regardez cette réponse. Cette réponse fonctionne pour moi stackoverflow.com/a/51720501/7655085

254voto

mpolat Points 789

Ce n'est pas recommandé mais vous pouvez accéder à la base de données sur le thread principal avec allowMainThreadQueries()

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

25 votes

Room ne permet pas d'accéder à la base de données sur le thread principal, sauf si vous avez appelé allowMainThreadQueries() sur le constructeur parce qu'il pourrait potentiellement verrouiller l'interface utilisateur pendant une longue période de temps. Les requêtes asynchrones (les requêtes qui renvoient des LiveData ou RxJava Flowable ) sont exempts de cette règle puisqu'ils exécutent la requête de manière asynchrone sur un thread d'arrière-plan lorsque cela est nécessaire.

4 votes

Merci, c'est très utile pour la migration, car je veux tester que la salle fonctionne comme prévu avant de convertir les chargeurs en données réelles.

5 votes

@JideGuruTheProgrammer Non, ça ne devrait pas. Dans certains cas, cela pourrait ralentir considérablement votre application. Les opérations doivent être effectuées de manière asynchrone.

93voto

AjahnCharles Points 1258

Kotlin Coroutines (Clair et concis)

AsyncTask est vraiment maladroit. Les coroutines sont une alternative plus propre (il suffit de saupoudrer quelques mots-clés et votre code synchrone devient asynchrone).

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

Dépendances (ajoute des scopes de coroutine pour les composants de l'arc) :

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

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

-- Mises à jour :
08-Mai-2019 : Salle 2.1 prend désormais en charge suspend
13-Sep-2019 : Mis à jour pour utiliser Composants de l'architecture portée

1 votes

Avez-vous une erreur de compilation avec le @Query abstract suspend fun count() en utilisant le mot-clé suspendre ? Pouvez-vous avoir l'amabilité de vous pencher sur cette question similaire : stackoverflow.com/questions/48694449/

0 votes

@Robin - Oui, je le fais. Mon erreur ; j'utilisais suspend sur une méthode DAO publique (non annotée) qui appelait une méthode protégée non suspendue. @Query fonction. Lorsque j'ajoute le mot-clé suspendre à la fonction interne @Query aussi, la compilation échoue effectivement. Il semblerait que l'astuce de suspend et de Room soit en conflit (comme vous le mentionnez dans votre autre question, la version compilée de suspend renvoie une continuation que Room ne peut pas gérer).

0 votes

C'est tout à fait logique. Je vais l'appeler avec des fonctions coroutine à la place.

77voto

mcastro Points 460

L'accès à la base de données sur le thread principal verrouillant l'interface utilisateur est l'erreur, comme l'a dit Dale.

--EDIT 2--

Puisque de nombreuses personnes peuvent tomber sur cette réponse... La meilleure option aujourd'hui, d'une manière générale, est celle des coroutines Kotlin. Room les prend désormais en charge directement (actuellement en version bêta). https://kotlinlang.org/docs/reference/coroutines-overview.html https://developer.Android.com/jetpack/androidx/releases/room#2.1.0-beta01

--EDIT 1--

Pour les gens qui se demandent... Vous avez d'autres options. Je vous recommande de jeter un coup d'œil aux nouveaux composants ViewModel et LiveData. LiveData fonctionne très bien avec Room. https://developer.Android.com/topic/libraries/architecture/livedata.html

Une autre option est le RxJava/RxAndroid. Plus puissant mais plus complexe que LiveData. https://github.com/ReactiveX/RxJava

--Réponse originale--

Créez une classe statique imbriquée (pour éviter les fuites de mémoire) dans votre activité en étendant 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();
        }
    }
}

Ou vous pouvez créer une classe finale dans son propre fichier.

Ensuite, exécutez-la dans la méthode signUpAction(View view) :

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

Dans certains cas, vous pourriez également vouloir conserver une référence à l'AgentAsyncTask dans votre activité afin de pouvoir l'annuler lorsque l'activité est détruite. Mais vous devrez alors interrompre vous-même toute transaction.

En outre, votre question sur l'exemple de test de Google... Ils déclarent dans cette page web :

L'approche recommandée pour tester l'implémentation de votre base de données est écrire un test JUnit qui s'exécute sur un appareil Android. Comme ces tests ne nécessitent pas la création d'une activité, ils devraient être plus rapides à d'exécution que les tests de l'interface utilisateur.

Pas d'activité, pas d'interface utilisateur.

45 votes

Suis-je censé faire cette énorme tâche asynchrone (que vous avez proposée dans l'exemple de code) chaque fois que j'ai besoin d'accéder à la base de données ? C'est une douzaine de lignes de code au lieu d'une seule pour obtenir des données de la base de données. Vous avez également proposé de créer une nouvelle classe, mais cela signifie-t-il que je dois créer une nouvelle classe AsyncTask pour chaque appel à la base de données d'insertion/sélection ?

8 votes

Vous avez d'autres options, oui. Vous pourriez vouloir jeter un coup d'œil aux nouveaux composants ViewModel et LiveData. Lorsque vous utilisez LiveData, vous n'avez pas besoin de l'AsyncTask, l'objet sera notifié dès que quelque chose change. developer.Android.com/topic/libraries/architecture/ developer.Android.com/topic/libraries/architecture/ Il y a aussi AndroidRx (bien qu'il fasse à peu près la même chose que LiveData), et les Promesses. Lorsque vous utilisez l'AsyncTask, vous pouvez architecturer de telle manière que vous pouvez inclure plusieurs opérations dans une AsyncTask ou les séparer.

0 votes

@Piotrek - Kotlin intègre désormais l'asynchronisme (bien qu'il soit signalé comme expérimental). Voir ma réponse qui est comparativement triviale. La réponse de Samuel Robert couvre Rx. Je ne vois pas de réponse à LiveData ici, mais cela pourrait être le meilleur choix si vous voulez un observable.

53voto

Samuel Robert Points 2755

Pour tous les RxJava o RxAndroid o RxKotlin les amoureux de la nature

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

3 votes

Si je place ce code dans une méthode, comment retourner le résultat de l'opération de la base de données ?

0 votes

@EggakinBaconwalker J'ai override fun getTopScores(): Observable<List<PlayerScore>> { return Observable .fromCallable({ GameApplication.database .playerScoresDao().getTopScores() }) .applySchedulers() }applySchedulers() Je fais juste fun <T> Observable<T>.applySchedulers(): Observable<T> = this.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())

0 votes

Cela ne fonctionnera pas pour IntentService. Parce que IntentService sera terminé une fois que son thread sera terminé.

36voto

Rizvan Points 1422

Vous ne pouvez pas l'exécuter sur le thread principal, utilisez plutôt des handlers, des threads asynchrones ou des threads de travail. Un exemple de code est disponible ici et un article sur la bibliothèque de chambre est disponible ici : Bibliothèque de la salle 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();
    }
});

Si vous voulez l'exécuter sur le fil principal, ce qui n'est pas la meilleure solution.

Vous pouvez utiliser cette méthode pour réaliser sur le thread principal Room.inMemoryDatabaseBuilder()

0 votes

Si j'utilise cette méthode pour obtenir les données (seulement getAllUsers()dans ce cas), comment retourner les données de cette méthode ? Une erreur apparaît, si je mets le mot "return" à l'intérieur de "run".

1 votes

Créez une méthode d'interface quelque part et ajoutez une classe anonyme pour récupérer les données d'ici.

2 votes

C'est la solution la plus simple pour l'insertion/mise à jour.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X