Download - Softshake 2013 Apiness SA 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