前言#
经过对基础知识的学习和积累,终于到了最后的实战,自定义数据库框架。框架的使用方法是参考一些流行的数据库框架,例如Litepal。
正文#
首先,我们来梳理一下我们这个框架的流程图:
首先从数据库的初始化开始,我们先定义xml的解析格式:
Test
1
- com.lzp.sqlframedemo.bean.Student
里面包含了数据库的名称,版本号,和要建表的类的路径。
在SQLFrame框架中完成xml的解析,并完成数据库的创建。
/**
* 初始化数据库
*/
public static void initSQLite(Context context) throws SQLiteInitException {
// 解析xml文件,获取数据库相关配置(名称,版本号,表)
String databaseName = null;
String version = null;
ArrayList tables = new ArrayList<>();
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
// 读取指定的xml
parser.setInput(context.getResources().getAssets().open("SQLFrame.xml"), "utf-8");
// 在assets中读取xml 只能用open方法,否则会出现FileNotFound异常
// parser = context.getResources().getAssets().openXmlResourceParser("SQLFrame.xml");
int eventType = parser.getEventType();
// 如果解析的表示不是文件的结束标签,循环读取这些标签
while (eventType != XmlPullParser.END_DOCUMENT) {
// 解析标签的名称
String nodeName = parser.getName();
switch (eventType) {
// 开始标签
case XmlPullParser.START_TAG:
// 数据库名称
if (nodeName.equals("name")) {
databaseName = parser.nextText();
}
// 数据库版本号
else if (nodeName.equals("version")) {
version = parser.nextText();
}
// 数据库的表
else if (nodeName.equals("item")) {
tables.add(parser.nextText());
}
break;
// 结束标签
case XmlPullParser.END_TAG:
break;
default:
break;
}
// 解析下一个标签
eventType = parser.next();
}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
// 抛出数据库初始化异常
throw new SQLiteInitException("please check the SQLFrame.xml has defined right!!!");
}
// 检查是否设置了数据库名称和版本号,否则抛出数据库初始化异常
if (databaseName == null || version == null) {
throw new SQLiteInitException("please check the SQLFrame.xml has defined right!!!");
}
// 初始化数据库
SQLFrameOpenHelper sqlFrameOpenHelper = new SQLFrameOpenHelper(context, databaseName, null, Integer.parseInt(version));
sqLiteDatabase = sqlFrameOpenHelper.getWritableDatabase();
// 循环建表
for (String className : tables) {
try {
createTable(Class.forName(className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
通过反射创建表:
/**
* 创建table
*
* @param clazz 通过参数类的注解,来创建指定的table
*/
private static void createTable(Class> clazz) {
try {
checkSqlInit();
StringBuilder sql = new StringBuilder("create table if not exists ");
// 先拿到table的名称
Table table = clazz.getAnnotation(Table.class);
sql.append(table.value());
sql.append("(");
// 获取所有要创建的table字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
sql.append(column.field());
sql.append(" ");
sql.append(column.type());
sql.append(",");
}
}
// 把最后的逗号去掉
sql.deleteCharAt(sql.length() - 1);
sql.append(")");
sqLiteDatabase.execSQL(sql.toString());
} catch (SQLiteNotInitException e) {
e.printStackTrace();
}
}
通过反射获取@Table,得到表的名称,再反射得到Class的被注解的属性,得到表要创建的字段和字段的属性。
里面的注释已经非常详细了,没有什么可说的了。
初始化操作一般都是建议在Application中的,所以自定义SQLFrameApplication:
package com.lzp.sqlframe.application;
import android.app.Application;
import com.lzp.sqlframe.sqllite.SQLUtil;
import com.lzp.sqlframe.sqllite.SQLiteInitException;
/**
* Created by li.zhipeng on 2017/3/15.
*
* 自定义Application
*/
public class SQLFrameApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
try {
SQLUtil.initSQLite(this);
} catch (SQLiteInitException e) {
e.printStackTrace();
}
}
}
下一步就是自定义注解,并定义一个基类,这个基类有继承的增删改查的方法,实现对某个类的对象的操作。
ok,先定义两个注解,@Table和@Column。
package com.lzp.sqlframe.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by li.zhipeng on 2017/3/10.
*
* 数据库表注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value(); // 不给默认值,如果不是设置默认是Bean的类名
}
package com.lzp.sqlframe.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by li.zhipeng on 2017/3/10.
*
* 数据库表字段注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
/**
* 字段名称
* */
String field();
/**
* 字段类型
* */
String type();
}
注解已经写好了,接下来就是想办法实现刚才说的基类了,我们命名为SQLFrameBaseBean,因为仅仅是参考学习,所以我这里就只写了保存(save)方法,其他方法(修改,删除)的作为练习,留给大家。看一下代码:
package com.lzp.sqlframe.sqllite;
import com.lzp.sqlframe.annotation.Column;
import com.lzp.sqlframe.annotation.Table;
import java.lang.reflect.Field;
import java.util.ArrayList;
import static com.lzp.sqlframe.sqllite.SQLUtil.checkSqlInit;
/**
* Created by li.zhipeng on 2017/3/13.
*
* 要保存到数据库中的bean的基类
*/
public class SQLFrameBaseBean {
/**
* 保存一个对象
*/
public void save() {
try {
checkSqlInit();
StringBuilder sql = new StringBuilder("insert into ");
// 先拿到table的名称
Table table = getClass().getAnnotation(Table.class);
sql.append(table.value());
sql.append("(");
// 获取class中所有的属性
Field[] fields = getClass().getDeclaredFields();
StringBuilder fieldStr = new StringBuilder();
StringBuilder valueStr = new StringBuilder();
ArrayList
还是通过反射得到表名和注解的数据库字段,然后进行sql操作,这样这个对象就可以调用自身的save方法,保存自己了。
现在还剩最后一项了,定义selectAll方法查询一个表的所有数据,换汤不换药,跟之前反射用法没什么区别:
/**
* 查询表中所有的信息
*/
public static List selectAll(Class clazz) {
Cursor cursor = null;
try {
// 检查是否已经初始化
checkSqlInit();
// 查询sql语句
StringBuilder sql = new StringBuilder("select * from ");
// 先拿到table的名称
Table table = clazz.getAnnotation(Table.class);
sql.append(table.value());
cursor = SQLUtil.getInstance().rawQuery(sql.toString(), null);
List result = new ArrayList<>();
// 循环得到cursor查询的数据
if (cursor != null) {
// 获取所有要创建的table字段
Field[] fields = clazz.getDeclaredFields();
while (cursor.moveToNext()) {
// 通过反射创建一个指定类型的对象
T t = clazz.newInstance();
for (Field field : fields) {
if (field != null) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
// 私有属性一定要设置此方法,否则无操作这个属性的权限
field.setAccessible(true);
// 通过反射,对对象的属性赋值
switch (column.type()) {
case "varchar":
field.set(t, cursor.getString(cursor.getColumnIndex(column.field())));
break;
case "Integer":
field.set(t, cursor.getInt(cursor.getColumnIndex(column.field())));
break;
}
}
}
}
result.add(t);
}
}
return result;
}
// 推荐的捕获异常的catch块写法,看到黄色警告就让我觉得浑身难受
catch (InstantiationException | IllegalAccessException | SQLiteNotInitException e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
ok,所有的工作都已经大功告成,我们来测试一下,看一下新的Module的结构:
首先别忘记了在assets中创建SQLFrame.xml,xml格式跟上面的完全一样。
看一下Student的代码:
package com.lzp.sqlframedemo.bean;
import com.lzp.sqlframe.annotation.Column;
import com.lzp.sqlframe.annotation.Table;
import com.lzp.sqlframe.sqllite.SQLFrameBaseBean;
/**
* Created by li.zhipeng on 2017/3/10.
*
* 学生信息类
*/
@Table("Student")
public class Student extends SQLFrameBaseBean{
/**
* 反射创建对象的时候,需要使用
* */
public Student(){}
/**
* 创建一个构造方法
*/
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Column(field = "student_id", type = "varchar")
private String id;
@Column(field = "student_name", type = "varchar")
private String name;
@Column(field = "student_age", type = "Integer")
private int age;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
MainActivity的代码:
package com.lzp.sqlframedemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import com.lzp.sqlframe.sqllite.SQLUtil;
import com.lzp.sqlframedemo.bean.Student;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.content);
// 查询数据库
refreshView();
// 点击保存一条新的Student数据,并重新查询刷新界面
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Student("001", "lisi", 18).save();
refreshView();
}
});
}
/**
* 查询数据库,刷新界面
* */
private void refreshView(){
StringBuilder sb = new StringBuilder();
List list = SQLUtil.selectAll(Student.class);
// 把所有的类的属性进行拼接
if(list != null && list.size() > 0){
for (Student student : list){
sb.append(student.getId()).append(",").append(student.getName()).append(",").append(student.getAge()).append(";");
}
textView.setText(sb.toString());
}
}
}
这就结束了吗??仔细想一想,对了我们的application还没设置呢:
如果需要自定义application,可以继承SQLFrameApplication,或者在onCreate中手动调用 SQLUtil.initSQLite(this);
看一下运行效果:
点击了按钮之后,下面的TextView就会增加一条Student的值,测试通过!!!
总结#
紧张的实战终于结束了,我们把之前学到的东西都使用在demo中,一个屌爆了的数据库框架的雏形已经显现出来,但是还远远达不到能够商用的水平,例如还没有完成数据库升级,没有修改和删除的操作,缺少了很多的代码健壮性判断等等,这些都需要一个长期积累的过程,但是我们掌握了核心的数据库操作部分,已经是巨大的收获。
最初提到的Litepal 是郭霖大神的杰作,已经维护了三年,功能非常的稳定而且效率也非常的高,这次的实战也是模仿Litepal的设计思路,致敬前辈们的奉献精神。他有个人微信公众号和博客,还出了书,大家都可以去学习。
源码已上传,有兴趣的朋友可以自己练习修改。
郭霖大神的博客链接:http://blog.csdn.net/guolin_blog
Litepal在github的地址链接:https://github.com/LitePalFramework/LitePal
源码下载链接,里面有之前的练习代码,不需要的请直接忽视