使用SQLiteOpenHelper的正确姿势

4,734 阅读10分钟

    前段时间写Android用到SQLIteOpenHelper时,踩了一些小坑,仔细思考了一下,踩坑的根源在于,我没有正确的理解SQLiteOpenHelper这个类。那么,我们一起来看看SQLiteOpenHelper究竟是个什么东东,以及如何正确地使用SQLiteOpenHelper,希望可以帮助博友们理解,避免踩到了类似的坑。

    本文的将首先介绍下SQLiteOpenHelper的作用,然后引申出一些在使用SQLiteOpenHelper时需要注意的事项,最后,通过一个小demo演示下SQLiteOpenHelper操作数据库


SQLiteOpenHelper是什么


    望文生义,从名字上看来,至少可以得到三个方面的信息。第一,和SQLite数据库有关。第二,和打开一个数据库有关。第三,这是一个帮助类。

     当然,这些都是从名字上联想到的,难免有主观臆断之嫌,哈哈。我们来看看官方的说法:A helper class to manage database creation and version management. 所以,官方把SQLiteOpenHelper的作用解释为:一个数据库创建和版本管理的帮助类。

     引起我注意的是,类名起的和解释不是很切合。既然是管理的帮助类,并且是管理数据库的创建和版本,那么为什么类名包含"Open"这个单词呢?为什么不叫SQLiteManageHelper,或者更准确的,SQLiteCreationAndUpgradeHelper呢?且听下文分解。

     我们知道,我们可以通过sql来操纵数据库,做增删改查。但是呢,如果是我们从头开始写一遍数据库逻辑的话,其实不只sql操作。一个简单的流程是这样的:打开数据库->sql操作数据库->关闭数据库。而打开数据库,又牵涉到了不少细节。比如:

       1.  这个数据库是否存在?如果不存在,就要考虑先创建数据库,创建各种数据表。

       2.  这个数据库是否需要升级,比如目前的数据库结构已经不能满足我们的需求了,我们需要更改数据库的结构,这个时候就要先升级数据库,更新数据库的结构。

       3.  这个数据库是否之前已经打开?一般来说,我们操作数据库结束之后,需要关闭数据库,以节约资源。但是如果我们之前打开的数据库还要继续使用,这时候再执行打开操作时,直接返回这个数据库就行了。

       这些细节实现起来,还是挺繁琐的,然而这些逻辑都是套路,一招鲜吃遍天,不需要每次都从头写一遍。于是,是时候SQLiteOpenHelper出场了。

       SQLiteOpenHelper把打开数据库的一系列需要考虑到的细节逻辑都封装了起来,这也是为什么包含了Open这个单词的原因了。因为SQLiteOpenHelper的主要工作就是把打开数据库的逻辑帮我们实现了。

       当然,打开数据库中涉及到的一些业务方面的逻辑,这是需要我们自己去实现的,SQLiteOpenHelper把这些用相应的接口分离了出来。

       onCreate:  创建数据库时会调用,我们需要在里面创建我们的数据表。onCreate只会调用一次,如果数据库已经存在,那么打开数据库是不会回调onCreate的。

       onUpgrade:  升级数据库时会调用,我们可以在这里面做升级操作。关于升级,还有一点需要注意的,后面会提到。

       onDowngrade: 降级数据库时会调用,既然存在升级的场景,也可能存在降级的场景,比如我们可能觉得新版的需求没有旧版的好,想要恢复以前的需求,这时候就需要降级处理了。

        onOpen: 打开数据库时会调用到。和onCreate有所区别的是,onCreate只在创建数据库时会调用,而onOpen在每次打开数据库时都会调用。

        onConfigure:配置数据库连接。比如设置外键约束等。

SQLiteOpenHelper的几个误区


      引入正题,我们来看看几个使用SQLiteOpenHelper可能踩到的坑或者误区。

      误区一:创建SQLiteOpenHelper对象的时候,会创建数据库或者连接数据库

      不知道用过SQLiteOpenHelper的朋友们有没有和我一样,曾经犯过这样的错误,以为创建SQLiteOpenHelper对象的时候,就会创建相应的数据库,或者连接已经存在的数据库。为什么会产生这样的误解呢?仔细想了想,根源出在构造函数上。

  1. public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,  
  2.             DatabaseErrorHandler errorHandler)  
public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,
            DatabaseErrorHandler errorHandler)

