Please note, this is a STATIC archive of website developer.mozilla.org from November 2016, cach3.com does not collect or store any user information, there is no "phishing" involved.

Almacenamiento

 

Almacenamiento en Firefox 2 es un API de base de datos y una forma de respaldo en sqlite. Es un recurso sólo disponible para peticiones seguras, lo que significa su uso exclusivo para código chrome y extensiones, no así para páginas web. Actualmente está "descongelada", lo que significa que la API está sujeta a cambiar en cualquier momento. Es probable que la API sufra algunas modificaciones entre la versión Alfa 2 y la versión final de Firefox 2, así como entre Firefox 2 y Firefox 3.

Aunque a veces se confunde el Almacenamiento con el WHATWG DOM #scs-del-lado-del-cliente (una característica de Firefox 2 que permite el almacenamiento persistente de datos) la API Almacenamiento, está reservada sólo para extensiones de autoría y componentes de Firefox.

El presente documento abarca la API mozStorage y algunas peculiaridades de sqlite. No cubre SQL o el sqlite "regular". Para esos casos usted debe consultar sus manuales de SQL. Es recomendable que revise también su documentación de sqlite y especialmente el lenguaje de consultas de sqlite. Para acceder a la ayuda de la API mozStorage, debe consultar en mozilla.dev.apps.firefox o al servicio de informaciones en news.mozilla.org. En caso de querer reportar errores, use el Bugzilla (Producto: "Herramientas", Componente: "Almacenamiento").

Revise Almacenamiento:Funcionamiento para indagar sobre el buen funcionamiento de la conexión a su base de datos.

Comenzando

mozStorage está diseñada como muchos otros sistemas de bases de datos. El procedimiento general para usarlo es:

  • Abrir una conexión a la base de datos de su elección.
  • Crear las definiciones para ejecutar la conexión.
  • Enlazar tantos parámetros a las definiciones como sea necesario.
  • Ejecutar las definiciones.
  • "Resetear" (reiniciar) las definiciones.

Abriendo una Conexión

La primera inicialización del servicio de almacenamiento debe hacerse en el hilo principal (un "hilo" es una unidad básica de ejecución). Se producirá un error si usted lo inicializa por cualquier otro hilo. Por lo tanto, si usted quisiera usar el servicio desde otro hilo, verifique llamando al getService desde el hilo principal para asegurarse que el servicio ha sido creado.

Ejemplo en C++ de la apertura de una conexión para "asdf.sqlite" en el directorio raiz del usuario:

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á definido en storage/build/mozStorageCID.h, y su valor es "@mozilla.org/storage/service;1"

Ejemplo 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);
Nota: La función OpenDatabase está sujeta a cambios. Esta deberá ser destacada y simplificada al máximo para hacerla menos accesible a problemas.
Atención: Evite la tendencia a darle a su base de datos la terminación ".sdb" (por 's'qlite 'd'ata 'b'ase). Esta extensión está     reservada en Windows para una "Base de datos de Aplicación Compatible" y sus modificaciones son guardadas automáticamente como parte del sistema para restaurar su funcionalidad.

Crear una sentencia

Existen dos formas de crear una sentencia. Si no hay parámetros y la sentencia no devuelve nada, use mozIStorageConnection.executeSimpleSQL.

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

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

En otro caso, se debería componer una sentencia utilizando 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");

Este ejemplo utiliza un comodín "?1" para un parámetro que será referenciado más tarde (véase la siguiente sección).

Tras componer la sentencia se pueden enlazar parámetros a ella, ejecutarla y reiniciarla una y otra vez. Si una sentencia se ejecuta muchas veces, el uso de una sentencia precompilada derivará en una mejora notable del rendimiento ya que la consulta SQL no necesita ser analizada sintácticamente cada vez.

Si está familiarizado con sqlite, sabrá que las sentencias preparadas son invalidadas cuando el esquema de la base de datos cambia. Afortunadamente, mozIStorageStatement detecta el error y recompilará la sentencia según se necesite. Por tanto, una vez que se ha creado una sentencia, no es necesario preocuparse ante un cambio de esquema. Todas las sentencias continuarán trabajando de forma transparente.

Asignando parámetros

Por lo general es mejor asignar todos los parámetros por separado en lugar de intentar construir cadenas SQL al vuelo que contengan los parámetros. Entre otras cosas, esto evita ataques de inyección SQL, ya que un parámetro asignado nunca podrá ser ejecutado como SQL.

Se pueden asignar parámetros cualquier sentencia que tenga comodines. Los comodines son accedidos mediante índice empezando por "?1", luego "?2"... Para asignar dichos comodines hay que usar funciones de sentencia BindXXXParameter(0) BindXXXParameter(1)...

Cuidado: Los índices en los comodines empiezan en 1. Los enteros pasados a las funciones de asignación cuentan desde 0. Esto significa que "?1" se corresponde al parámetro 0, "?2" al 1, etc...

Un comodín puede aparecer múltiples veces en la cadena SQL y todas sus instancias serán reemplazadas con el valor asignado. Los parámetros no asignados se interpretarán como NULL.

Las funciones de asignación disponibles en mozIStorageStatement (véase storage/public/mozIStorageStatement.idl) son:

  • 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, [array,const,size_is(aValueSize)] in octet aValue, in unsigned long ValueSize) (para datos binarios)

Ejemplo 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, "hello"); // "hello" will be substituted for "?1"
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->BindInt32Parameter(1, 1234); // 1234 will be substituted for "?2"
NS_ENSURE_SUCCESS(rv, rv);

Ejemplo Javascript:

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

Ejecutar una sentencia

La forma principal de ejecutar una sentencia es con mozIStorageStatement.executeStep. Esta función te permite enumerar todas las filas resultantes que la sentencia ha producido y te avisará cuando ya no haya más resultados.

Tras llamar a executeStep se pueden usar las funciones get de mozIStorageValueArray (véase storage/public/mozIStorageValueArray.idl. mozIStorageStatement implementa mozIStorageValueArray. Estas funciones son:

  • 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, [array,size_is(aDataSize)] out octet aData); aviso: los datos serán NULL si dataSize es 0.
  • boolean getIsNull(in unsigned long aIndex); Devuelve true si la celda es NULL (distinta de cadena vacía)

Se puede obtener el tipo de un valor con mozIStorageValueArray.getTypeOfIndex el cual devuelve el tipo de la columna especificada. Cuidado: sqlite no es una base de datos tipada. Se puede poner cualquier tipo en cualquier celda sin importar el tipo declarado para la columna. Si se pide un tipo diferente, sqlite hará lo que pueda para convertirlo y devolverá algún valor predeterminado si le es imposible. Por tanto, es imposible obtener errores a la hora de recuperar tipos aunque puede que te lleguen datos raros.

El código de C++ puede además usar las funciones AsInt32, AsDouble, etc... que devuelven el valor como valor devuelto de C++ más apropiado. Aunque hay que tener cuidado ya que no tendrás errores si el índice es inválido. Es imposible obtener cualquier otro error debido a que sqlite siempre convertirá los tipos, incluso cuando al hacerlo dejen de tener sentido.

Ejemplo C++:

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

Ejemplo Javascript:

while (statement.executeStep()) {
  var value = statement.GetInt32(0);
  // use the value...
}

mozIStorageStatement.execute() en una función apropiada cuando no se esperan datos al ejecutar la sentencia. Itera una vez la sentencia y la reinicia. Esto puede ser útil para sentencias de inserción ya que simplifica el código:

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

Reiniciar una sentencia

Es importante reiniciar sentencias que ya no van a ser usadas más. Las sentencias de escritura no reiniciadas mantendrán bloqueadas las tablas e impedirán a otras sentencias acceder a ellas. Las sentencias de lectura no reiniciadas impedirán la escritura.

Cuando un objeto sentencia es liberado, su sentencia de base de datos correspondiente es cerrada. Si estás usando C++ y sabes que todas las referencias serán destruidas, no tienes porque reiniciar explícitamente la sentencia. Además, si usas mozIStorageStatement.execute(), tampoco necesitas reiniciar explícitamente la sentencia; esta función la reiniciará por ti. En otro caso, hay que llamar a mozIStorageStatement.reset().

Los que utilicen JavaScript deben asegurarse de que las sentencias son reiniciadas, siendo particularmente cuidadoso con las excepciones. Seguro que querrás estar seguro de que reinicias tus sentencias incluso cuando una excepción es lanzada o de lo contrario los siguientes accesos a la base de datos no serán posibles. El reinicio de una sentencia es un proceso relativamente sencillo y no ocurrirá nada si ya estaba reiniciada, por lo que no hay que preocuparse por reinicios innecesarios.

var statement = connection.createStatement(...);
try {
  // uso de la sentencia...
} finally {
  statement.reset();
}

Los que usen C++ deben de hacer lo mismo. Existe un objeto de ámbito en storage/public/mozStorageHelper.h llamado mozStorageStatementScoper que asegura que una sentencia dada es reiniciada cuando se sale del ámbito al que pertenece. Es muy recomendable usar este objeto si es posible.

void someClass::someFunction()
{
  mozStorageStatementScoper scoper(mStatement)
  // uso de la sentencia
}

Transacciones

mozIStorageConnection posee funciones para comenzar y finalizar transacciones. Si no se usan transacciones de modo explícito, se creará una implícita para cada sentencia. Esto tiene consecuencias más directas relacionadas con el rendimiento. Existe algo así para cada transacción, especialmente para las confirmaciones (commits). Por tanto al final se notará una mejora en el rendimiento cuando se realicen múltiples sentencias en una fila si se ponen en una transacción. Véase Storage:Performance para más información relacionada con el rendimiento.

La gran diferencia entre otros sistemas de base de datos es que sqlite no soporta transacciones anidadas. Esto significa que una vez se ha abierto una transacción no se puede abrir otra. Se puede comprobar mozIStorageConnection.transactionInProgress para ver si una transacción está actualmente en progreso.

También se puede ejecutar "BEGIN TRANSACTION" y "END TRANSACTION" directamente como sentencias SQL (esto es lo que hace la conexión cuando se llaman a las funciones). Sin embargo, el uso de mozIStorageConnection.beginTransaction y las funciones relacionadas se recomienda encarecidamente ya que guarda el estado de la transacción en la conexión. De otro modo el atributo transacionInProgress tendrá un valor erróneo.

sqlite posee varios tipos de transacciones:

  • mozIStorageConnection.TRANSACTION_DEFERRED: El predeterminado. El bloqueo de la base de datos se adquiere cuando se necesita (generalmente la primera vez que se ejecuta una sentencia en la transacción).
  • mozIStorageConnection.TRANSACTION_IMMEDIATE: Obtiene un bloqueo de lectura inmediato sobre la base de datos.
  • mozIStorageConnection.TRANSACTION_EXCLUSIVE: Obtiene un bloqueo de escritura inmediato sobre la base de datos.

Se puede pasar este tipo de transacción a mozIStorageConnection.beginTransactionAs para determinar qué clase de transacción se necesita, teniendo en mente que si otra transacción se ha iniciado esta operación no tendrá éxito. Generalmente, el tipo predeterminado TRANSACTION_DEFERRED es suficiente y no se debería usar el resto de tipos a menos que realmente sepas por qué lo necesitas. Para más información, véase la documentación de sqlite sobre BEGIN TRANSACTION ybloqueos.

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

// ... uso de la conexión ...

if (ourTransaction)
  mDBConn.commitTransaction();

En C++ se puede usar la clase de ayuda mozStorageTransaction definida en storage/public/mozStorageHelper.h. Esta clase comenzará una transacción del tipo especificado utilizando la conexión especificada cuando se alcance el ámbito y confirmará o deshará la transacción cuando salga del ámbito. Si ya había una transacción en progreso, la clase de ayuda de transacciones no hará nada.

También tiene funciones para confirmaciones explícitas. El uso típico es cuando se crea la clase para que deshaga los cambios de modo predeterminado para luego confirmar explícitamente la transacción cuando el proceso haya concluido con éxito.

nsresult someFunction()
{
  // deferred transaction (the default) with rollback on failure
  mozStorageTransaction transaction(mDBConn, PR_FALSE);

  // ... uso de la conexión ...

  // todo ha ido bien, ahora confirmamos explícitamente
  return transaction.Commit();
}

Cómo corromper la base de datos

  • Abriendo más de una conexión al mismo fichero con nombres que no son exactamente el mismo que determinaría strcmp. Esto incluye "my.db" y "../dir/my.db" o, en Windows (sin distinguir minúsculas y mayúsculas) "my.db" y "My.db". Sqlite intentará manejar muchos de estos casos, aunque no deberías fiarte de ello.
  • Accediendo a una base de datos desde un enlace simbólico o duro.
  • Abriendo conexiones a la misma base de datos desde más de un hilo (véase "Thread safety" más abajo).
  • Accediendo a una conexión o sentencia desde más de un hilo (véase "Thread safety" más abajo).
  • Abriendo la base de datos desde un programa externo cuando aún está abierta en Mozilla. El cacheo rompe el bloqueo normal de ficheros en sqlite, el cual permite que esto sea hecho de forma segura.

Seguridad en hilos

Tanto el servicio mozStorage como sqlite son seguros a nivel de hilo. Sin embargo ningún otro objeto de mozStorage o de sqlite u operación es segura.

  • El servicio de almacenamiento debe ser creado en el hilo principal. si se quiere acceder al servicio desde otro hilo debería asegurarse que se llama a getService desde el hilo principal con antelación.
  • No se puede acceder a una conexión o a una sentencia desde múltiples hilos. Dichos objetos de almacenamiento ni sus representaciones en sqlite son seguros a nivel de hilo. Incluso si se utiliza bloqueo y se asegura que sólo un único hilo está haciendo algo a la vez pueden haber problemas. Este caso no ha sido comprobado y pueden haber algún que otro estado interno para hilos en sqlite. Se recomienda encarecidamente no hacer esto.
  • No se puede acceder a una única base de datos desde múltiples conexiones desde diferentes hilos. Normalmente sqlite permite esto. Sin embargo, nosotros hacemos sqlite3_enable_shared_cache(1); (véase modo compartido de caché en sqlite) el cual crea múltiples compartidas a la misma caché. Esto es importante para el rendimiento, sin embargo no existe bloqueo para el acceso a la caché, lo que significa que se romperá si se usa desde más de un hilo.

Bloqueo en SQLite

SQLite bloquea por completo la base de datos, es decir, se devolverá un SQLITE_BUSY a aquellos que estén leyendo e intenten escribir y lo mismo ocurrirá con aquellos que estén escribiendo e intenten leer. Una sentencia se considera activa desde el primer step() hasta que reset() es llamado. execute() llama tanto a step() como a reset(). Un problema común ocurre cuando se olvida reiniciar con reset() una sentencia después de haberla iterado con step().

Mientras que una conexión SQLite dada es capaz de manejar múltiples sentencias abiertas, su modelo de bloqueo limita lo que dichas sentencias pueden hacer concurrentemente (leer o escribir). De hecho es posible que múltiples sentencias estén activas leyendo a la vez. Sin embargo no es posible que múltiples sentencias estén leyendo y escribiendo a la vez en la misma tabla, incluso si son derivadas de la misma conexión.

SQLite posee un modelo de bloqueo a dos niveles: a nivel de conexión y a nivel de tabla. La mayoría de la gente estará familiarizada con el bloqueo a nivel de conexión (base de datos): múltiples lectores aunque sólo un escritor. Lo que bloquea el nivel de tabla (Árbol-B) es lo que puede a veces ser confundido (internamente, cada tabla en la base de datos posee su propio Árbol-B, por lo que "tabla" y "Árbol-B" son técnicamente sinónimos).

Bloqueo a nivel de tabla

Se podría pensar que si sólo se tiene una conexión y se bloquea la base de datos para escritura, se podrían usar múltiples sentencias para hacer todo lo que se quisiera. No del todo cierto. Hay que ser consciente del bloqueo a nivel de tabla (Árbol-B) el cual es mantenido por manejadores de sentencias a través de la base de datos (p.e. sentencias SELECT abiertas).

La regla general es esta: un manejador de sentencia puede no modificar una tabla (Árbol-B) la cual otros manejadores de sentencia están leyendo (tienen abiertos cursores sobre ella), incluso si ese manejador de sentencia comparte la misma conexión (contexto de transacción, bloqueo de base de datos, etc...) con otros manejadores de sentencias. Intentar hacerlo provocará el bloqueo (o devolverá SQLITE_BUSY).

Este problema generalmente aparece cuando se intenta iterar una tabla con una sentencia y modificar registros dentro de ella utilizando otra sentencia. Esto no funcionará (o conlleva una alta probabilidad de no funcionar, dependiendo de la implicación del optimizador (véase más abajo)). La sentencia de modificación provocará bloqueo ya que la sentencia de lectura tiene un cursor abierto en la tabla.

Trabajar con problemas de bloqueo

La solución es seguir (1) como se describe arriba. Teóricamente, (2) realmente no debería funcionar con SQLite 3.x. En este escenario, el bloqueo de la base de datos entra en juego (con múltiples conexiones) además del bloque de la tabla. La conexión 2 (conexión de modificación) no será capaz de modificar (escribir) en la base de datos mientras que la conexión 1 esté leyendo. La conexión 2 necesita un acceso exclusivo para ejecutar un comando SQL de modificación, acceso que no puede obtener mientras la conexión 1 tenga sentencias activas leyendo la base de datos (la conexión 1 posee un bloque de lectura compartido durante este tiempo el cual prohibe a cualquier otra conexión obtener un acceso exclusivo).

Otra opción es usar una tabla temporal. Crear una tabla temporal que contenga los resultados de la tabla en cuestión, iterarla (poniendo el bloqueo de la tabla de la sentencia de lectura en la tabla temporal) para que luego la sentencia de modificación pueda hacer cambios en la tabla real sin ningún problema. Esto puede ser hecho con sentencias derivadas de una única conexión (contexto de transacción). Este escenario a veces sucede en segundo plano ya que cosas como ORDER BY pueden producir tablas temporales internamente. Sin embargo, no es seguro asumir que el optimizador hará esto en todos los casos. Crear una tabla temporal explícitamente es la única forma segura de realizar esta última opción.

Etiquetas y colaboradores del documento

Etiquetas: 
 Colaboradores en esta página: 3lvin, Maharba, Mgjbot, Superruzafa, Oscaralderete, Nukeador
 Última actualización por: 3lvin,