Storage

  • Raccourci de la révision : Storage
  • Titre de la révision : Storage
  • ID de la révision : 116240
  • Créé :
  • Créateur : BenoitL
  • Version actuelle ? Non
  • Commentaire Relecture

Contenu de la révision

Storage est une API de base de données dans Firefox 2 et suivantes reposant sur sqlite. Elle est disponible aux appels privilégiés, c'est-à-dire à du code chrome et aux extensions, mais pas aux pages Web. Elle est toujours considérée comme en cours de développement, ce qui signifie que les API peuvent être modifiées à n'importe quel moment. Il se peut en effet que l'API soit légèrement modifiée entre Firefox 2 alpha 2 et Firefox 2, et également entre Firefox 2 et Firefox 3.

Storage peut parfois être confondu avec la fonctionnalité WHATWG DOM storage de Firefox 2 qui permet à des pages Web d'enregistrer des données permanentes. L'API Storage s'adresse uniquement aux auteurs d'extensions et aux composants de Firefox.

Ce document traite de l'API mozStorage et de quelques particularités de sqlite. Il ne traite pas du SQL ou de l'utilisation de sqlite. Pour ces autres informations, vous devrez consulter vos références favorites sur SQL. Vous pouvez également consulter la documentation sur sqlite et particulièrement celle sur la compréhension du langage de requêtes compris par sqlite. Pour obtenir plus d'aide sur l'API mozStorage, vous pouvez poster sur le serveur de news mozilla.dev.apps.firefox sur news.mozilla.org. Pour signaler des bogues, utilisez Bugzilla (product « Toolkit », component « Storage »).

Consultez Storage:Performance pour optimiser les performances de connexion de votre base de données.

SQLite Database Browser est un outil libre, disponible sur plusieurs plateformes, permettant l'examen de bases de données existantes et le test d'instructions SQL.

Préambule

mozStorage se présente comme n'importe quelle autres système de bases de données. La procédure complète d'utilisation est la suivante :

  • Ouverture d'une connexion vers la base de données de votre choix.
  • Création d'une requête à exécuter sur la connexion.
  • Liaison de paramètres à la requête si nécessaire.
  • Exécution de la requête.
  • Réinitialisation de la requête.

Ouverture d'une connexion

La première initialisation du service Storage doit se faire dans le thread principal. Vous obtiendrez une erreur en voulant l'initialiser dans un autre thread. Vous pouvez toutefois utiliser le service depuis un thread en appelant la méthode getService depuis le thread principal pour vérifier que le service a bien été créé.

Voici un exemple C++ d'ouverture d'une connexion vers « asdf.sqlite » dans le répertoire du profil de l'utilisateur :

nsCOMPtr<nsIFile> dbFile;
rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                            getter_AddRefs(dbFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = dbFile->Append(NS_LITERAL_STRING("asdf.sqlite"));
NS_ENSURE_SUCCESS(rv, rv);

mDBService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBService->OpenDatabase(dbFile, getter_AddRefs(mDBConn));
NS_ENSURE_SUCCESS(rv, rv);

MOZ_STORAGE_SERVICE_CONTRACTID est défini dans {{template.Source("storage/build/mozStorageCID.h")}}. Sa valeur est "@mozilla.org/storage/service;1".

Voici un exemple en JavaScript :

var file = Components.classes["@mozilla.org/file/directory_service;1"]
                     .getService(Components.interfaces.nsIProperties)
                     .get("ProfD", Components.interfaces.nsIFile);
file.append("asdf.sqlite");

var storageService = Components.classes["@mozilla.org/storage/service;1"]
                        .getService(Components.interfaces.mozIStorageService);
var mDBConn = storageService.openDatabase(file);
Note : la fonction openDatabase risque d'être modifiée à l'avenir. Elle sera probablement améliorée et simplifiée pour réduire les difficultés d'utilisation.

Il est déconseillé de nommer votre base de données avec une extension en « .sdb » pour sqlite database. En effet, Windows reconnaît cette extension comme une « base de données de compatibilité des applications » et les modifications sont inscrites dans la fonctionnalité de restauration système. Cela peut donc ralentir fortement les opérations sur le fichier.

Création d'une requête

Il existe deux méthodes pour créer une requête. Si vous n'avez aucun paramètre et si la requête ne renvoie aucune valeur, utilisez mozIStorageConnection.executeSimpleSQL.

C++ :
rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("CREATE TABLE foo (a INTEGER)"));

JS :
mDBConn.executeSimpleSQL("CREATE TABLE foo (a INTEGER)");

Autrement, vous devrez préparer une requête en utilisant mozIStorageConnection.createStatement :

C++ :
nsCOMPtr<mozIStorageStatement> statement;
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM foo WHERE a = ?1"),
                              getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);

JS :
var statement = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1");

Cet exemple utilise un sélecteur « ?1 » comme paramètre qui sera renseigné ultérieurement (voir la section suivante).

Après avoir préparé une requête, vous pouvez lui lier des paramètres, l'exécuter et la réinitialiser autant de fois que vous le souhaitez. Si vous devez faire une requête fréquemment, l'utilisation d'une requête précompilée augmentera les performances de manière significative car la requête SQL n'aura pas à être traitée à chaque fois.

Si vous êtes familier avec sqlite, vous devez savoir que les requêtes préparées deviennent invalides lorsque la structure de la base de données est modifiée. Heureusement, mozIStorageStatement détecte l'erreur et recompile la requête si nécessaire. Ainsi, après avoir créé une requête, vous n'avez pas à vous soucier d'une modification de structure ; toutes les requêtes continueront à fonctionner de manière transparente.

Liaison de paramètres

Il est généralement préférable de lier tous les paramètres séparément plutôt que d'essayer de construire à la volée des chaînes SQL contenant ces paramètres. Entre autres choses, ce mode de fonctionnement permet d'éviter une attaque par injection SQL puisque les paramètres liés ne sont jamais exécutés en SQL.

Les sélecteurs inclus dans la requête sont liés aux paramètres. Les sélecteurs sont indexés en commençant par « ?1 », puis « ?2 », et ainsi de suite. Vous devez utiliser les fonctions BindXXXParameter(0), BindXXXParameter(1), etc. pour lier ces sélecteurs.

