Softshake apiness l'envers du décor

Post on 13-Apr-2017

308 views 3 download

Transcript of Softshake apiness l'envers du décor

L’envers du décor24 Octobre 2013

Apiness SA

Laurent Kohler & Nicolas Marfurt

www.apiness-software.ch

Le décor

Services Web

Echange de données

Services Web

Echange de données

Back-end Web

Gestion du contenu de l’application(data model «business»)• Multilingue• Multi-utilisateurs• Gestion des autorisations

- par fonction- sur la donnée

Web services de synchronisation

Application mobile

Outil d’affichage de données

Fonctionne également hors ligne

Structure et design simple

Synchronisation des données- robuste- performante

Effort de développement

Effort de développement

Effort de développement

Un client demande «une petite application mobile multi-plateformes»

➡ aspect visuel et fonctionnel

➡ mais aussi développement important :- back-end Web- synchronisation

Situation «typique»

L’envers du décor

Web server & Web services

Persistance données côté client mobile

- Modèle objet (Core Data)

- Modèle relationnel (SQLite natif)

- Autre (XML, JSON, plist...)

Options

Contexte: back-end

LAMP

CMS Joomla

Données «structurées», modèle relationnel (MySQL)

Projet d’exemple Audio Guide

Modèle relationnelCorrespond au type de modèle du back-end

SQLite (sans Core Data)Optimisation du SQL et des paramètres db

Contexte: application iOSProjet d’exemple Audio Guide

Modèle de données

Modèle «server» != modèle «app»Exemples :

• Serveur - Données multilingue - Gestion des autorisations

• Application - Une langue sélectionnée

Au travail !

iOS - Cocoa

Options d’accès à SQLite

Options d’accès à SQLite

• Utilisation de Core Data

Options d’accès à SQLite

• Utilisation de Core Data

Options d’accès à SQLite

• Utilisation de Core Data

• Utilisation directe de la libraire SQLite native➡ Fonctions C

Options d’accès à SQLite

• Utilisation de Core Data

• Utilisation directe de la libraire SQLite native➡ Fonctions C

• Wrapper ou framework existant➡ FMDB

Options d’accès à SQLite

• Utilisation de Core Data

• Utilisation directe de la libraire SQLite native➡ Fonctions C

• Wrapper «maison»

• Wrapper ou framework existant➡ FMDB

- raison historique, plaisir d’explorer- mainmise sur le code (tests)

Options d’accès à SQLite

• Utilisation de Core Data

• Utilisation directe de la libraire SQLite native➡ Fonctions C

• Wrapper «maison»

• Wrapper ou framework existant➡ FMDB

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

MyProjectDatabaseController

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

MyProjectDatabaseController

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

MyClassResultSetMyProjectDatabaseController

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

MyProjectDatabaseController MyClassResultSet

Architecture

APSSQLite

SQLite

APSSQLiteDatabase APSSQLiteResultSetAPSSQLiteRequest

APSSQLiteDatabaseController APSSQLiteObjectPersistingprotocol

APSSQLiteObjectCodingprotocol

MyProjectDatabaseController MyClassMyClassResultSet

Intégration

Contrôleur Modèle

MyViewController

MyClassResultSet

MyClass

APSSQLite

MyP

roje

ctDa

taba

seC

ontro

ller

Demo

Appel SQLite

- (BOOL)executeCUDRequest:(APSSQLiteRequest *)request error:(NSError *__autoreleasing *)error {! BOOL success = YES;

! sqlite3_stmt *statement; int rc = sqlite3_prepare(self.db, [request.query UTF8String], -1, &statement, NULL); ! if(rc == SQLITE_OK) { rc = sqlite3_step(statement); ! ! if(rc != SQLITE_DONE) { *error = [[self class] errorWithCode:rc message:[NSString stringWithUTF8String:sqlite3_errmsg(self.db)]];! ! ! success = NO;! ! }! ! sqlite3_finalize(statement);! } else { *error = [[self class] errorWithCode:rc message:[NSString stringWithUTF8String:sqlite3_errmsg(self.db)]];! ! success = NO;! } ! return success;}

APSSQLiteDatabase

Appel SQLite

- (BOOL)executeCUDRequest:(APSSQLiteRequest *)request error:(NSError *__autoreleasing *)error {! BOOL success = YES;

! sqlite3_stmt *statement; int rc = sqlite3_prepare(self.db, [request.query UTF8String], -1, &statement, NULL); ! if(rc == SQLITE_OK) { rc = sqlite3_step(statement); ! ! if(rc != SQLITE_DONE) { *error = [[self class] errorWithCode:rc message:[NSString stringWithUTF8String:sqlite3_errmsg(self.db)]];! ! ! success = NO;! ! }! ! sqlite3_finalize(statement);! } else { *error = [[self class] errorWithCode:rc message:[NSString stringWithUTF8String:sqlite3_errmsg(self.db)]];! ! success = NO;! } ! return success;}

