web-dev-qa-db-fra.com

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

J'essaie un échantillon avec Bibliothèque de persistance de la pièce . 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éé 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 utilisant la sous-classe ci-dessous dans 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()
    }
}

Fonction implémentée 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 se bloque 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) 

On dirait que ce problème est lié à l'exécution de l'opération de base de données sur le thread principal. Cependant, l'exemple de code de test fourni dans le lien ci-dessus ne s'exécute pas sur un thread distinct:

@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 manque quelque chose ici? Comment puis-je le faire exécuter sans crash? Veuillez suggérer.

63
Devarshi

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

Créez une classe imbriquée statique (pour éviter les fuites de mémoire) dans votre activité d'extension 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 sur son propre fichier.

Puis exécutez-le dans la méthode signUpAction (View View):

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

Dans certains cas, vous pouvez également souhaiter conserver une référence à AgentAsyncTask dans votre activité afin de pouvoir l'annuler lorsque l'activité est détruite. Mais vous devrez interrompre vous-même toutes les transactions.

De plus, votre question sur l'exemple de test de Google ........ Ils indiquent 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 périphérique Android. Parce que ceux-ci les tests ne nécessitent pas de créer une activité, ils devraient être plus rapides. exécuter que vos tests d'interface utilisateur.

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

--MODIFIER--

Pour ceux qui se demandent ... Vous avez d’autres options ... Je vous recommande de jeter un coup d’œil sur les nouveaux composants ViewModel et LiveData. LiveData fonctionne parfaitement 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

41
mcastro

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()
83
mpolat

Pour tous les RxJava ou RxAndroid ou RxKotlin les amoureux là-bas

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

Code simple en utilisant Kotlin Coroutines

AsyncTask est vraiment maladroit. Koutlin coroutines est une alternative plus propre (essentiellement du code synchrone avec quelques mots-clés supplémentaires).

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

Pour utiliser async d'une activité, vous avez besoin d'un CoroutineScope. Vous pouvez utiliser votre activité comme ceci:

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
}

Le mot clé suspend garantit que les méthodes asynchrones ne sont appelées qu'à partir de blocs asynchrones. Toutefois, comme l'a noté @Robin, les méthodes annotées Room ne fonctionnent pas bien.

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

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

Vous ne pouvez pas l'exécuter sur le thread principal à la place, utilisez des gestionnaires, des threads asynchrones ou de travail. Un exemple de code est disponible ici et lisez l'article sur la bibliothèque de pièces ici: 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();
    }
});

Si vous voulez l'exécuter sur le thread principal, ce qui n'est pas préférable.

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

17
Rizvan

Avec la bibliothèque Jetbrains Anko, vous pouvez utiliser la méthode doAsync {..} pour exécuter automatiquement les appels de base de données. Cela résout le problème de verbosité que vous semblez avoir eu avec la réponse de mcastro. 

Exemple d'utilisation: 

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

Je l’utilise fréquemment pour les insertions et les mises à jour, mais pour certaines requêtes, je recommande d’utiliser le flux de production RX. 

8
Arsala Bangash

Une solution élégante de RxJava/Kotlin consiste à utiliser Completable.fromCallable , ce qui vous donnera un observable qui ne renverra pas de valeur, mais pourra être observé et souscrit sur un autre thread.

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

Ou à Kotlin:

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

Vous pouvez observer et vous abonner comme vous le feriez habituellement:

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

Le message d'erreur, 

Impossible d’accéder à la base de données sur le thread principal, car elle risquerait de verrouiller l’UI pendant une longue période.

Est assez descriptif et précis. La question est de savoir comment éviter d’accéder à la base de données sur le fil principal. C'est un sujet important, mais pour commencer, lisez à propos de AsyncTask (cliquez ici)

-----MODIFIER----------

Je vois que vous rencontrez des problèmes lorsque vous exécutez un test unitaire. Vous avez plusieurs choix pour résoudre ce problème:

  1. Exécutez le test directement sur la machine de développement plutôt que sur un périphérique Android (ou un émulateur). Cela fonctionne pour les tests centrés sur la base de données et ne s’intéresse pas vraiment à leur exécution sur un périphérique.

  2. Utilisez l'annotation@RunWith(AndroidJUnit4.class) pour exécuter le test sur l'appareil Android, mais pas dans le cadre d'une activité avec une interface utilisateur . Vous trouverez plus de détails à ce sujet dans ce tutoriel

3
Dale Wilson

Avec lambda, il est facile de fonctionner avec AsyncTask

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

Si vous êtes plus à l'aise avec Tâche asynchrone :

  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

Vous devez exécuter la demande en arrière-plan . Une manière simple pourrait être d'utiliser un Executors :

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

Vous pouvez simplement utiliser ce code pour le résoudre: 

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

Ou dans lambda, vous pouvez utiliser ce code:

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

Vous pouvez remplacer appDb.daoAccess().someJobes() par votre propre code;

2
Mostafa Rostami

Mise à jour: j'ai également reçu ce message lorsque j'essayais de créer une requête à l'aide de @RawQuery et SupportSQLiteQuery dans 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

Solution: créez la requête dans le ViewModel et transmettez-la au DAO.

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

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

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

        });
    }

Ou...

Vous ne devriez pas accéder à la base de données directement sur le thread principal, par exemple:

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

Vous devez utiliser AsyncTask pour les opérations de mise à jour, d'ajout et de suppression. 

Exemple:

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

    }
}

Si vous utilisez LiveData pour des opérations de sélection, vous n'avez pas besoin de AsyncTask.

2
live-love

Il suffit de faire les opérations de base de données dans un thread séparé. Comme ceci (Kotlin):

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

Vous pouvez autoriser l'accès à la base de données sur le thread principal, mais uniquement à des fins de débogage, vous ne devez pas le faire en production.

Voici la raison.

Remarque: Room ne prend pas en charge l'accès à la base de données sur le thread principal, sauf si vous avez appelé allowMainThreadQueries () sur le générateur, car il risque de verrouiller l'interface utilisateur pendant une longue période. Les requêtes asynchrones (requêtes qui renvoient des instances de LiveData ou Flowable) sont exemptées de cette règle car elles exécutent la requête de manière asynchrone sur un thread en arrière-plan en cas de besoin.

1
Nilesh Rathore

Vous pouvez utiliser Future et Callable. Ainsi, vous ne seriez pas obligé d'écrire un long asyncte et pourrez effectuer vos requêtes sans ajouter allowMainThreadQueries ().

Ma requête Dao: -

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

Ma méthode de dépôt: -

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

Pour des requêtes rapides, vous pouvez permettre à Room de l'exécuter sur le thread d'interface utilisateur.

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

Dans mon cas, je devais trouver sur la liste des utilisateurs cliqués dans la base de données ou non. Sinon, créez l'utilisateur et démarrez une autre activité

       @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