前言
我覺得數(shù)據(jù)庫的使用在一般應(yīng)用中使用的頻率是比較高的,相比于使用Android自帶的sqlite,雖然提供了一些API操作,但是操作起來還是比較復(fù)雜的婚脱。所以自然而然有很多開源的ORM數(shù)據(jù)庫框架,譬如GreenDao勺像、ORMLite障贸、Realm等開源庫,使用起來還是比較方便的吟宦。當(dāng)然用到SQLite的機(jī)會也是挺多的篮洁,畢竟開源庫也有一定的局限性,使用原生數(shù)據(jù)庫時(shí)殃姓,我通常是使用JakeWharton大神的SqlBrite開源庫袁波,這個(gè)庫是輕量級的sql輔助庫,配合RxJava能夠輕松將數(shù)據(jù)轉(zhuǎn)為流操作。當(dāng)然扯遠(yuǎn)了蜗侈,還是說說這篇文章要提到的自制數(shù)據(jù)庫篷牌。
場景
但是在某些場景下,這些數(shù)據(jù)庫就不夠好用了踏幻,譬如枷颊,當(dāng)應(yīng)用需要選擇省市縣三級聯(lián)動的時(shí)候,一般情況是是向網(wǎng)絡(luò)請求數(shù)據(jù)该面,每次選擇一項(xiàng)時(shí)夭苗,就要向服務(wù)器請求新的對應(yīng)的聯(lián)動數(shù)據(jù),這樣做雖然能保證及時(shí)性吆倦,但是用戶體驗(yàn)就沒那么好听诸,每次都需要網(wǎng)絡(luò)請求坐求,如果遇上網(wǎng)絡(luò)不好的情況或者數(shù)據(jù)量比較大的情況的時(shí)候蚕泽,這樣設(shè)計(jì)就有問題了。而且相對來說,省市縣這樣的數(shù)據(jù)其實(shí)挺大的须妻,并且其數(shù)據(jù)相對來說比較穩(wěn)定仔蝌,不會輕易發(fā)生大的改變,所以如果應(yīng)用能夠內(nèi)置這個(gè)數(shù)據(jù)庫文件荒吏,獲取數(shù)據(jù)直接從數(shù)據(jù)庫查詢敛惊,效率自然比從服務(wù)器拿的要高。
又譬如黃歷信息绰更、一些常用的電話號碼這些信息量比較大瞧挤,但是又不用經(jīng)常修改的信息,完全可以先做本地?cái)?shù)據(jù)庫處理儡湾。
方案
如何創(chuàng)建數(shù)據(jù)庫
如果要實(shí)現(xiàn)數(shù)據(jù)庫特恬,則必須先創(chuàng)建數(shù)據(jù)庫,創(chuàng)建數(shù)據(jù)庫有兩種方案:
- 方案一:就是手動創(chuàng)建數(shù)據(jù)庫徐钠,我是在Java平臺上癌刽,利用jdbc來創(chuàng)建數(shù)據(jù)庫的,其創(chuàng)建方式和MySQL的連接方式是十分類似的尝丐。當(dāng)然显拜,也可以利用數(shù)據(jù)庫制作工作來創(chuàng)建,譬如我用了SqliteStudio這個(gè)工具爹袁,這個(gè)工具既可以創(chuàng)建數(shù)據(jù)庫远荠,同時(shí)也能夠增刪查改數(shù)據(jù)庫信息。但是需要注意的亮點(diǎn)就是:
- 數(shù)據(jù)庫文件中必須有一個(gè)名為“android_metadata”的表呢簸,這個(gè)表只包括一個(gè)字段:locale矮台,也只需要一條記錄,默認(rèn)值為“en_US”根时。
- 數(shù)據(jù)庫文件中的其它表瘦赫,必須包括一個(gè)名字“_id”的關(guān)鍵字字段。其實(shí)這一點(diǎn)也未必需要蛤迎,不過我的建議是創(chuàng)建表的時(shí)候都加上這個(gè)字段确虱,設(shè)置為自動增長,因?yàn)樵谑褂胠istview的時(shí)候可以進(jìn)行cursor自定綁定替裆。再者校辩,當(dāng)我們用自定義表時(shí),習(xí)慣創(chuàng)建一個(gè)協(xié)議類來存儲表名辆童、字段名等信息宜咒,通常這個(gè)Entry類推薦實(shí)現(xiàn)BaseColumns這個(gè)接口,而BaseColumns這個(gè)接口就是自帶''__id''這個(gè)字段的把鉴。
- 方案二:利用Android的SQLiteOpenHelper來實(shí)現(xiàn)故黑,原理就是按照正常流程創(chuàng)建一個(gè)數(shù)據(jù)庫以及創(chuàng)建表儿咱,但是不寫入信息,然后可以利用adb命令將其從系統(tǒng)中取出场晶,這個(gè)只要程序運(yùn)行一次混埠,就可以生成對應(yīng)的數(shù)據(jù)庫,并且存放在data/data/<包名>/database/目錄下面诗轻,可以直接pull出來钳宪。這種方式比較簡單安全,而且能確保取出的數(shù)據(jù)庫能夠安全使用扳炬。
獲取數(shù)據(jù)
如何往數(shù)據(jù)庫里寫東西吏颖,當(dāng)然實(shí)現(xiàn)的方式還是有很多種,我選擇了用Java實(shí)現(xiàn)恨樟,因?yàn)楫吘贡容^熟悉侦高,我的想法是通過JDBC來打開數(shù)據(jù)庫并進(jìn)行讀寫,就拿我的獲取黃歷信息來說厌杜,由于只提供了獲取一天的接口奉呛,我只能夠一天天去獲取,而且獲取七十年的數(shù)據(jù)夯尽,這個(gè)當(dāng)然是比較大的并且不會變化的數(shù)據(jù)瞧壮。我的方案是先獲取指定日期的每天的天數(shù),再利用RxJava去循環(huán)獲取并存入數(shù)據(jù)庫匙握。主要操作類具體代碼如下:
public class HuangLiDbManager {
private static final String TAG = "HuangLiDbManager";
private static HuangLiDbManager sInstance;
public static HuangLiDbManager getInstance() {
if (sInstance == null) {
synchronized (HuangLiDbManager.class) {
if (sInstance == null)
sInstance = new HuangLiDbManager();
}
}
return sInstance;
}
/**
* 通過不停循環(huán)請求數(shù)據(jù)并且將數(shù)據(jù)存入sqlite
* @param dates
*/
public void requestData(List<String> dates) {
final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
Observable.from(dates)
.flatMap(new Func1<String, Observable<HuangLiInfo>>() {
@Override
public Observable<HuangLiInfo> call(final String s) {
return Observable.create(new Observable.OnSubscribe<HuangLiInfo>() {
@Override
public void call(final Subscriber<? super HuangLiInfo> subscriber) {
final Request request = new Request.Builder()
.get()
.url(APIConstant.HUANGLI_BASE_URL + s + "?key=" + APIConstant.HUANGLI_API_KEY)
.build();
Call callback = client.newCall(request);
callback.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
subscriber.onError(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
subscriber.onNext(new HuangLiInfo(s, response.body().string()));
}
});
}
});
}
})
.distinct()
.doOnNext(new Action1<HuangLiInfo>() {
@Override
public void call(HuangLiInfo huangLiInfo) {
saveDataToSqlite(huangLiInfo);
System.out.println(huangLiInfo.getDate());
}
})
.observeOn(Schedulers.newThread())
.subscribe();
}
/**
* 利用jdbc打開sqlite數(shù)據(jù)庫咆槽,并同步存入數(shù)據(jù)庫,避免多線程造成的問題
* @param info
*/
private synchronized void saveDataToSqlite(HuangLiInfo info) {
Connection connection = null;
try {
Class.forName("org.sqlite.JDBC");
connection = DriverManager.getConnection("jdbc:sqlite:HuangLi.db");
Statement statement = connection.createStatement();
statement.setQueryTimeout(30);
statement.executeUpdate(String.format("insert into huangli(date,content) values('%s','%s')", info.getDate(), info.getContent()));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
話說我獲取七十年的數(shù)據(jù)圈纺,足足跑了好幾個(gè)小時(shí)秦忿,不得不說寫的方式還是有待提高的,不足之處蛾娶,多多指教灯谣。還有就是RxJava真的是太好用了,這樣的邏輯用流操作很方便蛔琅。
利用數(shù)據(jù)庫
當(dāng)獲得有數(shù)據(jù)的數(shù)據(jù)庫后胎许,接下來就應(yīng)該該利用這個(gè)數(shù)據(jù)庫了。如果數(shù)據(jù)庫過大罗售,建議進(jìn)行壓縮或者進(jìn)行文件分割辜窑,因?yàn)锳ndroid的assets目錄下的文件是有文件大小限制的,據(jù)說在2.3以前都是不支持1M大小的文件讀取的寨躁,會報(bào)錯(cuò)
This file can not be opened as a file descriptor; it is probably compressed
所以如果文件過大穆碎,可以考慮將文件分割若干份1M大小的文件,在讀取時(shí)將文件合并职恳,可以參考這篇博客 所禀。
不過據(jù)我實(shí)踐所得谜悟,Android在6.0以上版本似乎沒有對assets下的大文件讀取有過大限制,我將數(shù)據(jù)庫文件壓縮后北秽,只有不到6M,放進(jìn)去后最筒,AssetManager是可以正常讀取的贺氓,并不會報(bào)錯(cuò),不知道是不是官方提高了asset下文件大小的限制床蜘,這個(gè)可以以后研究一下辙培。我的做法是將大文件壓縮后,然后當(dāng)程序第一次運(yùn)行時(shí)邢锯,將其解壓到對應(yīng)路徑的database目錄下扬蕊。具體操作類如下:
數(shù)據(jù)庫創(chuàng)建用到了HuangliDbHelper
public class HuangliDbHelper extends SQLiteOpenHelper {
private static final String TAG = "HuangliDbHelper";
//用戶數(shù)據(jù)庫文件的版本
private static final int DB_VERSION = 1;
public static String DB_PATH = "/data/data/com.nickming.huanglidemo/databases/";
public static String DB_NAME = "HuangLi.db";
private Context mContext;
public HuangliDbHelper(Context context) {
super(context, DB_PATH + DB_NAME, null, DB_VERSION);
this.mContext = context;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
}
public void createDataBase() {
boolean dbExist = checkDataBase();
if (dbExist) {
//數(shù)據(jù)庫已存在,不做任何操作
} else {
//創(chuàng)建數(shù)據(jù)庫
try {
File dir = new File(DB_PATH);
if (!dir.exists()) {
dir.mkdirs();
}
File dbf = new File(DB_PATH + DB_NAME);
if (dbf.exists()) {
dbf.delete();
}
SQLiteDatabase.openOrCreateDatabase(dbf, null);
//復(fù)制并且解壓壓縮文件到數(shù)據(jù)庫目錄下
ZipUtil.unZipAssetFileToDatabaseDirectory(mContext, "HuangLi.zip", DB_PATH);
} catch (IOException e) {
throw new Error("數(shù)據(jù)庫創(chuàng)建失敗");
}
}
}
/**
* 檢查數(shù)據(jù)庫是否存在
*
* @return
*/
private boolean checkDataBase() {
SQLiteDatabase checkDB = null;
String myPath = DB_PATH + DB_NAME;
try {
checkDB = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY);
} catch (SQLiteException e) {
Log.i(TAG, "checkDataBase: 數(shù)據(jù)庫不存在");
}
if (checkDB != null) {
checkDB.close();
}
return checkDB != null ? true : false;
}
}
在創(chuàng)建好丹擎,在Repository類里可以創(chuàng)建這個(gè)實(shí)例并且調(diào)用createDataBase()這個(gè)方法來復(fù)制尾抑,接下來就是可以正常的對這個(gè)數(shù)據(jù)進(jìn)行正常的增刪改查操作,再封裝一層蒂培,后面我就不寫了再愈。
結(jié)語
其實(shí)整個(gè)流程還是比較簡單的,在這個(gè)過程中也學(xué)到了很多的知識护戳,遇到了不少坑翎冲,僅此記錄一下,以防以后再次遇到這樣的需求不會踩坑哈媳荒!
晚安抗悍!