APSSQLiteDatabase

- (BOOL)executeCUDRequest:(APSSQLiteRequest *)request error:(NSError *__autoreleasing *)error {! BOOL success = YES;

! sqlite3_stmt *statement; int rc = sqlite3_prepare(self.db, [request.query UTF8String], -1, &statement, NULL); ! if(rc == SQLITE_OK) { rc = sqlite3_step(statement); ! ! if(rc != SQLITE_DONE) { *error = [[self class] errorWithCode:rc message:[NSString stringWithUTF8String:sqlite3_errmsg(self.db)]];! ! ! success = NO;! ! }! ! sqlite3_finalize(statement);! } else { *error = [[self class] errorWithCode:rc message:[NSString stringWithUTF8String:sqlite3_errmsg(self.db)]];! ! success = NO;! } ! return success;}

Exécution d’une requête

+ (TopicResultSet *)fetchAll { __block TopicResultSet *resultSet = nil; [[GuideDatabaseController sharedDatabaseController] performBlockAndWait:^(APSSQLiteDatabase *database) { NSString *query = @"SELECT * FROM audio_topic t WHERE t.isPublished = 1 ORDER BY t.sequence"; APSSQLiteRequest *request = [APSSQLiteRequest requestWithQuery:query]; request.resultClass = [TopicResultSet class]; NSError *error = nil; resultSet = [database executeFetchRequest:request error:&error]; // Handle error... }]; return resultSet;}

TopicResultSet : APSSQLiteResultSet

Exécution d’une requête

+ (TopicResultSet *)fetchAll { __block TopicResultSet *resultSet = nil; [[GuideDatabaseController sharedDatabaseController] performBlockAndWait:^(APSSQLiteDatabase *database) { NSString *query = @"SELECT * FROM audio_topic t WHERE t.isPublished = 1 ORDER BY t.sequence"; APSSQLiteRequest *request = [APSSQLiteRequest requestWithQuery:query]; request.resultClass = [TopicResultSet class]; NSError *error = nil; resultSet = [database executeFetchRequest:request error:&error]; // Handle error... }]; return resultSet;}

TopicResultSet : APSSQLiteResultSet

+ (TopicResultSet *)fetchAll { __block TopicResultSet *resultSet = nil; [[GuideDatabaseController sharedDatabaseController] performBlockAndWait:^(APSSQLiteDatabase *database) { NSString *query = @"SELECT * FROM audio_topic t WHERE t.isPublished = 1 ORDER BY t.sequence"; APSSQLiteRequest *request = [APSSQLiteRequest requestWithQuery:query]; request.resultClass = [TopicResultSet class]; NSError *error = nil; resultSet = [database executeFetchRequest:request error:&error]; // Handle error... }]; return resultSet;}

Affichage des données

- (void)viewDidLoad { [super viewDidLoad]; self.topicResultSet = [TopicResultSet fetchAll];}

MyViewController : UITableViewController

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.topicResultSet count];}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TopicTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TopicListViewControllerTopicCell]; NSDictionary *topic = self.topicResultSet[indexPath.row]; cell.textLabel.text = topic[TopicColumnTitle]; cell.detailTextLabel.text = topic[TopicColumnDescription]; return cell;}

@interface TopicListViewController ()

@property (strong, nonatomic) TopicResultSet *topicResultSet;

@end

Affichage des données

- (void)viewDidLoad { [super viewDidLoad]; self.topicResultSet = [TopicResultSet fetchAll];}

MyViewController : UITableViewController

@interface TopicListViewController ()

@property (strong, nonatomic) TopicResultSet *topicResultSet;

@end

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.topicResultSet count];}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TopicTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TopicListViewControllerTopicCell]; return cell;}

Affichage des données

- (void)viewDidLoad { [super viewDidLoad]; self.topicResultSet = [TopicResultSet fetchAll];}

MyViewController : UITableViewController

@interface TopicListViewController ()

@property (strong, nonatomic) TopicResultSet *topicResultSet;

@end

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.topicResultSet count];}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TopicTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TopicListViewControllerTopicCell]; return cell;}

Topic *topic = [self.topicResultSet topicForRowAtIndex:indexPath.row]; cell.textLabel.text = topic.title; cell.detailTextLabel.text = topic.topicDescription;

Base de données

• Mises à jour fréquentes mais en petit volume (news)

➡ Intégralité des données pas nécessaire

• Mises à jour en masse (catalogue produit)

• Installation en masse, mises à jour de petit volume (audio guide)

Scénarios d’évolution des données

ContexteProjet d’exemple Audio Guide

