J'évalue GreenDAO dans une application commerciale Android sur laquelle je vais travailler et je voulais déterminer le chemin de migration pour les mises à jour de schéma.
Ai-je raison d'affirmer que je devrais écrire un OpenHelper personnalisé qui fournit onUpdate () et extrait les transformations et stocke les données conformément au nouveau schéma? Cette hypothèse soulève des questions intéressantes sur la commande des appels et le partage des responsabilités.
Je n'ai trouvé aucune documentation sur la mise à jour du schéma et la migration des données pour GreenDAO.
Voici quelques articles de blog que j'ai écrits sur ce sujet:
Vous avez supposé correctement. Il n'y a pas de suivi des modifications entre les différentes versions de schéma aujourd'hui. Vous devez donc écrire vous-même le code SQL lorsque vous effectuez une mise à niveau du schéma.
En pensant à l'approche de pleonasmik (au fait, merci, c'était vraiment utile), j'ai créé une classe MigrationHelper.
Comment ça marche:
generateTempTables
)generateTempTables
)DaoMaster.dropAllTables
)DaoMaster.createAllTables
)restoreData
)restoreData
)Classe d'assistance à la migration:
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;
}
}
Et voici un exemple indiquant comment il devrait être appelé à la classe 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);
}
C'est le même code de @PedroOkawa qui fonctionne avec GreenDao 3. + avec des erreurs corrigées
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;
}
}
et l'usage est:
@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
peut être trouvé dans greendao et voici l'importation:
import org.greenrobot.greendao.database.StandardDatabase;
Merci encore à @PedroOkawa :)
Je pense que ma réponse à une question similaire peut aider avec cette approche. Si vous avez vraiment besoin de migrer des données, je vous suggère que, si vous devez par exemple gérer des modifications limitées ou des éléments non pris en charge dans SQLite, écrivez vous-même la migration. Par exemple, un exemple d'aide de migration (suivant l'approche que j'ai utilisée pour la réponse liée) pourrait être:
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);");
}
}
Pour ceux d'entre vous qui cherchent à mettre à jour la version du schéma de base de données sur greenDAO 3, ajoutez ceci au fichier build.gradle
de votre application au-dessus des dépendances:
apply plugin: 'org.greenrobot.greendao'
greendao {
schemaVersion 1
}
La solution de Pedro Okawa est correcte, mais vous devez écrire votre "UpgradeHelper" pour étendre "OpenHelper" car DaoMaster est écrasé à chaque fois que vous générez à nouveau le code DAO.
Exemple:
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);
}
}
Si vous cherchez simplement un moyen d'ajouter de nouvelles tables à votre schéma sans supprimer les données de votre utilisateur et que vous n'avez pas besoin de transformer les données existantes, jetez un coup d'œil à ma réponse à cette question pour un exemple discret de faire avec greenDao.