开发的项目采用了greendao 3.2.2,节省了一部分的写代码时间。
此次用到了一个浏览记录,和一个缓存管理类,用于保存浏览记录和缓存记录。所以就需要将数据写入数据库。
由于表示第一个版本创建过一部分,只能更新数据库,新建表,和新增加某些字段
于是乎找找看没有什么好的方法用于数据库升级,就找到了这里:
https://github.com/yuweiguocn/GreenDaoUpgradeHelper
一行代码解决数据库升级,牛!该作者的思路来自
先附上代码吧,核心思路都一样
1.建立一个临时表(由原表copy一份)
2.删除旧表
3.建立新表
4.将临时表的数据迁移到新表
源码如下
public final class MigrationHelper {
public static boolean DEBUG = false;
private static String TAG = "MigrationHelper";
private static final String SQLITE_MASTER = "sqlite_master";
private static final String SQLITE_TEMP_MASTER = "sqlite_temp_master";
private static WeakReference<ReCreateAllTableListener> weakListener;
public interface ReCreateAllTableListener{
void onCreateAllTables(Database db, boolean ifNotExists);
void onDropAllTables(Database db, boolean ifExists);
}
public static void migrate(SQLiteDatabase db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
printLog("【The Old Database Version】" + db.getVersion());
Database database = new StandardDatabase(db);
migrate(database, daoClasses);
}
public static void migrate(SQLiteDatabase db, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
weakListener = new WeakReference<>(listener);
migrate(db, daoClasses);
}
public static void migrate(Database database, ReCreateAllTableListener listener, Class<? extends AbstractDao<?, ?>>... daoClasses) {
weakListener = new WeakReference<>(listener);
migrate(database, daoClasses);
}
public static void migrate(Database database, Class<? extends AbstractDao<?, ?>>... daoClasses) {
printLog("【Generate temp table】start");
generateTempTables(database, daoClasses);
printLog("【Generate temp table】complete");
ReCreateAllTableListener listener = null;
if (weakListener != null) {
listener = weakListener.get();
}
if (listener != null) {
listener.onDropAllTables(database, true);
printLog("【Drop all table by listener】");
listener.onCreateAllTables(database, false);
printLog("【Create all table by listener】");
} else {
dropAllTables(database, true, daoClasses);
createAllTables(database, false, daoClasses);
}
printLog("【Restore data】start");
restoreData(database, daoClasses);
printLog("【Restore data】complete");
}
private static void generateTempTables(Database db, Class<? extends AbstractDao<?, ?>>... daoClasses) {
for (int i = 0; i < daoClasses.length; i++) {
String tempTableName = null;
DaoConfig daoConfig = new DaoConfig(db, daoClasses[i]);
String tableName = daoConfig.tablename;
if (!isTableExists(db, false, tableName)) {
printLog("【New Table】" + tableName);
continue;
}
try {
tempTableName = daoConfig.tablename.concat("_TEMP");
StringBuilder dropTableStringBuilder = new StringBuilder();
dropTableStringBuilder.append("DROP TABLE IF EXISTS ").append(tempTableName).append(";");
db.execSQL(dropTableStringBuilder.toString());
StringBuilder insertTableStringBuilder = new StringBuilder();
insertTableStringBuilder.append("CREATE TEMPORARY TABLE ").append(tempTableName);
insertTableStringBuilder.append(" AS SELECT * FROM ").append(tableName).append(";");
db.execSQL(insertTableStringBuilder.toString());
printLog("【Table】" + tableName +"\n ---Columns-->"+getColumnsStr(daoConfig));
printLog("【Generate temp table】" + tempTableName);
} catch (SQLException e) {
Log.e(TAG, "【Failed to generate temp table】" + tempTableName, e);
}
}
}
private static boolean isTableExists(Database db, boolean isTemp, String tableName) {
if (db == null || TextUtils.isEmpty(tableName)) {
return false;
}
String dbName = isTemp ? SQLITE_TEMP_MASTER : SQLITE_MASTER;
String sql = "SELECT COUNT(*) FROM " + dbName + " WHERE type = ? AND name = ?";
Cursor cursor=null;
int count = 0;
try {
cursor = db.rawQuery(sql, new String[]{"table", tableName});
if (cursor == null || !cursor.moveToFirst()) {
return false;
}
count = cursor.getInt(0);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null)
cursor.close();
}
return count > 0;
}
private static String getColumnsStr(DaoConfig daoConfig) {
if (daoConfig == null) {
return "no columns";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < daoConfig.allColumns.length; i++) {
builder.append(daoConfig.allColumns[i]);
builder.append(",");
}
if (builder.length() > 0) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
}
private static void dropAllTables(Database db, boolean ifExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
reflectMethod(db, "dropTable", ifExists, daoClasses);
printLog("【Drop all table by reflect】");
}
private static void createAllTables(Database db, boolean ifNotExists, @NonNull Class<? extends AbstractDao<?, ?>>... daoClasses) {
reflectMethod(db, "createTable", ifNotExists, daoClasses);
printLog("【Create all table by reflect】");
}
/**
* dao class already define the sql exec method, so just invoke it
*/
private static void reflectMethod(Database 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(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");
if (!isTableExists(db, true, tempTableName)) {
continue;
}
try {
// 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("REPLACE INTO ").append(tableName).append(" (");
insertTableStringBuilder.append(columnSQL);
insertTableStringBuilder.append(") SELECT ");
insertTableStringBuilder.append(columnSQL);
insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
db.execSQL(insertTableStringBuilder.toString());
printLog("【Restore data】 to " + tableName);
}
StringBuilder dropTableStringBuilder = new StringBuilder();
dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
db.execSQL(dropTableStringBuilder.toString());
printLog("【Drop temp table】" + tempTableName);
} catch (SQLException e) {
Log.e(TAG, "【Failed to restore data from temp table 】" + tempTableName, e);
}
}
}
private static List<String> getColumns(Database 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;
}
private static void printLog(String info){
if(DEBUG){
Log.d(TAG, info);
}
}
}
于是乎开开心心试用了。先看一下我的DownloadTaskBean类
@Entity
public class DownloadTaskBean {
public static final int STATUS_LOADING = 1;
public static final int STATUS_WAIT = 2;
public static final int STATUS_PAUSE = 3;
public static final int STATUS_ERROR = 4;
public static final int STATUS_FINISH = 5;
//任务名称 -> 名称唯一不重复
@Id
private String taskName;
//所属的bookId(外健)
private String bookId;
private long currentTime;
@ToMany(referencedJoinProperty = "taskName")
private List<BookChapterBean> bookChapterList;
//章节的下载进度,默认为初始状态
private int currentChapter = 0;
//最后的章节
private int lastChapter = 0;
//状态:正在下载、下载完成、暂停、等待、下载错误。
private volatile int status = STATUS_WAIT;
//总大小 -> (完成之后才会赋值)
private long size = 0;
/**
* Used to resolve relations
*/
@Generated(hash = 2040040024)
private transient DaoSession daoSession;
/**
* Used for active entity operations.
*/
@Generated(hash = 1584592296)
private transient DownloadTaskBeanDao myDao;
@Generated(hash = 1080238038)
public DownloadTaskBean(String taskName, String bookId, long currentTime,
int currentChapter, int lastChapter, int status, long size) {
this.taskName = taskName;
this.bookId = bookId;
this.currentTime = currentTime;
this.currentChapter = currentChapter;
this.lastChapter = lastChapter;
this.status = status;
this.size = size;
}
@Generated(hash = 2123101309)
public DownloadTaskBean() {
}
public String getTaskName() {
return this.taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
if (bookChapterList != null) {
for (BookChapterBean bean : bookChapterList) {
bean.setTaskName(getTaskName());
}
}
}
public String getBookId() {
return this.bookId;
}
public void setBookId(String bookId) {
this.bookId = bookId;
}
public int getCurrentChapter() {
return this.currentChapter;
}
public void setCurrentChapter(int currentChapter) {
this.currentChapter = currentChapter;
}
public int getLastChapter() {
return this.lastChapter;
}
public void setLastChapter(int lastChapter) {
this.lastChapter = lastChapter;
}
public int getStatus() {
return this.status;
}
public void setStatus(int status) {
this.status = status;
}
public long getSize() {
return this.size;
}
public void setSize(long size) {
this.size = size;
}
/**
* To-many relationship, resolved on first access (and after reset).
* Changes to to-many relations are not persisted, make changes to the target entity.
*/
@Generated(hash = 389263273)
public List<BookChapterBean> getBookChapterList() {
if (bookChapterList == null) {
final DaoSession daoSession = this.daoSession;
if (daoSession == null) {
throw new DaoException("Entity is detached from DAO context");
}
BookChapterBeanDao targetDao = daoSession.getBookChapterBeanDao();
List<BookChapterBean> bookChapterListNew = targetDao
._queryDownloadTaskBean_BookChapterList(taskName);
synchronized (this) {
if (bookChapterList == null) {
bookChapterList = bookChapterListNew;
}
}
}
return bookChapterList;
}
/**
* 这才是真正的列表使用类。
*/
public void setBookChapters(List<BookChapterBean> beans) {
bookChapterList = beans;
if (beans == null) return;
for (BookChapterBean bean : bookChapterList) {
bean.setTaskName(getTaskName());
}
}
public List<BookChapterBean> getBookChapters() {
if (daoSession == null) {
return bookChapterList;
} else {
return getBookChapterList();
}
}
/**
* Resets a to-many relationship, making the next get call to query for a fresh result.
*/
@Generated(hash = 1077762221)
public synchronized void resetBookChapterList() {
bookChapterList = null;
}
/**
* Convenient call for {@link org.greenrobot.greendao.AbstractDao#delete(Object)}.
* Entity must attached to an entity context.
*/
@Generated(hash = 128553479)
public void delete() {
if (myDao == null) {
throw new DaoException("Entity is detached from DAO context");
}
myDao.delete(this);
}
/**
* Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}.
* Entity must attached to an entity context.
*/
@Generated(hash = 1942392019)
public void refresh() {
if (myDao == null) {
throw new DaoException("Entity is detached from DAO context");
}
myDao.refresh(this);
}
/**
* Convenient call for {@link org.greenrobot.greendao.AbstractDao#update(Object)}.
* Entity must attached to an entity context.
*/
@Generated(hash = 713229351)
public void update() {
if (myDao == null) {
throw new DaoException("Entity is detached from DAO context");
}
myDao.update(this);
}
/**
* called by internal mechanisms, do not call yourself.
*/
@Generated(hash = 1923117869)
public void __setDaoSession(DaoSession daoSession) {
this.daoSession = daoSession;
myDao = daoSession != null ? daoSession.getDownloadTaskBeanDao() : null;
}
public long getCurrentTime() {
return this.currentTime;
}
public void setCurrentTime(long currentTime) {
this.currentTime = currentTime;
}
public void setCurrentTime(Long currentTime) {
this.currentTime = currentTime;
}
}
其中 currentTime 字段为新增字段
做了一下版本升级测试,修改一下数据库版本号。发现数据被完整保存下来了。于是乎,进行第二次测试,测试一下增加字段或者删除字段,看看效果如何。
于是,第二坑来了,删除字段之后,数据可以完整保存。
可是当我随机增加了一个int的字段testCode之后,发现数据库升级失败,定位到log,发现抛出这样一个异常
Android:android.database.sqlite.SQLiteConstraintException:Not null constraint failed
查找相关资料之后,发现可能是以下两种原因:
可能发生这种BUG的两种情况
1:定义的字段为NOT NULL,而插入时对应的字段为NULL
2:你定义的自动为PRIMARY,而插入时想插入的值已经在表中存在。
首先排除第二种情况,那么只能是第一种情况了。新增的testCode不能非空导致数据库迁移的时候失败。首先怀疑是MigrationHelper的sql语句由问题,定位到将临时表数据转移到新表的那行sql语句(insert into table (?,?,?) select (?,?,?) from tempTable),发现sql语句没有问题。
于是我去查找看看greendao有没有数据非空的注解,发现并没有。
查看greendao的相关issus,发现以下两个有用信息
https://github.com/yuweiguocn/GreenDaoUpgradeHelper/issues/23
https://github.com/greenrobot/greenDAO/issues/17
结论在于:由于greenDAO 3.0 生成的字段添加了非空约束。字段的类型设置为基本类型(如:int)默认会添加非空约束.
解决方案:为新增列添加默认值
restoreData 函数部分代码修改如下
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 + "`");
// }
String columnName = daoConfig.properties[j].columnName;
if (!columns.contains(columnName)) {
StringBuilder insertTableStringBuilder = new StringBuilder();
insertTableStringBuilder.append("ALTER TABLE "+tempTableName +" ADD COLUMN "+columnName+
getTableType(daoConfig.properties[j].type));
db.execSQL(insertTableStringBuilder.toString());
}
properties.add(columnName);
getTableType函数
private static Object getTableType(Class<?> type){
if(type.equals(int.class)){
return " INTEGER DEFAULT 0";
}
if(type.equals(long.class)){
return " Long DEFAULT 0";
}
if(type.equals(String.class)){
return " TEXT ";
}
if(type.equals(boolean.class)){
return " NUMERIC DEFAULT 0";
}
return " TEXT";
}
再次运行,完美解决。
参考文章:
https://blog.csdn.net/srg19910228/article/details/79095344
https://blog.csdn.net/caben_/article/details/74579050