一.簡介
上次封裝了一個GreenDao架構GreenDao的簡單封裝,感覺用别人的架構還不如自己寫一套,于是我就在原生SQLiteDatabase的基礎上封裝了一套架構,便于調用者使用。原生的SQLiteDatabase進行資料庫的一些操作都是基本上都是拼接原生的sqlite語言,然後去執行,這樣對于調用着來說使用繁瑣,耗時,例如插入一張表的時候,你要調用去建造一個contentValue對象,然後給他插入對應的表的字段名和值,例如:contentValue.put(“_id”,id);這個如果表裡有很多的字段的話你要寫好幾十行代碼,如果一個應用裡面有很多地方調用,我有得寫好幾十行代碼,是以這就造成了代碼的備援,繁瑣和開發的耗時,于是,面向對象式的資料庫架構就孕育而生了。。
二.搭建面向對象式資料庫架構
根據sqliteDatabase原生的api和sqlite語句去考慮,如何封裝
我們要填入什麼參數,根據這些參數去思考
先來看看兩個api的例子:
public long insert(String table, String nullColumnHack, ContentValues values) {
throw new RuntimeException(“Stub!”);
}
public int delete(String table, String whereClause, String[] whereArgs) {
throw new RuntimeException(“Stub!”);
}
再來看看sqlite語句
create table if not exists 表名(字段1名 字段1類型,字段2名 字段2類型…)
在原生的api和sqlite語句中我們不難發現,他們都需要傳入表的字段和值(contentValue需要put.(字段名,值)),不單單是這兩個例子,其他api和語句也都是要求填入字段的名稱和值,既然都要求拿到表的字段名和值,那我們就可以根據這個規律去封裝。那麼表的字段的名稱和值怎麼來,不可能每次都去表裡去copy吧,java裡有什麼辦法可以拿到一個類成員變量和值呢,對,就是反射,本文的重點也就是反射。
一.自動建表
我們先來看看表是如何建的吧,原生的建表語句是這樣的
create table if not exists 表名(字段1名 字段1類型,字段2名 字段2類型…)
我們可以先去定義表名和字段名,那麼該如何定義呢
①表名和字段名的定義
在greendao這個架構裡我們可以看到,它用注解的形式去定義,有注解的時候取到注解當表名,沒注解時取類名。那麼我們不妨也用這樣的方式去試試:
自定義注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbTable {
String value();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbField {
String value();
}
-
ElementType常用的就是以下三個
ElementType.TYPE寫在類名上
ElementType.FIELD寫在成員變量上
ElementType.METHOD寫在方法上
-
RetentionPolicy
RetentionPolicy.SOURCE僅保留在源檔案裡
RetentionPolicy.RUNTIME保留到運作時,但是編譯成class檔案時注解還存在,大家可以去試試
RetentionPolicy.CLASS保留到class檔案
注解的使用
注解的使用當然是在自己的表裡,而實體類就充當一張表
package com.example.lcp.database.entity;
import com.example.lcp.database.annotation.DbField;
import com.example.lcp.database.annotation.DbTable;
@DbTable("tb_user")
public class User {
@DbField("_id")
Long id;
@DbField("name")
String name;
@DbField("sex")
String sex;
public User(Long id, String name, String sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
public User() {
}
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return id+" "+name+" "+sex;
}
}
- @DbTable(“tb_user”)當有注解的時候就用注解定義的名字tb_user,當沒注解的時候就用類名,你可以定義自己喜歡的字段名,@DbField的作用相同
②自動建表
自動建表的真實目的是免去使用者每次都去拼接表名,字段名和字段類型,傳入類的位元組碼,上文我們提到,通過反射,拿到表名和字段名,在方法裡自動拼接并建立表。
public void init(SQLiteDatabase database, Class<T> tClass) {
this.database = database;
this.tClass = tClass;
//取到表名
if (tClass.getAnnotation(DbTable.class) == null) {
tableName = tClass.getSimpleName();
} else {
tableName = tClass.getAnnotation(DbTable.class).value();
}
if (!database.isOpen()) {
return;
}
//沒有初始化的時候去初始化(隻初始化一次)
if (initMap.get(tableName) == null) {
String strTable = createTable();
database.execSQL(strTable);
initMap.put(tableName, true);
}
}
- 通過反射拿到表名
- 當初始化了就不在執行建表操作
- 把已經初始化的表存到對象池裡
/**
* 建立表
*
* @return
*/
private String createTable() {
//這張表沒緩存就去建一個緩存空間
HashMap<String, Field> fieldHashMap = cashMap.get(tableName);
if (fieldHashMap != null && fieldHashMap.size() != ) {
return null;
}
fieldHashMap = new HashMap<>();
//create table if not exists tb_user(_id integer,name varchar(20),password varchar(20))
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("create table if not exists ");
stringBuffer.append(tableName + "(");
//拿到所有的成員變量
Field[] fields = tClass.getDeclaredFields();
for (Field field : fields) {
String name = null;
//把所有的通路權限打開
field.setAccessible(true);
//拿到成員變量的類型 Class<?>拿到這個類型會導緻得不到對應的成員變量類型
Class type = field.getType();
//如果沒注解
if (field.getAnnotation(DbField.class) == null) {
//成員變量的變量名
name = field.getName();
if (type == String.class) {
stringBuffer.append(name + " TEXT,");
} else if (type == Integer.class) {
stringBuffer.append(name + " INTEGER,");
} else if (type == Long.class) {
stringBuffer.append(name + " BIGINT,");
} else if (type == Double.class) {
stringBuffer.append(name + " DOUBLE,");
} else if (type == byte[].class) {
stringBuffer.append(name + " BLOB,");
} else {
continue;
}
} else {//有注解
//注解的名稱
name = field.getAnnotation(DbField.class).value();
if (type == String.class) {
stringBuffer.append(name + " TEXT,");
} else if (type == Integer.class) {
stringBuffer.append(name + " INTEGER,");
} else if (type == Long.class) {
stringBuffer.append(name + " BIGINT,");
} else if (type == Double.class) {
stringBuffer.append(name + " DOUBLE,");
} else if (type == byte[].class) {
stringBuffer.append(name + " BLOB,");
} else {
continue;
}
}
//儲存每張表的字段緩存,下次操作的時候直接用,不用再一次反射(但是儅退出應用時,這些緩存會清空)
if (name != null && field != null) {
fieldHashMap.put(name, field);
}
}
cashMap.put(tableName, fieldHashMap);
stringBuffer = stringBuffer.deleteCharAt(stringBuffer.length() - );
stringBuffer.append(")");
return stringBuffer.toString();
}
- 另建立一個對象池儲存每張表的字段
- 反射得到每個字段名和對應的類型,用對應的sqlite語句拼接起來
- 把字段名和對應字段儲存到對象池裡
二.封裝增删改查
我們上文有提到,想要封裝資料庫架構就要反射拿到每張表的字段和值
①那麼該如何反射拿到每張表的字段和值呢
我們在初始化建表的時候不是有把每個字段都存在一個對象池裡嗎,那我們就可以從裡面去拿,然後在通過實體類的位元組碼反射得到字段的值,不多說,先上代碼。
/**
* 拿到實體類的字段名和值
*
* @param tClass
* @return
*/
private Map<String, String> getMap(T tClass) {
HashMap<String, String> map = new HashMap<>();
//拿到每一個字段
HashMap<String, Field> hashMap = cashMap.get(tableName);
Iterator<Field> iterator = hashMap.values().iterator();
while (iterator.hasNext()) {
Field field = iterator.next();
field.setAccessible(true);
try {
//拿到字段的值
Object o = field.get(tClass);
if (o == null) {
continue;
}
String value = o.toString();
//字段名
String fieldName = null;
if (field.getAnnotation(DbField.class) != null) {
fieldName = field.getAnnotation(DbField.class).value();
} else {
fieldName = field.getName();
}
if (!TextUtils.isEmpty(fieldName) && !TextUtils.isEmpty(value)) {
map.put(fieldName, value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
- 通過緩存對象池cashMap拿到這張表的字段名和字段
- 通過每個字段拿到對應的值Object o = field.get(tClass);
- 把值儲存到map裡
②封裝contentValues
把第一步拿到的鍵值對一個個放進去
/**
* 根據map拼接contentValues
*
* @param map
* @return
*/
private ContentValues getValue(Map<String, String> map) {
ContentValues contentValues = new ContentValues();
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String next = iterator.next();
String s = map.get(next);
contentValues.put(next, s);
}
return contentValues;
}
③拼接查詢語句
上面兩個方法我們隻拿到了contentValue對象,但是原生api裡更多的是要求我們傳入條件語句,像
public int delete(String table, String whereClause, String[]
whereArgs) {
throw new RuntimeException(“Stub!”);
}
和
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) {
throw new RuntimeException(“Stub!”);
}
傳入的whereClause和,selection和數組whereArgs,selectionArgs,這些條件語句其實時這樣的:whereClause:”id=? and name=?”,whereArgs:new String[]{“1”,”laicp”},一個問号按順序對應數組裡的元素。而這些參數又需要我們去拼接,就像初始化建表那樣,但是我們這裡建立一個類去構造條件語句,上代碼:
public class Condition {
//"id=? and name=?" new String[]{"1","laicp"}
//"id=? and name=?"
public String whereClause;
//new String[]{"1","laicp"}
public String[] whereArgs;
public Condition(T where) {
ArrayList list = new ArrayList<>();
Map<String, String> map = getMap(where);
Iterator<String> iterator = map.keySet().iterator();
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("1=1");
while (iterator.hasNext()) {
String key = iterator.next();
String value = map.get(key);
list.add(value);
//拼接
stringBuffer.append(" and " + key + "=?");
}
whereClause = stringBuffer.toString();
//轉成數組
whereArgs = (String[]) list.toArray(new String[list.size()]);
}
}
④使用封裝的函數
上面三步已經把該做的都做了,下面使用起來就非常友善了
可以先定義一個接口IBaseDao
public interface IBaseDao<T> {
//增
long insert(T entity);
//删
void delete(T entity);
//改
void update(T entity,T where);
//查詢
List<T> query(T entity);
}
去實作它
@Override
public long insert(T entity) {
Map<String, String> map = getMap(entity);
ContentValues contentValues = getValue(map);
long insert = database.insert(tableName, null, contentValues);
return insert;
}
@Override
public void delete(T entity) {
Condition condition = new Condition(entity);
database.delete(tableName, condition.whereClause, condition.whereArgs);
}
@Override
public void update(T entity, T where) {//entity :要改成的内容 where : 在什麼條件上改
ContentValues contentValues = getValue(getMap(entity));
Condition condition = new Condition(where);
database.update(tableName, contentValues, condition.whereClause, condition.whereArgs);
}
@Override
public List<T> query(T entity) {
Condition condition = new Condition(entity);
//拿到查找實體(每條記錄)的遊标
Cursor cursor = database.query(tableName, null, condition.whereClause, condition.whereArgs,
null,
null,
null,
null);
ArrayList list = new ArrayList<>();
HashMap<String, Field> fieldHashMap = cashMap.get(tableName);
while (cursor.moveToNext()) {
Object item = null;
try {
item = entity.getClass().newInstance();
Iterator<String> iterator = fieldHashMap.keySet().iterator();
//給每個item設置成員變量,幷賦值
while (iterator.hasNext()) {
try {
//字段名
String key = iterator.next();
//字段
Field field = fieldHashMap.get(key);
Class type = field.getType();
//字段賦值并存到item裏
if (type == String.class) {
field.set(item, cursor.getString(cursor.getColumnIndex(key)));
} else if (type == Integer.class) {
field.set(item, cursor.getInt(cursor.getColumnIndex(key)));
} else if (type == Long.class) {
field.set(item, cursor.getLong(cursor.getColumnIndex(key)));
} else if (type == Double.class) {
field.set(item, cursor.getDouble(cursor.getColumnIndex(key)));
} else if (type == byte[].class) {
field.set(item, cursor.getBlob(cursor.getColumnIndex(key)));
} else {
continue;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
list.add(item);
}
return list;
}
“增”,“删”,和“改”會簡單一些,該用到的方法,上文已經封裝好了,查詢會比較複雜一點:
1. 先拿到遊标
2. 加兩成循環,外層循環去查找符合的類,記憶體循環,插入一個類的所有字段
Object item = entity.getClass().newInstance();
field.set(item, cursor.getString(cursor.getColumnIndex(key))
cursor.getColumnIndex():拿到字段的的下标,
cursor.getString():拿到對應的值
3. 把item裝到容器裡傳回
三.生産dao對象
定義一個工廠去生産IBasedao的實作類對象,後序的擴充非常友善
package com.example.lcp.database.BaseDao;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import java.io.File;
public class BaseDaoFactory2 {
private static final BaseDaoFactory2 instance = new BaseDaoFactory2();
private static SQLiteDatabase sqLiteDatabase;
public static BaseDaoFactory2 getInstance() {
return instance;
}
public BaseDaoFactory2() {
String sqlPath = "";
//建議存儲在sd裡,app解除安裝,下次安裝的時候檔案還在
if (existSDCard()) {
//getPath()和getAbsolutePath()的路徑是相同的
sqlPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/com.example.lcp.database/databases/";
} else {
//data/data/包名/databases/資料庫名稱.db (固定格式)
sqlPath = "data/data/com.example.lcp.database/databases/";
}
//如果檔案夾不存在就去建立檔案夾,如果不建立,報資料庫無法打開異常
File file = new File(sqlPath);
if (!file.exists()) {
file.mkdirs();
}
//打開或建立 (沒有則建立,有則打開)
sqLiteDatabase = SQLiteDatabase.openOrCreateDatabase(sqlPath + "lcp.db", null);
}
/**
* 是否存在sd卡
*
* @return
*/
private boolean existSDCard() {
if (android.os.Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return true;
} else {
return false;
}
}
public <T extends BaseDao2<M>, M> T getBaseDao2(Class<T> tClass, Class<M> mClass) {
try {
BaseDao2<M> baseDao2 = tClass.newInstance();
baseDao2.init(sqLiteDatabase, mClass);
return (T) baseDao2;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
-
在構造器裡把資料庫建立出來
注意:當你路徑不存在的時候去調用SQLiteDatabase.openOrCreateDatabase()會出現資料庫無法打開的異常,是以在此之前必須先判斷檔案夾是否存在,不存在的情況下去建立檔案夾
建議:最好把資料庫寫在sd裡,當使用者解除安裝app,下次安裝的時候db檔案還在
-
定義getBasedao方法,定義傳進來的T 必須是BaseDao的子類,即IBaseDao實作類的子類,(傳進來mClass為實體類位元組碼)
注意:當你調用newInstance時,BaseDao裡面的靜态變量不會重新初始化,會儲存剛剛存儲的資訊
三.架構的使用
我布局我就不貼了,就是幾個按鈕,直接貼代碼
package com.example.lcp.database;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void createDb(View view) {
BaseDaoImp baseDaoImp = BaseDaoFactory2.getInstance().getBaseDao2(BaseDaoImp.class, User.class);
baseDaoImp.insert(new User(L,"lcp","男"));
Toast.makeText(this,"執行成功",Toast.LENGTH_SHORT).show();
}
public void delete(View view) {
BaseDaoImp baseDaoImp = BaseDaoFactory2.getInstance().getBaseDao2(BaseDaoImp.class, User.class);
User user = new User();
user.setName("lcp");
//删除姓名為lcp的記錄
baseDaoImp.delete(user);
Toast.makeText(this, "執行成功", Toast.LENGTH_SHORT).show();
}
public void update(View view) {
BaseDaoImp baseDaoImp = BaseDaoFactory2.getInstance().getBaseDao2(BaseDaoImp.class, User.class);
User user = new User();
user.setName("lcp");
User user1 = new User();
user1.setId(L);
//把所有id為1的name改為“lcp”
baseDaoImp.update(user,user1);
Toast.makeText(this, "執行成功", Toast.LENGTH_SHORT).show();
}
public void query(View view) {
BaseDaoImp baseDaoImp = BaseDaoFactory2.getInstance().getBaseDao2(BaseDaoImp.class, User.class);
User user = new User();
user.setId(L);
//查詢id為1的所有記錄
List<User> list = baseDaoImp.query(user);
for (int i=;i<list.size();i++) {
User user1 = list.get(i);
String s = user1.toString();
Log.e("query",i+" == "+s);
}
Toast.makeText(this, "執行成功", Toast.LENGTH_SHORT).show();
}
}
-
删,更新,查詢要注意下,傳進去的對象既是條件,
例如:User user=new User(),user.setName(“lcp”)
User user1=new User(),user1.setId(1L)
dao.update(user,user1)
的意思為把id=1的所有記錄裡的姓名改為“lcp”
總結
以上就是所有内容,重點在于通過反射拿到表裡的字段和值,然後進行拼接,底層還是調用原生api哈,這樣寫擴充性是不是比較強呢,有哪些寫不好的地方還望大牛們舉薦。