android之orm数据库框架greendao的升级维护

几百年不写博客了,而刚好又一晚上失眠,可能由于刚放完假,还没适应过来吧,反正也睡不着就想着早起写篇文章记录下工作中遇到的一些问题,也顺便静下心,好让自己快速投入工作中。

由于以前做的APP都是小型的,所以对数据库的使用程度不高,一直用自带的手写数据库即可。去年9月份开始加入一个项目的开发,用到了数据库greendao,由于之前没用过,所以做之前查看了相关的资料,但是当做了之后才发现原来坑也挺大。

 greendao由于是orm框架,所以对数据库的操作确实是非常方便的,但是同时也存在一些问题,比如数据的读取,它是有缓存的,你会发现你存了一条数据后再去读取数据,结果并没有你刚插入的那条数据,原因是存在缓存,所以需要清理下缓存,其实这也不算是问题和BUG,应该是设计的初衷是为了一些常规不经常修改的数据,为了读取数据更快才加入的缓存,自己清理下缓存再读取即可。

废话不多说了,重点,每次我们更新数据库之后,比如新增字段、删减字段,需要对数据库的版本号进行增加,这样greendao才知道我们修改了数据库,需要进行数据库的升级,但是问题来了,greendao每次升级数据库都是把本地数据库全部重清空了,表也删了,然后再根据你新版本的表实体来新建新的数据库表,这样就会导致你的数据无法保留,每次一旦有数据库的更新,都会导致数据的丢失,所以网上出现了一些大神写的工具类,实现的思路其实也很简单,首先要屏蔽掉greendao的默认升级功能,然后:

1.首先创建临时表(字段名和属性跟旧表一模一样);

2.遍历所有表,把对应表的数据插入到临时表中;

3.删除原表,新建新表;

4.把临时表数据插入到新表中,然后删除临时表;

以上四个步骤就是网上最公认的数据库升级方式,实现的工具类代码如下:

package com.android.sdongpo.db;

import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;

import com.android.sdongpo.gen.DaoMaster;

import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.internal.DaoConfig;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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;
    }

    private static List<String> getColumns(Database 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;
    }

    public void migrate(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
        generateTempTables(db, daoClasses);
        DaoMaster.dropAllTables(db, true);
        DaoMaster.createAllTables(db, false);
        restoreData(db, daoClasses);
    }

    private void generateTempTables(Database 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) {
                        exception.printStackTrace();
                    }

                    createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);

                    if (daoConfig.properties[j].primaryKey) {
                        createTableStringBuilder.append(" PRIMARY KEY");
                    }

                    divider = ",";
                }
            }
            createTableStringBuilder.append(");");
            Log.i("lxq", "创建临时表的SQL语句: " + createTableStringBuilder.toString());
            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(";");
            Log.i("lxq", "在临时表插入数据的SQL语句:" + insertTableStringBuilder.toString());
            db.execSQL(insertTableStringBuilder.toString());
        }
    }

    private void restoreData(Database 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();
            ArrayList<String> propertiesQuery = 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);
                    propertiesQuery.add(columnName);
                } else {
                    try {
                        if (getTypeByClass(daoConfig.properties[j].type).equals("INTEGER")) {
                            propertiesQuery.add("0 as " + columnName);
                            properties.add(columnName);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            StringBuilder insertTableStringBuilder = new StringBuilder();

            insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(") SELECT ");
            insertTableStringBuilder.append(TextUtils.join(",", propertiesQuery));
            insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");

            StringBuilder dropTableStringBuilder = new StringBuilder();

            dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
            Log.i("lxq", "插入正式表的SQL语句:" + insertTableStringBuilder.toString());
            Log.i("lxq", "销毁临时表的SQL语句:" + dropTableStringBuilder.toString());
            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) || type.equals(int.class)) {
            return "INTEGER";
        }
        if (type.equals(Boolean.class) || type.equals(boolean.class)) {
            return "BOOLEAN";
        }

        Exception exception = new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString()));
        exception.printStackTrace();
        throw exception;
    }
}

以上就是网上最流行的工具类,是由国外一位大神写的,但是你会发现这无法满足我们的使用(大神提供思路,剩下的当然需要我们自己来完成),先说问题所在:

