几百年不写博客了,而刚好又一晚上失眠,可能由于刚放完假,还没适应过来吧,反正也睡不着就想着早起写篇文章记录下工作中遇到的一些问题,也顺便静下心,好让自己快速投入工作中。
由于以前做的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());
}
}
以上几个问题便是在项目做升级过程中数据库的升级所遇到的坑,肯定还有各种解决办法,以上是我自己的办法,有问题可以指出来,但是别骂街啊,以上代码都写了注释,就不多费口舌了,代码很简单,加上注释,看一看就很好理解了