手?jǐn)]一個(gè)簡(jiǎn)易Android數(shù)據(jù)庫(kù)框架

一、簡(jiǎn)述

眾所周知,移動(dòng)端(不管是Android還是iOS)使用的數(shù)據(jù)庫(kù)是Sqlite,這種小型的數(shù)據(jù)庫(kù)很適合移動(dòng)端存儲(chǔ)大量的數(shù)據(jù),使用上也跟mysql基本無(wú)差,但官方提供的API在操作性方面真不咋的,你必須掌握一定程度的sql語(yǔ)句,否則將很難駕馭。所以,有很多第三方的數(shù)據(jù)庫(kù)框架就開(kāi)始流行,如:GreenDao、Litepal等。這些ORM數(shù)據(jù)庫(kù)框架,可以幫助開(kāi)發(fā)者節(jié)省大量編寫(xiě)數(shù)據(jù)庫(kù)操作代碼的時(shí)間,只需對(duì)對(duì)象進(jìn)行賦值操作,便能作用到數(shù)據(jù)庫(kù)上,方便我們開(kāi)發(fā)更加復(fù)雜的業(yè)務(wù)邏輯。本篇的主題就是做一個(gè)簡(jiǎn)易的數(shù)據(jù)庫(kù)框架,使用設(shè)計(jì)模式、泛型、注解、反射這些高級(jí)技巧來(lái)實(shí)現(xiàn)。

二、數(shù)據(jù)庫(kù)常用操作

數(shù)據(jù)庫(kù)操作無(wú)非就是增刪改查(CRUD),而且一般操作數(shù)據(jù)庫(kù)表的類稱為Dao類,所以可以為這些Dao類抽取一個(gè)共同的接口:

public interface IBaseDao<M> {

    Long insert(M entity);

    Integer delete(M where);

    Integer update(M entitiy, M where);

    List<M> query(M where);

    List<M> query(M where, String orderBy);

    List<M> query(M where, String orderBy, Integer page, Integer pageCount);

}

我們要做的數(shù)據(jù)庫(kù)框架也是一個(gè)ORM框架,表現(xiàn)層不涉及任何sql語(yǔ)句,直接操作的是數(shù)據(jù)對(duì)象,但具體的數(shù)據(jù)類型在這個(gè)接口中并不清楚,所以使用泛型來(lái)表示。

三、Dao類工廠

一個(gè)程序,一般只有一個(gè)數(shù)據(jù)庫(kù),一個(gè)數(shù)據(jù)庫(kù)中會(huì)包含多張表,例如用戶表,權(quán)限表等,這就意味著,項(xiàng)目中,Dao類會(huì)有多個(gè),因?yàn)閿?shù)據(jù)庫(kù)操作無(wú)非是CRUD,所以可以確定它們的結(jié)構(gòu)相同,只是具體操作的表與字段不同(即數(shù)據(jù)類型不同),所以,使用“泛型+工廠”來(lái)生產(chǎn)這些Dao類最合適不過(guò)。下面先貼出Dao類工廠代碼,再一一分析:

public class BaseDaoFactory {

    private static String mDbPath;
    private SQLiteDatabase mDatabase;

    private static class Instance {
        public static BaseDaoFactory INSTANCE = new BaseDaoFactory();
    }

    public static BaseDaoFactory getInstance() {
        return Instance.INSTANCE;
    }

    // 初始化數(shù)據(jù)庫(kù)位置
    public static void init(String dbPath) {
        mDbPath = dbPath;
    }

    public BaseDaoFactory() {
        if (TextUtils.isEmpty(mDbPath)) {
            throw new RuntimeException("在使用BaseDaoFactory之前,請(qǐng)調(diào)用BaseDaoFactory.init()初始化好數(shù)據(jù)庫(kù)路徑。");
        }
        // 打開(kāi)數(shù)據(jù)庫(kù),得到數(shù)據(jù)庫(kù)對(duì)象
        mDatabase = SQLiteDatabase.openOrCreateDatabase(mDbPath, null);
    }