1.greendao主键是通过设置@Id来指定主键的,而坑爹的地方在于如果你设置实体类的id属性为主键,那么创建数据库的时候表中的字段明是_id,如果你不设置成主键,那么表中创建的字段名是ID,这就导致了问题了,因为创建临时表是根据旧表来的,假设我的旧表是主键,那么创建的临时表字段应该是_id,而新版本我取消了它主键特性,新表创建的就是ID字段,导致我从临时表迁移数据的时候找不到ID字段,所以这个字段就不会有值和数据,导致了数据丢失;

2.如果旧版本没有表A,新版新增了表A,那么又会出现异常,因为旧版本没有A,所以不会创建A的临时表,那么你进行数据迁移时,新表会从A_temp表(按照上面的工具类,临时表的表名是旧表名加"_temp"后缀)中取数据,由于表A_temp不存在,就会抛出异常崩溃,升级失败;

3.greendao中int和boolean类型会自动给加上not null修饰,所以如果你的表中这两个类型的没赋值,又会抛出异常崩溃,升级失败;

4.对于double类型和float类型的数据,无法迁移,因为工具类里面只做了int,boolean,long三个类型的处理;

那么这四个问题就是数据库升级最大的坑,任何一个都是致命的,那么如何修改呢?

问题4,增加类型判断即可,修改方法getTypeByClass:

/**
     * author: 夏金
     * time: 2019/12/6 11:41
     * use: ->解析字段类型
     */
    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) || type.equals(int.class)) {
            return "INTEGER";
        }
        if (type.equals(Boolean.class) || type.equals(boolean.class)) {
            return "BOOLEAN";
        }
        if (type.equals(Double.class) || type.equals(double.class)) {
            return "DOUBLE";
        }
        if (type.equals(Float.class) || type.equals(float.class)) {
            return "FLOAT";
        }
        Exception exception = new Exception(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString()));
        exception.printStackTrace();
        throw exception;
    }

问题3,解决思路是对于int和boolean类型没有值的我们手动赋个默认值(boolean在greendao中是使用0和1来表示的)

问题2,解决思路是,greendao中没有提供办法来判断表是否存在,我们可以自己手动写个select语句来判断表是否存在,如果select抛出异常代表该表不存在,反之则存在

问题2.3代码如下,修改restoreData方法如下:

