LitePal原始碼學習(2)——更新表流程(更新資料庫)
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<String> tableNamesToDrop = findTablesToDrop();
dropTables(tableNamesToDrop, mDb);
clearCopyInTableSchema(tableNamesToDrop);
}
按照順序,先看看如何判別哪些表需要刪除:
private List<String> findTablesToDrop() {
List<String> dropTableNames = new ArrayList<String>();
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<String> dropTableNames, SQLiteDatabase db) {
if (dropTableNames != null && !dropTableNames.isEmpty()) {
List<String> dropTableSQLS = new ArrayList<String>();
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<String> removeColumnNames, String tableName) {
if (removeColumnNames != null && !removeColumnNames.isEmpty()) {
execute(getRemoveColumnSQLs(removeColumnNames, tableName), mDb);
}
}
那麼來看看LitePal是如何在不丟失資料的情況下刪除表的某一列吧:
private List<String> getRemoveColumnSQLs(Collection<String> 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<String> sqls = new ArrayList<String>();
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<ColumnModel> 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<String> tableNamesToDrop = findIntermediateTablesToDrop();
dropTables(tableNamesToDrop, mDb);
clearCopyInTableSchema(tableNamesToDrop);
}
二三行和之前drop是相同的,只看findIntermediateTablesToDrop();
private List<String> findIntermediateTablesToDrop() {
List<String> intermediateTables = new ArrayList<String>();
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<String> 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> 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<ColumnModel> 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<String> findColumnsToRemove() {
String tableName = mTableModel.getTableName();
List<String> removeColumns = new ArrayList<String>();
List<ColumnModel> 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<ColumnModel> findColumnTypesToChange() {
List<ColumnModel> columnsToChangeType = new ArrayList<ColumnModel>();
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<ColumnModel> columnModelList) {
LogUtil.d(TAG, "do changeColumnsType");
List<String> columnNames = new ArrayList<String>();
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<String> getChangeColumnsConstraintsSQL() {
String alterToTempTableSQL = generateAlterToTempTableSQL(mTableModel.getTableName());//1
String createNewTableSQL = generateCreateTableSQL(mTableModel);//2
List<String> addForeignKeySQLs = generateAddForeignKeySQL();//3
String dataMigrationSQL = generateDataMigrationSQL(mTableModelDB);//4
String dropTempTableSQL = generateDropTempTableSQL(mTableModel.getTableName());//5
List<String> sqls = new ArrayList<String>();
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<String> generateAddForeignKeySQL() {
List<String> addForeignKeySQLs = new ArrayList<String>();
List<String> 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<AssociationsModel> 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<ColumnModel> columnModelList = new ArrayList<ColumnModel>();
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<String> sqls = new ArrayList<String>();
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操作的實現。