    public <T extends BaseDao<M>, M> T getDataHelper(Class<T> clazz, Class<M> entity) {
        T baseDao = null;
        try {
            baseDao = clazz.newInstance();
            baseDao.init(mDatabase, entity);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return baseDao;
    }
}

1、數(shù)據(jù)庫(kù)位置

使用SQLiteDatabase,可以在任意位置創(chuàng)建或打開(kāi)一個(gè)數(shù)據(jù)庫(kù),這樣的好處是:如果,用SQLiteOpenHelper來(lái)創(chuàng)建數(shù)據(jù)庫(kù),默認(rèn)會(huì)將數(shù)據(jù)庫(kù)文件創(chuàng)建到/data/data/包名/databases目錄下,當(dāng)應(yīng)用被刪除時(shí),數(shù)據(jù)庫(kù)也將同應(yīng)用一起被刪除,有時(shí)我們會(huì)有這樣的需求,在用戶重裝安裝App時(shí),可以使用之前的數(shù)據(jù)庫(kù)信息,例如:UC瀏覽器的書(shū)簽本地恢復(fù)功能。

// 初始化數(shù)據(jù)庫(kù)位置
public static void init(String dbPath) {
    mDbPath = dbPath;
}

public BaseDaoFactory() {
    if (TextUtils.isEmpty(mDbPath)) {
        throw new RuntimeException("在使用BaseDaoFactory之前,請(qǐng)調(diào)用BaseDaoFactory.init()初始化好數(shù)據(jù)庫(kù)路徑。");
    }
    // 打開(kāi)數(shù)據(jù)庫(kù),得到數(shù)據(jù)庫(kù)對(duì)象
    mDatabase = SQLiteDatabase.openOrCreateDatabase(mDbPath, null);
}

本框架可以讓開(kāi)發(fā)者自定義數(shù)據(jù)庫(kù)的存放位置,因?yàn)樵跇?gòu)造函數(shù)中會(huì)使用到該路徑對(duì)數(shù)據(jù)庫(kù)進(jìn)行創(chuàng)建,所以這里使用靜態(tài)方法的方式,在Dao工廠實(shí)例化之前先對(duì)其(mDbPath)進(jìn)行賦值。這種方式在很多第三方框架的源碼中很是常見(jiàn),一般在自定義的Application中對(duì)通過(guò)調(diào)用框架的init()方法對(duì)框架中必需的變量進(jìn)行賦值。

2、工廠單例

該框架中的Dao類工廠只要一個(gè)就夠了,所以需要用到單例模式,常見(jiàn)的單例模式有餓漢式和懶漢式,這里選用靜態(tài)內(nèi)部類單例模式,原因是什么呢?

1)餓漢式

餓漢式在類加載時(shí)就已經(jīng)初始化好了,不管項(xiàng)目中是否使用,都會(huì)占用內(nèi)存,雖然效率高,但開(kāi)發(fā)中一般不用。

public class BaseDaoFactory {
    private static BaseDaoFactory Instance = new BaseDaoFactory();
    public static BaseDaoFactory getInstance() {
        return Instance;
    }
}

2)懶漢式

懶漢式雖然解決了餓漢式的弊端,實(shí)現(xiàn)調(diào)用時(shí)創(chuàng)建單例,但線程不安全,當(dāng)然我們可以使用雙重檢測(cè)機(jī)制來(lái)解決,但這樣也降低了效率(至少第一次初始化時(shí)需要同步,降低了效率),是開(kāi)發(fā)中最常見(jiàn)的單例實(shí)現(xiàn)方式。

public class BaseDaoFactory {
    private static BaseDaoFactory mInstance;
    public static BaseDaoFactory getInstance() {
        if (mInstance == null) {
            synchronized (BaseDaoFactory.class) {
                if (mInstance == null) {
                    mInstance = new BaseDaoFactory();
                }
            }
        }
        return mInstance;
    }
}

3)靜態(tài)內(nèi)部類單例

靜態(tài)內(nèi)部類單例綜合了前面兩者的優(yōu)點(diǎn),即調(diào)用時(shí)創(chuàng)建單例,效率高且沒(méi)有線程安全問(wèn)題。當(dāng)在調(diào)用getInstance()方法創(chuàng)建工廠單例時(shí),靜態(tài)內(nèi)部類Instance才會(huì)被加載,同時(shí)初始化內(nèi)部類屬性INSTANCE,即初始化外部類BaseDaoFactory對(duì)象(Dao工廠),因?yàn)樵撿o態(tài)內(nèi)部類只會(huì)加載一次,所以該INSTANCE對(duì)象也只會(huì)被創(chuàng)建一次。

