IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Android Discussion :

SQLite et multithread


Sujet :

Android

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Septembre 2011
    Messages
    144
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Septembre 2011
    Messages : 144
    Par défaut SQLite et multithread
    Bonjour,

    J'ai récemment créé un sujet pour une problématique similaire, mais étant donné que j'ai effectué plusieurs changements entre temps et que certains points n'étaient pas clair, j'ai préféré partir sur un nouveau sujet avec une base solide d'explications et de détails. Du coup, le message est un peu long =D




    Côté application/fonctionnement

    Pour décrire le fonctionnement (simplifié et avec seulement la partie qui nous intéresse ici) de l'application, disons que j'ai :
    - Une activité de "démarrage" qui vérifie la connexion internet, certains params, etc.
    Si tout est ok, l'activité se termine et lance l'activité Menu.
    Si la base de donnée est vierge (premier lancement/données applications supprimées), une AsyncTask est lancée pour récupérer la liste des informations à insérer dans la base de données SQLite (pendant ce temps, l'utilisateur est "bloqué" avec un progressDialog). L'activité Menu est ensuite lancé.
    C'est aussi cette activité qui lancera le services de vérification et de mise à jour.

    - Une activité qui sert de Menu qui permet à l'utilisateur de choisir la liste à afficher.
    Il y a parfois quelques insertion sql à faire ici, l'utilisateur est bloqué par un progressDialog en attendant.
    Chaque bouton lancera l'activité "Liste", avec un contenu qui change un peu selon le bouton.

    - Une activité Liste pour afficher, comme son nom l'indique, une liste d'infos issue de la base SQLite.
    Juste au dessus de la liste, il y a un EditText : il permet de filtrer la liste (ce qui signifie donc l'exécution de requêtes SQL à chaque caractères saisis).


    Pour afficher les informations dans la liste, j'ai une classe qui étends CursorAdapter pour extraire les informations et filtrer selon l'EditText. A chaque fois qu'un caractère est saisi, une méthode "filtrer" de l'adapter est appelé et lancera une AsyncTask "FilterTask" (en prenant soin d'annuler le FilterTask précédent s'il y en avait) pour effectuer les requêtes.
    Selon les listes, il peut y avoir une mini-requête (une seule ligne) d'ajout ou de suppression, effectuée dans une AsyncTask spéciale lorsque l'utilisateur aura "touché" un élément de liste.

    Je pense que je vais me faire taper dessus car je n'utilise pas les Loaders / ContentProvider... mais j'ai assez de mal à comprendre comment ils fonctionnent. Donc j'ai essayé de bricoler un système moi même pour que les traitements SQL et les filtres ne tournent pas dans l'UI. Si vous avez des conseils ou des remarques, je suis preneur.





    Côté base de données

    J'ai donc ma classe qui étend SQLiteOpenHelper pour construire la table et pour garder une référence statique sur son instance (singleton). J'ai pu lire que c'était essentiel pour faire du multithread.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class DatabaseHelper extends SQLiteOpenHelper 
    {
     
    	private static DatabaseHelper mHelperInstance = null;
     
    	public static synchronized DatabaseHelper getHelperInstance(Context context) 
    	{
    	    if (mHelperInstance == null) 
    	    {
    	    	mHelperInstance = new DatabaseHelper(context.getApplicationContext());
    	    }
     
    	    return mHelperInstance;	    
    	}
     
     
     
    	public DatabaseHelper(Context context) 
    	{
    		super(context, DATABASE_NAME, null, DATABASE_VERSION);
    	}
     
     
    }
    En ce qui concerne la classe qui contient les méthodes pour effectuer les requêtes SQL, je l'ai modifié pour que l'ouverture et la fermeture de la base fonctionne avec du multithread (je ne sais pas si c'est le meilleur moyen, mais ça a l'air de fonctionner).

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class ClientsSQLite
    { 
     
    	private SQLiteDatabase sqliteDatabase; 
    	private DatabaseHelper databaseHelper;
     
    	private static int nb_instance = 0;
     
     
    	public ClientsSQLite(Context context)
    	{
    		databaseHelper = DatabaseHelper.getHelperInstance(context);
    		this.sqliteDatabase = databaseHelper.getWritableDatabase();
    	}
     
    	public void open()
    	{
    		if (nb_instance == 0)
    		{
    			this.sqliteDatabase = this.databaseHelper.getWritableDatabase();
    		}
     
    		nb_instance++;
    	}
     
     
    	public void close()
    	{
    		nb_instance--;
     
    		if (nb_instance == 0)
    		{
    			this.sqliteDatabase.close();
    		}
    	}
     
    	public void doMachinTruc(String machin, string truc)
    	{
    		this.sqliteDatabase.execSQL("........."); 
    	}
     
    ...
     
    }

    J'ai parfois vu certains bouts de codes qui ne gardaient pas de référence sur SQLiteDatabase, mais seulement sur le DatabaseHelper. Du coup, il n'y avait pas de méthode open/close et à chaque requête, il faut utiliser la méthode getWritableDatabase() pour récupérer une référence.
    Q1 : Qu'en pensez-vous ? Ça n'affecte pas trop les performances vu qu'il faut à chaque fois récupérer une référence + allouer un objet ?







    Côté mise à jour asynchrone des données

    Jusqu'ici, je ne sais pas si c'est "bien fait" ou si on peut mieux faire, mais tout marche bien.
    Ce que je souhaite faire maintenant, c'est qu'un Service se connecte de temps en temps à un serveur web pour savoir si l'application doit mettre à jour sa base SQLite; et si c'est le cas, il faut mettre à jour la table.
    La vérification est totalement transparente, pas besoin d’embêter l'utilisateur pour simplement demander à un serveur de dire "oui ou non" ^^
    Si le serveur répond non, le service se termine, la prochaine vérification se fera quelques jours plus tard.

    Si le serveur répond oui, c'est que les données ont changé : je suis obligé de vider la table, et de faire une insertion de masse.
    La raison principale de cette "vidange" est que le serveur est incapable de me dire "par rapport à ta version, il faut que tu insert ça ça et ça, que tu update ce truc, et que tu delete ces machins". Il ne peut que me dire "insert tout çaaaaaaaaaaaaaaaaaaaaa".
    Donc si une donnée est supprimée du serveur, ou modifiée, je n'ai pas trouvée d'autre solutions que de vider la table SQLite sur Android pour prendre en compte la modification.

    Q2 : Est-ce que vous voyez un autre moyen que de vider et de tout ré-insérer ?

    Q2 bis : A tout hasard, est-ce qu'il existe un petit framework/outils/truc permettant de rendre plus intelligent le serveur ?



    Le service étant asynchrone, et la durée des traitements pouvant varier, il est impossible de prévoir quand les insertions dans la base vont être effectué.
    Cela peut donc être problématique si l'application modifie déjà des données, ou si l'utilisateur filtres la liste (select).

    Pour commencer à contrer ce problème, j'ai d'abord mis en place un singleton (voir code au dessus) pour l'accès à la base de données.
    Vu que ce sont des insertions de masses, j'utilise une seule transaction + requête préparée pour améliorer les performances.
    Les transactions SQLite sont en mode "exclusive" sur Android donc, lorsqu'une transaction démarre, toutes les autres requêtes sont bloquées; que ça soit une modification (update, insert, delete) ou un simple select. Il ne devrait donc pas y avoir de données "corrompues".
    L'insertion de la mise à jour durant quelques secondes, l'utilisateur peut par contre être bloqué lorsqu'il utilise la base de données (s'il filtre la liste par exemple) et il devra attendre la fin de la transaction.

    Alors du coup, pour contrer ce problème qui contrait déjà un problème (faut suivre hein ), j'ai créé une seconde table identique qui n'est utilisée que par le Service. De plus, j'appelle la méthode yieldIfContendedSafely pour éviter que cette transaction soit bloquante. L'insertion dans cette table ne pose ainsi aucun problème et ne verouille donc jamais la base de données (utilisateur non bloqué).
    Ceci étant fait, il faut que je transfère les données de cette table dans l'originale pour que ça puisse être utilisable par l'application. Pour cela, je créé manuellement une transaction qui engloble la "vidange" de la table principale puis la requête "INSERT INTO table_principale SELECT * FROM table_secondaire". Cette transaction est volontairement bloquante car il faut protéger cette section critique, mais c'est extrèmement rapide (moins de 0,5 secondes sur émulateur, à la place de 6 secondes pour l'insertion depuis le JSON).
    Avec tout ce système, la mise à jour se fait totalement en arrière plan, et l'utilisateur n'est pas bloqué (peut-être une fraction de seconde lors de l'INSERT SELECT, mais ce sera surement rare/négligeable). Je pense utiliser le système de notification pour prévenir l'utilisateur lorsque la mise à jour commande (insert dans table secondaire), et lorsque la mise à jour est terminée.

    Q3 : En espérant que vous ayez réussi à comprendre mon explication, qu'en pensez-vous ? Avez-vous des suggestions, des améliorations... ou même un changement radical à y apporter ?

    Q4 : Si l'utilisateur est sur la listeview lorsque la mise à jour est terminée (et que la table secondaire a bien été insérée dans la table principale), la listview n'est pas rafraîchie : il faut quitter l'activité (retour Menu) puis revenir sur la listview. Je suppose que c'est dû au fait que le Cursor utilisé par l'adapter reste inchangé. Comment dire à l'adapter/au Cursor que le contenu de la base a changé, et qu'il faut se "réactualiser" ?





    Côté Service

    Pour l'instant, j'utilise une AsyncTask que je lance manuellement à différent endroits pour effectuer des tests sur la mise à jour. De plus, je n'ai jamais utilisé de Service et il y a encore trop de zones floues pour moi. J'ai donc quelques questions spécifiques aux Services Android :

    Q5 : Tout d'abord, par rapport à mes besoins, je n'arrive pas à déterminer s'il me faut un LocalService ou un RemoteService.
    Il ne faut pas qu'il tourne dans le thread UI, mais il n'a pas non plus besoin d'être utilisé par d'autres applications...

    Q6 : Pour ce qui est du "bind", je pense que je n'en ai pas besoin car le Service ne dépend pas des activités, et l'inverse est vrai aussi. De plus, le service s'arrête lui-même. Est-ce que j'ai bon, ou est-ce qu'il y a une raison que je ne vois pas qui m'obligerait à "binder" le service ?

    Q7 : Est-ce qu'on peut continuer d'éxécuter un Service lorsque l'application est terminée ? Est-ce valable autant pour un RemoteService qu'un LocalService ?

    Q8 : Si oui, lorsque l'application est relancée, est-ce qu'il existe un moyen de savoir si mon service est exécuté (ou non) afin d'éviter de le relancer deux fois ?



    Un grand merci d'avance pour votre aide (j'espère que je n'ai pas trop abusé avec la longueur et les questions).
    Si un point semble flou, n'hésitez pas à me demander plus de détails.

  2. #2
    Expert confirmé

    Homme Profil pro
    Ingénieur systèmes et réseaux
    Inscrit en
    Février 2007
    Messages
    4 253
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Ingénieur systèmes et réseaux
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Février 2007
    Messages : 4 253
    Billets dans le blog
    3
    Par défaut
    Bonjour,

    Q1: Personnellement je préfère gérer mon instance de database au niveau de l'application... On a grosso-modo un SQLiteDatabase statique, initialisé au démarrage de l'application, fermé quand l'application quitte. Tout le monde peut y avoir accès...

    Q2: Si le serveur peut être modifié... alors le mieux est oui d'avoir un truc genre "quelles sont les modification depuis telle date", et recevoir une liste d'objet "detruits", "mis à jour", ou "ajoutés" depuis cette date... Pour une grande quantité de données, cela peut faire une sacrée différence. C'est ce qu'on appelle des mises à jour incrémentales. L'astuce, est donc pour les données coté serveurs: de ne JAMAIS faire de delete (de toute manière, en data management c'est toujours une mauvaise idée), mais de "flaguer" l'objet comme étant détruit, et de conserver deux date: date de création, date de modification... "Détruire" un objet coté serveur, est mettre le flag "deleted" à vrai, et la date de modification à maintenant... "Insérer" coté serveur, est mettre le flag "deleted" à faux, et les dates de création/modification à maintenant.... "Mettre à jour" coté serveur, juste modifier la date de modification.
    Bien sur toutes les requêtes métier coté serveur devront vérifier ce flag deleted...
    Coté client du coup c'est facile, on recoit un objet flaggé "deleted" ? on le détruit... on recoit un objet dont la date de création > date demandée ? on le crée... les autres on les met juste à jour.

    Q2 bis. voir ci dessus.

    Q3 Effectivement, comme la quantité de données peut être importante, il vaut mieux dans ce cas créer un table temporaire (staging) la remplir indépendamment de la table primaire, et à la toute fin... faire un simple switch de tables.

    Q4 Cursor.reload(), adapter.notifyDataSetChanged().... mais il faut que le service signale qu'il a effectué les modifications (broadcast / listeners).

    Q5 LocalService

    Q6 Utiliser même un "IntentService" dans ce cas...

    Q7 Non et non... Un service est un élément à part entière de l'application, sauf qu'au contraire des activités il n'y a pas d'interface... Application ==> (Service(s), Activité(s))

    Q8 De toute manière un service n'existe qu'une fois... startService() ne lancera le service que s'il n'est pas déjà lancé.

  3. #3
    Membre confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Septembre 2011
    Messages
    144
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Septembre 2011
    Messages : 144
    Par défaut
    Pour commencer, je te remercie pour ta réponse rapide.


    Citation Envoyé par nicroman Voir le message
    Personnellement je préfère gérer mon instance de database au niveau de l'application... On a grosso-modo un SQLiteDatabase statique, initialisé au démarrage de l'application, fermé quand l'application quitte. Tout le monde peut y avoir accès...
    J'ai fait la même chose du coup, car ça me semble effectivement mieux (ça évite les ouvertures/fermetures, ça simplifie le multithread...).
    En plus, avant de faire ça, j'ai eu une exception Database locked lors d'un test (le service qui insert dans le secondaire, et une Asynctask qui insère dans une autre table).




    Citation Envoyé par nicroman Voir le message
    Effectivement, comme la quantité de données peut être importante, il vaut mieux dans ce cas créer un table temporaire (staging) la remplir indépendamment de la table primaire, et à la toute fin... faire un simple switch de tables.
    Q1 : Est-ce qu'il vaut mieux créer une "vraie" table temporaire (CREATE TEMP TABLE my_temp_table...), ou utiliser une simple table ?

    Q2 : Si oui, comment fonctionne une table temporaire ? Je veux dire par là, quand est-ce qu'elle est supprimée ?
    Est-ce que c'est automatique, est-ce qu'il faut la supprimer manuellement, la vider... ? Est-ce qu'il faut que je me "méfie" avant de faire le transfert vers la table principale (au cas où elle ait disparu) ... ?
    J'ai essayé, mais à la création, j'ai une erreur "no such table: Temp_Clients".




    Citation Envoyé par nicroman Voir le message
    Cursor.reload(), adapter.notifyDataSetChanged().... mais il faut que le service signale qu'il a effectué les modifications (broadcast / listeners).
    La méthode Cursor.requery (reload n'existe pas) ré-actualise bien la liste, mais elle est marquée comme obsolète (avec comme information : "Don't use this. Just request a new cursor, so you can do this asynchronously and update your list view once the new cursor comes back").

    A chaque fois qu'un caractère est saisie, ma méthode filtrer effectue un select dans la base de données et récupère un nouveau Cursor de manière asynchrone. Elle utilise ensuite la méthode changeCursor (de CursorAdapter) mais ça ne ré-actualise pas ma liste. Je ne vois pas ce qu'entends Google par "just request a new cursor" du coup...

    Q3 : Comment ré-actualiser le Cursor, vu que cette méthode est devenu obsolète ?

    Q4 : Pour ce qui est des signaux, est-ce qu'il est possible que le Service envoie un seul Broadcast qui sera traité différemment selon l'activité qui le reçoit ?
    Car actuellement, je ne vois pas comment le faire (vu qu'on doit créer une classe qui étend BroadcastReceiver et redéfinir onReceive...).

    [EDIT] Je viens de tomber sur ce sujet, je pense que je m'y prends mal... Je vais tester autrement.

    [EDIT 2] Oui c'est bien moi qui m'y suis mal pris... Je peux le recevoir sur deux activités différentes. Bon, reste que une de mes activités reçoit plusieurs fois le broadcast x)



    Citation Envoyé par nicroman Voir le message
    Non et non... Un service est un élément à part entière de l'application, sauf qu'au contraire des activités il n'y a pas d'interface... Application ==> (Service(s), Activité(s))
    J'ai essayé de fermer l'application juste après le lancement de l'IntentService, et celui-ci continu tout de même de s'éxecuter.

    Q5 : Est-ce que c'est normal ? Est-ce que ça peut être "dangereux" ?

    Q6 : Pour le coup, est-ce qu'il faut détecter la fin de l'application pour forcer l'arrêt de l'IntentService, ou on peut le laisser finir son travail ?




    Citation Envoyé par nicroman Voir le message
    De toute manière un service n'existe qu'une fois... startService() ne lancera le service que s'il n'est pas déjà lancé.
    J'ai effectué un test en appelant plusieurs fois à la suite startService (ou en fermant/ré-ouvrant l'application, ça donne la même chose) et l'IntentService est lancé plusieurs fois. Pas en simultané, mais il y a un système de queue qui relancera ce même Service autant de fois que startService a été appelé.

    Q7 : Est-ce qu'on peut éviter ça ?




    Q8 : Dans un IntentService, je dois aussi appeler stopSelf (ou cette "extension" gère l'arrêt soi-même) ?
    A quoi servent les autres méthode stop*** ?

Discussions similaires

  1. SQLite et MultiThreading (via Wrapper C++)
    Par Aymerik dans le forum SQLite
    Réponses: 8
    Dernier message: 17/05/2010, 09h55
  2. qui connait sqlite ?
    Par Emmanuel Lecoester dans le forum SQLite
    Réponses: 23
    Dernier message: 19/02/2010, 13h44
  3. [WinAPI C++] MultiThreading?
    Par Gruik dans le forum Windows
    Réponses: 2
    Dernier message: 25/03/2004, 00h08
  4. [Win32]App multithread
    Par billyboy dans le forum Windows
    Réponses: 5
    Dernier message: 25/09/2003, 09h57
  5. Multithreading sous HP Ux 11
    Par pykoon dans le forum Autres éditeurs
    Réponses: 1
    Dernier message: 18/10/2002, 23h36

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo