Copia de seguridad / restauración de Android: ¿cómo hacer una copia de seguridad de una base de datos interna?

86

Implementé un BackupAgentHelperuso del proporcionado FileBackupHelperpara hacer una copia de seguridad y restaurar la base de datos nativa que tengo. Esta es la base de datos que normalmente usa junto con ContentProvidersy en la que reside /data/data/yourpackage/databases/.

Uno pensaría que este es un caso común. Sin embargo, los documentos no están claros sobre qué hacer: http://developer.android.com/guide/topics/data/backup.html . No hayBackupHelper específicamente para estas bases de datos típicas. Por lo tanto, utilicé elFileBackupHelper , apunté a mi archivo .db en " /databases/", introduje bloqueos alrededor de cualquier operación db (como db.insert) en my ContentProviders, e incluso intenté crear el /databases/directorio " " antes onRestore()porque no existe después de la instalación.

He implementado una solución similar con SharedPreferenceséxito en una aplicación diferente en el pasado. Sin embargo, cuando pruebo mi nueva implementación en el emulador-2.2, veo que se realiza una copia de seguridad LocalTransportdesde los registros, así como que se realiza (y se onRestore()llama) una restauración . Sin embargo, el archivo db en sí nunca se crea.

Tenga en cuenta que todo esto es después de una instalación y antes del primer lanzamiento de la aplicación, después de que se haya realizado la restauración. Aparte de eso, mi estrategia de prueba se basó en http://developer.android.com/guide/topics/data/backup.html#Testing .

Tenga en cuenta también que no estoy hablando de una base de datos sqlite que administro yo mismo, ni de realizar copias de seguridad en una tarjeta SD, un servidor propio o en otro lugar.

Vi una mención en los documentos sobre las bases de datos que aconsejan usar una costumbre, BackupAgentpero no parece estar relacionada:

Sin embargo, es posible que desee ampliar BackupAgent directamente si necesita: * Hacer una copia de seguridad de los datos en una base de datos. Si tiene una base de datos SQLite que desea restaurar cuando el usuario vuelva a instalar su aplicación, necesita crear un BackupAgent personalizado que lea los datos apropiados durante una operación de copia de seguridad, luego cree su tabla e inserte los datos durante una operación de restauración.

Un poco de claridad por favor.

Si realmente necesito hacerlo yo mismo hasta el nivel de SQL, entonces me preocupan los siguientes temas:

  • Abrir bases de datos y transacciones. No tengo idea de cómo cerrarlos de una clase de singleton fuera del flujo de trabajo de mi aplicación.

  • Cómo notificar al usuario que se está realizando una copia de seguridad y la base de datos está bloqueada. Puede llevar mucho tiempo, por lo que es posible que deba mostrar una barra de progreso.

  • Cómo hacer lo mismo en la restauración. Según tengo entendido, la restauración puede ocurrir justo cuando el usuario ya ha comenzado a usar la aplicación (y a ingresar datos en la base de datos). Por lo tanto, no puede presumir de restaurar los datos respaldados en su lugar (eliminando los datos vacíos o antiguos). Tendrá que unirse de alguna manera, lo que para cualquier base de datos no trivial es imposible debido a las identificaciones.

  • Cómo actualizar la aplicación después de que se realiza la restauración sin que el usuario se quede atascado en algún punto ahora inalcanzable.

  • ¿Puedo estar seguro de que la base de datos ya se ha actualizado en la copia de seguridad o restauración? De lo contrario, es posible que el esquema esperado no coincida.

pjv
fuente
Tengo el mismo problema ...
No hay forma de db de agujero de copia de seguridad simple?
Jin35
Necesito hacer una copia de seguridad de una carpeta usando FileBackupHelper. ¿Usar el método de guardar la base de datos me permitiría guardar esa carpeta que tiene muchas subcarpetas y subarchivos debajo?
coolcool1994
¿Alguna vez consiguió que esto funcionara correctamente?
DeNitE Appz
Sí, ver más abajo. También respondí mi propia pregunta.
pjv

Respuestas:

21

Un enfoque más limpio sería crear un personalizado BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

    public DbBackupHelper(Context ctx, String dbName) {
        super(ctx, ctx.getDatabasePath(dbName).getAbsolutePath());
    }
}

y luego agréguelo a BackupAgentHelper:

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}
Yanchenko
fuente
2
@logray: Ese código es exactamente el mismo que en la pregunta. Edición innecesaria.
Linuxios
@Linuxios Estoy de acuerdo, sin embargo, creo que es mejor proporcionar la atribución adecuada al autor, en lugar de simplemente pegar el código a ciegas ... considerando que el OP / publicación original incluía un enlace desde su aplicación.
logray
3
Importante : La (documentación) ( developer.android.com/guide/topics/data/backup.html#Files ) para realizar copias de seguridad de archivos indica que la operación no es segura para subprocesos , por lo que debe sincronizar sus métodos de base de datos y sus agentes de copia de seguridad
nicopico
La documentación de @yanchenko para FileBackupHelper dice: "Nota: Esto debe usarse solo con archivos de configuración pequeños, no con archivos binarios grandes". Entonces, una base de datos a menudo calificaría como un archivo binario grande ...
IgorGanapolsky
¡Esto en realidad no funciona! (a menos que me falte algo ...). El problema es que pasa la ruta absoluta de la base de datos al FileBackupHelper pero FileBackupHelper antepone la ruta con la ruta del directorio de archivos, por ejemplo. datos / datos / <su paquete> / archivos que dan como resultado una ruta incorrecta algo como datos / datos / <su paquete> / archivos / datos / datos / <su paquete> / bases de datos / <Nombre de base de datos> cuando todo lo que desea es DB nombre absoluto y dado que la superclase antepone el directorio de archivos, necesita hacer la ruta relativa al directorio de archivos.
bitrock
34

Después de revisar mi pregunta, pude hacer que funcionara después de ver cómo lo hace ConnectBot . ¡Gracias Kenny y Jeffrey!

En realidad, es tan fácil como agregar:

FileBackupHelper hosts = new FileBackupHelper(this,
    "../databases/" + HostDatabase.DB_NAME);
addHelper(HostDatabase.DB_NAME, hosts);

a tu BackupAgentHelper.

El punto que me faltaba era el hecho de que tendrías que usar una ruta relativa con " ../databases/".

Aún así, esta no es de ninguna manera una solución perfecta. Los documentos para FileBackupHelpermencionar, por ejemplo: " FileBackupHelperdebe usarse solo con archivos de configuración pequeños, no archivos binarios grandes ", siendo este último el caso de las bases de datos SQLite.

Me gustaría recibir más sugerencias, información sobre lo que se espera de nosotros (cuál es la solución adecuada) y consejos sobre cómo podría fallar.

pjv
fuente
1
Probablemente debería agregar que, aunque esto no es oficial, ha funcionado muy bien todo este tiempo.
pjv
6
Los caminos de codificación rígida son malos.
Puntero nulo
@pjv, entonces, ¿sincroniza cada interacción de db? Estoy haciendo lo mismo y tratando de averiguar si necesito para sincronizar db.open()a través db.close()o simplemente insertdeclaraciones o qué.
NSouth
22

Esta es una forma más limpia de hacer copias de seguridad de bases de datos como archivos. No hay rutas codificadas.

class MyBackupAgent extends BackupAgentHelper{
   private static final String DB_NAME = "my_db";

   @Override
   public void onCreate(){
      FileBackupHelper dbs = new FileBackupHelper(this, DB_NAME);
      addHelper("dbs", dbs);
   }

   @Override
   public File getFilesDir(){
      File path = getDatabasePath(DB_NAME);
      return path.getParentFile();
   }
}

Nota: anula getFilesDir para que FileBackupHelper funcione en el directorio de bases de datos, no en el directorio de archivos.

Otra pista: también puede usar databaseList para obtener todos sus DB y nombres de fuentes de esta lista (sin la ruta principal) en FileBackupHelper. Entonces todas las bases de datos de la aplicación se guardarían en la copia de seguridad.

Puntero nulo
fuente
¡Combinar esto con la solución dada por @pjv funciona perfectamente! Puede que no sea exactamente lo que las especificaciones tenían en mente ... ¡pero funciona bastante bien!
Tom
1
Eso no sirve de mucho si tiene bases de datos y archivos normales para respaldar.
Dan Hulme
1
@Dan: Utilice varios agentes en ese caso.
pjv
@Pointer Null ¿Podría elaborar un poco más un getFilesDir (), por qué es realmente necesario y por qué? ¿Como funciona?
powder366
7

El uso FileBackupHelperpara hacer una copia de seguridad / restaurar sqlite db plantea algunas preguntas serias:
1. ¿Qué sucede si la aplicación usa el cursor recuperado ContentProvider.query()y el agente de copia de seguridad intenta anular todo el archivo?
2. El enlace es un buen ejemplo de prueba perfecta (baja entrofia). Desinstala la aplicación, la vuelve a instalar y se restaura la copia de seguridad. Sin embargo, la vida puede ser brutal. Eche un vistazo al enlace . Imaginemos un escenario en el que un usuario compra un dispositivo nuevo. Como no tiene su propio conjunto, el agente de respaldo usa el conjunto de otro dispositivo. La aplicación está instalada y su backupHelper recupera el archivo antiguo con un esquema de versión db menor que el actual. SQLiteOpenHelperllamadas onDowngradecon la implementación predeterminada:

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can't downgrade database from version " +
            oldVersion + " to " + newVersion);
}

No importa lo que haga el usuario, no puede usar su aplicación en el nuevo dispositivo.