private static class Instance {
    public static BaseDaoFactory INSTANCE = new BaseDaoFactory();
}

public static BaseDaoFactory getInstance() {
    return Instance.INSTANCE;
}

// 單例模式,一般會(huì)將構(gòu)造函數(shù)私有化,以保證不會(huì)被外界初始化。
private BaseDaoFactory() {
   ...
}

3、生產(chǎn)Dao類

Dao工廠會(huì)提供一個(gè)public方法來(lái)供外界獲取需要的Dao類,而外界只需要傳入具體Dao類和數(shù)據(jù)實(shí)體類對(duì)應(yīng)的class即可。

public <T extends BaseDao<M>, M> T getDataHelper(Class<T> clazz, Class<M> entity) {
    T baseDao = null;
    try {
        baseDao = clazz.newInstance();
        baseDao.init(mDatabase, entity);
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return baseDao;
}

可能這部分代碼比較迷的地方就是<T extends BaseDao<M>, M>了(先拋開(kāi)方法體中的具體實(shí)現(xiàn)),其實(shí)這只是方法聲明泛型的方式而已。我們知道,類上聲明泛型很簡(jiǎn)單,只需要在類后面加上<T>(字母隨意)即可,如:

// 類上泛型可以聲明多個(gè)
public class Person<M,T> {
    ...
}

而方法上聲明泛型跟類上聲明泛型有些區(qū)別——泛型聲明的位置不同,方法上的泛型需要在修飾符(public等)與返回值之間聲明,如:

public <T> void eat(){
    ...
}

上面的代碼不會(huì)報(bào)錯(cuò),但一點(diǎn)意義都沒(méi)有,因?yàn)榉盒蜎](méi)被使用到,一般方法上聲明的泛型可作為方法參數(shù)類型和返回值類型,如:

// 同樣,方法泛型也可以聲明多個(gè)
public <T,M> T eat(T t, M m){
    ...
}

可以這樣認(rèn)為,默認(rèn)泛型表示的是Object(編碼時(shí)),有時(shí)我們需要更加精確泛型的類型,這可以通過(guò)extends辦到,如:

public <T extends Person> void doSomething(T t){
    ...
}

這時(shí),傳入doSomething()的參數(shù)必須是Person的子類(Boy或Girl),且在編碼時(shí),可以在方法體中使用t調(diào)用Person中聲明的方法,如eat(),如果不使用extends來(lái)指定泛型T的具體類型,那么在編碼時(shí),t會(huì)被認(rèn)為是Object類型,也就沒(méi)法調(diào)用eat()這類自定義的方法了。到這里,回頭再看上面的代碼,應(yīng)該就不會(huì)迷了。

四、數(shù)據(jù)庫(kù)操作封裝

前面一開(kāi)始的時(shí)候就已經(jīng)為Dao類抽取了一個(gè)共同的接口,規(guī)范了Dao類的基本操作,而且,我們不想在具體的Dao類中直接操作數(shù)據(jù)庫(kù),所以,在這兩者中間必須有一層來(lái)完成數(shù)據(jù)庫(kù)操作,并對(duì)各操作方法進(jìn)行封裝,它就是BaseDao,以下是該類中可供外界調(diào)用的方法:

public abstract class BaseDao<M> implements IBaseDao<M> {
    protected boolean init(SQLiteDatabase database, Class<M> entity) {
        ...
    }
    @Override
    public Long insert(M entity) {
        ...
    }
    @Override
    public Integer delete(M where) {
        ...
    }

    @Override
    public Integer update(M entitiy, M where) {
        ...
    }

    @Override
    public List<M> query(M where) {
        ...
    }

    @Override
    public List<M> query(M where, String orderBy) {
        ...
    }

    @Override
    public List<M> query(M where, String orderBy, Integer page, Integer pageCount) {
        ...
    }
}

在框架設(shè)計(jì)中,要切記,不需要被外界(表現(xiàn)層)所知的方法或?qū)傩哉?qǐng)盡量私有化,一來(lái)對(duì)框架安全,二來(lái)避免團(tuán)隊(duì)開(kāi)發(fā)中不必要的沖突。因?yàn)锽aseDao的init()方法只被DaoFactory調(diào)用,且兩者均在同包下,故使用protected修飾。

1、自定義注解:TbName和TbField

在對(duì)BaseDao進(jìn)行解析前,先來(lái)說(shuō)說(shuō)兩個(gè)十分重要的注解——TbName和TbField。幾乎市面上所有的ORM數(shù)據(jù)庫(kù)框架,都會(huì)用到自定義注解來(lái)對(duì)一個(gè)數(shù)據(jù)實(shí)體進(jìn)行描述,比如User類的類名對(duì)應(yīng)表的表名,有可能是user,也有可能是t_user,那開(kāi)發(fā)者就可以使用框架提供的注解(TbName)來(lái)進(jìn)行自定義表名了,同理,類中的屬性名對(duì)應(yīng)表的字段名也是如此,不過(guò)對(duì)于表的初始化還需要知道表字段的長(zhǎng)度,所以表字段注解(TbField)還多了一個(gè)length屬性。

/**
 * 表名注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TbName {
    String value();
}

/**
 * 表字段注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TbField {
    String value();
    int length();
}

一方面,BaseDao對(duì)TbName和TbField的解析是處于程序運(yùn)行階段,所以這2個(gè)注解必須是運(yùn)行時(shí)可見(jiàn),即Retention的值必須是RetentionPolicy.RUNTIME。
另一方面,TbName是要注解在類上的,所以其Target的值是ElementType.TYPE;而TbField則是注解在類屬性上的,所以其Target的值是ElementType.FIELD。

2、Dao類初始化

在Dao工廠中就已經(jīng)使用了這個(gè)BaseDao的init()方法,來(lái)對(duì)Dao類進(jìn)行一些通用的初始化工作,下面就來(lái)看看它都初始化了什么:

/**
 * 初始化表操作對(duì)象,一般包括:創(chuàng)建表、獲取表字段與類字段的映射關(guān)系
 */
protected boolean init(SQLiteDatabase database, Class<M> entity) {
    mDatabase = database;
    mEntityClass = entity;
    
    // 往后的操作必須是基于數(shù)據(jù)庫(kù)已經(jīng)打開(kāi)的前提下
    if (!database.isOpen()) {
        return false;
    }

    // 獲取表名
    TbName tbName = entity.getAnnotation(TbName.class);
    mTbName = tbName == null ? entity.getSimpleName() : tbName.value();

    // 獲取表映射字段
    if (!genFieldMap()) {
        return false;
    }

    // 創(chuàng)建數(shù)據(jù)庫(kù)
    if (!createTable(database)) {
        return false;
    }

    return true;
}

在這個(gè)init()方法中,可以看到,注解TbName率先被該框架使用到了,當(dāng)開(kāi)發(fā)者有使用該注解時(shí),表名以TbName注解中的值為表名,否則以類名作為表名。

注解是“靜態(tài)的”,在類加載時(shí)就已經(jīng)固定了,也就是說(shuō)運(yùn)行時(shí)無(wú)法修改其值(javassist方式除外),所以,可以利用類的class對(duì)象,通過(guò) getAnnotation(注解.class).注解屬性() 這種方法也獲取注解的屬性值。

1)獲取表字段與類字段的映射關(guān)系

我們知道,類的屬性名可能會(huì)與表的字段名不同,而B(niǎo)aseDao中的很多后續(xù)操作都會(huì)跟這兩者打交道,所以,在BaseDao的初始化過(guò)程中將這兩者的關(guān)系使用Map進(jìn)行保存,方便后續(xù)的各種操作。

private boolean genFieldMap() {
    mFieldMap = new HashMap<>();
    Field[] fields = mEntityClass.getFields();
    if (fields == null || fields.length == 0) {
        Log.e(TAG, "獲取不到類中字段");
        return false;
    }
    for (Field field : fields) {
        field.setAccessible(true);
        TbField tbField = field.getAnnotation(TbField.class);
        mFieldMap.put(tbField == null ? field.getName() : tbField.value(), field);
    }
    return true;
}    

反射中的幾個(gè)小知識(shí)點(diǎn):

  • Field[] fields = mEntityClass.getFields();// 得到類中的public字段,包括父類。
  • Field[] fields = mEntityClass.getDeclaredFields();// 得到類中聲明的字段(不管是public、protected、private),不包括父類。
  • field.setAccessible(true);// 將私有屬性或final屬性可以被訪問(wèn)

考慮到數(shù)據(jù)實(shí)體類可能會(huì)使用繼承的方式來(lái)拓展父類,即會(huì)用到父類的屬性值,所以使用getFields()方法,但代價(jià)就是實(shí)體類中的屬性必須是public的。

