add 2018/6/14
上一篇我讲述了LitePal建立表(创建数据库的流程),链接戳这里。这一篇看看LitePal是如何做到简便的升级数据库的。
加入你两张表Singer和Musiic,并且已经存了数据,结果发现Music表名字多打了一个 i ,Singer多了一个字段,并且想添加一张表Album,那么应该怎么做呢?使用过系统原生SQLite的人应该知道这将非常麻烦,但用LitePal却很简单,只需要按照“直觉”做:把Musiic类改名,把Singer类中多的字段删除,新建Album类并在litepal.xml中注册,最后将version加1并进行任何数据库操作就可以了。那么我们看他是如何实现的吧。
如上做过之后调用LitePal.getDataBase()会更新数据库,我在上一篇中讲过这个调用流程,这次由于在xml解析后得到的version比原version高,所以会在LitePalOpenHelper中执行onUpgrade:
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Generator.upgrade(db);
SharedUtil.updateVersion(LitePalAttr.getInstance().getExtraKeyName(), newVersion);
}
第二行我就不展开了,很显然是将当前version以SharedPreferences进行存储,我们看Generator.upgrade(db):
static void upgrade(SQLiteDatabase db) {
drop(db);
create(db, false);
updateAssociations(db);
upgradeTables(db);
addAssociation(db, false);
}
简单的一行代码LitePal要进行五项操作,我先总体描述他们所做的工作:
(1)drop:将原本在mapping中声明但本次不在的表删除(最后总要更新Table_Schema,之后我不会再提了);
(2)create:将原本不在mapping中但本次添加的表创建;
(3)updateAssociations:若关联的两张表有任意一张被删除,将它们之间的关联删除;
(4)upgradeTables:更新实体类做了修改的表,如上面提到的删除某个字段;
(5)addAssociation:添加新产生的关联。
看似内容很多,但和创建表有很多相似之处,先来看
(1)Generator.drop(db):
private static void drop(SQLiteDatabase db) {
Dropper dropper = new Dropper();
dropper.createOrUpgradeTable(db, false);
}
Dropper.createOrUpgradeTable(db, false):
@Override
protected void createOrUpgradeTable(SQLiteDatabase db, boolean force) {
mTableModels = getAllTableModels();
mDb = db;
dropTables();
}
getAllTableModels这样的在上一篇提到过的方法就直接略过了,接下来是dropTables():
private void dropTables() {
List tableNamesToDrop = findTablesToDrop();
dropTables(tableNamesToDrop, mDb);
clearCopyInTableSchema(tableNamesToDrop);
}
按照顺序,先看看如何判别哪些表需要删除:
private List findTablesToDrop() {
List dropTableNames = new ArrayList();
Cursor cursor = null;
try {
cursor = mDb.query(Const.TableSchema.TABLE_NAME, null, null, null, null, null, null);//查询Table_Schema表所有数据
if (cursor.moveToFirst()) {
do {
String tableName = cursor.getString(cursor
.getColumnIndexOrThrow(Const.TableSchema.COLUMN_NAME));//表名
int tableType = cursor.getInt(cursor
.getColumnIndexOrThrow(Const.TableSchema.COLUMN_TYPE));//0普通表,1中间表
if (shouldDropThisTable(tableName, tableType)) {
// need to drop tableNameDB
LogUtil.d(TAG, "need to drop " + tableName);
dropTableNames.add(tableName);
}
} while (cursor.moveToNext());
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return dropTableNames;
}
private boolean shouldDropThisTable(String tableName, int tableType) {
return !BaseUtility.containsIgnoreCases(pickTableNamesFromTableModels(), tableName)
&& tableType == Const.TableSchema.NORMAL_TABLE;
}
findTablesToDrop()这个方法通过查询Table_Schema表,对存储的每个表名进行判断是否删除,将要删除表的表名全部放入一个List返回。判断的方法是shouldDropThisTable(String tableName, int tableType),会删除xml中不再存在且是普通类型的表。实际进行删除表的方法贴一下,我就不解释了,之前有类似的:
protected void dropTables(List dropTableNames, SQLiteDatabase db) {
if (dropTableNames != null && !dropTableNames.isEmpty()) {
List dropTableSQLS = new ArrayList();
for (int i = 0; i < dropTableNames.size(); i++) {
dropTableSQLS.add(generateDropTableSQL(dropTableNames.get(i)));
}
execute(dropTableSQLS, db);
}
}
最后更新一下Table_Schema,一个drop操作就结束了。
(2)create(db, false):和创建表操作的create使用的是同一个方法,只是第二个参数不再是true而是false,还有印象的话,false表示对已经存在的表不做任何操作,只新建不存在的表。
(3)updateAssociations(db):
private static void updateAssociations(SQLiteDatabase db) {
AssociationUpdater associationUpgrader = new Upgrader();
associationUpgrader.addOrUpdateAssociation(db, false);
}
可能会觉得addOrUpdateAssociation这个方法比较眼熟,但千万不要搞混了,在新建表建立表关联的时候使用的方法是继承自AssociationCreator,在这里用的是继承自AssociationUpdater。
@Override
protected void addOrUpdateAssociation(SQLiteDatabase db, boolean force) {
mAssociationModels = getAllAssociations();
mDb = db;
removeAssociations();
}
getAllAssociations()分析过了但要注意,因为LitePalBase以及其子类每次使用都是new出来的,mAssociationModels都为空,所以每次调用getAllAssociations()都会重新审查所有关联模型,而不是使用之前的关联模型。好了,现在进入删除关联模型的方法removeAssociations():
/**
* When the association between two tables are no longer associated in the
* classes, database should remove the foreign key column or intermediate
* join table that keeps these two tables associated.
*/
private void removeAssociations() {
removeForeignKeyColumns();
removeIntermediateTables();
removeGenericTables();
}
removeForeignKeyColumns()是针对一对一和多对一关联的:
private void removeForeignKeyColumns() {
for (String className : LitePalAttr.getInstance().getClassNames()) {
TableModel tableModel = getTableModel(className);
removeColumns(findForeignKeyToRemove(tableModel), tableModel.getTableName());
}
}
findForeignKeyToRemove(tableModel)顾名思义,这个方法很容易理解但内容很多,我就不贴出来了。直接到removeColumns看看吧:
protected void removeColumns(Collection removeColumnNames, String tableName) {
if (removeColumnNames != null && !removeColumnNames.isEmpty()) {
execute(getRemoveColumnSQLs(removeColumnNames, tableName), mDb);
}
}
那么来看看LitePal是如何在不丢失数据的情况下删除表的某一列吧:
private List getRemoveColumnSQLs(Collection removeColumnNames, String tableName) {
TableModel tableModelFromDB = getTableModelFromDB(tableName);
String alterToTempTableSQL = generateAlterToTempTableSQL(tableName);//1
LogUtil.d(TAG, "generateRemoveColumnSQL >> " + alterToTempTableSQL);
String createNewTableSQL = generateCreateNewTableSQL(removeColumnNames, tableModelFromDB);//2
LogUtil.d(TAG, "generateRemoveColumnSQL >> " + createNewTableSQL);
String dataMigrationSQL = generateDataMigrationSQL(tableModelFromDB);//3
LogUtil.d(TAG, "generateRemoveColumnSQL >> " + dataMigrationSQL);
String dropTempTableSQL = generateDropTempTableSQL(tableName);//4
LogUtil.d(TAG, "generateRemoveColumnSQL >> " + dropTempTableSQL);
List sqls = new ArrayList();
sqls.add(alterToTempTableSQL);
sqls.add(createNewTableSQL);
sqls.add(dataMigrationSQL);
sqls.add(dropTempTableSQL);
return sqls;
}
原来如此,LitePal执行了四条SQL语句去达到目的,分别是:
(1)将表A重命名为A_temp;
(2)新建没有X_id列的表A;
(3)将A_temp中的数据存到A中;
(4)删除A_temp表。
(1)、(2)、(4)都是比较简单的,我们来看看(3):
protected String generateDataMigrationSQL(TableModel tableModel) {
String tableName = tableModel.getTableName();
List columnModels = tableModel.getColumnModels();
if (!columnModels.isEmpty()) {
StringBuilder sql = new StringBuilder();
sql.append("insert into ").append(tableName).append("(");
boolean needComma = false;
for (ColumnModel columnModel : columnModels) {
if (needComma) {
sql.append(", ");
}
needComma = true;
sql.append(columnModel.getColumnName());
}
sql.append(") ");
sql.append("select ");
needComma = false;
for (ColumnModel columnModel : columnModels) {
if (needComma) {
sql.append(", ");
}
needComma = true;
sql.append(columnModel.getColumnName());
}
sql.append(" from ").append(getTempTableName(tableName));
return sql.toString();
} else {
return null;
}
}
也挺简单的,主要还是考验sql语句的基础,将临时表中除了外键之外的列全部复制到新表中。接下来是删除中间表:
private void removeIntermediateTables() {
List tableNamesToDrop = findIntermediateTablesToDrop();
dropTables(tableNamesToDrop, mDb);
clearCopyInTableSchema(tableNamesToDrop);
}
二三行和之前drop是相同的,只看findIntermediateTablesToDrop();
private List findIntermediateTablesToDrop() {
List intermediateTables = new ArrayList();
for (String tableName : DBUtility.findAllTableNames(mDb)) {
if (DBUtility.isIntermediateTable(tableName, mDb)) {
boolean dropIntermediateTable = true;
for (AssociationsModel associationModel : mAssociationModels) {
if (associationModel.getAssociationType() == Const.Model.MANY_TO_MANY) {
String intermediateTableName = DBUtility.getIntermediateTableName(
associationModel.getTableName(),
associationModel.getAssociatedTableName());
if (tableName.equalsIgnoreCase(intermediateTableName)) {
dropIntermediateTable = false;
}
}
}
if (dropIntermediateTable) {
// drop the intermediate join table
intermediateTables.add(tableName);
}
}
}
LogUtil.d(TAG, "findIntermediateTablesToDrop >> " + intermediateTables);
return intermediateTables;
}
就是简单的检查所有中间表,看看是否在 现存多对多关联模型应该生成的表 里面,不在则删除。
接下来是:
private void removeGenericTables() {
List tableNamesToDrop = findGenericTablesToDrop();
dropTables(tableNamesToDrop, mDb);
clearCopyInTableSchema(tableNamesToDrop);
}
findGenericTablesToDrop()方法与findIntermediateTablesToDrop() 方法逻辑完全相同,就不贴了。后两个方法前面的讲过了,这一块应该是没什么疑问了。
至此第三步也完成了,接下来是(4)upgradeTables(db):
private static void upgradeTables(SQLiteDatabase db) {
Upgrader upgrader = new Upgrader();
upgrader.createOrUpgradeTable(db, false);
}
@Override
protected void createOrUpgradeTable(SQLiteDatabase db, boolean force) {
mDb = db;
for (TableModel tableModel : getAllTableModels()) {
mTableModel = tableModel;
mTableModelDB = getTableModelFromDB(tableModel.getTableName());
LogUtil.d(TAG, "createOrUpgradeTable: model is " + mTableModel.getTableName());
upgradeTable();
}
}
这里就是将每一张表都进行一次upgradeTable(),当然,不需要更新的表肯定会跳过
/**
* Upgrade table actions. Include remove dump columns, add new columns and
* change column types. All the actions above will be done by the description
* order.
*/
private void upgradeTable() {
if (hasNewUniqueOrNotNullColumn()) {
// Need to drop the table and create new one. Cause unique column can not be added, and null data can not be migrated.
createOrUpgradeTable(mTableModel, mDb, true);
// add foreign keys of the table.
Collection associationsInfo = getAssociationInfo(mTableModel.getClassName());
for (AssociationsInfo info : associationsInfo) {
if (info.getAssociationType() == Const.Model.MANY_TO_ONE
|| info.getAssociationType() == Const.Model.ONE_TO_ONE) {
if (info.getClassHoldsForeignKey().equalsIgnoreCase(mTableModel.getClassName())) {
String associatedTableName = DBUtility.getTableNameByClassName(info.getAssociatedClassName());
addForeignKeyColumn(mTableModel.getTableName(), associatedTableName, mTableModel.getTableName(), mDb);
}
}
}
} else {
hasConstraintChanged = false;
removeColumns(findColumnsToRemove());
addColumns(findColumnsToAdd());
changeColumnsType(findColumnTypesToChange());
changeColumnsConstraints();
}
}
update 2018/6/14 15:50
这一块要考虑的事情很多,我们一一分析。首先要判断是否有新的(新添加的列或者在原有列上新添加了注释)非空或者唯一列,如果有要先删除这张表再新建。因为SQLite 只支持 ALTER TABLE 的有限子集。在 SQLite 中,ALTER TABLE 命令允许用户重命名表,或向现有表添加一个新的列。重命名列,删除一列,或从一个表中添加或删除约束都是不可能的(但是LitePal用了一些技巧支持了删除约束)。
private boolean hasNewUniqueOrNotNullColumn() {
List columnModelList = mTableModel.getColumnModels();
for (ColumnModel columnModel : columnModelList) {
ColumnModel columnModelDB = mTableModelDB.getColumnModelByName(columnModel.getColumnName());
if (columnModel.isUnique()) {
if (columnModelDB == null || !columnModelDB.isUnique()) {
return true;
}
}
if (columnModelDB != null && !columnModel.isNullable() && columnModelDB.isNullable()) {
return true;
}
}
return false;
}
这里看一下mTableModelDB和mTableModel的定义就好了,带DB的是目前数据库中的TableModel,不带DB的是要变成的TableModel,这样应该就很好理解了。检查新TableModel的每个唯一或非空列,如果在原(DB中)TableModel中它不存在或者不是非空或唯一的,那么返回true。
然后回到createOrUpgradeTable(mTableModel, mDb, true)这个方法,这是新建表用到的方法,作用是强制新建表,已存在则先删除,并没有备份数据。可能有人会有疑问,那我之前有数据怎么办?凉拌。新添非空或者唯一约束由于原数据可能存在的空值和重复很难迁移数据。当然只要封装的好,这些功能也可以做到,但在LitePal这样一个轻量级的SQLite框架中是没有必要的,并且用户的不当使用可能会带来更多的问题。综上,当你使用SQLite建表时,有约束的列一定要先确认好。
新建了表后,要添加关联,虽然这里用的是AssociationsInfo,但和之前使用AssociationsModel也基本相同,就略过了。
else {
hasConstraintChanged = false;
removeColumns(findColumnsToRemove());
addColumns(findColumnsToAdd());
changeColumnsType(findColumnTypesToChange());
changeColumnsConstraints();
}
没有新添约束列则执行else,一共四步,分别是:
(1)删除列;(2)新添列;(3)改变列数据类型;(4)修改约束(后面会解释)。
我们一个一个来看。
(1)removeColumns(findColumnsToRemove()):
private List findColumnsToRemove() {
String tableName = mTableModel.getTableName();
List removeColumns = new ArrayList();
List columnModelList = mTableModelDB.getColumnModels();
for (ColumnModel columnModel : columnModelList) {
String dbColumnName = columnModel.getColumnName();
if (isNeedToRemove(dbColumnName)) {
removeColumns.add(dbColumnName);
}
}
LogUtil.d(TAG, "remove columns from " + tableName + " >> " + removeColumns);
return removeColumns;
}
private boolean isNeedToRemove(String columnName) {
return isRemovedFromClass(columnName) && !isIdColumn(columnName)
&& !isForeignKeyColumn(mTableModel, columnName);
}
很简单,要删除的列是在新class里不存在的成员,且它不是id列,且它不是外键。
实际删除列的操作removeColumns(findColumnsToRemove())一通调用实际上最后执行的方法还是
execute(getRemoveColumnSQLs(removeColumnNames, tableName), mDb);
(2)addColumns(findColumnsToAdd()):
这个我就不展开了,判定方法与remove相同,而且SQLite本身是支持添加列的,所以没什么特别的
(3)changeColumnsType(findColumnTypesToChange());
private List findColumnTypesToChange() {
List columnsToChangeType = new ArrayList();
for (ColumnModel columnModelDB : mTableModelDB.getColumnModels()) {
for (ColumnModel columnModel : mTableModel.getColumnModels()) {
if (columnModelDB.getColumnName().equalsIgnoreCase(columnModel.getColumnName())) {
if (!columnModelDB.getColumnType().equalsIgnoreCase(columnModel.getColumnType())) {
if (columnModel.getColumnType().equalsIgnoreCase("blob") && TextUtils.isEmpty(columnModelDB.getColumnType())) {
// Case for binary array type upgrade. Do nothing under this condition.
} else {
// column type is changed
columnsToChangeType.add(columnModel);
}
}
if (!hasConstraintChanged) {
// for reducing loops, check column constraints change here.
LogUtil.d(TAG, "default value db is:" + columnModelDB.getDefaultValue() + ", default value is:" + columnModel.getDefaultValue());
if (columnModelDB.isNullable() != columnModel.isNullable() ||
!columnModelDB.getDefaultValue().equalsIgnoreCase(columnModel.getDefaultValue()) ||
(columnModelDB.isUnique() && !columnModel.isUnique())) { // unique constraint can not be added
hasConstraintChanged = true;
}
}
}
}
}
return columnsToChangeType;
}
这里要注意findColumnTypesToChange()里有一步是判断是否有列约束要变,包括改变默认值,删除非空或唯一约束。虽然这些在SQLite中都不支持,但由于实现比较简单并且不会产生不良后果(delete永远比add简单),所以LitePal还是做了处理。如果存在需要修改列约束的列,hasConstraintChanged被置为true,最终会在第(4)步处理。
先回到(3),看看它是怎么处理TypeChange的:
private void changeColumnsType(List columnModelList) {
LogUtil.d(TAG, "do changeColumnsType");
List columnNames = new ArrayList();
if (columnModelList != null && !columnModelList.isEmpty()) {
for (ColumnModel columnModel : columnModelList) {
columnNames.add(columnModel.getColumnName());
}
}
removeColumns(columnNames);
addColumns(columnModelList);
}
removeColumns(columnNames)已经是老生常谈了,addColumns操作在SQLite是支持的,所以这两个方法我就不展开了。这里我只简单讲述一下过程,就当会议之前的内容了。removeColumns会将原表重命名为temp表,然后新建表,迁移数据,再删除temp表,addColumns则使用alter table xx add colum语句添加列,这样就做到了改变列的type。
(4)changeColumnsConstraints():
private void changeColumnsConstraints() {
if (hasConstraintChanged) {
LogUtil.d(TAG, "do changeColumnsConstraints");
execute(getChangeColumnsConstraintsSQL(), mDb);
}
}
private List getChangeColumnsConstraintsSQL() {
String alterToTempTableSQL = generateAlterToTempTableSQL(mTableModel.getTableName());//1
String createNewTableSQL = generateCreateTableSQL(mTableModel);//2
List addForeignKeySQLs = generateAddForeignKeySQL();//3
String dataMigrationSQL = generateDataMigrationSQL(mTableModelDB);//4
String dropTempTableSQL = generateDropTempTableSQL(mTableModel.getTableName());//5
List sqls = new ArrayList();
sqls.add(alterToTempTableSQL);
sqls.add(createNewTableSQL);
sqls.addAll(addForeignKeySQLs);
sqls.add(dataMigrationSQL);
sqls.add(dropTempTableSQL);
LogUtil.d(TAG, "generateChangeConstraintSQL >> ");
for (String sql : sqls) {
LogUtil.d(TAG, sql);
}
LogUtil.d(TAG, "<< generateChangeConstraintSQL");
return sqls;
}
改变列约束一共需要5步,1245连起来就是删除列的步骤,那么我们来看一下3是做什么的:
/**
* Generate a SQL List for adding foreign keys. Changing constraints job should remain all the
* existing columns including foreign keys. This method add origin foreign keys after creating
* table.
* @return A SQL List for adding foreign keys.
*/
private List generateAddForeignKeySQL() {
List addForeignKeySQLs = new ArrayList();
List foreignKeyColumns = getForeignKeyColumns(mTableModel);
for (String foreignKeyColumn : foreignKeyColumns) {
if (!mTableModel.containsColumn(foreignKeyColumn)) {
ColumnModel columnModel = new ColumnModel();
columnModel.setColumnName(foreignKeyColumn);
columnModel.setColumnType("integer");
addForeignKeySQLs.add(generateAddColumnSQL(mTableModel.getTableName(), columnModel));
}
}
return addForeignKeySQLs;
}
很好理解,注释说明了,改变列约束要保留所有外键列,所以要将不在mTableModel中的外键加上。
update 2018/6/17
(5)addAssociation(db, false):
这个方法在新建表里出现过了,但当时force是true,我们来回忆一下force的含义
private void addAssociations(Collection associatedModels, SQLiteDatabase db,
boolean force) {
for (AssociationsModel associationModel : associatedModels) {
if (Const.Model.MANY_TO_ONE == associationModel.getAssociationType()
|| Const.Model.ONE_TO_ONE == associationModel.getAssociationType()) {
addForeignKeyColumn(associationModel.getTableName(),
associationModel.getAssociatedTableName(),
associationModel.getTableHoldsForeignKey(), db);
} else if (Const.Model.MANY_TO_MANY == associationModel.getAssociationType()) {
createIntermediateTable(associationModel.getTableName(),
associationModel.getAssociatedTableName(), db, force);
}
}
for (GenericModel genericModel : getGenericModels()) {
createGenericTable(genericModel, db, force);
}
}
private void createIntermediateTable(String tableName, String associatedTableName,
SQLiteDatabase db, boolean force) {
List columnModelList = new ArrayList();
ColumnModel column1 = new ColumnModel();
column1.setColumnName(tableName + "_id");
column1.setColumnType("integer");
ColumnModel column2 = new ColumnModel();
column2.setColumnName(associatedTableName + "_id");
column2.setColumnType("integer");
columnModelList.add(column1);
columnModelList.add(column2);
String intermediateTableName = DBUtility.getIntermediateTableName(tableName,
associatedTableName);
List sqls = new ArrayList();
if (DBUtility.isTableExists(intermediateTableName, db)) {
if (force) {
sqls.add(generateDropTableSQL(intermediateTableName));
sqls.add(generateCreateTableSQL(intermediateTableName, columnModelList, false));
}
} else {
sqls.add(generateCreateTableSQL(intermediateTableName, columnModelList, false));
}
execute(sqls, db);
giveTableSchemaACopy(intermediateTableName, Const.TableSchema.INTERMEDIATE_JOIN_TABLE, db);
}
force为true代表如果表存在一定先删除再新建,为false则跳过。
好了,至此,更新表的操作也到此结束了,下一篇可能是CRUD操作的实现。