Android數據庫代碼自動生成器

Android數據庫代碼自動生成器

有時候使用數據庫框架來操作數據庫數據對我們的App來講並不一定十分適用,其實使用Google提供的SQLiteOpenHelper也十分直觀,在數據庫框架還沒有流行的時候,我們的App也基本都是用的是這種方式對數據庫的處理。

但有時候一個App裏可能會存在N多張表,又懶的用框架寫那麼多註釋構建再拷貝代碼,但使用SQLiteOpenHelper同樣也有大量的重複性代碼,或者是同模式的代碼。於是,根據自己所開發的App大部分功能來看,歸納了這幾點數據庫操作需求:

  1. 創建表的語句
  2. 基本的增刪改查,其中刪改查根據主鍵_id作爲條件
  3. 增刪改查語句中,不可避免的Java對象和數據庫數據的相互轉換
  4. Java實體類和數據庫列名之間的對應關係(方便我們糾錯排查)

基於這幾點需求,我們初步定一下我們的實現方案:

  1. 創建一個Java基本類
  2. 根據基本類,構建數據模型(即創建一個新的Java類,用來存放相關的數據庫操作方法)
  3. 如果對類的某些字段有特殊需求,也可以進行特殊處理,因此需要添加註解,如主鍵,列名或者表名,唯一標識等等

Step1. 構建註解

接下來第一步,我們需要創建自己的註解類型,分爲兩種註解類型,類註解和屬性註解,分別對應數據庫的表和列,在這裏我們需要注意:

  1. 是不是這個Bean的所有非staticfinal修飾的屬性都需要創建到這張表中;
  2. 這個Bean有沒有主鍵列對應的屬性;
  3. 存不存在唯一約束;
  4. 屬性有沒有指定的列名;
  5. 如果不存在註解,即默認相關方案創建數據庫相關轉換規則。

把這些需求反應到我們的註解當中:

表註解:

/**
 * 表註解
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    /**
     * 設置表名
     */
    String name() default "";
    /**
     * 是否所有屬性建表列
     * @return
     */
    boolean all() default false;
}

屬性註解:

/**
 * 列註解
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    /**
     * 數據庫列名
     * @return
     */
    String name() default "";
    /**
     * 是否主鍵
     * @return
     */Key() default false;
    /**
    boolean primary
     * 是否唯一約束
     * @return
     */
    boolean unique() default false;
}

接下來的工作就是我們的核心,如果給定一個Bean類,首先要獲取該Bean類的所有屬性,且不能被修飾爲staticfinal的屬性,然後根據獲取的屬性集合轉爲表的列。



Step2. 對Bean類進行反射提取

有了基本的註解,我們就可以增加我們的特殊需求了,當然沒有註解,我們也有自己默認的處理方案。在這部分的處理當中,我們首先要做的是,一個Java類的基本屬性對應到表的這個映射過程中,我們涉及到了兩個命名規則:

  1. 列名,也就是我們SQLite中Table的Column名稱,可通過註解進行自定義,默認爲屬性名的駝峯轉小寫字母的下劃線寫法
  2. 映射關係名稱,也就是我們在Java程序中,涉及到列名使用,如增刪改查所需的列名參數,當然需要抽取成一個個公共的String對象,固定是屬性名的駝峯轉大寫字母的下劃線寫法

同時,我們的屬性名和屬性對應的SQLite類型以及是否主鍵的標誌,我們將這些統一封裝成Bean類的一個屬性轉SQLite-Table-Column的一個相關配置的實體信息,即一種映射關係:

public class ColumnDetail {

    /**
     * 列名
     */
    private String columnName;
    /**
     * 屬性名
     */
    private String fieldName;
    /**
     * 定義名稱,可以用來被調用的列名、表名的引用對象(即columnName值的引用對象)
     */
    private String relativeName;
    /**
     * SQLite類型
     */
    private String type;
    /**
     * 是否主鍵
     */
    private boolean isPrimaryKey;

    public ColumnDetail(String columnName, String fieldName, String connectName, String type, boolean isPrimaryKey) {
        this.columnName = columnName;
        this.fieldName = fieldName;
        this.relativeName = connectName;
        this.type = type;
        this.isPrimaryKey = isPrimaryKey;
    }

    public String getRelativeName() {
        return relativeName;
    }

    public void setRelativeName(String relativeName) {
        this.relativeName = relativeName;
    }

    public String getFieldName() {
        return fieldName;
    }

    public void setFieldName(String fieldName) {
        this.fieldName = fieldName;
    }

    public String getColumnName() {
        return columnName;
    }