Attention : les indices des sélecteurs débutent à partir de 1. Les entiers passés aux fonctions de liaison débutent à partir de 0. Cela signifie que « ?1 » correspond au paramètre 0, « ?2 » correspond au paramètre 1, etc.

Un sélecteur peut apparaître plusieurs fois dans la chaîne SQL et tous les instances seront remplacées par la valeur liée. Les paramètres non liés seront interprétés comme NULL.

Les fonctions de liaison disponibles dans mozIStorageStatement (voir {{template.Source("storage/public/mozIStorageStatement.idl")}}) sont :

  • bindUTF8StringParameter(in unsigned long aParamIndex, in AUTF8String aValue)
  • bindStringParameter(in unsigned long aParamIndex, in AString aValue)
  • bindDoubleParameter(in unsigned long aParamIndex, in double aValue)
  • bindInt32Parameter(in unsigned long aParamIndex, in long aValue)
  • bindInt64Parameter(in unsigned long aParamIndex, in long long aValue)
  • bindNullParameter(in unsigned long aParamIndex)
  • bindBlobParameter(in unsigned long aParamIndex, {{mediawiki.external('array,const,size_is(aValueSize)')}} in octet aValue, in unsigned long ValueSize) (pour des données binaires)

Exemple C++ :

nsCOMPtr<mozIStorageStatement> statement;
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM foo WHERE a = ?1 AND b > ?2"),
                              getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->BindUTF8StringParameter(0, "bonjour"); // "bonjour" sera substitué à "?1"
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->BindInt32Parameter(1, 1234); // 1234 sera substitué à "?2"
NS_ENSURE_SUCCESS(rv, rv);

Exemple Javascript :

var statement = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1 AND b > ?2");
statement.bindUTF8StringParameter(0, "bonjour");
statement.bindInt32Parameter(1, 1234);

Exécution d'une requête

La principale manière d'exécuter une requête est d'utiliser la fonction mozIStorageStatement.executeStep. Cette fonction vous permet de récupérer chaque ligne produite par la requête en vous notifiant lorsqu'il n'y a plus de résultats.

