Android SQLite 数据库升级终极模板代码

前言:最近苦思冥想一个问题,热爱技术的人怎么能够在一个充满规章制度的氛围里好好的生存下去并能初心不改呢?这就像为啥拍一集电视剧的报酬就是你好几年的收入一样,毕竟现实就是这么个情况,想不明白就来写写文章吧。


不登高山不知天之大,不临深谷不知地之厚。   

一、场景

在数据库升级时,不同的数据库版本表结构是不同的,那么怎么确保数据库升级后用户的数据不会丢失呢?比如 V1.0 表A有10列,V1.1由于业务需求表A需要增加两列,在升级时我们该怎么做?

二、传统写法

/**
 * @author lh 2016/9/1
 * @deprecated 
 */
public class DBHelper extends SQLiteOpenHelper {
    private static final String DB_NAME = "video.db";//数据库名
    private static final int DB_VERSION = 1;//数据库当前版本号

    public DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        //第一次安装app会执行这个方法
        //创建表A
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        //数据库版本升级时会执行这个方法
        //第一步将表A重命名为temp_A
        //第二步创建新表A,此时表结构已加了2列
        //第三步将temp_A表中的数据插入到表A
        //第四步删除临时表temp_A
    }
}
代码很简单,但是这样就真解决问题了吗?数据库从1升级到2要写一个业务变化,从2升级到3也要写一个业务变化,那是否考虑过数据库版本从1升级到3应该先升级到2再升级到3这种情况呢?不论正确与否,这种写法毫无扩展性可言,每次表结构发生变化都要改动这个类。下面看下模板写法。

三、模板写法

/**
 * @author lh 2016/9/1
 */
public class DBHelper extends SQLiteOpenHelper {
    private static final String DB_NAME = "video.db";
    private static final int DB_VERSION = VersionFactory.getCurrentDBVersion();
    private static volatile DBHelper instance = null;

    private DBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    public static DBHelper getDBHelper(Context context) {
        if (instance == null) {
            synchronized (DBHelper.class) {
                if (instance == null)
                    instance = new DBHelper(context);
            }
        }
        return instance;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        /**创建视频信息表*/
        db.execSQL(SqlUtil.createSqlForVideo());
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        update(db, oldVersion, newVersion);
    }

    /**
     * 数据库版本递归更新
     *
     * @param oldVersion 数据库当前版本号
     * @param newVersion 数据库升级后的版本号
     * @author lh
     * @retrun void
     */
    public static void update(SQLiteDatabase db, int oldVersion, int newVersion) {
        Upgrade upgrade = null;
        if (oldVersion < newVersion) {
            oldVersion++;
            upgrade = VersionFactory.getUpgrade(oldVersion);
            if (upgrade == null) {
                return;
            }
            upgrade.update(db);
            update(db, oldVersion, newVersion);
        }
    }
}

DBHelper.update方法是一个递归实现,主要是解决数据库跨版本升级。下面看一下Upgrade类

/**
 * @author lh 2016/9/1
 */
public abstract class Upgrade {
    public abstract void update(SQLiteDatabase db);
}
Upgrade是一个抽象类,就定义了一个数据库升级的方法update(SQLiteDatabase db),以后数据库版本每升级一次,就创建一个子类继承Upgrade类,实现update方法,在方法内执行你的业务逻辑,比如表结构变更;

接着我们来看一下工厂类VersionFactory,他的作用就是提供一个工厂方法构建对象。

原始工厂写法:

/**
 * @author lh 2016/9/1
 * @deprecated 
 */
public class VersionFactory {
    /**
     * 根据数据库版本号获取对象
     *
     * @param i
     * @return upgrade
     */
    public static Upgrade getUpgrade(int i) {
        Upgrade upgrade = null;
        switch (i) {
            case 2:
                upgrade = new VersionSecond();
                break;
            case 3:
                upgrade = new VersionThird();
                break;
            case 4:
                upgrade = new VersionFourth();
                break;
        }
        return upgrade;
    }
}

这样写虽然达到了分别创建对象的效果,但是每次升级还是要来修改这个类,我就遇到一次,我同事在写分支语句的时候漏掉了break,导致对象创建出错,从而对应版本号的数据库升级逻辑未能执行导致app升级安装出错。自那以后我就决定要优化这玩意儿,减少出错的风险。我先贴一个Upgrade的实现类VersionSecond帮助大家理解

/**
 * @author lh 2016/9/1
 */
public class VersionSecond extends Upgrade {
    @Override
    public void update(SQLiteDatabase db) {
        //数据库版本升级时会执行这个方法
        //第一步将表A重命名为temp_A
        //第二步创建新表A,此时表结构已加了2列
        //第三步将temp_A表中的数据插入到表A
        //第四步删除临时表temp_A
    }
}
紧接着我们来看一下优化后的工厂模式写法

/**
 * @author lh 2016/9/1
 */
