实现基于注解(Annotation)的数据库框架(五)最终的实战

前言#

经过对基础知识的学习和积累,终于到了最后的实战,自定义数据库框架。框架的使用方法是参考一些流行的数据库框架,例如Litepal。

正文#

首先,我们来梳理一下我们这个框架的流程图:

实现基于注解(Annotation)的数据库框架(五)最终的实战_第1张图片
这里写图片描述

首先从数据库的初始化开始,我们先定义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 values = new ArrayList<>(); for (Field field : fields) { // 对这个属性设置方法权限,否则无法访问私有成员变量,出现异常 field.setAccessible(true); Column column = field.getAnnotation(Column.class); // 判断这个属性是否被注解标记了 if (column != null) { String f = column.field(); fieldStr.append(f); fieldStr.append(","); values.add(String.valueOf(field.get(this))); valueStr.append("?,"); } } // 把最后的逗号去掉 fieldStr.deleteCharAt(fieldStr.length() - 1); valueStr.deleteCharAt(valueStr.length() - 1); sql.append(fieldStr); sql.append(") values ("); sql.append(valueStr); sql.append(")"); SQLUtil.getInstance().execSQL(sql.toString(), values.toArray()); } catch (SQLiteNotInitException | IllegalAccessException e) { e.printStackTrace(); } } }

还是通过反射得到表名和注解的数据库字段,然后进行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的结构:

实现基于注解(Annotation)的数据库框架(五)最终的实战_第2张图片
这里写图片描述

首先别忘记了在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);

看一下运行效果:

实现基于注解(Annotation)的数据库框架(五)最终的实战_第3张图片
这里写图片描述

点击了按钮之后,下面的TextView就会增加一条Student的值,测试通过!!!

总结#

紧张的实战终于结束了,我们把之前学到的东西都使用在demo中,一个屌爆了的数据库框架的雏形已经显现出来,但是还远远达不到能够商用的水平,例如还没有完成数据库升级,没有修改和删除的操作,缺少了很多的代码健壮性判断等等,这些都需要一个长期积累的过程,但是我们掌握了核心的数据库操作部分,已经是巨大的收获。

最初提到的Litepal 是郭霖大神的杰作,已经维护了三年,功能非常的稳定而且效率也非常的高,这次的实战也是模仿Litepal的设计思路,致敬前辈们的奉献精神。他有个人微信公众号和博客,还出了书,大家都可以去学习。

源码已上传,有兴趣的朋友可以自己练习修改。

郭霖大神的博客链接:http://blog.csdn.net/guolin_blog

Litepal在github的地址链接:https://github.com/LitePalFramework/LitePal

源码下载链接,里面有之前的练习代码,不需要的请直接忽视

你可能感兴趣的:(实现基于注解(Annotation)的数据库框架(五)最终的实战)