Android數據庫代碼自動生成器
有時候使用數據庫框架來操作數據庫數據對我們的App來講並不一定十分適用,其實使用Google提供的SQLiteOpenHelper
也十分直觀,在數據庫框架還沒有流行的時候,我們的App也基本都是用的是這種方式對數據庫的處理。
但有時候一個App裏可能會存在N多張表,又懶的用框架寫那麼多註釋構建再拷貝代碼,但使用SQLiteOpenHelper
同樣也有大量的重複性代碼,或者是同模式的代碼。於是,根據自己所開發的App大部分功能來看,歸納了這幾點數據庫操作需求:
- 創建表的語句
- 基本的增刪改查,其中刪改查根據主鍵_id作爲條件
- 增刪改查語句中,不可避免的Java對象和數據庫數據的相互轉換
- Java實體類和數據庫列名之間的對應關係(方便我們糾錯排查)
基於這幾點需求,我們初步定一下我們的實現方案:
- 創建一個Java基本類
- 根據基本類,構建數據模型(即創建一個新的Java類,用來存放相關的數據庫操作方法)
- 如果對類的某些字段有特殊需求,也可以進行特殊處理,因此需要添加註解,如主鍵,列名或者表名,唯一標識等等
Step1. 構建註解
接下來第一步,我們需要創建自己的註解類型,分爲兩種註解類型,類註解和屬性註解,分別對應數據庫的表和列,在這裏我們需要注意:
- 是不是這個Bean的所有非
static
和final
修飾的屬性都需要創建到這張表中; - 這個Bean有沒有主鍵列對應的屬性;
- 存不存在唯一約束;
- 屬性有沒有指定的列名;
- 如果不存在註解,即默認相關方案創建數據庫相關轉換規則。
把這些需求反應到我們的註解當中:
表註解:
/**
* 表註解
*/
@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類的所有屬性,且不能被修飾爲static
和final
的屬性,然後根據獲取的屬性集合轉爲表的列。
Step2. 對Bean類進行反射提取
有了基本的註解,我們就可以增加我們的特殊需求了,當然沒有註解,我們也有自己默認的處理方案。在這部分的處理當中,我們首先要做的是,一個Java類的基本屬性對應到表的這個映射過程中,我們涉及到了兩個命名規則:
- 列名,也就是我們SQLite中Table的Column名稱,可通過註解進行自定義,默認爲屬性名的駝峯轉小寫字母的下劃線寫法
- 映射關係名稱,也就是我們在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類,注意,需要解析非static
和final
修飾的變量:
/**
* 獲取屬性和列的對應關係
* 如果該屬性指定了列名,則創建表時用指定列名,否則默認將屬性名稱的駝峯式寫法轉下劃線寫法
* 引用類型採用屬性名稱的駝峯式轉下劃線的方式定義
* 如果實體類中指定了主鍵,則引用屬性和列名都用_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();
}
}
附上源碼地址:
數據庫代碼自動生成器源碼
附上效果圖:
執行前:
執行後:
導包後: