Android上自制SQLite数据库并且在应用中使用自制数据库的探究

前言

我觉得数据库的使用在一般应用中使用的频率是比较高的,相比于使用Android自带的sqlite,虽然提供了一些API操作,但是操作起来还是比较复杂的。所以自然而然有很多开源的ORM数据库框架,譬如GreenDao、ORMLite、Realm等开源库,使用起来还是比较方便的。当然用到SQLite的机会也是挺多的,毕竟开源库也有一定的局限性,使用原生数据库时,我通常是使用JakeWharton大神的SqlBrite开源库,这个库是轻量级的sql辅助库,配合RxJava能够轻松将数据转为流操作。当然扯远了,还是说说这篇文章要提到的自制数据库。

场景

但是在某些场景下,这些数据库就不够好用了,譬如,当应用需要选择省市县三级联动的时候,一般情况是是向网络请求数据,每次选择一项时,就要向服务器请求新的对应的联动数据,这样做虽然能保证及时性,但是用户体验就没那么好,每次都需要网络请求,如果遇上网络不好的情况或者数据量比较大的情况的时候,这样设计就有问题了。而且相对来说,省市县这样的数据其实挺大的,并且其数据相对来说比较稳定,不会轻易发生大的改变,所以如果应用能够内置这个数据库文件,获取数据直接从数据库查询,效率自然比从服务器拿的要高。
又譬如黄历信息、一些常用的电话号码这些信息量比较大,但是又不用经常修改的信息,完全可以先做本地数据库处理。

方案

如何创建数据库

如果要实现数据库,则必须先创建数据库,创建数据库有两种方案:

  1. 方案一:就是手动创建数据库,我是在Java平台上,利用jdbc来创建数据库的,其创建方式和MySQL的连接方式是十分类似的。当然,也可以利用数据库制作工作来创建,譬如我用了SqliteStudio这个工具,这个工具既可以创建数据库,同时也能够增删查改数据库信息。但是需要注意的亮点就是:
    1. 数据库文件中必须有一个名为“android_metadata”的表,这个表只包括一个字段:locale,也只需要一条记录,默认值为“en_US”。
    2. 数据库文件中的其它表,必须包括一个名字“_id”的关键字字段。其实这一点也未必需要,不过我的建议是创建表的时候都加上这个字段,设置为自动增长,因为在使用listview的时候可以进行cursor自定绑定。再者,当我们用自定义表时,习惯创建一个协议类来存储表名、字段名等信息,通常这个Entry类推荐实现BaseColumns这个接口,而BaseColumns这个接口就是自带''__id''这个字段的。
  2. 方案二:利用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 dates) {
        final OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(20, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)
                .build();
        Observable.from(dates)
                .flatMap(new Func1>() {
                    @Override
                    public Observable call(final String s) {
                        return Observable.create(new Observable.OnSubscribe() {
                            @Override
                            public void call(final Subscriber 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() {
                    @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()这个方法来复制,接下来就是可以正常的对这个数据进行正常的增删改查操作,再封装一层,后面我就不写了。

结语

其实整个流程还是比较简单的,在这个过程中也学到了很多的知识,遇到了不少坑,仅此记录一下,以防以后再次遇到这样的需求不会踩坑哈!

晚安!

你可能感兴趣的:(Android上自制SQLite数据库并且在应用中使用自制数据库的探究)