    public void setColumnName(String columnName) {
        this.columnName = columnName;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public boolean isPrimaryKey() {
        return isPrimaryKey;
    }

    public void setPrimaryKey(boolean primaryKey) {
        isPrimaryKey = primaryKey;
    }
}

eg:

ColumnDetail屬性 屬性說明 demo
fieldName JAVA實體類的屬性 mName
columnName SQLite的Table的Column m_name
relativeName 用來映射fieldName和columnName的關聯關係名稱靜態字符串常量public static final String M_NAME = “m_name”; M_NAME
type SQLite的Table的Column的類型 TEXT

有了映射關係的準備之後,我們來解析我們的Bean類,注意,需要解析非staticfinal修飾的變量:

/**
 * 獲取屬性和列的對應關係
 * 如果該屬性指定了列名,則創建表時用指定列名,否則默認將屬性名稱的駝峯式寫法轉下劃線寫法
 * 引用類型採用屬性名稱的駝峯式轉下劃線的方式定義
 * 如果實體類中指定了主鍵,則引用屬性和列名都用_id。
 * 如果沒有指定主鍵,若屬性中存在int或long類型的_id,默認爲主鍵,否則創建一個名爲_ID的屬性和列
 * 只能指定一個主鍵,否則會使創建SQL語句出現問題
 * 注意,註解的primaryKey必須爲long類型,否則生成的JAVA文件會有錯
 * 目前支持的JAVA類型有int、long、float、double、String、byte,對應SQLite的INTEGER、TEXT、REAL
 * @param clazz 實體類
 * @return 對應關係集合
 * @throws Exception
 */
private static List<ColumnDetail> getColumns(Class<?> clazz) throws Exception {
    List<ColumnDetail> colums = new ArrayList<>();
    List<Integer> unAssigned = new ArrayList<>();
    // 是否全部建列
    boolean isAll = true;
    // 是否存在指定非主鍵列
    boolean isColumn = false;
    // 是否存在指定註解@Table
    boolean isTable = false;
    boolean hasPrimaryKey = false;
    // 判斷是否有@Table註解並標記isTable
    // 如果有則判斷是否全部屬性建列,如果沒有@Table註解,則默認全部屬性建列,並標記isAll
    if (clazz.isAnnotationPresent(Table.class)) {
        Table table = clazz.getAnnotation(Table.class);
        isTable = true;
        isAll = table.all();
    }
    Field[] fields = clazz.getDeclaredFields();
    if (fields != null) {
        for (Field field : fields) {
            // 排除掉靜態變量和常量
            if (!Modifier.isStatic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers())) {
                // 如果使用@Column註解,則按照指定要求建列
                if (field.isAnnotationPresent(Column.class)) {
                    String columnName;
                    Column column = field.getAnnotation(Column.class);
                    columnName = column.name();
                    boolean isPrimaryKey = false;
                    String type;
                    if (int.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) {
                        // 主鍵
                        if (column.primaryKey()) {
                            columnName = "_id";
                            isPrimaryKey = true;
                            hasPrimaryKey = true;
                            type = "INTEGER PRIMARY KEY AUTOINCREMENT";
                        } else {
                            type = "INTEGER";
                        }
                    } else if (String.class.isAssignableFrom(field.getType())) {
                        type = "TEXT";
                    } else if (float.class.isAssignableFrom(field.getType()) || double.class.isAssignableFrom(field.getType())) {
                        type = "REAL";
                    } else if (byte.class.isAssignableFrom(field.getType())) {
                        type = "BLOB";
                    } else {
                        throw new Exception("TYPE ERROR:  " + field.getType() + "  :  " + field.getName());
                    }
                    // 如果指定列名爲空,則默認駝峯轉下劃線方式命名
                    if (StringUtil.isEmpty(columnName)) {
                        columnName = StringUtil.humpToUnderline(field.getName()).toLowerCase();
                    }
                    // 存在非主鍵的指定列,並標記
                    if (!column.primaryKey()){
                        isColumn = true;
                    }
                    if (column.unique()){
                        type = type + " UNIQUE";
                    }
                    // 添加到集合中
                    colums.add(new ColumnDetail(columnName.toLowerCase(), field.getName(), StringUtil.humpToUnderline(field.getName()).toUpperCase(), type, isPrimaryKey));
                } else if (isAll) {
                    // 非指定列
                    String columnName = StringUtil.humpToUnderline(field.getName()).toLowerCase();
                    boolean isPrimaryKey = false;
                    if (field.getName().equalsIgnoreCase("_ID")) {
                        isPrimaryKey = true;
                        hasPrimaryKey = true;
                    }
                    String type;
                    if (int.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) {
                        if (isPrimaryKey) {
                            type = "INTEGER PRIMARY KEY AUTOINCREMENT";
                        } else {
                            type = "INTEGER";
                        }
                    } else if (String.class.isAssignableFrom(field.getType())) {
                        type = "TEXT";
                    } else if (float.class.isAssignableFrom(field.getType()) || double.class.isAssignableFrom(field.getType())) {
                        type = "REAL";
                    } else if (byte.class.isAssignableFrom(field.getType())) {
                        type = "BLOB";
                    } else {
                        throw new Exception("TYPE ERROR:  " + field.getType() + "  :  " + field.getName());
                    }
                    // 如果非主鍵列,則將集合的遊標記錄下,當確定除指定屬性不建列時,從集合中移除
                    if (!isPrimaryKey){
                        unAssigned.add(unAssigned.size());
                    }
                    colums.add(new ColumnDetail(columnName, field.getName(), columnName.toUpperCase(), type, isPrimaryKey));
                } else {
                    // 爲防止自增主鍵列對應的屬性_id被重複創建,在這裏獲取
                    if (field.getName().equalsIgnoreCase("_ID")) {
                        hasPrimaryKey = true;
                        colums.add(new ColumnDetail("_id", field.getName(), "_ID", "INTEGER PRIMARY KEY AUTOINCREMENT", true));
                    }
                }
            }
        }
        // 當指定了@Table的all屬性爲true或者除主鍵列的屬性有註解外沒有任何註解的情況下,全部屬性自建列,否則只有指定的屬性和主鍵對應的屬性建列
        if ((isTable && !isAll) || (!isTable && isColumn)){
            for (int i = 0; i < unAssigned.size(); i++) {
                colums.remove(unAssigned.get(i) + i);
            }
        }
        // 如果類不存在指定的主鍵對應的屬性時,創建_id的屬性,並創建自增主鍵列,該條對應關係的屬性名稱爲空,方便後續在實體類中增加該屬性
        if (!hasPrimaryKey) {
            colums.add(new ColumnDetail("_id", "", "_ID","INTEGER PRIMARY KEY AUTOINCREMENT", true));
        }
    }
    return colums;
}

除了解析列之外,還有表名需要獲取:

/**
 * 獲取實體類的表名(如果沒有用@Table註解,則創建表的sql語句中表名默認爲類名小寫)
 * @param clazz 實體類
 * @return 註解的表名(如果沒有指定表名則返回null)
 */
private static String getTableName(Class<?> clazz) {
    String name = null;
    if (clazz.isAnnotationPresent(Table.class)) {
        Table table = clazz.getAnnotation(Table.class);
        name = "tb_" + StringUtil.humpToUnderline(table.name()).toLowerCase();
    }
    return name;
}


Step3.表的創建

有了屬性和列的對應關係,即List<ColumnDetail>,就可以動手先出相關的SQL語句了。不過我們需要先將表名和列名寫成靜態常量以供調用,如:public static final String M_NAME = "m_name";

/**
 * 生成創建表的SQL語句和以_ID爲查詢條件的靜態常量語句
 * 創建表的SQL語句的靜態常量名爲CREATE_TABLE
 * 創建以_ID爲查詢條件的靜態常量名爲_ID_CLAUSE
 * public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COLUMN + " TYPE)";
 * CREATE_TABLE拼接語句的使用使用StringBuilder
 * public static final String _ID_CLAUSE = _ID + " = ?";
 * @param columns {@link #getColumns}
 * @return 創建表的SQL語句
 * @throws Exception
 */
public static String createTable(List<ColumnDetail> columns) throws Exception {
    String sql = null;
    StringBuilder builder = new StringBuilder("\tpublic static final String CREATE_TABLE = new StringBuilder(\"CREATE TABLE \")").append(".append(TABLE_NAME).append(\"( \")");
    if (!columns.isEmpty()) {
        for (ColumnDetail column : columns) {
            builder.append("\n\t\t\t.append(").append(column.getRelativeName()).append(").append(\" ");
            if (column.isPrimaryKey()) {
                builder.append(column.getType()).append(" , \")");
            } else {
                builder.append(column.getType()).append(", \")");
            }
        }
        int start = builder.length() - ", \")".length();
        builder.replace(start, start + 2, ")");
        builder.append(".toString();\n");
        builder.append("\n\tpublic static final String _ID_CLAUSE = _ID + \" = ?\";\n");
        sql = builder.toString();
    }
    return sql;
}

