¿Actualización de esquema GreenDAO y migración de datos?
Estoy evaluando GreenDAO para su consideración en una aplicación comercial de Android que estaré trabajando y quería determinar la ruta de migración para las actualizaciones de esquema.
¿Estoy correcto al afirmar que tendría que escribir un OpenHelper personalizado que proporciona el onUpdate () y extrae las transformaciones y almacena los datos de acuerdo con el nuevo esquema? Esta suposición plantea algunas preguntas interesantes acerca del ordenamiento de las llamadas y el reparto de la responsabilidad.
- ¿Cómo carga todos los campos de una base de datos y ordeno el resultado después de una fila con GreenDao?
- NoClassDefFoundError al usar greenDao
- Cómo implementar herencia de tabla en GreenDao
- GreenDAO soporta múltiples relaciones entre tablas
- GreenDAO 40 segundos para insertar 600 registros
No he podido encontrar ninguna documentación alrededor de la actualización del esquema y de la migración de datos para GreenDAO.
Aquí hay un montón de artículos de blog que he escrito sobre este tema:
- Revisión de greenDAO
- Parte 1 – Generación de esquemas
- Parte 2 – Migración de esquemas
- Parte 3 – Prueba de migración de esquema
- El grupo de conexiones no ha podido conceder una conexión a subproceso
- GreenDAO groupby cláusula
- ¿Por qué GreenDAO no soporta completamente el operador LIKE?
- Consulta SQLite de Android con clave externa utilizando la tormenta o una alternativa
- GreenDAO - clave primaria en varias columnas
- Forma adecuada para agregar columnas de índice en greenDao?
- Persistencia de entidades bidireccionales con Green-DAO
- Cómo limpiar / eliminar base de datos greenDao
Asumiste correctamente. No existe ningún seguimiento de cambios entre versiones de esquemas diferentes en la actualidad. Por lo tanto, necesitas escribir SQL cuando realices actualizaciones de esquemas.
Pensando en el enfoque del pleonasmik (por cierto, gracias, fue muy útil), creé una clase MigrationHelper.
Cómo funciona:
- La clase captura todos los Daos que tienes
- Crea las tablas temporales basadas en el esquema de la versión anterior (método
generateTempTables
) - Importe todos los datos a estas nuevas tablas (método
generateTempTables
) - Eliminar todas las tablas de la versión antigua (método
DaoMaster.dropAllTables
) - Crea las tablas de la nueva versión (método
DaoMaster.createAllTables
) - Actualiza las tablas de la nueva versión de los temporales (método
restoreData
) -
restoreData
todas las tablas temporales (métodorestoreData
)
Migración Clase de ayuda:
import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import android.util.Log; import com.crashlytics.android.Crashlytics; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.internal.DaoConfig; import greendao.DaoMaster; /** * Created by pokawa on 18/05/15. */ public class MigrationHelper { private static final String CONVERSION_CLASS_NOT_FOUND_EXCEPTION = "MIGRATION HELPER - CLASS DOESN'T MATCH WITH THE CURRENT PARAMETERS"; private static MigrationHelper instance; public static MigrationHelper getInstance() { if(instance == null) { instance = new MigrationHelper(); } return instance; } public void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { generateTempTables(db, daoClasses); DaoMaster.dropAllTables(db, true); DaoMaster.createAllTables(db, false); restoreData(db, daoClasses); } private void generateTempTables(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for(int i = 0; i < daoClasses.length; i++) { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String divider = ""; String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); ArrayList<String> properties = new ArrayList<>(); StringBuilder createTableStringBuilder = new StringBuilder(); createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" ("); for(int j = 0; j < daoConfig.properties.length; j++) { String columnName = daoConfig.properties[j].columnName; if(getColumns(db, tableName).contains(columnName)) { properties.add(columnName); String type = null; try { type = getTypeByClass(daoConfig.properties[j].type); } catch (Exception exception) { Crashlytics.logException(exception); } createTableStringBuilder.append(divider).append(columnName).append(" ").append(type); if(daoConfig.properties[j].primaryKey) { createTableStringBuilder.append(" PRIMARY KEY"); } divider = ","; } } createTableStringBuilder.append(");"); db.execSQL(createTableStringBuilder.toString()); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tempTableName).append(" ("); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(") SELECT "); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(" FROM ").append(tableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); } } private void restoreData(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for(int i = 0; i < daoClasses.length; i++) { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); ArrayList<String> properties = new ArrayList(); for (int j = 0; j < daoConfig.properties.length; j++) { String columnName = daoConfig.properties[j].columnName; if(getColumns(db, tempTableName).contains(columnName)) { properties.add(columnName); } } StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" ("); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(") SELECT "); insertTableStringBuilder.append(TextUtils.join(",", properties)); insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";"); StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE ").append(tempTableName); db.execSQL(insertTableStringBuilder.toString()); db.execSQL(dropTableStringBuilder.toString()); } } private String getTypeByClass(Class<?> type) throws Exception { if(type.equals(String.class)) { return "TEXT"; } if(type.equals(Long.class) || type.equals(Integer.class) || type.equals(long.class)) { return "INTEGER"; } if(type.equals(Boolean.class)) { return "BOOLEAN"; } Exception exception = new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString())); Crashlytics.logException(exception); throw exception; } private static List<String> getColumns(SQLiteDatabase db, String tableName) { List<String> columns = new ArrayList<>(); Cursor cursor = null; try { cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 1", null); if (cursor != null) { columns = new ArrayList<>(Arrays.asList(cursor.getColumnNames())); } } catch (Exception e) { Log.v(tableName, e.getMessage(), e); e.printStackTrace(); } finally { if (cursor != null) cursor.close(); } return columns; } }
Y aquí está un ejemplo de cómo debería ser llamado en la clase DaoMaster.java:
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by migrating all tables data"); MigrationHelper.getInstance().migrate(db, UserDao.class, ItemDao.class); }
Creo que mi respuesta a una pregunta similar puede ayudar con este enfoque. Si realmente necesitas migrar datos te sugiero que si por ejemplo necesitas lidiar con algunos cambios de restricciones o cosas que no son compatibles con SQLite, tú mismo escribes la migración. Por ejemplo un ejemplo de un ayudante migrator (siguiendo el enfoque que hice en la respuesta vinculada) podría ser:
public class DBMigrationHelper6 extends AbstractMigratorHelper { /* Upgrade from DB schema 6 to schema 7 , version numbers are just examples*/ public void onUpgrade(SQLiteDatabase db) { /* Create a temporal table where you will copy all the data from the previous table that you need to modify with a non supported sqlite operation */ db.execSQL("CREATE TABLE " + "'post2' (" + // "'_id' INTEGER PRIMARY KEY ," + // 0: id "'POST_ID' INTEGER UNIQUE ," + // 1: postId "'USER_ID' INTEGER," + // 2: userId "'VERSION' INTEGER," + // 3: version "'TYPE' TEXT," + // 4: type "'MAGAZINE_ID' TEXT NOT NULL ," + // 5: magazineId "'SERVER_TIMESTAMP' INTEGER," + // 6: serverTimestamp "'CLIENT_TIMESTAMP' INTEGER," + // 7: clientTimestamp "'MAGAZINE_REFERENCE' TEXT NOT NULL ," + // 8: magazineReference "'POST_CONTENT' TEXT);"); // 9: postContent /* Copy the data from one table to the new one */ db.execSQL("INSERT INTO post2 (_id, POST_ID, USER_ID, VERSION, TYPE, MAGAZINE_ID, SERVER_TIMESTAMP, CLIENT_TIMESTAMP, MAGAZINE_REFERENCE, POST_CONTENT)" + " SELECT _id, POST_ID, USER_ID, VERSION, TYPE, MAGAZINE_ID, SERVER_TIMESTAMP, CLIENT_TIMESTAMP, MAGAZINE_REFERENCE, POST_CONTENT FROM post;"); /* Delete the previous table */ db.execSQL("DROP TABLE post"); /* Rename the just created table to the one that I have just deleted */ db.execSQL("ALTER TABLE post2 RENAME TO post"); /* Add Index/es if you want them */ db.execSQL("CREATE INDEX " + "IDX_post_USER_ID ON post" + " (USER_ID);"); } }
Este es el mismo código de @PedroOkawa que funciona con el GreenDao 3. + con errores fijos:
import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import android.text.TextUtils; import org.greenrobot.greendao.AbstractDao; import org.greenrobot.greendao.database.Database; import org.greenrobot.greendao.database.StandardDatabase; import org.greenrobot.greendao.internal.DaoConfig; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Createdby PedroOkawa and modified by MBH on 16/08/16. */ public final class MigrationHelper { public static void migrate(SQLiteDatabase sqliteDatabase, Class<? extends AbstractDao<?, ?>>... daoClasses) { StandardDatabase db = new StandardDatabase(sqliteDatabase); generateNewTablesIfNotExists(db, daoClasses); generateTempTables(db, daoClasses); dropAllTables(db, true, daoClasses); createAllTables(db, false, daoClasses); restoreData(db, daoClasses); } public static void migrate(StandardDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { generateNewTablesIfNotExists(db, daoClasses); generateTempTables(db, daoClasses); dropAllTables(db, true, daoClasses); createAllTables(db, false, daoClasses); restoreData(db, daoClasses); } private static void generateNewTablesIfNotExists(StandardDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "createTable", true, daoClasses); } private static void generateTempTables(StandardDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("CREATE TEMP TABLE ").append(tempTableName); insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); } } private static void dropAllTables(StandardDatabase db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "dropTable", ifExists, daoClasses); } private static void createAllTables(StandardDatabase db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { reflectMethod(db, "createTable", ifNotExists, daoClasses); } /** * dao class already define the sql exec method, so just invoke it */ private static void reflectMethod(StandardDatabase db, String methodName, boolean isExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) { if (daoClasses.length < 1) { return; } try { for (Class cls : daoClasses) { Method method = cls.getDeclaredMethod(methodName, Database.class, boolean.class); method.invoke(null, db, isExists); } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } private static void restoreData(StandardDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) { for (int i = 0; i < daoClasses.length; i++) { DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]); String tableName = daoConfig.tablename; String tempTableName = daoConfig.tablename.concat("_TEMP"); // get all columns from tempTable, take careful to use the columns list List<String> columns = getColumns(db, tempTableName); ArrayList<String> properties = new ArrayList<>(columns.size()); for (int j = 0; j < daoConfig.properties.length; j++) { String columnName = daoConfig.properties[j].columnName; if (columns.contains(columnName)) { properties.add(columnName); } } if (properties.size() > 0) { final String columnSQL = TextUtils.join(",", properties); StringBuilder insertTableStringBuilder = new StringBuilder(); insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" ("); insertTableStringBuilder.append(columnSQL); insertTableStringBuilder.append(") SELECT "); insertTableStringBuilder.append(columnSQL); insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";"); db.execSQL(insertTableStringBuilder.toString()); } StringBuilder dropTableStringBuilder = new StringBuilder(); dropTableStringBuilder.append("DROP TABLE ").append(tempTableName); db.execSQL(dropTableStringBuilder.toString()); } } private static List<String> getColumns(StandardDatabase db, String tableName) { List<String> columns = null; Cursor cursor = null; try { cursor = db.rawQuery("SELECT * FROM " + tableName + " limit 0", null); if (null != cursor && cursor.getColumnCount() > 0) { columns = Arrays.asList(cursor.getColumnNames()); } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) cursor.close(); if (null == columns) columns = new ArrayList<>(); } return columns; } }
Y el uso es:
@Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { MigrationHelper.migrate(new StandardDatabase(sqLiteDatabase), UserDao.class, ItemDao.class); // OR you can use it like this (Dont use both it is example of 2 different usages) MigrationHelper.migrate(sqLiteDatabase, UserDao.class, ItemDao.class); }
StandardDatabase
se puede encontrar en greendao y esta es la importación:
import org.greenrobot.greendao.database.StandardDatabase;
Gracias de nuevo a @PedroOkawa 🙂
Pedro Okawa 'es correcta, pero necesitas escribir tu "UpgradeHelper" para extender "OpenHelper" porque DaoMaster se sobrescribe cada vez que vuelves a generar código DAO.
Ejemplo:
public class UpgradeHelper extends DaoMaster.OpenHelper { public UpgradeHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) { super(context, name, factory); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by migrating all tables data"); MigrationHelper.getInstance().migrate(db, UserDao.class, ItemDao.class, AnotherClassToGenerateDao.class); }
}
Para aquellos que busquen actualizar la versión del esquema de base de datos en greenDAO 3, agregue esto al archivo build.gradle
su aplicación por encima de las dependencias:
apply plugin: 'org.greenrobot.greendao' greendao { schemaVersion 1 }
Si sólo está buscando una manera de agregar nuevas Tablas a su esquema sin eliminar los datos de su usuario y no necesita transformar ningún dato existente, eche un vistazo a mi respuesta a esta pregunta para un ejemplo discreto de cómo hacerlo con GreenDao.