Optimisation: appeler son modèle au fur et à  mesure d'un affichage

muqaddarmuqaddar Administrateur
Dans un projet j'ai une grosse requête SQL bien compliquée dans un modèle.
Cette requête peut charger jusqu'à  1000 lignes.

Dans la boucle qui suit cette requête (qui va donc boucler 1000 fois), j'ai une autre requête simple. Je sais, les requêtes imbriquées c'est le mal, mais parfois on ne peut pas faire autrement vu la complexité des informations qu'on souhaite obtenir.

Ma question: ne vaut-il pas mieux appeler la requête de la boucle lors de l'affichage des éléments ?
Cas typique: une UITableView.

A quoi bon demander tout de suite les 1000 requêtes, si on affiche que 20 lignes en même temps dans sa table ? Pourquoi ne pas demander la requête simple au moment de l'affichage (toujours en appelant le modèle évidemment) ?

Si on ne scroll pas, ces requêtes ne seront jamais appelées. Evidement, au moment du scroll on va perdre un peu de temps de calcul, en plus de l'affichage, mais n'est-ce pas mieux ?

Réponses

  • AliGatorAliGator Membre, Modérateur
    juin 2011 modifié #2
    Si tu fais les requêtes SQL lors de l'affichage, le scroll de ta tableView va être très lent.

    Par contre, tu peux faire du prefetching en background.
    Par exemple typiquement tu prévois un NSArray de 1000 éléments prêt à  contenir tes résultats de tes 1000 requêtes.

    Puis une fois ta requête principale exécutée, tu boucles sur tes 1000 résultats de cette requête principale, et pour chacun, tu construits ta requête SQL secondaire correspondante, et tu la lances dans une NSOperation asynchrone. Chaque opération asynchrone va exécuter sa requête SQL puis récupérer le résultat (et construire un NSObject pour le représenter) et stocker ce résultat dans ton NSArray. Bon il faut prévoir :
    • Une valeur tampon dans le NSArray pour quand le résultat n'est pas encore disponible (car la NSOperation correspondante ne s'est pas encore exécutée). Tu peux par exemple stocker [NSNull null] en attendant. Ou même stocker la NSOperation qui va servir à  récupérer le résultat, comme ça si tu récupères une NSOperation tu sauras que le résultat n'est pas encore dispo, mais tu pourras aussi augmenter la priorité de ladite opération ou autre.
    • Penser à  protéger les accès au NSArray* subresults par des @synchronized.

    // ivars à  initialiser dans ton init (et releaser dans ton dealloc):<br />// NSMutableArray* subresults<br />// NSOperationQueue* queue<br /><br />[subresults removeAllObjects];<br />int i=0;<br />while(sqlite3_step(parent_stmt) == SQLITE_ROW) {<br />&nbsp; // Récupérer tous les éléments de l&#039;enregistrement qui te seront nécessaires pour la requête secondaire<br />&nbsp; // dans une variable locale (avant qu&#039;on ne fasse avancer le curseur avec le prochain sqlite3_step et qu&#039;ils soient perdus),<br />&nbsp; // ces variables locales seront capturées par le block ci-après donc même si le NSOperationBlock s&#039;exécute plus tard<br />&nbsp; // et qu&#039;on a eu le temps de faire d&#039;autres itérations de ce while entre temps, grace à  la magie des blocks autocapturants ça va marcher.<br />&nbsp; int wineID = sqlite3_column_int(parent_stmt,0);<br />&nbsp; int wineryID = sqlite3_column_int(parent_stmt,1);<br /><br />&nbsp; // Créer la NSOperation qui va exécuter la requête secondaire<br />&nbsp; NSOperation* op = [NSOperation blockOperationWithBlock:^{<br />&nbsp; &nbsp; // Ici, on crée la requête secondaire à  partir des infos de l&#039;enregistrement de la requête principale<br />&nbsp; &nbsp; sqlite3_stmt* subStmt;<br />&nbsp; &nbsp; sqlite3_prepare_v2(db,&quot;SELECT * FROM wines WHERE wineID=? AND winery=?&quot;,-1,&amp;subStmt,NULL);<br />&nbsp; &nbsp; sqlite3_bind_int(subStmt,0,wineID); // variable locale externe au block mais capturée par copie par ce dernier<br />&nbsp; &nbsp; sqlite3_bind_int(subStmt,1,wineryID); // variable locale externe au block mais capturée par copie par ce dernier<br />&nbsp; &nbsp; // Puis on exécute la requête<br />&nbsp; &nbsp; sqlite3_step(subStmt);<br />&nbsp; &nbsp; // Et enfin on construit le résultat pour le stocker dans le NSArray<br />&nbsp; &nbsp; int year = sqlite3_column_int(subStmt,1);<br />&nbsp; &nbsp; double note = sqlite3_column_double(subStmt,2);<br />&nbsp; &nbsp; ....<br />&nbsp; &nbsp; WineInfo* w = [WineInfo wineInfoWithYear:year note:note ....];<br />&nbsp; &nbsp; [self setWineInfo:w forWineAtIndex:i];<br />&nbsp; }];<br />&nbsp; [subresults addObject:op]; // en attendant d&#039;avoir le résultat, on garde la NSOperation dans le tableau à  la place, ça peut être utile<br />&nbsp; [queue addOperation:op];<br />&nbsp; ++i; // On passe à  l&#039;enregistrement suivant de parentStmt<br />}<br /><br />-(void)setWineInfo:(WineInfo*)w forWineAtIndex:(int)idx {<br />&nbsp; // ici il serait bon de faire les vérification d&#039;usage pour éviter un OutOfBounds mais bon<br /><br />&nbsp; // synchronized pour éviter les acces concurrents entre les NSOperations et le thread principal<br />&nbsp; @synchronized(self) {<br />&nbsp; &nbsp; [subresults replaceObjectAtIndex:idx withObject:w];<br />&nbsp; }<br />}<br />-(WineInfo*)wineInfoAtIndex:(int)idx {<br />&nbsp; // ici il serait bon de faire les vérification d&#039;usage pour éviter un OutOfBounds mais bon<br /><br />&nbsp; // synchronized pour éviter les acces concurrents entre les NSOperations et le thread principal<br />&nbsp; @synchronized(self) {<br />&nbsp; &nbsp; id obj = [[[subresults objectAtIndex:idx] retain] autorelease];<br />&nbsp; &nbsp; if ([obj isKindOfClass:[NSOperation class]]) {<br />&nbsp; &nbsp; &nbsp; NSOperation* op = (NSOperation*)obj;<br />&nbsp; &nbsp; &nbsp; // info pas encore prête, on peut alors par exemple augmenter sa priorité<br />&nbsp; &nbsp; &nbsp; [op setQueuePriority:NSOperationQueuePriorityVeryHigh];<br />&nbsp; &nbsp; &nbsp; // puis par exemple demander à  recharger la tableView quand l&#039;opération sera terminée et le résultat prêt<br />&nbsp; &nbsp; &nbsp; [op setCompletionBlock:^{ [self.tableView reloadData]; }];<br />&nbsp; &nbsp; &nbsp; return nil;<br /><br />&nbsp; &nbsp; &nbsp; /* Alternative (déconseillée car bloquante) :<br />&nbsp; &nbsp; &nbsp;  * on peut simplement blocker le code jusqu&#039;à  ce que l&#039;opération soit prête<br />&nbsp; &nbsp; &nbsp;  * Attention dans ce cas à  bien mettre ce waitUntilFinished à  l&#039;extérieur du @synchronized pour éviter un deadlock !!<br />&nbsp; &nbsp; &nbsp;  * // [op waitUntilFinished]; // à  sortir du @synchronized(self) !!!<br />&nbsp; &nbsp; &nbsp;  * // re-récupérer la nouvelle valeur dans le tableau et la retourner<br />&nbsp; &nbsp; &nbsp;  * // return [[[subresults objectAtIndex:idx] retain] autorelease];<br />&nbsp; &nbsp; &nbsp;  */<br />&nbsp; &nbsp; } else {<br />&nbsp; &nbsp; &nbsp; // sinon c&#039;est bon on a déjà  le vrai WineInfo résultat dans le tableau, on le retourne direct.<br />&nbsp; &nbsp; &nbsp; return obj;<br />&nbsp; &nbsp; }<br />&nbsp; }<br />}
    
    Bon j'ai rien testé de tout ça, j'ai tout tapé en live donc je te laisse vérifier.
  • muqaddarmuqaddar Administrateur
    12:15 modifié #3
    Wouah !!!
    Tu me ponds ça juste avant l'heure de l'apéro ! T'es fou !  >:)

    Je crois que j'ai à  peu près compris le principe. Je vais tester ça très prochainement, d'autant plus qu'en fait j'ai peut-être 3 ou 4 sous requêtes et pas 1.

    Pour info, ma requête principale fabrique actuellement un array de dicos dans le modèle. Array qui sera épluché à  la lecture dans l'UITableView. Du classique.

    Utiliser les block ne me gène plus car les prochaines versions de mon app seront iOS 4 et >.
    Merci pour cette grosse suggestion et ce gros bout de code !
Connectez-vous ou Inscrivez-vous pour répondre.