public class VersionFactory {
    /**
     * 根据数据库版本号获取对应的对象
     * @param i
     * @return
     */
    public static Upgrade getUpgrade(int i) {
        Upgrade upgrade = null;
//	List> list = ClassUtil.getClasses("com.upsoft.ep.app.module.dbupdate");
        if (null != list && list.size() > 0) {
            try {
                for (String className : list) {
                    Class cls = null;
                    cls = Class.forName(className);
                    if (Upgrade.class == cls.getSuperclass()) {
                        VersionCode versionCode = cls.getAnnotation(VersionCode.class);
                        if (null == versionCode) {
                            throw new IllegalStateException(cls.getName() + "类必须使用VersionCode类注解");
                        } else {
                            if (i == versionCode.value()) {
                                upgrade = (Upgrade) cls.newInstance();
                                break;
                            }
                        }
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                throw new IllegalStateException("没有找到类名,请检查list里面添加的类名是否正确!");
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return upgrade;
    }

    static Set list = new LinkedHashSet<>();

    static {
        list.add("com.upsoft.ep.app.module.dbupdate.VersionSecond");
        list.add("com.upsoft.ep.app.module.dbupdate.VersionThird");
        list.add("com.upsoft.ep.app.module.dbupdate.VersionFourth");
        list.add("com.upsoft.ep.app.module.dbupdate.VersionFifth");
    }

    /**
     * 得到当前数据库版本
     *
     * @return
     */
    public static int getCurrentDBVersion() {
        return list.size() + 1;
    }
}
先看getUpgrade方法,我注释了一句代码
//	List> list = ClassUtil.getClasses("com.upsoft.ep.app.module.dbupdate");
这句代码的作用是获取包名下的所有java文件,试想一下,数据库表结构发生更新,我们就创建一个对应的Upgrade的子类去实现数据库操作的相关逻辑,而这个子类是在固定的包名下进行创建,我们如果获取了这个包的所有java文件,就意味着我们获取到了数据库更新的次数,这个次数再加上1是不是就是我们当前的数据库版本号呢?如果是这样,那么我们的数据库版本号就做到了根据算法自动叠加,想想就有点小激动呢~我可以负责任的告诉大家答案就是这样的。不过:

android毕竟是android,包建强(App研发录作者,10几年移动开发经验)说过Android程序员做不好Java,Java程序员也做不好Android,因为各自都有自己的一套规则,不深入是不能够理解的。当然也不排除有全能的~

在Java里是可以获取到某个包名下的java文件,但是在Android里却不行,因为Android虚拟机把java文件编译成了dex文件,所以运行时状态下就不存在这样的文件了,这也是我注释掉这句代码的原因,因为没用,获取到的永远都为空~~~

接着往下看:

遍历存放Upgrade子类全路径的集合,

通过反射的方式获取类对象
判断是否是Upgrade的子类

接着获取VersionCode注解

VersionCode versionCode = cls.getAnnotation(VersionCode.class);
我们来看看VersionCode是个什么鬼?

/**
 * @author lh 2016/9/1
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface VersionCode {
    int value() default 1;
}
该注解的作用是定义Upgrade子类的数据库版本号,在子类使用,例如:

/**
 * @author lh 2016/9/1
 */
@VersionCode(2)
public class VersionSecond extends Upgrade {
    @Override
    public void update(SQLiteDatabase db) {
        //数据库版本升级时会执行这个方法
        //第一步将表A重命名为temp_A
        //第二步创建新表A,此时表结构已加了2列
        //第三步讲temp_A表中的数据插入到表A
        //第四步删除临时表temp_A
    }
}
回到getUpdate方法

获取到Update子类的注解后,我们将其和传入的数据库版本号进行比较,如果相等,则返回该对象,否则继续遍历直到获取到正确的对象为止。

if (Upgrade.class == cls.getSuperclass()) {
     VersionCode versionCode = cls.getAnnotation(VersionCode.class);
        if (null == versionCode) {
            throw new IllegalStateException(cls.getName() + "类必须使用VersionCode类注解");
        } else {
            if (i == versionCode.value()) {
                upgrade = (Upgrade) cls.newInstance();
                break;
            }
        }
 }
getCurrentDBVersion这个方法就比较简单,直接返回的Upgrade子类列表的数量加1作为数据库的当前版本号

本版本实现的优势:

1、首先采用了单例模式,如果不懂什么单例以及线程安全请自行google。

2、采用递归的方式解决了数据库跨越升级的问题

3、采用工厂模式达到了易扩展

4、采用注解和反射优化了工厂模式的短板

5、数据库当前的版本也是由VersionFactory.getCurrentDBVersion()动态获得,再也不用担心数据库升级直接操作DBHelper类导致的代码错误风险。

另每次创建Upgrade子类都需要添加到list列表里面去,这样的话还是会修改到VersionFactory类(虽然做了异常检查处理),你们具体实现的时候可以做成可配置的比如写在XML文件里,使用时进行XML解析即可。

那眼尖的童鞋又会问Android里不是应该少用注解吗?会影响性能~我可以放心的告诉大家,经过测试,我模拟了200次的数据库更新操作,用正常的方式实现和用反射+注解的方式实现相差时间在毫秒级别(10毫秒左右),基本可以忽略。


最后~写文章好累!

不登高山不知天之大,不临深谷不知地之厚。

                                                            

你可能感兴趣的:(android)