Après un appel de executeStep, vous récupérez les données par une des fonctions de mozIStorageValueArray (voir {{template.Source("storage/public/mozIStorageValueArray.idl")}}). mozIStorageStatement implémente mozIStorageValueArray. Ces fonctions sont :

  • long getInt32(in unsigned long aIndex);
  • long long getInt64(in unsigned long aIndex);
  • double getDouble(in unsigned long aIndex);
  • AUTF8String getUTF8String(in unsigned long aIndex);
  • AString getString(in unsigned long aIndex);
  • void getBlob(in unsigned long aIndex, out unsigned long aDataSize, {{mediawiki.external('array,size_is(aDataSize)')}} out octet aData); Attention : la donnée sera NULL si dataSize est à 0.
  • boolean getIsNull(in unsigned long aIndex); Renvoie true si la cellule est NULL (ce qui différent d'une chaîne vide).

Vous pouvez obtenir le type de la valeur d'une colonne spécifiée avec mozIStorageValueArray.getTypeOfIndex. Faites attention, sqlite n'est pas une base de données typée. N'importe quel type de données peut être placé dans une cellule, indépendamment du type de la colonne. Si vous lisez une donnée d'un type différent, sqlite fera de son mieux pour la convertir, et vous proposera une valeur par défaut si c'est impossible. De ce fait, il n'est pas possible d'obtenir des erreurs de typage, mais cela peut engendrer des résultats surprenants.

Les codes C++ peuvent également utiliser des fonctions AsInt32, AsDouble, etc. pour adapter la valeur retournée à un type C++. Prenez garde toutefois car aucune erreur ne vous sera signalée si votre index est invalide. D'autres erreurs sont impossibles car sqlite convertira toujours les types, même si cela n'a aucun sens.

Exemple C++ :

PRBool hasMoreData;
while (NS_SUCCEEDED(statement->ExecuteStep(&hasMoreData)) && hasMoreData) {
  PRInt32 value = statement->AsInt32(0);
  // utiliser la valeur...
}

Exemple Javascript :

while (statement.executeStep()) {
  var value = statement.getInt32(0);
  // utiliser la valeur...
}

mozIStorageStatement.execute() est une fonction pratique lorsque votre requête ne renvoie pas de valeurs. Elle effectue la requête en une seule étape et se réinitialise. Elle sert surtout pour des requêtes d'insertion en simplifiant le code :

var statement = mDBConn.createStatement("INSERT INTO my_table VALUES (?1)");
statement.bindInt32Parameter(52);
statement.execute();

Réinitialisation d'une requête

Il est important de réinitialiser les requêtes qui ne servent plus. Une requête d'écriture non réinitialisée laissera un verrou sur les tables et interdira à d'autres requêtes d'y accéder. Une requête de lecture non réinitialisée interdira toute écriture.

Lorsque l'objet de requête est libéré, la base de données à laquelle il était lié est fermée. Si vous utilisez C++ et savez que toutes les références seront détruites, vous n'avez pas à réinitialiser explicitement la requête. De même, avec l'appel de la fonction mozIStorageStatement.execute(), il est inutile de réinitialiser la requête ; cette fonction le fera pour vous. Dans les autres cas, appelez mozIStorageStatement.reset().

En JavaScript, toutes les requêtes doivent être réinitialisées. Faites attention au sujet des exceptions. Assurez vous que vos requêtes soient réinitialisées même si une exception est déclenchée, sinon l'accès à la base de données ne sera plus possible. La réinitialisation d'une requête est une opération légère sans conséquences, donc n'hésitez pas à effectuer même des réinitialisations superflues.

var statement = connection.createStatement(...);
try {
  // utiliser la requête...
} finally {
  statement.reset();
}

Les scripts C++ doivent faire de même. L'objet de contexte mozStorageStatementScoper dans {{template.Source("storage/public/mozStorageHelper.h")}} s'assurera qu'une requête donnée est réinitialisée lorsque le contexte est quitté. Il est fortement recommandé d'utiliser cet objet.

void someClass::someFunction()
{
  mozStorageStatementScoper scoper(mStatement)
  // utiliser la requête...
}

Transactions

mozIStorageConnection dispose de fonctions pour débuter et clore des transactions. Même si vous n'utilisez pas explicitement de transactions, une transaction implicite sera créée pour chacune de vos requêtes. Ceci a des implications majeures en termes de performances. Chaque transaction, et en particulier les validations, occasionne un délai supplémentaire. Les performances seront meilleures si vous placez plusieurs requêtes dans une même transaction. Consultez Storage:Performances pour plus d'informations sur les performances.

La différence principale avec d'autres systèmes de bases de données est que sqlite ne gère pas les transactions imbriquées. C'est-à-dire qu'une fois une transaction ouverte, vous ne pouvez pas en ouvrir une autre. Vous pouvez vérifier si une transaction est en cours de traitement grâce à mozIStorageConnection.transactionInProgress.

Vous pouvez également exécuter les commandes SQL « BEGIN TRANSACTION » et « END TRANSACTION » directement (c'est ce que fait la connexion avec l'appel des fonctions). Cependant, il est fortement recommandé d'utiliser mozIStorageConnection.beginTransaction et des fonctions associées parce qu'elles mémorisent l'état de la transaction dans la connexion. Dans le cas contraire, l'attribut transactionInProgress aura une valeur erronée.

sqlite comprend différents types de transactions :

  • mozIStorageConnection.TRANSACTION_DEFERRED : par défaut. Le verrou sur la base de données est obtenu lorsque c'est nécessaire (normalement la première fois où vous exécutez une requête dans la transaction).
  • mozIStorageConnection.TRANSACTION_IMMEDIATE : verrouillage immédiat en lecture de la base de données.
  • mozIStorageConnection.TRANSACTION_EXCLUSIVE : verrouillage immédiat en écriture de la base de données.

Vous pouvez définir le type de la transaction en le transmettant par mozIStorageConnection.beginTransactionAs. Gardez en tête que si une autre transaction a déjà démarré, cette opération échouera. Habituellement, le type par défaut TRANSACTION_DEFERRED suffit, et à moins de savoir exactement ce que vous faites, vous n'aurez pas besoin des autres types. Pour plus d'informations, consultez la document sqlite sur BEGIN TRANSACTION et le verrouillage.

var ourTransaction = false;
if (mDBConn.transactionInProgress) {
  ourTransaction = true;
  mDBConn.beginTransactionAs(mDBConn.TRANSACTION_DEFERRED);
}

// ... utiliser la connexion ...

if (ourTransaction)
  mDBConn.commitTransaction();

Dans un code C++, vous pouvez utiliser la classe helper mozStorageTransaction définie dans {{template.Source("storage/public/mozStorageHelper.h")}}. Cette classe démarrera une transaction du type donné sur la connexion spécifiée lorsqu'elle rentre dans le contexte d'exécution, et fera une validation ou une annulation de la transaction lorsqu'elle sort du contexte. Si une autre transaction est en cours, la classe helper de transaction n'effectuera aucune action.

Elle dispose également de fonctions pour réaliser explicitement des validations. L'utilisation classique est de définir dans la classe un comportement d'annulation (rollback) par défaut, et d'ensuite valider explicitement la transaction si le processus a réussi :

nsresult someFunction()
{
  // Définir (par défaut) la transaction avec une annulation en cas d'échec
  mozStorageTransaction transaction(mDBConn, PR_FALSE);

  // ... utiliser la connexion ...

  // tout s'est bien passé, alors validation explicite
  return transaction.Commit();
}

Comment corrompre votre base de données

  • Ouvrez plus d'une connexion vers le même fichier dont le nom n'est pas déterminé strictement identique par un strcmp, comme par exemple « my.db » et « ../dir/my.db » ou sous Windows (insensible à la casse) « my.db » et « My.db ». Sqlite essaiera de traiter chacun de ces cas, mais vous ne devriez pas compter là dessus.
  • Accédez à une base de données depuis un lien symbolique ou physique.
  • Ouvrez des connexions vers la même base de données depuis plus d'un thread (voir « Sécurité des threads » ci-dessous).
  • Accédez à une connexion ou une requête depuis plus d'un thread (voir « Sécurité des threads » ci-dessous).
  • Ouvrez la base de données depuis un programme externe pendant qu'elle est ouverte dans Mozilla. Le cache de Mozilla corrompt le fichier verrou normal dans sqlite qui devrait lui permettre de travailler en sécurité.

Sécurité des threads

Le service mozStorage et sqlite prennent en compte la sécurité des threads. Cependant, aucun des autres objets mozStorage ou sqlite, ou aucune des opérations n'est à considérer comme thread-safe.

  • Le service Storage doit être créé dans le thread principal. Si vous désirez accéder au service depuis un autre thread, assurez vous d'avoir appelé getService à l'avance depuis le processus principal.
  • Vous ne pouvez pas accéder à une connexion ou une requête depuis des threads différents. Ces objets Storage ne sont pas thread-safe, et les représentations qu'en fait sqlite ne le sont pas non plus. Même en vous assurant par verrouillage qu'un seul processus travaillera sur la base en même temps, il peut y avoir des problèmes. Ce cas n'a pas été testé, et il peut y avoir certains états internes par thread dans sqlite. Il est fortement déconseillé de faire cela.
  • Vous ne pouvez pas accéder à une unique base de données depuis plusieurs connexions provenant de différents threads. Normalement, sqlite le permet. Cependant, sqlite3_enable_shared_cache(1);a été activé (voir sqlite shared-cache mode), ce qui fait que les différentes connexions partagent le même cache. Ceci est important en terme de performances. En revanche, il n'y a pas de verrou sur les accès au cache, ce qui signifie sa corruption si vous le sollicitez depuis plusieurs processus.

Verrous SQLite

SQLite verrouille la totalité de la base de données ; ainsi, lorsqu'une lecture active est en cours, toute tentative d'écriture recevra un SQLITE_BUSY, et lorsqu'une écriture active est en cours, toute tentative de lecture recevra un SQLITE_BUSY. Une requête est considérée comme active à partir de la première fonction step() jusqu'à ce que la fonction reset() soit appelée. execute() effectue ces deux opérations en une seule étape. Un problème courant est d'oublier de réinitialiser (reset()) une requête après avoir terminé la boucle step().

Bien qu'une connexion sqlite soit capable de gérer plusieurs requêtes ouvertes, son modèle de verrouillage limite ce qu'elles peuvent faire simultanément (lecture ou écriture). En fait, il est possible pour plusieurs requêtes d'être actives en lecture en même temps. Mais il n'est pas possible qu'elles puissent lire et écrire en même temps sur la même table — même si elles dérivent de la même connexion.

Sqlite a deux niveaux de verrou : un au niveau de la connexion et un au niveau de la table. La plupart des utilisateurs sont habitués au verrou du niveau connexion (base de données) : lecture multiple mais une seule écriture. Les verrous du niveau table (B-Tree) sont ce qui peut devenir moins clair (en interne, chaque table de la base de données dispose de son propre B-Tree, donc les « tables » et « B-Tree » sont techniquement synonymes).

Verrous au niveau de la table

Vous devez penser que si vous possédez une seule connexion qui verrouille la base de données en écriture, vous pouvez utiliser plusieurs requêtes pour faire ce que vous voulez. Ce n'est pas entièrement le cas. Vous devez considérer le verrou de niveau table (B-Tree) qui est maintenu par le gestionnaire de requêtes lors du parcours de la base de données (c'est-à-dire les requêtes d'ouverture SELECT).

La règle générale est la suivante : un gestionnaire de requête ne peut pas modifier une table (B-Tree) qu'un autre gestionnaire de requêtes est en train de lire (avec un pointeur ouvert dessus) — même si le gestionnaire partage la même connexion (contexte de transaction, verrou de base de données, etc.) avec d'autres gestionnaires. Toute tentative sera bloquée (ou retournera SQLITE_BUSY).

Ce problème se présente lorsque vous essayez de parcourir une table avec une requête en modifiant des enregistrements en même temps. Cela ne fonctionnera pas (ou aura une forte probabilité de ne pas fonctionner, selon les optimisations de performances utilisées (voir ci-dessous). La requête en écriture sera bloquée car la requête en lecture a un pointeur ouvert sur la table.

Résolutions des problèmes de verrouillage

La solution est de suivre la méthode (1) comme ce qui est décrit plus haut. Théoriquement, la méthode (2) ne fonctionne pas avec SQLite 3.x. Dans ce scénario, les verrous de la base de données s'ajoutent (avec de multiples connexions) aux verrous de table. La connexion 2 (connexion de modification) ne pourra pas modifier (écrire dans) la base de données pendant que la connexion 1 (connexion de lecture) est en train de la lire. La connexion 2 nécessite un verrou exclusif pour exécuter une commande SQL de modification, mais elle ne peut pas l'obtenir tant que la connexion 1 effectue ses requêtes de lecture sur la base (la connexion 1 a un verrou partagé pendant ce temps, ce qui exclut toute possibilité d'obtention d'un verrou exclusif).

Une autre option consiste à passer par une table temporaire. Créez une table temporaire qui contient les résultats intéressants de la table, parcourez la (ce qui place un verrou de lecture sur la table temporaire) et ensuite effectuez sans problème les modifications sur la table réelle par des requêtes d'écriture. Ceci peut être réalisé avec des requêtes dérivées d'une connexion unique (contexte de transaction). Ce scénario s'effectue parfois en arrière plan, comme dans le cas d'un tri ORDER BY qui génère des tables temporaires en interne. Il ne faut toutefois pas s'attendre à ce que l'optimiseur le fasse dans tous les cas. La création explicite d'une table temporaire est le moyen le plus sûr pour réaliser cette dernière option.

{{ wiki.languages( { "en": "en/Storage", "es": "es/Almacenamiento", "pl": "pl/Storage" } ) }}

Source de la révision

<p>
</p><p>Storage est une API de base de données dans <a href="fr/Firefox_2">Firefox 2</a> et suivantes reposant sur <a class="external" href="http://www.sqlite.org/">sqlite</a>. Elle est disponible aux appels privilégiés, c'est-à-dire à du code <a href="fr/Chrome">chrome</a> et aux <a href="fr/Extensions">extensions</a>, mais <i>pas</i> aux pages Web. Elle est toujours considérée comme en cours de développement, ce qui signifie que les API peuvent être modifiées à n'importe quel moment. Il se peut en effet que l'API soit légèrement modifiée entre Firefox 2 alpha 2 et Firefox 2, et également entre Firefox 2 et Firefox 3.
</p><p>Storage peut parfois être confondu avec la fonctionnalité <a class="external" href="http://www.whatwg.org/specs/web-apps/current-work/#scs-client-side">WHATWG DOM storage</a> de Firefox 2 qui permet à des pages Web d'enregistrer des données permanentes. L'API Storage s'adresse uniquement aux auteurs d'extensions et aux composants de Firefox.
</p><p>Ce document traite de l'API mozStorage et de quelques particularités de sqlite. Il <i>ne</i> traite <i>pas</i> du SQL ou de l'utilisation de sqlite. Pour ces autres informations, vous devrez consulter vos références favorites sur SQL. Vous pouvez également consulter <a class="external" href="http://www.sqlite.org/docs.html">la documentation sur sqlite</a> et particulièrement celle sur <a class="external" href="http://www.sqlite.org/lang.html">la compréhension du langage de requêtes compris par sqlite</a>. Pour obtenir plus d'aide sur l'API mozStorage, vous pouvez poster sur le serveur de news mozilla.dev.apps.firefox sur news.mozilla.org. Pour signaler des bogues, utilisez <a class="external" href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Toolkit&amp;component=Storage">Bugzilla</a> (product « Toolkit », component « Storage »).
</p><p>Consultez <a href="fr/Storage/Performance">Storage:Performance</a> pour optimiser les performances de connexion de votre base de données.
</p><p><a class="external" href="http://sqlitebrowser.sourceforge.net/">SQLite Database Browser</a> est un outil libre, disponible sur plusieurs plateformes, permettant l'examen de bases de données existantes et le test d'instructions SQL.
</p>
<h4 name="Pr.C3.A9ambule"> Préambule </h4>
<p>mozStorage se présente comme n'importe quelle autres système de bases de données. La procédure complète d'utilisation est la suivante :
</p>
<ul><li> Ouverture d'une connexion vers la base de données de votre choix.
</li><li> Création d'une requête à exécuter sur la connexion.
</li><li> Liaison de paramètres à la requête si nécessaire.
</li><li> Exécution de la requête.
</li><li> Réinitialisation de la requête.
</li></ul>
<h4 name="Ouverture_d.27une_connexion"> Ouverture d'une connexion </h4>
<p>La première initialisation du service Storage doit se faire dans le thread principal. Vous obtiendrez une erreur en voulant l'initialiser dans un autre thread. Vous pouvez toutefois utiliser le service depuis un thread en appelant la méthode getService depuis le thread principal pour vérifier que le service a bien été créé.
</p><p>Voici un exemple C++ d'ouverture d'une connexion vers « asdf.sqlite » dans le répertoire du profil de l'utilisateur :
</p>
<pre>nsCOMPtr&lt;nsIFile&gt; dbFile;
rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                            getter_AddRefs(dbFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = dbFile-&gt;Append(NS_LITERAL_STRING("asdf.sqlite"));
NS_ENSURE_SUCCESS(rv, rv);

mDBService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &amp;rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBService-&gt;OpenDatabase(dbFile, getter_AddRefs(mDBConn));
NS_ENSURE_SUCCESS(rv, rv);
</pre>
<p><code>MOZ_STORAGE_SERVICE_CONTRACTID</code> est défini dans {{template.Source("storage/build/mozStorageCID.h")}}. Sa valeur est <code>"@mozilla.org/storage/service;1"</code>.
</p><p>Voici un exemple en JavaScript :
</p>
<pre>var file = Components.classes["@mozilla.org/file/directory_service;1"]
                     .getService(Components.interfaces.nsIProperties)
                     .get("ProfD", Components.interfaces.nsIFile);
file.append("asdf.sqlite");

var storageService = Components.classes["@mozilla.org/storage/service;1"]
                        .getService(Components.interfaces.mozIStorageService);
var mDBConn = storageService.openDatabase(file);
</pre>
<dl><dd><div class="note">Note : la fonction openDatabase risque d'être modifiée à l'avenir. Elle sera probablement améliorée et simplifiée pour réduire les difficultés d'utilisation.</div>
</dd></dl>
<p>Il est déconseillé de nommer votre base de données avec une extension en « <code>.sdb</code> » pour <b>s</b>qlite <b>d</b>ata<b>b</b>ase. En effet, Windows reconnaît cette extension comme une « base de données de compatibilité des applications » et les modifications sont inscrites dans la fonctionnalité de restauration système. Cela peut donc ralentir fortement les opérations sur le fichier.
</p>
<h4 name="Cr.C3.A9ation_d.27une_requ.C3.AAte"> Création d'une requête </h4>
<p>Il existe deux méthodes pour créer une requête. Si vous n'avez aucun paramètre et si la requête ne renvoie aucune valeur, utilisez <code>mozIStorageConnection.executeSimpleSQL</code>.
</p>
<pre>C++ :
rv = mDBConn-&gt;ExecuteSimpleSQL(NS_LITERAL_CSTRING("CREATE TABLE foo (a INTEGER)"));

JS :
mDBConn.executeSimpleSQL("CREATE TABLE foo (a INTEGER)");
</pre>
<p>Autrement, vous devrez préparer une requête en utilisant <code>mozIStorageConnection.createStatement</code> :
</p>
<pre>C++ :
nsCOMPtr&lt;mozIStorageStatement&gt; statement;
rv = mDBConn-&gt;CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM foo WHERE a = ?1"),
                              getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);

JS :
var statement = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1");
</pre>
<p>Cet exemple utilise un sélecteur « <code>?1</code> » comme paramètre qui sera renseigné ultérieurement (voir la section suivante).
</p><p>Après avoir préparé une requête, vous pouvez lui lier des paramètres, l'exécuter et la réinitialiser autant de fois que vous le souhaitez. Si vous devez faire une requête fréquemment, l'utilisation d'une requête précompilée augmentera les performances de manière significative car la requête SQL n'aura pas à être traitée à chaque fois.
</p><p>Si vous êtes familier avec sqlite, vous devez savoir que les requêtes préparées deviennent invalides lorsque la structure de la base de données est modifiée. Heureusement, <code>mozIStorageStatement</code> détecte l'erreur et recompile la requête si nécessaire. Ainsi, après avoir créé une requête, vous n'avez pas à vous soucier d'une modification de structure ; toutes les requêtes continueront à fonctionner de manière transparente.
</p>
<h4 name="Liaison_de_param.C3.A8tres"> Liaison de paramètres </h4>
<p>Il est généralement préférable de lier tous les paramètres séparément plutôt que d'essayer de construire à la volée des chaînes SQL contenant ces paramètres. Entre autres choses, ce mode de fonctionnement permet d'éviter une attaque par injection SQL puisque les paramètres liés ne sont jamais exécutés en SQL.
</p><p>Les sélecteurs inclus dans la requête sont liés aux paramètres. Les sélecteurs sont indexés en commençant par « <code>?1</code> », puis « <code>?2</code> », et ainsi de suite. Vous devez utiliser les fonctions <code>BindXXXParameter(0)</code>, <code>BindXXXParameter(1)</code>, etc. pour lier ces sélecteurs.
</p>
<dl><dd><div class="note">Attention : les indices des sélecteurs débutent à partir de 1. Les entiers passés aux fonctions de liaison débutent à partir de 0.  Cela signifie que « <code>?1</code> » correspond au paramètre 0, « <code>?2</code> » correspond au paramètre 1, etc.</div>
</dd></dl>
<p>Un sélecteur peut apparaître plusieurs fois dans la chaîne SQL et tous les instances seront remplacées par la valeur liée. Les paramètres non liés seront interprétés comme NULL.
</p><p>Les fonctions de liaison disponibles dans <code>mozIStorageStatement</code> (voir {{template.Source("storage/public/mozIStorageStatement.idl")}}) sont :
</p>
<ul><li> <code>bindUTF8StringParameter(in unsigned long aParamIndex, in AUTF8String aValue)</code>
</li><li> <code>bindStringParameter(in unsigned long aParamIndex, in AString aValue)</code>
</li><li> <code>bindDoubleParameter(in unsigned long aParamIndex, in double aValue)</code>
</li><li> <code>bindInt32Parameter(in unsigned long aParamIndex, in long aValue)</code>
</li><li> <code>bindInt64Parameter(in unsigned long aParamIndex, in long long aValue)</code>
</li><li> <code>bindNullParameter(in unsigned long aParamIndex)</code>
</li><li> <code>bindBlobParameter(in unsigned long aParamIndex, {{mediawiki.external('array,const,size_is(aValueSize)')}} in octet aValue, in unsigned long ValueSize)</code> (pour des données binaires)
</li></ul>
<p>Exemple C++ :
</p>
<pre>nsCOMPtr&lt;mozIStorageStatement&gt; statement;
rv = mDBConn-&gt;CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM foo WHERE a = ?1 AND b &gt; ?2"),
                              getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);
rv = statement-&gt;BindUTF8StringParameter(0, "bonjour"); // "bonjour" sera substitué à "?1"
NS_ENSURE_SUCCESS(rv, rv);
rv = statement-&gt;BindInt32Parameter(1, 1234); // 1234 sera substitué à "?2"
NS_ENSURE_SUCCESS(rv, rv);
</pre>
<p>Exemple Javascript :
</p>
<pre>var statement = mDBConn.createStatement("SELECT * FROM foo WHERE a = ?1 AND b &gt; ?2");
statement.bindUTF8StringParameter(0, "bonjour");
statement.bindInt32Parameter(1, 1234);
</pre>
<h4 name="Ex.C3.A9cution_d.27une_requ.C3.AAte"> Exécution d'une requête </h4>
<p>La principale manière d'exécuter une requête est d'utiliser la fonction <code>mozIStorageStatement.executeStep</code>. Cette fonction vous permet de récupérer chaque ligne produite par la requête en vous notifiant lorsqu'il n'y a plus de résultats.
</p><p>Après un appel de <code>executeStep</code>, vous récupérez les données par une des fonctions de <code>mozIStorageValueArray</code> (voir {{template.Source("storage/public/mozIStorageValueArray.idl")}}). <code>mozIStorageStatement</code> implémente <code>mozIStorageValueArray</code>. Ces fonctions sont :
</p>
<ul><li> <code>long getInt32(in unsigned long aIndex);</code>
</li><li> <code>long long getInt64(in unsigned long aIndex);</code>
</li><li> <code>double getDouble(in unsigned long aIndex);</code>
</li><li> <code>AUTF8String getUTF8String(in unsigned long aIndex);</code>
</li><li> <code>AString getString(in unsigned long aIndex);</code>
</li><li> <code>void getBlob(in unsigned long aIndex, out unsigned long aDataSize, {{mediawiki.external('array,size_is(aDataSize)')}} out octet aData);</code> Attention : la donnée sera NULL si dataSize est à 0.
</li><li> <code>boolean getIsNull(in unsigned long aIndex);</code> Renvoie true si la cellule est NULL (ce qui différent d'une chaîne vide).
</li></ul>
<p>Vous pouvez obtenir le type de la valeur d'une colonne spécifiée avec <code>mozIStorageValueArray.getTypeOfIndex</code>. Faites attention, sqlite n'est pas une base de données typée. N'importe quel type de données peut être placé dans une cellule, indépendamment du type de la colonne. Si vous lisez une donnée d'un type différent, sqlite fera de son mieux pour la convertir, et vous proposera une valeur par défaut si c'est impossible. De ce fait, il n'est pas possible d'obtenir des erreurs de typage, mais cela peut engendrer des résultats surprenants.
</p><p>Les codes C++ peuvent également utiliser des fonctions <code>AsInt32</code>, <code>AsDouble</code>, etc. pour adapter la valeur retournée à un type C++. Prenez garde toutefois car aucune erreur ne vous sera signalée si votre index est invalide. D'autres erreurs sont impossibles car sqlite convertira toujours les types, même si cela n'a aucun sens.
</p><p>Exemple C++ :
</p>
<pre>PRBool hasMoreData;
while (NS_SUCCEEDED(statement-&gt;ExecuteStep(&amp;hasMoreData)) &amp;&amp; hasMoreData) {
  PRInt32 value = statement-&gt;AsInt32(0);
  // utiliser la valeur...
}
</pre>
<p>Exemple Javascript :
</p>
<pre>while (statement.executeStep()) {
  var value = statement.getInt32(0);
  // utiliser la valeur...
}
</pre>
<p><code>mozIStorageStatement.execute()</code> est une fonction pratique lorsque votre requête ne renvoie pas de valeurs. Elle effectue la requête en une seule étape et se réinitialise. Elle sert surtout pour des requêtes d'insertion en simplifiant le code :
</p>
<pre>var statement = mDBConn.createStatement("INSERT INTO my_table VALUES (?1)");
statement.bindInt32Parameter(52);
statement.execute();
</pre>
<h4 name="R.C3.A9initialisation_d.27une_requ.C3.AAte"> Réinitialisation d'une requête </h4>
<p>Il est important de réinitialiser les requêtes qui ne servent plus. Une requête d'écriture non réinitialisée laissera un verrou sur les tables et interdira à d'autres requêtes d'y accéder. Une requête de lecture non réinitialisée interdira toute écriture.
</p><p>Lorsque l'objet de requête est libéré, la base de données à laquelle il était lié est fermée. Si vous utilisez C++ et savez que toutes les références seront détruites, vous n'avez pas à réinitialiser explicitement la requête. De même, avec l'appel de la fonction <code>mozIStorageStatement.execute()</code>, il est inutile de réinitialiser la requête ; cette fonction le fera pour vous. Dans les autres cas, appelez <code>mozIStorageStatement.reset()</code>.
</p><p>En JavaScript, toutes les requêtes doivent être réinitialisées. Faites attention au sujet des exceptions. Assurez vous que vos requêtes soient réinitialisées même si une exception est déclenchée, sinon l'accès à la base de données ne sera plus possible. La réinitialisation d'une requête est une opération légère sans conséquences, donc n'hésitez pas à effectuer même des réinitialisations superflues.
</p>
<pre>var statement = connection.createStatement(...);
try {
  // utiliser la requête...
} finally {
  statement.reset();
}
</pre>
<p>Les scripts C++ doivent faire de même. L'objet de contexte <code>mozStorageStatementScoper</code> dans {{template.Source("storage/public/mozStorageHelper.h")}} s'assurera qu'une requête donnée est réinitialisée lorsque le contexte est quitté. Il est fortement recommandé d'utiliser cet objet.
</p>
<pre>void someClass::someFunction()
{
  mozStorageStatementScoper scoper(mStatement)
  // utiliser la requête...
}
</pre>
<h4 name="Transactions"> Transactions </h4>
<p><code>mozIStorageConnection</code> dispose de fonctions pour débuter et clore des transactions. Même si vous n'utilisez pas explicitement de transactions, une transaction implicite sera créée pour chacune de vos requêtes. Ceci a des implications majeures en termes de performances. Chaque transaction, et en particulier les validations, occasionne un délai supplémentaire. Les performances seront meilleures si vous placez plusieurs requêtes dans une même transaction. Consultez <a href="fr/Storage/Performances">Storage:Performances</a> pour plus d'informations sur les performances.
</p><p>La différence principale avec d'autres systèmes de bases de données est que sqlite ne gère pas les transactions imbriquées. C'est-à-dire qu'une fois une transaction ouverte, vous ne pouvez pas en ouvrir une autre. Vous pouvez vérifier si une transaction est en cours de traitement grâce à <code>mozIStorageConnection.transactionInProgress</code>.
</p><p>Vous pouvez également exécuter les commandes SQL « <code>BEGIN TRANSACTION</code> » et « <code>END TRANSACTION</code> » directement (c'est ce que fait la connexion avec l'appel des fonctions). Cependant, il est <i>fortement</i> recommandé d'utiliser <code>mozIStorageConnection.beginTransaction</code> et des fonctions associées parce qu'elles mémorisent l'état de la transaction dans la connexion. Dans le cas contraire, l'attribut <code>transactionInProgress</code> aura une valeur erronée.
</p><p>sqlite comprend différents types de transactions :
</p>
<ul><li> mozIStorageConnection.TRANSACTION_DEFERRED : par défaut. Le verrou sur la base de données est obtenu lorsque c'est nécessaire (normalement la première fois où vous exécutez une requête dans la transaction).
</li></ul>
<ul><li> mozIStorageConnection.TRANSACTION_IMMEDIATE : verrouillage immédiat en lecture de la base de données.
</li></ul>
<ul><li> mozIStorageConnection.TRANSACTION_EXCLUSIVE : verrouillage immédiat en écriture de la base de données.
</li></ul>
<p>Vous pouvez définir le type de la transaction en le transmettant par <code>mozIStorageConnection.beginTransactionAs</code>. Gardez en tête que si une autre transaction a déjà démarré, cette opération échouera. Habituellement, le type par défaut TRANSACTION_DEFERRED suffit, et à moins de savoir exactement ce que vous faites, vous n'aurez pas besoin des autres types. Pour plus d'informations, consultez la document sqlite sur <a class="external" href="http://www.sqlite.org/lang_transaction.html">BEGIN TRANSACTION</a> et <a class="external" href="http://www.sqlite.org/lockingv3.html">le verrouillage</a>.
</p>
<pre>var ourTransaction = false;
if (mDBConn.transactionInProgress) {
  ourTransaction = true;
  mDBConn.beginTransactionAs(mDBConn.TRANSACTION_DEFERRED);
}

// ... utiliser la connexion ...

if (ourTransaction)
  mDBConn.commitTransaction();
</pre>
<p>Dans un code C++, vous pouvez utiliser la classe helper <code>mozStorageTransaction</code> définie dans {{template.Source("storage/public/mozStorageHelper.h")}}. Cette classe démarrera une transaction du type donné sur la connexion spécifiée lorsqu'elle rentre dans le contexte d'exécution, et fera une validation ou une annulation de la transaction lorsqu'elle sort du contexte. Si une autre transaction est en cours, la classe helper de transaction n'effectuera aucune action.
</p><p>Elle dispose également de fonctions pour réaliser explicitement des validations. L'utilisation classique est de définir dans la classe un comportement d'annulation (rollback) par défaut, et d'ensuite valider explicitement la transaction si le processus a réussi :
</p>
<pre>nsresult someFunction()
{
  // Définir (par défaut) la transaction avec une annulation en cas d'échec
  mozStorageTransaction transaction(mDBConn, PR_FALSE);

  // ... utiliser la connexion ...

  // tout s'est bien passé, alors validation explicite
  return transaction.Commit();
}
</pre>
<h4 name="Comment_corrompre_votre_base_de_donn.C3.A9es"> Comment corrompre votre base de données </h4>
<ul><li> Lisez ce document : <a class="external" href="http://www.sqlite.org/lockingv3.html">File locking and concurrency in sqlite version 3</a>, en particulier le chapitre sur la corruption.
</li></ul>
<ul><li> Ouvrez plus d'une connexion vers le même fichier dont le nom n'est pas déterminé strictement identique par un <code>strcmp</code>, comme par exemple « <code>my.db</code> » et « <code>../dir/my.db</code> » ou sous Windows (insensible à la casse) « <code>my.db</code> » et « <code>My.db</code> ». Sqlite essaiera de traiter chacun de ces cas, mais vous ne devriez pas compter là dessus.
</li></ul>
<ul><li> Accédez à une base de données depuis un lien symbolique ou physique.
</li></ul>
<ul><li> Ouvrez des connexions vers la même base de données depuis plus d'un thread (voir « Sécurité des threads » ci-dessous).
</li></ul>
<ul><li> Accédez à une connexion ou une requête depuis plus d'un thread (voir « Sécurité des threads » ci-dessous).
</li></ul>
<ul><li> Ouvrez la base de données depuis un programme externe pendant qu'elle est ouverte dans Mozilla. Le cache de Mozilla corrompt le fichier verrou normal dans sqlite qui devrait lui permettre de travailler en sécurité.
</li></ul>
<h4 name="S.C3.A9curit.C3.A9_des_threads"> Sécurité des threads </h4>
<p>Le service mozStorage et sqlite prennent en compte la sécurité des threads. Cependant, aucun des autres objets mozStorage ou sqlite, ou aucune des opérations n'est à considérer comme thread-safe.
</p>
<ul><li> Le service Storage doit être créé dans le thread principal. Si vous désirez accéder au service depuis un autre thread, assurez vous d'avoir appelé <code>getService</code> à l'avance depuis le processus principal.
</li></ul>
<ul><li> Vous ne pouvez pas accéder à une connexion ou une requête depuis des threads différents. Ces objets Storage ne sont pas thread-safe, et les représentations qu'en fait sqlite ne le sont pas non plus. Même en vous assurant par verrouillage qu'un seul processus travaillera sur la base en même temps, il peut y avoir des problèmes. Ce cas n'a pas été testé, et il peut y avoir certains états internes par thread dans sqlite. Il est fortement déconseillé de faire cela.
</li></ul>
<ul><li> Vous ne pouvez pas accéder à une unique base de données depuis plusieurs connexions provenant de différents threads. Normalement, sqlite le permet. Cependant, <code>sqlite3_enable_shared_cache(1);</code>a été activé (voir <a class="external" href="http://www.sqlite.org/sharedcache.html">sqlite shared-cache mode</a>), ce qui fait que les différentes connexions partagent le même cache. Ceci est important en terme de performances. En revanche, il n'y a pas de verrou sur les accès au cache, ce qui signifie sa corruption si vous le sollicitez depuis plusieurs processus.
</li></ul>
<h4 name="Verrous_SQLite"> Verrous SQLite </h4>
<p>SQLite verrouille la totalité de la base de données ; ainsi, lorsqu'une lecture active est en cours, toute tentative d'écriture recevra un SQLITE_BUSY, et lorsqu'une écriture active est en cours, toute tentative de lecture recevra un SQLITE_BUSY. Une requête est considérée comme active à partir de la première fonction <code>step()</code> jusqu'à ce que la fonction <code>reset()</code> soit appelée. <code>execute()</code> effectue ces deux opérations en une seule étape. Un problème courant est d'oublier de réinitialiser (<code>reset()</code>) une requête après avoir terminé la boucle <code>step()</code>.
</p><p>Bien qu'une connexion sqlite soit capable de gérer plusieurs requêtes ouvertes, son modèle de verrouillage limite ce qu'elles peuvent faire simultanément (lecture ou écriture). En fait, il est possible pour plusieurs requêtes d'être actives en lecture en même temps. Mais il n'est pas possible qu'elles puissent lire et écrire en même temps <i>sur la même table</i> — même si elles dérivent de la même connexion.
</p><p>Sqlite a deux niveaux de verrou : un au niveau de la connexion et un au niveau de la table. La plupart des utilisateurs sont habitués au verrou du niveau connexion (base de données) : lecture multiple mais une seule écriture. Les verrous du niveau table (B-Tree) sont ce qui peut devenir moins clair (en interne, chaque table de la base de données dispose de son propre B-Tree, donc les « tables » et « B-Tree » sont techniquement synonymes).
</p>
<h5 name="Verrous_au_niveau_de_la_table"> Verrous au niveau de la table </h5>
<p>Vous devez penser que si vous possédez une seule connexion qui verrouille la base de données en écriture, vous pouvez utiliser plusieurs requêtes pour faire ce que vous voulez. Ce n'est pas entièrement le cas. Vous devez considérer le verrou de niveau table (B-Tree) qui est maintenu par le gestionnaire de requêtes lors du parcours de la base de données (c'est-à-dire les requêtes d'ouverture SELECT).
</p><p>La règle générale est la suivante : un gestionnaire de requête <b>ne</b> peut <b>pas</b> modifier une table (B-Tree) qu'un autre gestionnaire de requêtes est en train de lire (avec un pointeur ouvert dessus) — même si le gestionnaire partage la même connexion (contexte de transaction, verrou de base de données, etc.) avec d'autres gestionnaires. <i>Toute tentative sera bloquée (ou retournera SQLITE_BUSY)</i>.
</p><p>Ce problème se présente lorsque vous essayez de parcourir une table avec une requête en modifiant des enregistrements en même temps. Cela ne fonctionnera pas (ou aura une forte probabilité de ne pas fonctionner, selon les optimisations de performances utilisées (voir ci-dessous). La requête en écriture sera bloquée car la requête en lecture a un pointeur ouvert sur la table.
</p>
<h5 name="R.C3.A9solutions_des_probl.C3.A8mes_de_verrouillage"> Résolutions des problèmes de verrouillage </h5>
<p>La solution est de suivre la méthode (1) comme ce qui est décrit plus haut. Théoriquement, la méthode (2) ne fonctionne pas avec SQLite 3.x. Dans ce scénario, les verrous de la base de données s'ajoutent (avec de multiples connexions) aux verrous de table. La connexion 2 (connexion de modification) ne pourra pas modifier (écrire dans) la base de données pendant que la connexion 1 (connexion de lecture) est en train de la lire. La connexion 2 nécessite un verrou exclusif pour exécuter une commande SQL de modification, mais elle ne peut pas l'obtenir tant que la connexion 1 effectue ses requêtes de lecture sur la base (la connexion 1 a un verrou partagé pendant ce temps, ce qui exclut toute possibilité d'obtention d'un verrou exclusif).
</p><p>Une autre option consiste à passer par une table temporaire. Créez une table temporaire qui contient les résultats intéressants de la table, parcourez la (ce qui place un verrou de lecture sur la table temporaire) et ensuite effectuez sans problème les modifications sur la table réelle par des requêtes d'écriture. Ceci peut être réalisé avec des requêtes dérivées d'une connexion unique (contexte de transaction). Ce scénario s'effectue parfois en arrière plan, comme dans le cas d'un tri ORDER BY qui génère des tables temporaires en interne. Il ne faut toutefois pas s'attendre à ce que l'optimiseur le fasse dans tous les cas. La création explicite d'une table temporaire est le moyen le plus sûr pour réaliser cette dernière option.
</p>{{ wiki.languages( { "en": "en/Storage", "es": "es/Almacenamiento", "pl": "pl/Storage" } ) }}
Revenir à cette révision