dbName是数据库的名字,version是数据库的版本。既然构造SQLiteOpenHelper的时候,又是传dbName,又是传version的,我就想当然的以为这时候会创建数据库并且连接上数据库了。然后打开源码发现,什么都没发生[看不下去]

  1. public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,  
  2.             DatabaseErrorHandler errorHandler) {  
  3.         if (version < 1throw  new IllegalArgumentException("Version must be >= 1, was " + version);  
  4.   
  5.         mContext = context;  
  6.         mName = name;  
  7.         mFactory = factory;  
  8.         mNewVersion = version;  
  9.         mErrorHandler = errorHandler;  
  10.     }  
public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,
            DatabaseErrorHandler errorHandler) {
        if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);

        mContext = context;
        mName = name;
        mFactory = factory;
        mNewVersion = version;
        mErrorHandler = errorHandler;
    }
SQLiteOpenHelper的构造函数里只是简单的记录下传过来的参数。

     真正创建数据库的时机是,显示调用打开数据库的api才会执行。也就是getReadableDatabase()和getWriteableDatabase()。这两个方法都是调用了

  1. private SQLiteDatabase getDatabaseLocked(boolean writable)  
private SQLiteDatabase getDatabaseLocked(boolean writable)
而创建数据库,配置数据库,升级数据库,打开数据库这些逻辑都是在这个方法里面实现的。

     误区二:在onCreate, onOpen等回调里面调用getReadableDatabase或getWriteableDatabase

     因为前面解释得很清楚了,getReadableDatabase或getWriteableDatabase是触发(创建数据库、打开数据库、升级数据库、降级数据库、配置数据库)的入口,如果在这些回调里面,又触发了getReadableDatabase或getWriteableDatabase的话,你没看错,这变成递归调用了。

     所以,不能再onCreate, onOpen, onConfigure, onUpgrade, onDowngrade回调里面调用打开数据库的方法。

     误区三:升级数据库时,只需要考虑从最近的一个版本升级到最新的版本

     这种做法是错误的。举个栗子。

     假设我们的app一共发了三个版本A,B,C,并且版本A的数据库version是1,版本B的数据库version升到了2,版本C的数据库version升到了3。

     版本B里面的数据库升级逻辑是

  1. @Override  
  2.     public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion,  int newVersion) {  
  3.         // 在student表里面加了一个age字段  
  4.     }  
@Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        // 在student表里面加了一个age字段
    }
     版本C里面的数据库升级逻辑是
  1. @Override  
  2.     public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion,  int newVersion) {  
  3.         // 在student表里面加了一个hobby字段  
  4.     }  
@Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        // 在student表里面加了一个hobby字段
    }
     这会导致一个很严重的问题。假如用户从app的版本A升级到版本B再升级到版本C,一切沿着我们的设想,没有问题。但是如果用户在收到版本B的更新时,拒绝了更新请求;而收到版本C的更新时,同意了更新。那么问题来了,student表里面增加了hobby字段,但少了age字段!因为用户拒绝了版本B的更新,意味着没有执行版本B里面的数据库升级逻辑。

     正确的做法是这样的,在版本C里面,判断老版本,如果是1,说明是从版本A升级过来的,那么就应该添加age, hobby字段;如果是2,说明是从版本B升级过来的,那么只需要添加hobby字段,因为版本B里面已经有age字段,不需要添加了。

  1. @Override  
  2.     public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion,  int newVersion) {  
  3.         if (oldVersion == 1) {  
  4.             // 在student表里面加了age, hobby字段  
  5.         } else if (oldVersion == 2) {  
  6.             // 在student表里面加了hobby字段  
  7.         }  
  8.     }  
@Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        if (oldVersion == 1) {
            // 在student表里面加了age, hobby字段
        } else if (oldVersion == 2) {
            // 在student表里面加了hobby字段
        }
    }
     长话短说,升级数据库,一定要考虑以前的所有版本,对每个版本都进行适配。

     当然,有升级的场景,就有降级的场景。逻辑是类似的,降级的时候,要考虑所有的高版本,对每个高本版都进行适配。

SQLiteOpenHelper的小Demo


     做了一个小demo,实现了几个小功能:

     1.创建数据库student_db,创建数据表student

     2.添加一行数据到student表

     3.修改一行数据

     4.删除一行数据

     5.按名字查询student表

     6.升级数据库,给student表添加一个age字段,默认值是20岁

      下面是demo的下载链接,欢迎下载:

Sqlite Demo

总结


    1.SQLiteOpenHelper名字里包含Open,就是想告诉我们,SQLiteOpenHelper的主要工作就是把打开数据库的逻辑帮我们实现了。

    2.创建SQLiteOpenHelper对象的时候,不会真正创建数据库或者连接数据库。调用getReadableDatabase或getWriteableDatabase才会触发该操作。

    3.升级数据库,一定要考虑以前的所有版本,对每个版本都进行适配。降级的时候,要考虑所有的高版本,对每个高本版都进行适配。

    4.在onCreate, onOpen等回调不能调用getReadableDatabase或getWriteableDatabase,会产生递归调用。

感谢您的耐心阅读,以上如果有错误的地方或者理解有失偏颇,请留言指正,谢谢~~