• Installation de contenu type «audio guide musée»

- Texte, images, audio

- Création par volume de 100 à 1000 éléments environ

- Mise à jour sporadique et en petite quantité

Approches

• Option 1 : «naïve» (la plus simple)

• Option 2 : «typique SQLite» (transaction)

• Option 3 : «insertion en masse»

• Option 4 : «insertion en masse» + transaction

2 tables relativement petites

- table parent : 12 colonnes (int, float, varchar)

- table détail : 7 colonnes (int, varchar), foreign key parent

Mesure

- nombre d’insertions par seconde

- données de référence : 1000 parents et 2000 détails

Contexte

Insertion ligne par ligne

Option 1

FOR EACH ROW

EXEC:INSERT INTO TBL (c1,c2...) SELECT 123, ‘abc’,...

Insertion ligne par ligne

Option 1

FOR EACH ROW

EXEC:INSERT INTO TBL (c1,c2...) SELECT 123, ‘abc’,...

insertions /sec

Insertion ligne par ligne

Option 1

FOR EACH ROW

EXEC:INSERT INTO TBL (c1,c2...) SELECT 123, ‘abc’,...

insertions /sec

Trop lent !

Option 2

BEGIN TRANSACTION

FOR EACH ROW

EXEC:INSERT INTO TBL (c1,c2...) SELECT 123, ‘abc’,...

COMMIT (or rollback)

Insertion ligne par ligne…

dans une transaction

Option 2

BEGIN TRANSACTION

FOR EACH ROW

EXEC:INSERT INTO TBL (c1,c2...) SELECT 123, ‘abc’,...

COMMIT (or rollback)

Insertion ligne par ligne…

dans une transaction

insertions /sec

Option 2

BEGIN TRANSACTION

FOR EACH ROW

EXEC:INSERT INTO TBL (c1,c2...) SELECT 123, ‘abc’,...

COMMIT (or rollback)

Insertion ligne par ligne…

dans une transaction

2.5x plus rapide !

insertions /sec

Option 3

EXEC:INSERT INTO TBL (c1,c2) VALUES (123, ‘Sqlite’),(345, ‘Soft’),(678, ‘Shake’);

Insertion de plusieurs lignes en une commande insert

Option 3

EXEC:INSERT INTO TBL (c1,c2) VALUES (123, ‘Sqlite’),(345, ‘Soft’),(678, ‘Shake’);

SQLite ne connait pas insert many values !(dépend des versions de SQLite…)

Insertion de plusieurs lignes en une commande insert

Option 3 - syntaxe SQLite

EXEC:INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’UNION SELECT 345, ‘Soft’UNION SELECT 678, ‘Shake’;

Insertion de plusieurs lignes en une commande insert

Option 3 - syntaxe SQLite

EXEC:INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’UNION SELECT 345, ‘Soft’UNION SELECT 678, ‘Shake’;

Limite SQLite :

au maximum 500 union select !

Insertion de plusieurs lignes en une commande insert

Option 3 - syntaxe SQLiteInsertion par paquets de 500 lignes

FOR EACH ROW BUILD INSERT STATEMENT WHEN «ENOUGH» EXEC: INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’ UNION SELECT 345, ‘Soft’ UNION SELECT 678, ‘Shake’;

Option 3 - syntaxe SQLiteInsertion par paquets de 500 lignes

FOR EACH ROW BUILD INSERT STATEMENT WHEN «ENOUGH» EXEC: INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’ UNION SELECT 345, ‘Soft’ UNION SELECT 678, ‘Shake’;

Code un peu pluscomplexe

Option 4Insertion par paquets de 500 lignes…dans une transactionBEGIN TRANSACTION

FOR EACH ROW BUILD INSERT STATEMENT WHEN «ENOUGH» EXEC: INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’ UNION SELECT 345, ‘Soft’ UNION SELECT 678, ‘Shake’;

COMMIT (or rollback)

Option 4Insertion par paquets de 500 lignes…dans une transaction

Insertion «rapide»de nombreuses lignes

BEGIN TRANSACTION

FOR EACH ROW BUILD INSERT STATEMENT WHEN «ENOUGH» EXEC: INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’ UNION SELECT 345, ‘Soft’ UNION SELECT 678, ‘Shake’;

COMMIT (or rollback)

Comparaison des 4 options

insertions /sec

Comparaison des 4 options

Option 4 45x plus rapide que l’option 11500 insertions / seconde sur iPad 3600 insertions / seconde sur iPhone 4

insertions /sec

Option 4Insertion par paquets de 500 lignes…dans une transaction

Performance satisfaisante dans le contexteAu prix d’un code un peu plus complexe

BEGIN TRANSACTION

