Back to project page LitePal.
The source code is released under:
Apache License
If you think the Android project LitePal listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) Tony Green, Litepal Framework Open Source Project *// w w w . j a v a 2 s . co m * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.litepal.tablemanager; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import org.litepal.exceptions.DatabaseGenerateException; import org.litepal.parser.LitePalAttr; import org.litepal.tablemanager.model.AssociationsModel; import org.litepal.tablemanager.model.TableModel; import org.litepal.util.Const; import org.litepal.util.DBUtility; import org.litepal.util.BaseUtility; import org.litepal.util.LogUtil; import android.database.sqlite.SQLiteDatabase; /** * Upgrade the associations between model classes into tables. Creating new * tables and adding new foreign key columns are done in * {@link AssociationUpdater}. So this class just deal with the simple job of * removing foreign key columns and dropping dump intermediate join tables. * * @author Tony Green * @since 1.0 */ public abstract class AssociationUpdater extends Creator { public static final String TAG = "AssociationUpdater"; /** * A collection contains all the association models. */ private Collection<AssociationsModel> mAssociationModels; /** * Instance of SQLiteDatabase. */ protected SQLiteDatabase mDb; /** * Analysis the {@link TableModel} by the purpose of subclasses, and * generate a SQL to do the intention job. The implementation of this method * is totally delegated to the subclasses. */ @Override protected abstract void createOrUpgradeTable(SQLiteDatabase db, boolean force); /** * {@link AssociationUpdater} does two jobs. Removing foreign key columns * when two models are not associated anymore, and remove the intermediate * join tables when two models are not associated anymore. */ @Override protected void addOrUpdateAssociation(SQLiteDatabase db, boolean force) { mAssociationModels = getAllAssociations(); mDb = db; removeAssociations(); } /** * This method looks around all the columns in the table, and judge which of * them are foreign key columns. * * @param tableModel * Use the TableModel to get table name and columns name to * generate SQL. * @return All the foreign key columns in a list. */ protected List<String> getForeignKeyColumns(TableModel tableModel) { List<String> foreignKeyColumns = new ArrayList<String>(); Set<String> columnNamesDB = getTableModelFromDB(tableModel.getTableName()).getColumnNames(); for (String columnNameDB : columnNamesDB) { if (isForeignKeyColumnFormat(columnNameDB)) { if (!BaseUtility.containsIgnoreCases(tableModel.getColumnNames(), columnNameDB)) { // Now this is a foreign key column. LogUtil.d(TAG, "getForeignKeyColumnNames >> foreign key column is " + columnNameDB); foreignKeyColumns.add(columnNameDB); } } } return foreignKeyColumns; } /** * Judge the passed in column is a foreign key column or not. Each column * name ends with _id will be considered as foreign key column. * * @param tableModel * Use the TableModel to get table name and columns name to * generate SQL. * @param columnName * The column to judge. * @return Return true if it's foreign column, otherwise return false. */ protected boolean isForeignKeyColumn(TableModel tableModel, String columnName) { return BaseUtility.containsIgnoreCases(getForeignKeyColumns(tableModel), columnName); } /** * Look from the database to find a table named same as the table name in * table model. Then iterate the columns and types of this table to create a * new instance of table model. If there's no such a table in the database, * then throw DatabaseGenerateException. * * @param tableName * The table name use to get table model from database. * @return A table model object with values from database table. * @throws DatabaseGenerateException */ protected TableModel getTableModelFromDB(String tableName) { return DBUtility.findPragmaTableInfo(tableName, mDb); } /** * Drop the tables by the passing table name. * * @param dropTableNames * The names of the tables that need to drop. * @param db * Instance of SQLiteDatabase. */ protected void dropTables(List<String> dropTableNames, SQLiteDatabase db) { if (dropTableNames != null && !dropTableNames.isEmpty()) { String[] dropTableSQLS = new String[dropTableNames.size()]; for (int i = 0; i < dropTableSQLS.length; i++) { dropTableSQLS[i] = generateDropTableSQL(dropTableNames.get(i)); } execute(dropTableSQLS, db); } } /** * When some fields are removed from class, the table should synchronize the * changes by removing the corresponding columns. * * @param removeColumnNames * The column names that need to remove. * @param tableName * The table name to remove columns from. */ protected void removeColumns(Collection<String> removeColumnNames, String tableName) { if (removeColumnNames != null && !removeColumnNames.isEmpty()) { execute(getRemoveColumnSQLs(removeColumnNames, tableName), mDb); } } /** * The values in table_schame should be synchronized with the model tables * in the database. If a model table is dropped, the corresponding data * should be removed from table_schema too. * * @param tableNames * The table names need to remove from table_schema. */ protected void clearCopyInTableSchema(List<String> tableNames) { if (tableNames != null && !tableNames.isEmpty()) { StringBuilder deleteData = new StringBuilder("delete from "); deleteData.append(Const.TableSchema.TABLE_NAME).append(" where"); boolean needOr = false; for (String tableName : tableNames) { if (needOr) { deleteData.append(" or "); } needOr = true; deleteData.append(" lower(").append(Const.TableSchema.COLUMN_NAME).append(") "); deleteData.append("=").append(" lower('").append(tableName).append("')"); } LogUtil.d(TAG, "clear table schema value sql is " + deleteData); String[] sqls = { deleteData.toString() }; execute(sqls, mDb); } } /** * 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(); } /** * Analyzing the table models, then remove all the foreign key columns if * their association in model classes are no longer exist any more. */ private void removeForeignKeyColumns() { for (String className : LitePalAttr.getInstance().getClassNames()) { TableModel tableModel = getTableModel(className); removeColumns(findForeignKeyToRemove(tableModel), tableModel.getTableName()); } } /** * If there're intermediate join tables for two tables, when the two classes * are not associated, the join table should be dropped. */ private void removeIntermediateTables() { List<String> tableNamesToDrop = findIntermediateTablesToDrop(); dropTables(tableNamesToDrop, mDb); clearCopyInTableSchema(tableNamesToDrop); } /** * This method gives back the names of the foreign key columns that need to * remove, cause their associations in the classes are no longer exist. * * @param tableModel * Use the TableModel to get table name and columns name to * generate SQL. * @return The foreign key columns need to remove in a list. */ private List<String> findForeignKeyToRemove(TableModel tableModel) { List<String> removeRelations = new ArrayList<String>(); List<String> foreignKeyColumns = getForeignKeyColumns(tableModel); String selfTableName = tableModel.getTableName(); for (String foreignKeyColumn : foreignKeyColumns) { String associatedTableName = DBUtility.getTableNameByForeignColumn(foreignKeyColumn); if (shouldDropForeignKey(selfTableName, associatedTableName)) { removeRelations.add(foreignKeyColumn); } } LogUtil.d(TAG, "findForeignKeyToRemove >> " + tableModel.getTableName() + " " + removeRelations); return removeRelations; } /** * When many2many associations are no longer exist between two models, the * intermediate join table should be dropped from database. This method * helps find out those intermediate join tables which should be dropped * cause their associations in classes are done. * * @return A list with all intermediate join tables to drop. */ 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; } /** * Generate a SQL for renaming the table into a temporary table. * * @param tableName * The table name use to alter to temporary table. * @return SQL to rename table. */ private String generateAlterToTempTableSQL(String tableName) { StringBuilder sql = new StringBuilder(); sql.append("alter table ").append(tableName).append(" rename to ") .append(getTempTableName(tableName)); return sql.toString(); } /** * Generate a SQL to create new table by the table model from database. Also * it will remove the columns that need to remove before generating the SQL. * * @param removeColumnNames * The column names need to remove. * @param tableName * The table name use to create new table. * @return SQL to create new table. */ private String generateCreateNewTableSQL(Collection<String> removeColumnNames, String tableName) { TableModel tableModelDB = getTableModelFromDB(tableName); for (String removeColumnName : removeColumnNames) { tableModelDB.removeColumnIgnoreCases(removeColumnName); } return generateCreateTableSQL(tableModelDB); } /** * Generate a SQL to do the data migration job to avoid losing data. * * @param removeColumnNames * The column names need to remove. * @param tableName * The table name use to migrate data. * @return SQL to migrate data. */ private String generateDataMigrationSQL(Collection<String> removeColumnNames, String tableName) { List<String> columnNames = new ArrayList<String>(); for (String columnName : getTableModelFromDB(tableName).getColumnNames()) { if (!BaseUtility.containsIgnoreCases(removeColumnNames, columnName)) { columnNames.add(columnName); } } if (!columnNames.isEmpty()) { StringBuilder sql = new StringBuilder(); sql.append("insert into ").append(tableName).append("("); boolean needComma = false; for (String columnName : columnNames) { if (needComma) { sql.append(", "); } needComma = true; sql.append(columnName); } sql.append(") "); sql.append("select "); needComma = false; for (String columnName : columnNames) { if (needComma) { sql.append(", "); } needComma = true; sql.append(columnName); } sql.append(" from ").append(getTempTableName(tableName)); return sql.toString(); } else { return null; } } /** * Generate a SQL to drop the temporary table. * * @param tableName * The table name use to drop temporary table. * @return SQL to drop the temporary table. */ private String generateDropTempTableSQL(String tableName) { return generateDropTableSQL(getTempTableName(tableName)); } /** * Removing or resizing columns from tables must need a temporary table to * store data, and here's the table name. * * @param tableName * The table name use to generate temporary table name. * @return Temporary table name */ private String getTempTableName(String tableName) { return tableName + "_temp"; } /** * This method create a SQL array for the whole remove dump columns job. * * @param removeColumnNames * The column names need to remove. * @param tableName * The table name to remove from. * @return A SQL array contains create temporary table, create new table, * migrate data and drop temporary table. */ private String[] getRemoveColumnSQLs(Collection<String> removeColumnNames, String tableName) { String alterToTempTableSQL = generateAlterToTempTableSQL(tableName); LogUtil.d(TAG, "generateRemoveColumnSQL >> " + alterToTempTableSQL); String createNewTableSQL = generateCreateNewTableSQL(removeColumnNames, tableName); LogUtil.d(TAG, "generateRemoveColumnSQL >> " + createNewTableSQL); String dataMigrationSQL = generateDataMigrationSQL(removeColumnNames, tableName); LogUtil.d(TAG, "generateRemoveColumnSQL >> " + dataMigrationSQL); String dropTempTableSQL = generateDropTempTableSQL(tableName); LogUtil.d(TAG, "generateRemoveColumnSQL >> " + dropTempTableSQL); String[] sqls = { alterToTempTableSQL, createNewTableSQL, dataMigrationSQL, dropTempTableSQL }; return sqls; } /** * Judge if the current iterated foreign key column should be dropped. It is * only used in {@link #findForeignKeyToRemove(TableModel)} when iterating * the foreign key column list. When this foreign key can not be found in * the association model collection, this foreign key should be dropped. * * @param selfTableName * The table name of currently table model. * @param associatedTableName * The associated table name of current table model. * @return If the foreign key currently iterated should be dropped, return * true. Otherwise return false. */ private boolean shouldDropForeignKey(String selfTableName, String associatedTableName) { for (AssociationsModel associationModel : mAssociationModels) { if (associationModel.getAssociationType() == Const.Model.ONE_TO_ONE) { if (selfTableName.equalsIgnoreCase(associationModel.getTableHoldsForeignKey())) { if (associationModel.getTableName().equalsIgnoreCase(selfTableName)) { if (isRelationCorrect(associationModel, selfTableName, associatedTableName)) { return false; } } else if (associationModel.getAssociatedTableName().equalsIgnoreCase( selfTableName)) { if (isRelationCorrect(associationModel, associatedTableName, selfTableName)) { return false; } } } } else if (associationModel.getAssociationType() == Const.Model.MANY_TO_ONE) { if (isRelationCorrect(associationModel, associatedTableName, selfTableName)) { return false; } } } return true; } /** * Judge if the tableName1 equals {@link AssociationsModel#getTableName()} * and tableName2 equals {@link AssociationsModel#getAssociatedTableName()}. * * @param associationModel * The association model to get table name and associated table * name. * @param tableName1 * The table name to match table name from association model. * @param tableName2 * The table name to match associated table name from association * model. * @return Return true if the description is true, otherwise return false. */ private boolean isRelationCorrect(AssociationsModel associationModel, String tableName1, String tableName2) { return associationModel.getTableName().equalsIgnoreCase(tableName1) && associationModel.getAssociatedTableName().equalsIgnoreCase(tableName2); } }