开发者

How to do database schema migrations in Android?

Is there a stan开发者_如何学JAVAdard way to do database schema migrations on Android? For example, user installs newer version of my Android app but the new version needs to make updates to the database schema (and wiping the user's database and starting over is not an option!). So I need to run some ALTER statements and/or copy tables the first time my new version runs.


Yes SQLiteOpenHelper has support for migrating between different versions of DB schemas.

Upgrading is done by implementing

public abstract void onUpgrade (SQLiteDatabase db, int oldVersion, int newVersion)

And Rolling back to a previous version is also supported :

public abstract void onDowngrade (SQLiteDatabase db, int oldVersion, int newVersion)


With a little bit of thought, you can nearly automate a lot of the SQLiteOpenHelper methods. Have a look at this blog post http://www.greenmoonsoftware.com/2012/02/sqlite-schema-migration-in-android/

Update: I ran into some issues if the database updates take a while to complete. Have a look at this blog entry for an implementation that works for me. http://www.greenmoonsoftware.com/2012/05/android-developer-how-to-safely-update-the-application-database/


As of version 3.0 Flyway now supports database migrations on Android. It works with SQLite, but also with any other database you wish to use (local in-memory one, or a DB on the server for two-tier apps).

Migrations can be written in both SQL and Java.


All the above answers concerning SQLiteOpenHelper are correct, but they all contain a kind of antipattern - creating/modifying DB structure with Strings. It makes both development and maintenance more expensive. Sometimes a migration consists of many statements, some of them may be quite big. Writing them as Strings, without any syntax higlighting... well, for small structures it might work, but for some bigger ones it'd be a nightmare.

A better way to manage schema changes would be to keep each migration script in external file and make SQLiteOpenHelper's onUpgrade method execute them automatically, in the proper order. Here's an article covering this topic: http://www.vertabelo.com/blog/sqlite-on-android-handling-database-structure-changes. I hope this helps.


You can enhance default SQLiteOpenHelper with library Android-Migrator, that allow you to apply migrations from sql files in android assets folder.


I know this is an old one but I have developed an open source eclipse plugin that is a domain specific language (DSL written with XText) that allows you to specify a database as a list of migrations.

It generates your ContentProvider, SqliteOpenHelper and Contract for you, into an API that resembles an API like the Contacts API or Media API.

I have written a basic guide that helps you get started here http://robotoworks.com/mechanoid-plugin/mechanoid-db/

So far it supports create table, create view and alter table statements, I hope to fully implement Sqlite syntax where appropriate for this DSL.

Hope that helps and sorry for the spam! :)


yanchenko's answer is correct, however, you might want to take a look at Droid Migrate, which is a framework I wrote that makes extensive use of SQLiteOpenHelper to facilitate automatic SQLite migrations (much like how Rails does it).


I also wanted something like flyway what I used in my Spring projects, but for android it seems not easy to make it work, so I created a very basic class which basically does the same.

So just put the sql files to the assets folder here: assets/dbmigration/yourdatabasenamelowercase in the format vN.sql where N is the version number. Then from onUpgrade just call like this:

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    new DbMigrationHelper().doMigrate(context, db, DATABASE_NAME, oldVersion, newVersion);
}

onCreate is basically the same, but with fixed version parameters: 0 and 1

And this doMigrate method executes all the scripts in between the fromVersion - toVersion range (excluding fromVersion) in the proper order. So if you call it with e.g. 2 and 5 it will execute the v3.sql then v4.sql and then v5.sql from the proper asset folder. Each script can contain multiple statements (they must be separated with semicolons) and the scripts can contain full line sql comments (started with --). I hope that it can be useful for someone :) So the class:

/**
 * This class helps getting sql resources from asset for migration and to split complex scripts to sql statements automatically.<br>
 * To create assets folder: right click on app (Project view) > new > folder > Assets folder ... and just click finish now you have src/main/assets folder.
 */
   public class DbMigrationHelper {

    private static final String TAG = "DbMig";
   
        public void doMigrate(Context context, SQLiteDatabase db, String dbName, int fromVersion, int toVersion){
            Objects.requireNonNull(context,"Context can not be null");
            Objects.requireNonNull(db, "Database can not be null");
            Objects.requireNonNull(dbName,"Database name can not be null");
            if(fromVersion > toVersion){
                throw new RuntimeException("old version (" + fromVersion + ") > new version (" + toVersion + ")");
            }
            if(fromVersion == toVersion){
                Log.d(TAG,"Old and New versions are the same: " + fromVersion + " no migration will be done");
                return;
            }
            try {
                for (int i = fromVersion + 1; i <= toVersion; i++){
                    Log.i(TAG,"Migrating to version " + i);
                    String script = inputStreamToString(context.getAssets().open("dbmigration/"+dbName.toLowerCase()+"/v"+i+".sql"));
                    executeScript(script, db);
                    Log.i(TAG,"Migration to v" +i+ " has done.");
                }
                Log.i(TAG,"Migration finished.");
            } catch (Exception e) {
                throw new RuntimeException("Migration script problem: "+e.getMessage(),e);
            }
        }
    
        private void executeScript(String script, SQLiteDatabase db){
            Objects.requireNonNull(script,"Script is null");
            List<String> statements = splitSqlScriptToStatements(script);
            for(String statement : statements){
                Log.i(TAG, "Executing: "+statement);
                db.execSQL(statement);
            }
        }
    
        private String inputStreamToString(InputStream is) throws IOException {
            StringBuilder sb = new StringBuilder();
            try(BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8 ))){
                String str;
                while ((str = br.readLine()) != null) {
                    sb.append(str).append("\n");
                }
                return sb.toString();
            }finally {
                is.close();
            }
        }
        /**
         * Splits sql script to statements. Removes empty lines and FULL LINE comments (started with --).<br>
         * <b>All statements must be terminated with ;</b>
         * @param sqlScript the full script content
         * @return Sql statements in the exact order they are in the script
         */
        private List<String> splitSqlScriptToStatements(String sqlScript){
            List<String> res = new LinkedList<>();
            if (sqlScript == null) {
                return res;
            }
            List<String> valuableLines = removeCommentedAndEmptyLines(splitToLines(sqlScript));
            StringBuilder buffer = new StringBuilder("");
            for(String line : valuableLines){
                if(buffer.length()>0){
                    buffer.append(" ");
                }
                buffer.append(line.trim());
                if(line.trim().endsWith(";")){
                    res.add(buffer.toString());
                    buffer = new StringBuilder("");
                }
            }
            if(buffer.length()>0){
                res.add(buffer.toString());
            }
            return res;
        }
    
        /**
         * Splits the given full text by \n
         * @param sql
         * @return not null list of lines
         */
        private List<String> splitToLines(String sql){
            List<String> res = new LinkedList<>();
            if(sql == null){
                return res;
            }
            return Arrays.asList(sql.split("\\r?\\n"));
        }
        /**
         *
         * @param src
         * @return non empty list of lines containing no comments no empty lines
         */
        private List<String> removeCommentedAndEmptyLines(List<String> src){
            List<String> res = new LinkedList<>();
            if(src == null){
                return res;
            }
            for(String s : src){
                if(s != null && !s.trim().startsWith("--") && !s.trim().isEmpty()){
                    res.add(s);
                }
            }
            return res;
        }
    }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