FOR EACH ROW BUILD INSERT STATEMENT WHEN «ENOUGH» EXEC: INSERT INTO TBL (c1,c2) SELECT 123, ‘Sqlite’ UNION SELECT 345, ‘Soft’ UNION SELECT 678, ‘Shake’;

COMMIT (or rollback)

Optionadoptée !

• Coder «if exists update else insert»

• Long à développer, long à exécuter

• Update forcément itératif avec SQLite (1 by 1)

Insert et Update

• Approche SQLite efficace : INSERT OR REPLACE INTO...

➡ Si validation intégrité référentielle (foreign key)

Différer le contrôle au moment du commit :PRAGMA defer_foreign_keys = ON

Optionadoptée !

• Parfait pour des données provenant à 100% du serveur

• Attention si certaines colonnes sont maintenues localement !

• Le «insert or replace» modifie toutes les colonnes

➡ Option possible :

Utiliser un sub-select pour obtenir la valeur locale

Insert et Replace

Insert et Replace - exemple

EXEC:INSERT INTO news (newsId, newsTitle,newsDesc, hasBeenRead) SELECT 123,‘SoftShake 2013’,’24 et 25 octobre 2013’,(SELECT hasBeenRead FROM news WHERE newsId = 123)UNION SELECT ....

Insert et Replace - exemple

EXEC:INSERT INTO news (newsId, newsTitle,newsDesc, hasBeenRead) SELECT 123,‘SoftShake 2013’,’24 et 25 octobre 2013’,(SELECT hasBeenRead FROM news WHERE newsId = 123)UNION SELECT ....

workaround !30% plus lent.

• Lorsque certaines colonnes sont maintenues localement

• Data model :

- une table pour les données «serveur»

- une table pour les données «locale»

- une view pour accéder aux 2 facilement

• Pas de perte de performance

• Approche robuste

Insert et Replace - commentaires

Optionadoptée !

• Côté serveur

- «soft delete» (flag data inactive)

• Côté application

- delete physique (à la fin de la synchronisation)

Delete

• SQLite est comparable aux autres base de données

• Après de nombreux insert/update/delete, besoin de :

- Récupérer l’espace avec : VACUUM

Réorganisation DB

• A l’ancienne; «une fois par semaine» ?

• Souvent; après chaque synchro ?

• Lorsque jugé nécessaire ?

- Evaluer les pages vides avec PRAGMA freelist_count

- Si «nombreuses» pages inutilisées : effectuer le «vacuum»

Quand réorganiser ?

• A l’ancienne; «une fois par semaine» ?

• Souvent; après chaque synchro ?

• Lorsque jugé nécessaire ?

- Evaluer les pages vides avec PRAGMA freelist_count

- Si «nombreuses» pages inutilisées : effectuer le «vacuum»

Quand réorganiser ?

vacuum est assez rapidesurtout s’il est effectué avant que la db soit totalement désorganisée

Important de le faire, «peu importe» quand !

Options SQLite• Journal mode

• Mode «delete» par défaut

• Attention au mode Write Ahead Log (WAL)

• Meilleure concurrence d’accès

• Meilleure performance «maintenant»...

- Car une partie du travail différée à plus tard !

- Sur device mobile, comportement potentiellement ennuyeux

• SQLite est excellent et très performant !

• En définissant bien les indexes, pas de soucis de performance du côté des select

• Utiliser EXPLAIN pour analyser ce que fait SQLite

Pour conclure

Merci à vous tous pour votre attention !

NoSQL

?

Cor

e Da

ta ?

SQLi

te ?

Back

-end

W

eb?

Discussion

BACKUP SLIDES

Backend web - gestion des données

Outils existants (connus)

• Contraintes par rapport au data model

• (Très) rapide pour des fonctions simples

• Risque d’atteindre une impasse

Développement «sur mesure»

• trop long, trop coûteux

Backend web - gestion des données

• «middle-tier» commun qui génère le html en fonction de paramètres et accès aux données par procédures stockées

• Code PHP stable, on génère/écrit seulement le sql

• Rapide

• Optimisation des stored procedures toujours possible

• Outil «maison» : JEdit

• Développement par Apiness (Marc Perroulaz, L.Kohler)

Gestion des données - nos choix

• Version SQLite

• - iOS 6: version 3.7.13

• - iOS 7.0: version 3.7.13

• Options Core Data

• Utilisation du mode WAL par défaut depuis iOS 7

• Possibilité de passer des options SQLite à Core Data, entre autre le mode de journalisation:

• @{ NSSQLitePragmasOption : @{@"journal_mode" : @"DELETE"} }

• Trace Core Data (iOS 7 / sans analyse)

• - pragma journal_mode=wal

• - pragma cache_size=200

• - pragma page_count

• - pragma freelist_count

Core Data - iOS 7 - Sqlite