2)創(chuàng)建表

Dao類的初始化工作也包括了表的創(chuàng)建。一方面,因?yàn)椴荒茉诿看蝿?chuàng)建并初始化Dao類時(shí)都去重新創(chuàng)建一次表,所以這里就用到了sql語(yǔ)句中的 if not exists 關(guān)鍵字來(lái)避免重復(fù)創(chuàng)建表的問(wèn)題。另一方面,其實(shí)創(chuàng)建表的sql語(yǔ)句是一種模板,兩個(gè)不同的表在使用sql創(chuàng)建時(shí),無(wú)非就是表名、字段名、字段類型和字段長(zhǎng)度不同,而恰好,這些不同的元素可以使用反射+TbField注解來(lái)獲取,從而實(shí)現(xiàn)sql語(yǔ)句的動(dòng)態(tài)拼接,結(jié)合上一步得到的表字段與類字段的映射關(guān)系(mFieldMap),代碼可以這么寫(xiě):

/**
 * 創(chuàng)建表(可以被子類重寫(xiě),方便靈活擴(kuò)展)
 */
protected boolean createTable(SQLiteDatabase database) {
    StringBuilder sb = new StringBuilder();
    for (Map.Entry<String, Field> entry : mFieldMap.entrySet()) {
        String columnName = entry.getKey();
        Field field = entry.getValue();
        TbField tbField = field.getAnnotation(TbField.class);
        int length = tbField == null ? 255 : tbField.length();
        String type = "";
        Class<?> fieldType = field.getType();
        if (fieldType == String.class) {
            type = "varchar";
        } else if (fieldType == int.class || fieldType == Integer.class) {
            type = "int";
        } else if (fieldType == double.class || fieldType == Double.class) {
            type = "double";
        } else if (fieldType == float.class || fieldType == Float.class) {
            type = "float";
        }
        if (TextUtils.isEmpty(type)) {
            Log.e(TAG, type.getClass().getName() + "是不支持的字段");
        } else {
            sb.append(columnName + " " + type + "(" + length + "),");
        }
    }
    sb.deleteCharAt(sb.lastIndexOf(","));
    String s = sb.toString();
    if (TextUtils.isEmpty(s)) {
        Log.e(TAG, "獲取不到表字段信息");
        return false;
    }
    String sql = "create table if not exists " + mTbName + " (" + s + ") ";
    Log.e(TAG, sql);
    database.execSQL(sql);
    return true;
}

到這里,Dao類的初始化工作就完了,下面進(jìn)行CRUD的封裝。

3、增

我們知道,若使用原生的SQLiteDatabase將數(shù)據(jù)插入到表中,需要將數(shù)據(jù)先封裝成ContentValues對(duì)象,再調(diào)用其insert()方法來(lái)執(zhí)行數(shù)據(jù)插入操作。那么,現(xiàn)在我們擁有了一個(gè)數(shù)據(jù)實(shí)體,要做的,就是將這個(gè)數(shù)據(jù)實(shí)體轉(zhuǎn)成ContentValues對(duì)象,再使用SQLiteDatabase的insert()方法來(lái)執(zhí)行插入,可以說(shuō)我們就是對(duì)SQLiteDatabase進(jìn)行封裝,總結(jié)上面的理論,代碼分如下三步:

  1. 將對(duì)象中的屬性轉(zhuǎn)成鍵值對(duì)values。
  2. 將鍵值對(duì)values轉(zhuǎn)成ContentValues對(duì)象。
  3. 使用SQLiteDatabase的insert()方法進(jìn)行數(shù)據(jù)插入。

結(jié)合mFieldMap,實(shí)現(xiàn)表數(shù)據(jù)插入的代碼可以這么寫(xiě):