在這個過程中,爲了我們方便後續改查刪使用主鍵作爲條件,我們同時將查詢條件也寫成靜態常量方便調用:public static final String _ID_CLAUSE = _ID + " = ?";

接下來的工作也很簡單了,將數據庫數據和JAVA對象進行相互轉化的語句構建:

JAVA對象轉數據庫類型的鍵值對ContetnValues獲取:

/**
 * 生成增刪改的所需要用到的鍵值對方法語句
 * 主鍵默認自增,所以方法中不會包含將_ID設置到鍵值對的語句
 * 創建的方法名爲 getKeyAndValue,方法參數爲實體類對象info(非空)
 * 方法類型默認靜態,方法返回鍵值對ContentValues
 * public static ContentValue getKeyAndValue(Object obj){
 *     ContentValue cv = new ContentValue();
 *     cv.put(RELATIVE_NAME, obj.field); 
 *     return cv;
 * }
 * @param clazz 實體類
 * @param columns {@link #getColumns}
 * @return 生成創建鍵值對的方法語句
 * @throws Exception
 */
public static String createCV(Class<?> clazz, List<ColumnDetail> columns) throws Exception {
    StringBuilder builder = new StringBuilder("\tpublic static ContentValues getKeyAndValue(");
    builder.append(clazz.getSimpleName()).append(" info){\n").append("\t\tContentValues cv = new ContentValues();\n");
    for (ColumnDetail column : columns) {
        if (!StringUtil.isEmpty(column.getFieldName())) {
            // 對非主鍵列的所有屬性值裝載
            if (!column.isPrimaryKey()) {
                builder.append("\t\tcv.put(").append(column.getRelativeName()).append(", info.").append(column.getFieldName()).append(");\n");
            }
        }
    }
    builder.append("\t\treturn cv;\n\t}\n");
    return builder.toString();
}

數據庫遊標Cursor轉Java類型的獲取:

/**
 * 生成讀取表後轉實體類的對應方法語句
 * 根據反射的字段類型判斷需要獲取的基本類型
 * 如果指定了主鍵或存在_id則用指定主鍵的屬性名稱,否則默認_id
 * 創建的方法名爲 getModel,參數爲實體類對象info(非空)和遊標對象cr(Cursor),方法返回值爲空,通過參數的引用賦值
 * public static void getModel(Object obj, Cursor cr){
 *     info.field = cr.getInt(cr.getColumnIndex(RELATIVE_NAME));     // int類型,RELATIVE_NAME替換稱列的關聯靜態常量
 *     info.field = cr.getLong(cr.getColumnIndex(RELATIVE_NAME));    // long類型,RELATIVE_NAME替換稱列的關聯靜態常量
 *     info.field = cr.getFloat(cr.getColumnIndex(RELATIVE_NAME));   // float類型,RELATIVE_NAME替換稱列的關聯靜態常量
 *     info.field = cr.getDouble(cr.getColumnIndex(RELATIVE_NAME));  // double類型,RELATIVE_NAME替換稱列的關聯靜態常量
 *     info.field = cr.getString(cr.getColumnIndex(RELATIVE_NAME));  // string類型,RELATIVE_NAME替換稱列的關聯靜態常量
 *     info.field = cr.getBlob(cr.getColumnIndex(RELATIVE_NAME));    // blob類型,RELATIVE_NAME替換稱列的關聯靜態常量
 *     info._id = cr.getLong(cr.getColumnIndex(_ID)); // 主鍵賦值
 * }
 * @param clazz 實體類
 * @param columns   {@link #getColumns}
 * @return  讀取表後轉實體類的對應方法語句
 * @throws Exception
 */
