天天看點

面向對象式資料庫架構的手寫

一.簡介

上次封裝了一個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哈,這樣寫擴充性是不是比較強呢,有哪些寫不好的地方還望大牛們舉薦。