@Override
public Long insert(M entity) {
    try {
        Map<String, String> values = getValues(entity);
        ContentValues cv = getContentValues(values);
        return mDatabase.insert(mTbName, null, cv);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return 0L;
}

/**
 * 將對(duì)象中的屬性轉(zhuǎn)成鍵值對(duì)(列名--值)
 */
private Map<String, String> getValues(M entity) throws IllegalAccessException {
    Map<String, String> result = new HashMap<>();
    for (Map.Entry<String, Field> entry : mFieldMap.entrySet()) {
        Object value = entry.getValue().get(entity);
        result.put(entry.getKey(), value == null ? "" : value.toString());
    }
    return result;
}

/**
 * 將鍵值對(duì)轉(zhuǎn)成ContentValues
 */
private ContentValues getContentValues(Map<String, String> values) {
    ContentValues cv = new ContentValues();
    for (Map.Entry<String, String> val : values.entrySet()) {
        cv.put(val.getKey(), val.getValue());
    }
    return cv;
}

4、刪

要實(shí)現(xiàn)刪除表數(shù)據(jù)功能,需要使用到SQLiteDatabase的delete()方法,其中whereClause和whereArgs是關(guān)鍵。又因?yàn)樵摽蚣苁且粋€(gè)ORM框架,在表現(xiàn)層需要將刪除條件使用數(shù)據(jù)實(shí)體進(jìn)行封裝,而框架內(nèi)部則是對(duì)傳入的數(shù)據(jù)實(shí)體進(jìn)行解析,將對(duì)象中屬性值不為null的屬性拿出來(lái)作為刪除的條件(這也意味著常見(jiàn)的數(shù)據(jù)類型不能用了,如int,但可以使用Integer來(lái)替換),可分為兩步:

  1. 將對(duì)象中的屬性轉(zhuǎn)成鍵值對(duì)whereMap。
  2. 使用Condition類的構(gòu)造函數(shù)對(duì)whereMap中value不為null的鍵值對(duì)取出來(lái)拼接。

綜上所述,BaseDao的表數(shù)據(jù)刪除實(shí)現(xiàn)代碼如下:

@Override
public Integer delete(M where) {
    try {
        Map<String, String> whereMap = getValues(where);
        Condition condition = new Condition(whereMap);
        return mDatabase.delete(mTbName, condition.whereClause, condition.whereArgs);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return 0;
}

class Condition {
    public Condition(Map<String, String> whereMap) {

        StringBuilder sb = new StringBuilder();
        List<String> list = new ArrayList<>();

        for (Map.Entry<String, String> entry : whereMap.entrySet()) {
            if (!TextUtils.isEmpty(entry.getValue())) {
                sb.append("and " + entry.getKey() + "=? ");
                list.add(entry.getValue());
            }
        }
        this.whereClause = sb.delete(0, 4).toString();
        this.whereArgs = list.toArray(new String[list.size()]);
    }

    String whereClause;
    String[] whereArgs;
}

whereClause是刪除條件,是個(gè)字符串,需要使用?來(lái)作為占位符,多個(gè)條件需要使用and關(guān)鍵字連接,如:name=? and password=?

whereArgs則是對(duì)whereClause中占位符進(jìn)行數(shù)值替換的字體串?dāng)?shù)組,如:new String[]{"LQR","123456"}

5、改

通過(guò)前面對(duì)表數(shù)據(jù)增和刪的代碼實(shí)現(xiàn),更新數(shù)據(jù)部分就比較好理解了,因?yàn)镾QLiteDatabase的update()方法需要用到的參數(shù)有ContentValues對(duì)象,whereClause和whereArgs,其實(shí)就是將增和刪的代碼實(shí)現(xiàn)相加起來(lái)而已,這就不多廢話,實(shí)現(xiàn)的代碼如下:

@Override
public Integer update(M entitiy, M where) {
    try {
        Map<String, String> values = getValues(entitiy);
        ContentValues cv = getContentValues(values);

        Map<String, String> whereMap = getValues(where);
        Condition condition = new Condition(whereMap);

        return mDatabase.update(mTbName, cv, condition.whereClause, condition.whereArgs);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return 0;
}

6、查

終于到了CRUD的最后一步:數(shù)據(jù)查詢(Retrieve)。可以說(shuō)這是數(shù)據(jù)庫(kù)操作中最重要且使用頻率最高的一部分了,同樣的,表數(shù)據(jù)查詢還是用到了SQLiteDatabase,使用其query()方法來(lái)進(jìn)行表數(shù)據(jù)查詢,它的參數(shù)也比較多,這里就只封裝三種查詢:

  1. 將符合條件的表數(shù)據(jù)全部查詢出來(lái)。
  2. 將符合條件的表數(shù)據(jù)查詢出來(lái),并可以排序。
  3. 將符合條件的表數(shù)據(jù)查詢出來(lái),除了可以排序,還可以分頁(yè)查詢。

需要注意的就是分頁(yè)查詢,因?yàn)镾QLiteDatabase的第一頁(yè)是從0開(kāi)始的,而我希望的是表現(xiàn)層從1開(kāi)始,所以框架代碼中會(huì)對(duì)其進(jìn)行自減處理。這三個(gè)方法的代碼具體實(shí)現(xiàn)如下:

@Override
public List<M> query(M where) {
    return query(where, null);
}

@Override
public List<M> query(M where, String orderBy) {
    return query(where, orderBy, null, null);
}

@Override
public List<M> query(M where, String orderBy, Integer page, Integer pageCount) {
    List<M> list = null;
    Cursor cursor = null;
    try {
        String limit = null;
        if (page != null && pageCount != null) {
            int startIndex = --page;
            limit = (startIndex < 0 ? 0 : startIndex) + "," + pageCount;
        }

        if (where != null) {
            Map<String, String> whereMap = getValues(where);
            Condition condition = new Condition(whereMap);
            cursor = mDatabase.query(mTbName, null, condition.whereClause, condition.whereArgs, null, null, orderBy, limit);
        } else {
            cursor = mDatabase.query(mTbName, null, null, null, null, null, orderBy, limit);
        }

        // 將查詢出來(lái)的表數(shù)據(jù)轉(zhuǎn)成對(duì)象集合
        list = getDataList(cursor);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } finally {
        if (cursor != null) {
            cursor.close();
            cursor = null;
        }
    }
    return list;
}

/**
 * 通過(guò)游標(biāo),將表中數(shù)據(jù)轉(zhuǎn)成對(duì)象集合
 */
private List<M> getDataList(Cursor cursor) throws IllegalAccessException, InstantiationException {
    if (cursor != null) {
        List<M> result = new ArrayList<>();
        // 遍歷游標(biāo),獲取表中一行行的數(shù)據(jù)
        while (cursor.moveToNext()) {

            // 創(chuàng)建對(duì)象
            ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();// 獲取當(dāng)前new的對(duì)象的 泛型的父類 類型
            Class<M> clazz = (Class<M>) pt.getActualTypeArguments()[0];// 獲取第一個(gè)類型參數(shù)的真實(shí)類型
            M item = clazz.newInstance();

            // 遍歷表字段,使用游標(biāo)一個(gè)個(gè)取值,賦值給新創(chuàng)建的對(duì)象。
            Iterator<String> iterator = mFieldMap.keySet().iterator();
            while (iterator.hasNext()) {
                // 找到表字段
                String columnName = iterator.next();
                // 找到表字段對(duì)應(yīng)的類屬性
                Field field = mFieldMap.get(columnName);

                // 根據(jù)類屬性類型,使用游標(biāo)獲取表中的值
                Object val = null;
                Class<?> fieldType = field.getType();
                if (fieldType == String.class) {
                    val = cursor.getString(cursor.getColumnIndex(columnName));
                } else if (fieldType == int.class || fieldType == Integer.class) {
                    val = cursor.getInt(cursor.getColumnIndex(columnName));
                } else if (fieldType == double.class || fieldType == Double.class) {
                    val = cursor.getDouble(cursor.getColumnIndex(columnName));
                } else if (fieldType == float.class || fieldType == Float.class) {
                    val = cursor.getFloat(cursor.getColumnIndex(columnName));
                }

                // 反射給對(duì)象屬性賦值
                field.set(item, val);
            }
            // 將對(duì)象添加到集合中
            result.add(item);
        }
        return result;
    }
    return null;
}

至此,這個(gè)簡(jiǎn)易的數(shù)據(jù)庫(kù)框架就寫(xiě)好了,下面來(lái)測(cè)試一下。

五、測(cè)試

Activity的布局很簡(jiǎn)單,我就不貼了,就是幾個(gè)簡(jiǎn)單的按鈕而已。

1、測(cè)試前準(zhǔn)備

1)User

一個(gè)簡(jiǎn)單的數(shù)據(jù)實(shí)體類,沒(méi)什么好說(shuō)的,看代碼。

@TbName("tb_user")
public class User {

    @TbField(value = "tb_name", length = 30)
    public String username;

    @TbField(value = "tb_password", length = 20)
    public String password;

    @TbField(value = "tb_age", length = 11)
    public Integer age;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public User(String username, String password, int age) {
        this.username = username;
        this.password = password;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "[username:" + this.username + ", password:" + this.getPassword() + ", age:" + this.getAge() + "]";
    }
}

2)UserDao

可以看到這個(gè)UserDao中其實(shí)沒(méi)什么代碼,但它可以通過(guò)重寫(xiě)父類createTable()方法來(lái)更靈活的創(chuàng)建表,或自定義一些其它的方法來(lái)擴(kuò)展其父類的功能。

public class UserDao extends BaseDao<User> {
    // @Override
    // protected boolean createTable(SQLiteDatabase database) {
    // database.execSQL("create table if not exists t_user(tb_name varchar(30),tb_password varchar(10))");
    // return super.createTable(database);
    // }
}

2、功能測(cè)試

1)初始化

這個(gè)框架需要指定一個(gè)數(shù)據(jù)庫(kù)位置,我們?cè)贏ctivity的onCreate()方法中調(diào)用框架的init()方法來(lái)指定,建議最好放到自定義的Application中。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_customer_db_frame);
    BaseDaoFactory.init(new File(getFilesDir(), "csdn_lqr.db").getAbsolutePath());
    mUserDao = BaseDaoFactory.getInstance().getDataHelper(UserDao.class, User.class);
    mUser = new User("CSDN_LQR", "123456", 10);
}

2)增

往user表中插入一條username為"CSDN_LQR",password為"123456"的數(shù)據(jù):

public void insert(View view) {
    Long insert = mUserDao.insert(mUser);
    Toast.makeText(getApplicationContext(), "添加了" + (insert != -1 ? 1 : 0) + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
}

3)刪

從user表中刪除一條username為"CSDN_LQR"的數(shù)據(jù):

public void delete(View view) {
    User where = new User();
    where.setUsername("CSDN_LQR");
    Integer delete = mUserDao.delete(where);
    Toast.makeText(getApplicationContext(), "刪除了" + delete + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
}

4)改

將user表中username為"CSDN_LQR"的數(shù)據(jù)進(jìn)行修改:

public void update(View view) {
    User user = new User("LQR_CSDN", "654321", 9);

    User where = new User();
    where.setUsername("CSDN_LQR");

    Integer update = mUserDao.update(user, where);
    Toast.makeText(getApplicationContext(), "修改了" + update + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
}

5)查

a. 將符合條件的表數(shù)據(jù)全部查詢出來(lái)

將user表中所有username為"CSDN_LQR"的數(shù)據(jù)全部查詢出來(lái):

public void query1(View view) {
    User where = new User();
    where.setUsername("CSDN_LQR");

    List<User> list = mUserDao.query(where);
    int query = list == null ? 0 : list.size();
    Toast.makeText(getApplicationContext(), "查出了" + query + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
    for (User user : list) {
        System.out.println(user);
    }
}

b. 將符合條件的表數(shù)據(jù)查詢出來(lái),并排序

將user表中的數(shù)據(jù)按age的正反序分別查詢出來(lái):

public void query2(View view) {
    List<User> list = mUserDao.query(null, "tb_age asc");
    int query = list == null ? 0 : list.size();
    Toast.makeText(getApplicationContext(), "查出了" + query + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
    for (User user : list) {
        System.out.println(user);
    }
}

public void query3(View view) {
    List<User> list = mUserDao.query(null, "tb_age desc");
    int query = list == null ? 0 : list.size();
    Toast.makeText(getApplicationContext(), "查出了" + query + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
    for (User user : list) {
        System.out.println(user);
    }
}

c. 將符合條件的表數(shù)據(jù)查詢出來(lái),并分頁(yè)。

只查詢user表中的前2條數(shù)據(jù):

public void query4(View view) {
    User where = new User();

    List<User> list = mUserDao.query(where, null, 1, 2);
    int query = list == null ? 0 : list.size();
    Toast.makeText(getApplicationContext(), "查出了" + query + "條數(shù)據(jù)", Toast.LENGTH_SHORT).show();
    for (User user : list) {
        System.out.println(user);
    }
}

大成功,撒花。

最后貼下Demo地址:

https://github.com/GitLqr/SimpleDbFrame

歡迎關(guān)注微信公眾號(hào):全棧行動(dòng)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,165評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,595評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,633評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,435評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,943評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,175評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,713評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,599評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,788評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,034評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,412評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,664評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,408評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,747評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容