Sugeriría usar ContentResolverpara obtener datos -> serializar (sin _ids) para respaldo y deserializar -> insertar datos para restaurar.

Nota: la obtención / inserción de datos se realiza a través de ContentResolver, lo que evita problemas de moneda corriente. La serialización se realiza en su backupAgent. Si hace su propio mapeo de objetos cursor <->, serializar un elemento puede ser tan simple como implementar Serializablecon el transientcampo _id en la clase que representa su entidad.

También usaría inserto a granel, es decir ContentProviderOperation ejemplo y CursorLoader.setUpdateThrottlede manera que la aplicación no se ha quedado atascado con el reinicio del cargador de cambio de datos de copia de seguridad durante el proceso de restauración.

Si se encuentra en una situación de degradación, puede optar por cancelar la restauración de datos o restaurar y actualizar ContentResolver con campos relevantes para la versión degradada.

Estoy de acuerdo en que el tema no es fácil, no está bien explicado en los documentos y aún quedan algunas preguntas, como el tamaño de los datos masivos, etc.

Espero que esto ayude.

Piotr Bazán
fuente
Repites algunas preguntas válidas. Pero no veo cómo la serialización de la base de datos resuelve el problema. No ayuda con los problemas de concurrencia. Y si no puede escribir un script para degradar la base de datos, entonces no puede escribir un script para deserializar datos antiguos.
pjv
@pjv Si usa ContentResolver, resuelve problemas de concurrencia por usted.
IgorGanapolsky
@IgorGanapolsky Sí, creo que es cierto. Sin embargo, si la serialización es lo suficientemente lenta y el usuario ya está usando la aplicación e ingresando datos, puede entrar en conflicto con los datos que está restaurando. ¿Puede hacerlo de forma atómica y antes de que el usuario acceda a la aplicación?
pjv
@pjv Puede registrar un ContentObserver para sincronizar los datos que está observando para recibir notificaciones. Por lo tanto, no tiene que preocuparse por los conflictos.
IgorGanapolsky
Su punto sobre las versiones de la base de datos es bueno: supongo que si guarda su versión de la base de datos (en preferencias compartidas, por ejemplo, suponiendo que también haga una copia de seguridad), cuando esté restaurando debería poder utilizar la lógica existente para actualizar la base de datos a su versión actual en el método onDowngrade (). Si aún no tiene el código de actualización, ¡no le molestará mantener los datos de todos modos!
Ben Neill
3

A partir de Android M, ahora hay una API de copia de seguridad / restauración de datos completos disponible para las aplicaciones. Esta nueva API incluye una especificación basada en XML en el manifiesto de la aplicación que permite al desarrollador describir qué archivos respaldar de manera semántica directa: 'respaldar la base de datos llamada "mydata.db"'. Esta nueva API es mucho más fácil de usar para los desarrolladores: no tiene que realizar un seguimiento de las diferencias o solicitar un pase de respaldo explícitamente, y la descripción XML de qué archivos respaldar significa que a menudo no necesita escribir ningún código en absoluto.

(Usted puede involucrarse incluso en una copia de seguridad de todos los datos / operación de restauración para obtener una devolución de llamada cuando la restauración ocurre, por ejemplo. Es flexible de esa manera.)

Consulte la sección Configuración de la copia de seguridad automática para aplicaciones en developer.android.com para obtener una descripción de cómo usar la nueva API.

ctate
fuente
Esto es solo para Android 6.0 (API 23), si el usuario tiene una versión anterior, aún debe implementar la copia de seguridad del valor clave.
live-love
0

Una opción será construirlo en la lógica de la aplicación por encima de la base de datos. En realidad, creo que pide a gritos tal nivel. No estoy seguro de si ya lo está haciendo, pero la mayoría de las personas (a pesar del enfoque del cursor del administrador de contenido de Android) introducirán algunos mapeos ORM, ya sea personalizados o con un enfoque orm-lite. Y lo que preferiría hacer en este caso es:

  1. para asegurarse de que su aplicación funcione bien cuando la aplicación / datos se agreguen en segundo plano con nuevos datos agregados / eliminados mientras la aplicación ya se inició
  2. para hacer algo de Java-> protobuf o incluso simplemente mapeo de serialización de Java y escribir su propio BackupHelper para leer los datos del flujo y simplemente agregarlos a la base de datos ...

Entonces, en este caso, en lugar de hacerlo a nivel de base de datos, hágalo a nivel de aplicación.

Jarek Potiuk
fuente
El problema no es cómo obtener datos de la base de datos en BackupHelperAgent o viceversa. Lea las 5 viñetas al final de mi pregunta.
pjv
@JarekPotiuk Dijiste: "la mayoría de la gente (a pesar del enfoque del cursor del administrador de contenido de Android)". Pero, ¿qué le hace pensar que la mayoría de la gente evitaría utilizar ContentProvider y ContentResolver para acceder a la base de datos?
IgorGanapolsky