前言
我觉得数据库的使用在一般应用中使用的频率是比较高的,相比于使用Android自带的sqlite,虽然提供了一些API操作,但是操作起来还是比较复杂的。所以自然而然有很多开源的ORM数据库框架,譬如GreenDao、ORMLite、Realm等开源库,使用起来还是比较方便的。当然用到SQLite的机会也是挺多的,毕竟开源库也有一定的局限性,使用原生数据库时,我通常是使用JakeWharton大神的SqlBrite开源库,这个库是轻量级的sql辅助库,配合RxJava能够轻松将数据转为流操作。当然扯远了,还是说说这篇文章要提到的自制数据库。
场景
但是在某些场景下,这些数据库就不够好用了,譬如,当应用需要选择省市县三级联动的时候,一般情况是是向网络请求数据,每次选择一项时,就要向服务器请求新的对应的联动数据,这样做虽然能保证及时性,但是用户体验就没那么好,每次都需要网络请求,如果遇上网络不好的情况或者数据量比较大的情况的时候,这样设计就有问题了。而且相对来说,省市县这样的数据其实挺大的,并且其数据相对来说比较稳定,不会轻易发生大的改变,所以如果应用能够内置这个数据库文件,获取数据直接从数据库查询,效率自然比从服务器拿的要高。
又譬如黄历信息、一些常用的电话号码这些信息量比较大,但是又不用经常修改的信息,完全可以先做本地数据库处理。
方案
如何创建数据库
如果要实现数据库,则必须先创建数据库,创建数据库有两种方案:
- 方案一:就是手动创建数据库,我是在Java平台上,利用jdbc来创建数据库的,其创建方式和MySQL的连接方式是十分类似的。当然,也可以利用数据库制作工作来创建,譬如我用了SqliteStudio这个工具,这个工具既可以创建数据库,同时也能够增删查改数据库信息。但是需要注意的亮点就是:
- 数据库文件中必须有一个名为“android_metadata”的表,这个表只包括一个字段:locale,也只需要一条记录,默认值为“en_US”。
- 数据库文件中的其它表,必须包括一个名字“_id”的关键字字段。其实这一点也未必需要,不过我的建议是创建表的时候都加上这个字段,设置为自动增长,因为在使用listview的时候可以进行cursor自定绑定。再者,当我们用自定义表时,习惯创建一个协议类来存储表名、字段名等信息,通常这个Entry类推荐实现BaseColumns这个接口,而BaseColumns这个接口就是自带''__id''这个字段的。
- 方案二:利用Android的SQLiteOpenHelper来实现,原理就是按照正常流程创建一个数据库以及创建表,但是不写入信息,然后可以利用adb命令将其从系统中取出,这个只要程序运行一次,就可以生成对应的数据库,并且存放在data/data/<包名>/database/目录下面,可以直接pull出来。这种方式比较简单安全,而且能确保取出的数据库能够安全使用。
获取数据
如何往数据库里写东西,当然实现的方式还是有很多种,我选择了用Java实现,因为毕竟比较熟悉,我的想法是通过JDBC来打开数据库并进行读写,就拿我的获取黄历信息来说,由于只提供了获取一天的接口,我只能够一天天去获取,而且获取七十年的数据,这个当然是比较大的并且不会变化的数据。我的方案是先获取指定日期的每天的天数,再利用RxJava去循环获取并存入数据库。主要操作类具体代码如下:
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;
}
/**
* 通过不停循环请求数据并且将数据存入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数据库,并同步存入数据库,避免多线程造成的问题
* @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();
}
}
}
}
}
话说我获取七十年的数据,足足跑了好几个小时,不得不说写的方式还是有待提高的,不足之处,多多指教。还有就是RxJava真的是太好用了,这样的逻辑用流操作很方便。
利用数据库
当获得有数据的数据库后,接下来就应该该利用这个数据库了。如果数据库过大,建议进行压缩或者进行文件分割,因为Android的assets目录下的文件是有文件大小限制的,据说在2.3以前都是不支持1M大小的文件读取的,会报错
This file can not be opened as a file descriptor; it is probably compressed
所以如果文件过大,可以考虑将文件分割若干份1M大小的文件,在读取时将文件合并,可以参考这篇博客 。
不过据我实践所得,Android在6.0以上版本似乎没有对assets下的大文件读取有过大限制,我将数据库文件压缩后,只有不到6M,放进去后,AssetManager是可以正常读取的,并不会报错,不知道是不是官方提高了asset下文件大小的限制,这个可以以后研究一下。我的做法是将大文件压缩后,然后当程序第一次运行时,将其解压到对应路径的database目录下。具体操作类如下:
数据库创建用到了HuangliDbHelper
public class HuangliDbHelper extends SQLiteOpenHelper {
private static final String TAG = "HuangliDbHelper";
//用户数据库文件的版本
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) {
//数据库已存在,不做任何操作
} else {
//创建数据库
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);
//复制并且解压压缩文件到数据库目录下
ZipUtil.unZipAssetFileToDatabaseDirectory(mContext, "HuangLi.zip", DB_PATH);
} catch (IOException e) {
throw new Error("数据库创建失败");
}
}
}
/**
* 检查数据库是否存在
*
* @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: 数据库不存在");
}
if (checkDB != null) {
checkDB.close();
}
return checkDB != null ? true : false;
}
}
在创建好,在Repository类里可以创建这个实例并且调用createDataBase()这个方法来复制,接下来就是可以正常的对这个数据进行正常的增删改查操作,再封装一层,后面我就不写了。
结语
其实整个流程还是比较简单的,在这个过程中也学到了很多的知识,遇到了不少坑,仅此记录一下,以防以后再次遇到这样的需求不会踩坑哈!
晚安!