一.简介
上次封装了一个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哈,这样写扩展性是不是比较强呢,有哪些写不好的地方还望大牛们举荐。