public static String createModel(Class<?> clazz, List<ColumnDetail> columns) throws Exception {
    StringBuilder builder = new StringBuilder("\tpublic static void getModel(");
    builder.append(clazz.getSimpleName()).append(" info, Cursor cr){\n");
    if (columns != null) {
        for (ColumnDetail column : columns) {
            if (!StringUtil.isEmpty(column.getFieldName())) {
                if (!column.isPrimaryKey()) {
                    builder.append("\t\tinfo.").append(column.getFieldName()).append(" = ");
                    Field field = clazz.getDeclaredField(column.getFieldName());
                    if (int.class.isAssignableFrom(field.getType())){
                        builder.append("cr.getInt(");
                    } else if(long.class.isAssignableFrom(field.getType())) {
                        builder.append("cr.getLong(");
                    } else if (String.class.isAssignableFrom(field.getType())) {
                        builder.append("cr.getString(");
                    } else if (float.class.isAssignableFrom(field.getType())) {
                        builder.append("cr.getFloat(");
                    } else if (double.class.isAssignableFrom(field.getType())) {
                        builder.append("cr.getDouble(");
                    } else if (byte.class.isAssignableFrom(field.getType())) {
                        builder.append("cr.getBlob(");
                    } else {
                        throw new Exception("TYPE ERROR:  " + field.getType() + "  :  " + field.getName());
                    }
                    builder.append("cr.getColumnIndex(").append(column.getRelativeName()).append("));\n");
                } else {
                    builder.append("\t\tinfo.").append(column.getFieldName()).append(" = cr.getLong(cr.getColumnIndex(_ID));\n");
                }
            } else {
                // 如果不存在屬性對應主鍵列,則用創建的屬性_id獲取主鍵值
                builder.append("\t\tinfo._id = cr.getLong(cr.getColumnIndex(_ID));\n");
            }
        }
    }
    builder.append("\t}\n");
    return builder.toString();
}

最後,我們可以構建我們的CRUD方法了:

/**
 * 創建增刪改查方法
 * insert, 單體插入返回行號或根據行號判定是否新增成功,集合插入,可根據需求自行修改,包括開啓事務,中斷插入等操作
 * update,包括參數primaryKey和obj對象,可自行修改參數爲clazz對象的方法以滿足需求,返回類型爲int值,影響行數
 * delete,包括參數primaryKey和obj對象,可自行修改參數爲clazz對象的方法以滿足需求,返回類型爲int值,影響行數
 * query, 包括查詢全部(無參查詢,集合輸出)和按primaryKey查詢單體、obj對象查詢單體和集合,可修改obj對象查詢方式以自定義滿足需求
 * @param clazz
 * @param details
 * @param dbObj
 * @return
 */
