天天看点

面向对象式数据库框架的手写

一.简介

上次封装了一个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();
}
           
  1. ElementType常用的就是以下三个

    ElementType.TYPE写在类名上

    ElementType.FIELD写在成员变量上

    ElementType.METHOD写在方法上

  2. 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);
        }
    }
           
  1. 通过反射拿到表名
  2. 当初始化了就不在执行建表操作
  3. 把已经初始化的表存到对象池里
/**
     * 创建表
     *
     * @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();
    }
           
  1. 另创建一个对象池保存每张表的字段
  2. 反射得到每个字段名和对应的类型,用对应的sqlite语句拼接起来
  3. 把字段名和对应字段保存到对象池里

二.封装增删改查

我们上文有提到,想要封装数据库框架就要反射拿到每张表的字段和值

①那么该如何反射拿到每张表的字段和值呢

我们在初始化建表的时候不是有把每个字段都存在一个对象池里吗,那我们就可以从里面去拿,然后在通过实体类的字节码反射得到字段的值,不多说,先上代码。

/**
     * 拿到实体类的字段名和值
     *
     * @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;
    }
           
  1. 通过缓存对象池cashMap拿到这张表的字段名和字段
  2. 通过每个字段拿到对应的值Object o = field.get(tClass);
  3. 把值保存到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;
    }
}
           
  1. 在构造器里把数据库创建出来

    注意:当你路径不存在的时候去调用SQLiteDatabase.openOrCreateDatabase()会出现数据库无法打开的异常,所以在此之前必须先判断文件夹是否存在,不存在的情况下去创建文件夹

    建议:最好把数据库写在sd里,当用户卸载app,下次安装的时候db文件还在

  2. 定义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哈,这样写扩展性是不是比较强呢,有哪些写不好的地方还望大牛们举荐。