/**
     * author: 夏金
     * time: 2019/12/6 11:41
     * use: ->数据迁移并删除临时表
     * 注意,int和boolean类型生成表的时候会自动加上not null,所以这两种类型要做下处理,必须给个默认值
     */
    private void restoreData(Database 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();
            ArrayList<String> propertiesQuery = new ArrayList();
            // 使用查询来判断对应的临时表是否存在,不存在就不进行数据迁移,否则会报异常
            try {
                db.rawQuery("SELECT COUNT(1) FROM " + tempTableName, null);
            } catch (Exception e) {
                continue;
            }
            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;
                if (getColumns(db, tempTableName).contains(columnName)) {
                    Log.i("sql_log", tableName + "临时表中有:" + columnName);
                    propertiesQuery.add(columnName);
                    properties.add(columnName);
                } else {
                    Log.i("sql_log", tableName + "临时表中没有:" + columnName);
                    try {
                        if (getTypeByClass(daoConfig.properties[j].type).equals("INTEGER")) {
                            propertiesQuery.add("0 as " + columnName);
                            properties.add(columnName);
                        } else if (getTypeByClass(daoConfig.properties[j].type).equals("BOOLEAN")) {
                            propertiesQuery.add("0 as " + columnName);
                            properties.add(columnName);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            StringBuilder insertTableStringBuilder = new StringBuilder();

            insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
            insertTableStringBuilder.append(TextUtils.join(",", properties));
            insertTableStringBuilder.append(") SELECT ");
            insertTableStringBuilder.append(TextUtils.join(",", propertiesQuery));
            insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");

            StringBuilder dropTableStringBuilder = new StringBuilder();

            dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
            Log.i("sql_log", "插入正式表的SQL语句:" + insertTableStringBuilder.toString());
            Log.i("sql_log", "销毁临时表的SQL语句:" + dropTableStringBuilder.toString());
            db.execSQL(insertTableStringBuilder.toString());
            db.execSQL(dropTableStringBuilder.toString());
        }
    }

问题1,解决思路是:既然设不设置主键会导致字段名不一样,那么我们就需要做兼容,比如新表是_id,那么我们不仅要从临时表中查_id,还要查ID,这样才不会遗漏主键导致的字段名的变更,具体代码实现,修改工具类的generateTempTables方法,代码如下:

/**
     * author: 夏金
     * time: 2019/12/6 11:40
     * use: ->创建临时表保存原数据
     * 主要是防止取消或者新设置了id属性为主键,id是不是主键,greendao生成的字段名也会不一样
     * 比如,上个版本是某个实体类的id属性不是主键,那表的的字段就是"ID";然后这个版本我设置了它作为主键,那么新版本greendao生成的对应字段是"_id"
     * 升级时,先看旧表中有没有新表的字段,如果新表中已经被删除了,就不需要创建临时字段;如果新表中还在,旧表中也有,那么创建字段
     * 问题来了,旧版本是"ID",新版本是"_id",所以临时表不会生成"ID"或"_id"的字段,所以需要对以实体类id字段生成的表字段做处理
     * 如果旧表是"ID",新表是"_id",那就给临时表生成"_id",然后把旧表的值赋给它,因为新表从临时表取数据是根据新表字段名来的,所以临时表用新表的id格式
     * 所以单独对实体类属性的id坐下处理
     */
    private void generateTempTables(Database 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<>(); // 新表的字段名列表(用来生成临时表)
            ArrayList<String> oldProperties = new ArrayList<>(); // 旧表的字段名(用来select后赋值给临时表的,所以不能用新表的字段,因为旧表的字段是旧字段)

            // 使用查询来判断表是否存在,不存在就表示是新版本新生成的表,不生成临时表,否则会报异常
            try {
                db.rawQuery("SELECT COUNT(1) FROM " + tableName, null);
            } catch (Exception e) {
                continue;
            }

            StringBuilder createTableStringBuilder = new StringBuilder();

            createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" (");
            Log.i("sql_log", "旧表的字段名列表: " + new Gson().toJson(getColumns(db, tableName)));
            for (int j = 0; j < daoConfig.properties.length; j++) {
                String columnName = daoConfig.properties[j].columnName;
                Log.i("sql_log", "新表的字段名有: " + columnName);
                if (getColumns(db, tableName).contains(columnName)) { // 看旧表中是否有新表的字段名
                    properties.add(columnName);
                    oldProperties.add(columnName);
                    String type = null;
                    try {
                        type = getTypeByClass(daoConfig.properties[j].type);
                    } catch (Exception exception) {
                        exception.printStackTrace();
                    }
                    createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);
                    if (daoConfig.properties[j].primaryKey) {
                        createTableStringBuilder.append(" PRIMARY KEY");
                    }
                    divider = ",";
                } else { // 处理其他特殊的id的字段格式
                    if (columnName.equals("ID")) { // 新表是"ID"
                        if (getColumns(db, tableName).contains("_id")) { // 旧表是"_id"
                            properties.add(columnName);
                            oldProperties.add("_id");
                            String type = null;
                            try {
                                type = getTypeByClass(daoConfig.properties[j].type);
                            } catch (Exception exception) {
                                exception.printStackTrace();
                            }
                            createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);
                            if (daoConfig.properties[j].primaryKey) {
                                createTableStringBuilder.append(" PRIMARY KEY");
                            }
                            divider = ",";
                        }
                    } else if (columnName.equals("_id")) { // 新表是"_id"
                        if (getColumns(db, tableName).contains("ID")) { // 旧表是"ID"
                            properties.add(columnName);
                            oldProperties.add("ID");
                            String type = null;
                            try {
                                type = getTypeByClass(daoConfig.properties[j].type);
                            } catch (Exception exception) {
                                exception.printStackTrace();
                            }
                            createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);
                            if (daoConfig.properties[j].primaryKey) {
                                createTableStringBuilder.append(" PRIMARY KEY");
                            }
                            divider = ",";
                        }
                    }
                }
            }
            createTableStringBuilder.append(");");
            Log.i("sql_log", "创建临时表的SQL语句: " + createTableStringBuilder.toString());
            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(",", oldProperties));
            insertTableStringBuilder.append(" FROM ").append(tableName).append(";");
            Log.i("sql_log", "在临时表插入数据的SQL语句:" + insertTableStringBuilder.toString());
            db.execSQL(insertTableStringBuilder.toString());
        }
    }

以上几个问题便是在项目做升级过程中数据库的升级所遇到的坑,肯定还有各种解决办法,以上是我自己的办法,有问题可以指出来,但是别骂街啊,以上代码都写了注释,就不多费口舌了,代码很简单,加上注释,看一看就很好理解了

发布了33 篇原创文章 · 获赞 49 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/gsw333/article/details/104151946