public static String createCRUD(Class<?> clazz, List<ColumnDetail> details, String dbObj){
    String primaryField = "_id";
    for (ColumnDetail detail : details) {
        if (detail.isPrimaryKey()){
            primaryField = detail.getFieldName();
            if (null != primaryField && primaryField.isEmpty()){
                primaryField = "_id";
            }
            break;
        }
    }
    String clazzName = clazz.getSimpleName();
    StringBuilder builder = new StringBuilder("\t");
    // insert, 單體插入返回行號或根據行號判定是否新增成功,集合插入,可根據需求自行修改,包括開啓事務,中斷插入等操作
    // 返回long類型,插入的行號,主鍵自增,因此行號=主鍵
    builder.append("public static long insert(").append(clazzName).append(" info){\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("info.").append(primaryField).append(" = db.insert(TABLE_NAME, null, getKeyAndValue(info));\n\t\t")
            .append("return info.").append(primaryField).append(";\n\t}\n\n\t");
    // 返回布爾類型是否插入成功
    builder.append("public static boolean insertRow(").append(clazzName).append(" info){\n\t\t")
            .append("return insert(info) > 0;\n\t}\n\n\t");
    // 集合插入
    builder.append("public static boolean insertRow(List<").append(clazzName).append("> infos){\n\t\t")
            .append("boolean isInsert = false;\n\t\t")
            .append("for(").append(clazzName).append(" info : infos){\n\t\t\t")
            .append("isInsert &= insertRow(info);\n\t\t}\n\t\t")
            .append("return isInsert;\n\t}\n\n\t");
    // update,參數obj對象,返回類型爲int值,影響行數, 可自行添加方法以滿足需求
    // obj對象
    builder.append("public static int update(").append(clazzName).append(" info){\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("return db.update(TABLE_NAME, getKeyAndValue(info), _ID_CLAUSE, new String[]{String.valueOf(info.")
            .append(primaryField).append(")});\n\t}\n\n\t");
    // delete,包括參數primaryKey和obj對象,可自行修改參數爲clazz對象的方法以滿足需求,返回類型爲int值,影響行數
    // primaryKey
    builder.append("public static int delete(long _id){\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("return db.delete(TABLE_NAME, _ID_CLAUSE, new String[]{String.valueOf(_id)});\n\t}\n\n\t");
    // obj對象
    builder.append("public static int delete(").append(clazzName).append(" info){\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("return db.delete(TABLE_NAME, _ID_CLAUSE, new String[]{String.valueOf(info.")
            .append(primaryField).append(")});\n\t}\n\n\t");
    // query, 包括查詢全部(無參查詢,集合輸出)和按primaryKey查詢單體、obj對象查詢單體和集合,可修改obj對象查詢方式以自定義滿足需求
    // 無參數,查詢全部
    builder.append("public static List<").append(clazzName).append("> queryAll(){\n\t\t")
            .append("ArrayList<").append(clazzName).append("> infos = new ArrayList<>();\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("Cursor cr = db.query(TABLE_NAME, null, null, null, null, null, null);\n\t\t")
            .append(clazzName).append(" info;\n\t\t")
            .append("while (cr.moveToNext()){\n\t\t\t")
            .append("info = new ").append(clazzName).append("();\n\t\t\t")
            .append("getModel(info, cr);\n\t\t\t")
            .append("infos.add(info);\n\t\t}\n\t\treturn infos;\n\t}\n\n\t");
    // 參數primaryKey,查詢單條
    builder.append("public static ").append(clazzName).append(" query(long _id){\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("Cursor cr = db.query(TABLE_NAME, null, _ID_CLAUSE, new String[]{String.valueOf(_id)}, null, null, null);\n\t\t")
            .append(clazzName).append(" info = new ").append(clazzName).append("();\n\t\t")
            .append("if (cr.moveToNext()){\n\t\t\t")
            .append("getModel(info, cr);\n\t\t}\n\t\t")
            .append("return info;\n\t}\n\n\t");
    // 參數obj,查詢單條
    builder.append("public static ").append(clazzName).append(" query(").append(clazzName).append(" info){\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("Cursor cr = db.query(TABLE_NAME, null, _ID_CLAUSE, new String[]{String.valueOf(info.").append(primaryField).append(")}, null, null, null);\n\t\t")
            .append(clazzName).append(" data = new ").append(clazzName).append("();\n\t\t")
            .append("if (cr.moveToNext()){\n\t\t\t")
            .append("getModel(data, cr);\n\t\t}\n\t\t")
            .append("return data;\n\t}\n\n\t");
    // 參數obj,查詢列表
    builder.append("public static List<").append(clazzName).append("> queryAll(").append(clazzName).append(" info){\n\t\t")
            .append("ArrayList<").append(clazzName).append("> infos = new ArrayList<>();\n\t\t")
            .append("SQLiteDatabase db = ").append(dbObj).append(";\n\t\t")
            .append("Cursor cr = db.query(TABLE_NAME, null, _ID_CLAUSE, new String[]{String.valueOf(info.").append(primaryField).append(")}, null, null, null);\n\t\t")
            .append(clazzName).append(" data;\n\t\t")
            .append("while (cr.moveToNext()){\n\t\t\t")
            .append("data = new ").append(clazzName).append("();\n\t\t\t")
            .append("getModel(data, cr);\n\t\t\t")
            .append("infos.add(data);\n\t\t}\n\t\treturn infos;\n\t}\n");
    return builder.toString();
}

新建一個Model類,將上述方法構建出來的內容輸出到這個類中,就完成了我們的基本類似模塊的快速構建。



Step4.Bean文件的反寫

寫到這裏,其實大部分的工作已經完成了,當然還有些小細節的內容需要我們靈活處置,在這裏補充一個內容,就是Bean文件的反寫?
爲什麼反寫?
其實原因很簡單,當很久之後翻過我們的代碼看,我們需要很耐心的去一一比對我們的哪些屬性需要構建爲表的列,對應的列名又是什麼,尤其在查看數據庫又發現大量列的時候(SQLite是一個輕量級的數據庫,因此當數據很多的時候,我們也一般很少用到表關聯去分割我們的數據),這時候把我們自定義的註解再反寫到Bean上會起到一個註釋的作用。
這裏我們採用了正則匹配查詢的模式,一行一行的判定是不是我們的屬性定義所在的行(當然也會有很多突發的小情況,比如說這個類並非要給裸類,而是存在很多註釋或者不規則的情況,那麼反寫的代碼也會存在一定的問題),如果是,切該屬性行沒有匹配到註解,那麼在這個屬性行的上面添加上我們的註解,如果匹配到對應的註解,則替換爲更規範的註解方式。

/**
 * 將構建的表名列名替換掉實體類的註解值
 * 如果存在類的註解@Table,將類的註解@Table的name值替換稱model類對應的常量
 * 如果存在屬性的註解@Column,將屬性的註解@Column的name值替換稱model類對應的常量
 * 如果不存在表註解@Table,則將表名的註解添加上,name值爲model類對應的常量
 * 如果不存在屬性註解@Column,則將表對應的列的註解添加上,name值model類對應的常量
 * 使用正則表達式的匹配查詢的方式對每一行進行判斷,如果該行存在指定的屬性,則進行替換
 * 行的結束標誌是;
 * 注意行內不要有註釋
 * 針對主鍵做特殊處理
 * 引入相應的包
 * 處理結果
 * @Table(name = modelClass.TABLE_NAME)         // 是否全部建列屬性即 @Table(all = true) 或 @Table(all = true) 則會被刪除掉,可自行添加作爲標記
 * public class Object {
 *     @Column(name = modelClass.RELATIVE_NAME)
 *     public String field;
 *     @Column(name = modelClass._ID, primaryKey = true)
 *     public long _id;
 * }
 * 正則表達式的用法:
 * [\s\S]   匹配任意字符(\s爲空白字符,\S爲非空白字符)
 * .        匹配非換行之外的字符(\n\r)
 * \t       縮進符
 * *        匹配0-任意次
 * +        匹配1-任意次(至少匹配一次)
 * ?        匹配0或1次(至多匹配一次)
 * []       字符集合,包含包含的任意一個字符的內容。對某些特殊符號進行匹配時,也需要用[]包住
 * [^]      字符集合,匹配未包含任意一個字符集的內容
 * \b       匹配左右邊界,即邊界存在空白字符即可匹配,在左匹配做邊界,在右匹配右邊界
 * $        匹配行尾
 * ^        匹配行首
 * JAVA中String.replace(String target, String replacement)和String.replaceAll(String regex, String replacement)的區別
 * replace查找target的內容,並替換成指定內容replacement
 * replaceAll通過正則匹配查找內容,並替換成指定內容replacement
 * @param clazz 實體類
 * @param modelClazz    生成對應常量名稱和SQL語句、映射方法的model類
 * @param filename  實體類的路徑
 * @param columns   {@link #getColumns}
 * @return  修改後的實體類
 * @throws Exception
 */
public static String writeReflect(Class<?> clazz, Class<?> modelClazz, String filename, List<ColumnDetail> columns) throws Exception {
    StringBuilder builder = new StringBuilder();
    File file = new File(filename);
    FileReader reader = new FileReader(file);
    BufferedReader br = new BufferedReader(reader);
    String line;
    while ((line = br.readLine()) != null) {
        builder.append(line).append("\n");
    }
    br.close();
    reader.close();
    Field[] fields = clazz.getDeclaredFields();
    String content = builder.toString();
    if (fields != null) {
        boolean isNeedPrimaryKey = false;
        for (ColumnDetail column : columns) {
            if (!StringUtil.isEmpty(column.getFieldName())) {
                Field field = clazz.getDeclaredField(column.getFieldName());
                // 匹配查詢,查詢@Column...public/private/protected/default javaType fieldName;(目前只支持public模式,方便賦值)
                String regexAnnotation = "(\\t*)( *)(((@Column( *\\t*[(] *[^)]*[)] *\\t*))|((@Column *\\t*)))|())";
                String regexDeclare = Modifier.isPrivate(field.getModifiers()) ? "private" : Modifier.isPublic(field.getModifiers()) ? "public" : Modifier.isProtected(field.getModifiers()) ? "protect" : "";
                String regexType = field.getType().getSimpleName();
                String regexName = field.getName();
                String regex = regexAnnotation + "\\s*" + regexDeclare + "\\s*" + regexType + "\\s*" + regexName + "\\s*;" + "\\t* *";
                StringBuilder newLine = new StringBuilder("\n");
                builder.append("\t");
                if (column.isPrimaryKey()) {
                    newLine.append("\n\t@Column(name = ").append(modelClazz.getSimpleName()).append("._ID, primaryKey = true)");
                } else {
                    newLine.append("\t@Column(name = ").append(modelClazz.getSimpleName()).append(".").append(column.getRelativeName()).append(")");
                }
                newLine.append("\n\t");
                newLine.append("" == regexDeclare ? "" : regexDeclare + " ").append(regexType).append(" ").append(regexName).append(";");
                content = content.replaceAll(regex, newLine.toString());
            } else {
                isNeedPrimaryKey = true;
            }
        }
        Pattern pattern = Pattern.compile("((((@Table)[\\s\\S]*)|())public\\s*class\\s*" + clazz.getSimpleName() + ")((\\s*[{])|( +[^{]*[{]))");
        Matcher m = pattern.matcher(content);
        if (m.find()) {
            String result = m.group();
            pattern = Pattern.compile("@Table\\s*([\\(]+[^)]*[\\)]+)*");
            m = pattern.matcher(result);
            String tableResult = result;
            if (m.find()){
                tableResult = result.replace(m.group(), "");
            }
            String replaceContent = "import " + modelClazz.getName() + ";\nimport cn.ximoon.framework.db.Column;\n" + "import cn.ximoon.framework.db.Table;\n\n@Table(name = " + modelClazz.getSimpleName() + ".TABLE_NAME)\n" + tableResult + "\n";
            if (isNeedPrimaryKey){
                replaceContent = replaceContent + "\n\n\t@Column(name = " + modelClazz.getSimpleName() + "._ID, primaryKey = true)\n\tpublic long _id;\n";
            }
            content = content.replace(result, replaceContent);
        }
        System.out.println(content);
    }
    return content;
}


Step5.工具的調用

接下來我們將所有完成的方法進行組合,並修改Bean類完成註解的替換和覆寫Model類完成數據庫的基礎操作,同時如果有需要導包的地方也可以手動導入package,進一步減少我們的工作。
在這裏需要注意,我們定義了五個參數,涉及到兩個類,Bean和Model,以及這兩個類的實際文件路徑,路徑的獲取方式通過工程目錄結構右鍵類文件,選擇Copy Path,快捷鍵Ctrl+Shift+C即可,最後一個的參數是整個工程數據庫持久化引擎的獲取方式,通過繼承SQLiteOpenHelper,並調用單例模式,獲取SQLiteDatabase

/**
 * 生成實體類的基本SQL語句,包括表的創建SQL和表查詢結果和實體類的相互轉化方法以及表名列名的屬性常量
 * 對實體類做基本替換
 * 注意modelClass覆寫模式,即內容全部被替換成新內容,CRUD需要導入SQLiteDatabase的對象需要的類,創建完後可手動導入
 * 目前對屬性只支持public,如果爲其他修飾符(private、protected、default),在賦值和取值對時候可能存在問題,同包可能不存在問題,私有化請自行更改代碼
 * 即 info.field = ""; 和 info.setField("");
 * @param clazz 實體類
 * @param modelClazz    生成對應常量名稱和SQL語句、映射方法的model類
 * @param beanName  實體類的路徑
 * @param modelName model類的路徑
 * @param dbobj    如果需要創建CRUD方法,則必須傳入SQLiteDatabase獲取的方法,以文本的方式傳入
 *              eg: DBOpenHelp.getInstance().getSQLiteDatabase();
 *              生成 SQLiteDatabase db = DBOpenHelp.getInstance().getSQLiteDatabase();
 *              用來增刪改查
 *  er@throws Exception
 */
public static void writeFile(Class<?> clazz, Class<?> modelClazz, String beanName, String modelName, String dbobj) throws Exception {
    List<ColumnDetail> columns = getColumns(clazz);
    if (null != columns && columns.size() > 0) {
        String dbColumns = createDBColumns(clazz, columns);
        String createTable = createTable(columns);
        String createCV = createCV(clazz, columns);
        String createModel = createModel(clazz, columns);
        String createCRUD = "";
        if (!StringUtil.isEmpty(dbobj)) {
            createCRUD = "\n" + createCRUD(clazz, columns, dbobj) + "\n";
        }
        // 對modelClass覆寫,並導入所需要對包
        StringBuilder builder = new StringBuilder("")
                .append(modelClazz.getPackage())
                .append(";\n\nimport android.content.ContentValues;\n")
                .append("import android.database.Cursor;\n")
                .append("import android.database.sqlite.SQLiteDatabase;\n")
                .append("import android.text.TextUtils;\n\n")
                .append("import java.util.ArrayList;\n")
                .append("import java.util.List;\n")
                .append("\nimport ")
                .append(clazz.getPackage().getName())
                .append(".")
                .append(clazz.getSimpleName())
                .append(";\n")
                .append("\npublic class ")
                .append(modelClazz.getSimpleName())
                .append(" {\n\n")
                .append(dbColumns)
                .append("\n")
                .append(createTable)
                .append("\n")
                .append(createCRUD)
                .append(createCV)
                .append("\n")
                .append(createModel)
                .append("\n}");
        String content = writeReflect(clazz, modelClazz, beanName, columns);
        FileWriter fileWriter = new FileWriter(beanName);
        fileWriter.write(content);
        fileWriter.flush();
        fileWriter.close();
        content = builder.toString();
        System.out.println(content);
        fileWriter = new FileWriter(modelName);
        fileWriter.write(content);
        fileWriter.flush();
        fileWriter.close();
    }
}

附上源碼地址:
數據庫代碼自動生成器源碼

附上效果圖:

執行前:

執行前Bean.class

執行前Model.class

執行後:

執行後Bean.class

導包前Model.class

導包後:
導包後Model.class

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章