Android 数据库框架litepal的使用

来源:郭霖大神
http://blog.csdn.net/guolin_blog/article/details/38461239

要想熟练地操作任何一个数据库,最最基本的要求就是要懂SQL语言,这也是每个程序员都应该掌握的技能。虽说SQL博大精深,要想精通确实很难,但最基本的一些建表命令,增删改查,大家还是必须要学会的。

SQL(Structured Query Language)是一种标准的数据库查询语言,即所有的关系型数据库都会支持它,只不过每种数据库对SQL语言的支持与标准存在着细微的不同。我们无须关心其它数据库对SQL语言的支持情况,这里我们只要把重点放在SQLite上就可以了。下面我将使用模拟器来对SQLite支持的各种命令进行演示,如果你想用手机的话也可以,但要确保你的手机已经Root,并且包含sqlite3这个命令文件。

首先确保模拟器已经连接上了电脑,然后在命令行输入adb shell进入控制台,如下图所示:

3012329-554fceddb24c9f07

注意#符号表示我们当前已经是超级用户了,如果显示的是$符号,表示当前只是普通用户而已,这时还需要输入su命令切换一下用户身份才行。

有了超级用户权限之后,我们能做的事情就很多了,这里我们先查看一下系统自带的联系人表吧。进入到/data/data目录下,如下图所示:

3012329-c13b6dc419499da2

所有应用程序的本地存储文件都是存放在这个目录下面的。为了要让不同应用程序之间的数据容易区别开来,Android是使用应用程序包名进行分开管理,也就是说每个应用程序的本地存储文件都会存放在自己应用程序包名的那个目录下,这里我们ls一下看看有多少子目录:

3012329-e3a475ae270aa471

OK,确实有很多,毕竟手机上所有的应用程序都在这里。其中,com.android.providers.contacts中存放的就是联系人的相关数据,我们进入到这个目录再ls一下:

3012329-cd2c148b5abe354a

可以看到,目前有databases、files、lib和shared_prefs这几个子目录。其中databases肯定是用于存放数据库文件的,files是用于存放普通文本文件的,lib是用于存放so库的,shared_prefs则是用于存放shared文件的。这是Android数据持久化的几种可选方式,对这部分内容不太了解的朋友可以参考《第一行代码——Android》的第六章。

接着进入到databases目录中,再ls:

3012329-b48a19973dea8788

其中后缀名为journal的文件是日志文件,我们不用管,contacts2.db和profile.db才是真正的数据库文件,可以使用sqlite3命令来打开数据库,如下图所示:

3012329-fab36ef4e413ae11

好的,数据库已经打开了,那么我们怎么才能知道当前数据库中有哪些表呢?很简单,.table命令就可以做到了:

3012329-44fa7c5b2067ae85

哇,竟然有这么多张表!是的,联系人的数据结构非常复杂,很多的数据都是分表存储的。这里我们随便挑一张表,比如说accounts表,如果我想知道这张表中有哪些列应该怎么办呢?在MySQL中可以使用desc accounts这个命令,但SQLite却不认识这个命令,毕竟它们是有差异化的。SQLite中可以使用pragma table_info(TABLE_NAME)这个命令来查看表的数据结构,如下图所示:

3012329-6bbd79dc86458669

可以看到,一共显示了三条结果,表示accounts表中共有三列。但是,所有的字段都缩在了一行里面,并用“|”符号分隔,这样我们很难看出每个字段的含义。很简单,只需要换一种显示模式就行了,比如说line模式就挺不错的。输入.mode line命令切换显示模式,然后重新运行pragma命令,结果如下图所示:

3012329-57d4da57c26aadb6

怎么样,这样就清晰多了吧?这三列的列名分别是account_name、account_type和data_set,数据类型都是TEXT(字符串),允许为空,并且都不是主键。好,那我现在想查一查accounts表中的数据呢?这就太简单了,使用select语句就可以了,如下所示:

3012329-8181207ffde4c951

恩?怎么只有一条空数据啊。貌似模拟器上默认就是这样的,如果你用的是手机的话,这里应该就可以查到真正的数据了。不过没关系,我们可以在设置里面手动添加一个邮箱账户,如下图所示:

3012329-ba5502af9661588f

现在再来重新查询一遍accounts表,如下所示:

3012329-6da50a7f2b10862c

OK,添加的新账户已经成功查出来了。

除了查询命令之外,还有其它的增删改命令都和标准的SQL语法是相同的,即insert、delete和update,由于比较简单,我就不再赘述了。比较值得一提的是,每个SQLite数据库中都还有一个隐藏的sqlite_master表,这里记载了当前数据库中所有表的建表语句,可以使用select * from sqlite_master命令进行查看:

3012329-93a473a69dc3938c

结果太多了是不是?一屏根本就显示不下嘛。不要着急,别忘了我们使用的是select命令,可以使用where语句来过滤出我们想要查询的那部分内容,如下图所示:

3012329-5c47419582fc66f4

OK,CREATE TABLE accounts (account_name TEXT, account_type TEXT, data_set TEXT) 这就是accounts表的建表语句了,通过这种方式我们可以查询到任意一张表的建表语句,从而对我们学习和分析数据库表结构有所帮助。

有些朋友可能会觉得,每次都要输入select命令来查询表中的数据太麻烦了。没错,而且还要保证手机是连接在电脑上的时候才能查询,确实太不方便。幸运的是,有些手机软件已经提供了数据库表查询的功能,使得我们随时随地都可以方便地查看数据库中的数据,比如Root Explorer这款软件就不错。

仍然是确保你的手机已经Root,然后安装Root Explorer,打开软件之后按照我们前面介绍的路径,进入/data/data/com.android.providers.contacts/databases,点击contacts2.db数据库,选择内置数据库查看器,然后随便点击一张表就可以查看到里面的数据了,如下图所示:

3012329-8ef159149c200012

使用这种方法,我们可以随时查看数据库表中的最新数据,直观又方便,在程序开发的时候可以起到非常大的帮助。

操作数据库的第一步当然是创建表了,传统创建表的方法相信大多数人都知道,那么今天我除了会展示传统的建表方法之外,还会讲解LitePal这个框架的基本用法,并使用它来完成同样的建表操作,让大家体会到使用框架来操作数据库的魅力。

那么先来简单介绍一下吧,LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发时最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表、増删改查的操作。并且LitePal很“轻”,jar包只有100k不到,而且近乎零配置,这一点和Hibernate这类的框架有很大区别。目前LitePal的源码已经托管到了GitHub上,地址是 https://github.com/LitePalFramework/LitePal

OK,简单介绍完了LitePal,我们还是先来看一下,在传统的Android开发中,需要怎么去创建表。

传统的建表方式

其实为了方便我们对数据库表进行管理,Android本身就提供了一个帮助类:SQLiteOpenHelper。这个类集创建和升级数据库于一身,并且自动管理了数据库版本,算是一个非常好用的工具。

那我们现在就来试试SQLiteOpenHelper的用法吧。首先你要知道SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。本篇文章只需要把注意力放在创建数据库这里就行了,升级数据库我们会在下一篇文章中去讨论。

新建一个MySQLiteHelper类并让它继承SQLiteOpenHelper,这样一个最基本的数据库帮助类的代码如下所示:


public class MySQLiteHelper extends SQLiteOpenHelper {

    public MySQLiteHelper(Context context, String name, CursorFactory factory,
                          int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

其中,当数据库创建的时候会调用onCreate()方法,在这里去执行建表操作就可以了。比如说我们想新建一张news表,其中有title,content,publishdate,commentcount这几列,分别代表着新闻标题、新闻内容、发布时间和评论数,那么代码就可以这样写:

public class MySQLiteHelper extends SQLiteOpenHelper {

    public static final String CREATE_NEWS = "create table news ("
            + "id integer primary key autoincrement, "
            + "title text, "
            + "content text, "
            + "publishdate integer,"
            + "commentcount integer)";

    public MySQLiteHelper(Context context, String name, CursorFactory factory,
                          int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_NEWS);
    }  
        
}  

可以看到,我们把建表语句定义成了一个常量,然后在onCreate()方法中去执行了这条建表语句,news表也就创建成功了。这条建表语句虽然简单,但是里面还是包含了一些小的细节,我来解释一下。首先,根据数据库的范式要求,任何一张表都应该是有主键的,所以这里我们添加了一个自增长的id列,并把它设为主键。然后title列和content列都是字符串类型的,commentcount列是整型的,这都很好理解,但是publishdate列该怎么设计呢?由于SQLite中并不支持存储日期这种数据类型,因此我们需要将日期先转换成UTC时间(自1970年1月1号零点)的毫秒数,然后再存储到数据库中,因此publishdate列也应该是整型的。

现在,我们只需要获取到SQLiteDatabase的实例,数据库表就会自动创建了,如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

感觉很简单很方便是吗?那你就太容易满足了,下面我们就来学习一下LitePal的基本用法,看一看使用这个框架是如何实现同样的功能的。

LitePal的基本用法

虽说LitePal宣称是近乎零配置,但也只是“近乎”而已,它还是需要进行一些简单配置才可以使用的,那么我们第一步就先快速学习一下LitePal的配置方法。

快速配置

1. 引入Jar包或源码

首先我们需要将LitePal的jar包引入到项目当中,可以点击这里查看LitePal的最新版本,选择你需要的下载即可。下载好了jar包之后,把它复制到项目的libs目录中就算是引入成功了,如下图所示:

3012329-cfbdc40bb1a2af8b

如果你不想用jar包的话,也可以把LitePal的源码下载下来,然后作为一个library库导入到Eclipse当中,再让我们的项目去引用这个library库就可以了。

2. 配置litepal.xml

接着在项目的assets目录下面新建一个litepal.xml文件,并将以下代码拷贝进去:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

配置文件相当简单,<dbname>用于设定数据库的名字,<version>用于设定数据库的版本号,<list>用于设定所有的映射模型,我们稍后就会用到。

3. 配置LitePalApplication

由于操作数据库时需要用到Context,而我们显然不希望在每个接口中都去传一遍这个参数,那样操作数据库就显得太繁琐了。因此,LitePal使用了一个方法来简化掉Context这个参数,只需要在AndroidManifest.xml中配置一下LitePalApplication,所有的数据库操作就都不用再传Context了,如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

当然,有些程序可能会有自己的Application,并在这里配置过了。比如说有一个MyApplication,如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

没有关系,这时只需要修改一下MyApplication的继承结构,让它不要直接继承Application类,而是继承LitePalApplication类,就可以使用一切都能正常工作了,代码如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

但是,有些程序可能会遇到一些更加极端的情况,比如说MyApplication需要继承另外一个AnotherApplication,并且这个AnotherApplication还是在jar包当中的,不能修改它的代码。这种情况应该算是比较少见了,但是如果你遇到了的话也不用急,仍然是有解释方案的。你可以把LitePal的源码下载下来,然后把src目录下的所有代码直接拷贝到你项目的src目录下面,接着打开LitePalApplication类,将它的继承结构改成继承自AnotherApplication,再让MyApplication继承自LitePalApplication,这样所有的Application就都可以在一起正常工作了。

仅仅三步,我们就将所有的配置工作全部完成了,并且这是一件一本万利的事情,自此以后,你就可以开心地体验LitePal提供的各种便利了,就让我们从建表开始吧。

开始建表

前面在介绍的时候已经说了,LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而我们使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

但是我们为什么要使用对象关系映射模式呢?这主要是因为大多数的程序员都很擅长面向对象编程,但其中只有少部分的人才比较精通关系型数据库。而且数据库的SQL语言晦涩难懂,就算你很精通它,恐怕也不喜欢经常在代码中去写它吧?而对象关系映射模式则很好地解决了这个问题,它允许我们使用面向对象的方式来操作数据库,从而可以从晦涩难懂的SQL语言中解脱出来。

那么接下来我们就看一看LitePal中是如何建表的吧。根据对象关系映射模式的理念,每一张表都应该对应一个模型(Model),也就是说,如果我们想要建一张news表,就应该有一个对应的News模型类。新建一个News类,如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

然后,表中的每一列其实就是对应了模型类中的一个字段,比如news表中有id、title、content、publishdate、commentcount这几个列,那么在News类中就也应该有这几个字段,代码如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

其中id这个字段可写可不写,因为即使不写这个字段,LitePal也会在表中自动生成一个id列,毕竟每张表都一定要有主键的嘛。

这里我要特别说明一下,LitePal的映射规则是非常轻量级的,不像一些其它的数据库框架,需要为每个模型类单独配置一个映射关系的XML,LitePal的所有映射都是自动完成的。根据LitePal的数据类型支持,可以进行对象关系映射的数据类型一共有8种,int、short、long、float、double、boolean、String和Date。只要是声明成这8种数据类型的字段都会被自动映射到数据库表中,并不需要进行任何额外的配置。

现在模型类已经建好了,我们还差最后一步,就是将它配置到映射列表当中。编辑assets目录下的litepal.xml文件,在<list>标签中加入News模型类的声明:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

注意这里一定要填入News类的完整类名。

OK,这样所有的工作就都已经完成了,现在只要你对数据库有任何的操作,news表就会被自动创建出来。比如说LitePal提供了一个便捷的方法来获取到SQLiteDatabase的实例,如下所示:

SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

调用一下上述代码,news表就应该已经创建成功了。我们使用在上一篇文章中学到的SQLite命令来查看一下,打开demo.db数据库,输入.table命令,结果如下图所示:

3012329-3fd21a171ff716d6

可以看到,news表已经存在了。另外两张android_metadata和table_schema表是自动生成的,我们不用理。接下来我们还可以再查询一下news表的建表语句,如下图所示:

3012329-587c6cf8044b792f

这就是LitePal根据News类中的字段自动帮我们生成的建表语句,由此也说明,建表操作已经成功完成了。

传统的升级表方式

上面我们借助MySQLiteHelper已经创建好了news这张表,这也是demo.db这个数据库的第一个版本。然而,现在需求发生了变更,我们的软件除了能看新闻之外,还应该允许用户评论,所以这时就需要对数据库进行升级,添加一个comment表。

该怎么做呢?添加一个comment表的建表语句,然后在onCreate()方法中去执行它?没错,这样的话,两张表就会同时创建了,代码如下所示:


public class MySQLiteHelper extends SQLiteOpenHelper {
    
    public static final String CREATE_NEWS = "create table news ("
            + "id integer primary key autoincrement, "
            + "title text, "
            + "content text, "
            + "publishdate integer,"
            + "commentcount integer)";
    
    public static final String CREATE_COMMENT = "create table comment ("
            + "id integer primary key autoincrement, "
            + "content text)";

    public MySQLiteHelper(Context context, String name, CursorFactory factory,
            int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_NEWS);
        db.execSQL(CREATE_COMMENT);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }

}

这对于第一次安装我们软件的用户来说是完全可以正常工作的,但是如果有的用户已经安装过上一版的软件,那么很遗憾,comment表是创建不出来的,因为之前数据库就已经创建过了,onCreate()方法是不会重新执行的。

对于这种情况我们就要用升级的方式来解决了,看到MySQLiteHelper构造方法中的第四个参数了吗,这个就是数据库版本号的标识,每当版本号增加的时候就会调用onUpgrade()方法,我们只需要在这里处理升级表的操作就行了。比较简单粗暴的方式是将数据库中现有的所有表都删除掉,然后重新创建,代码如下所示:


public class MySQLiteHelper extends SQLiteOpenHelper {
    
    ......

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_NEWS);
        db.execSQL(CREATE_COMMENT);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists news");
        onCreate(db);
    }

}

可以看到,当数据库升级的时候,我们先把news表删除掉,然后重新执行了一次onCreate()方法,这样就保证数据库中的表都是最新的了。

但是,如果news表中本来已经有数据了,使用这种方式升级的话,就会导致表中的数据全部丢失,所以这并不是一种值得推荐的升级方法。那么更好的升级方法是什么样的呢?这就稍微有些复杂了,需要在onUpgrade()方法中根据版本号加入具体的升级逻辑,我们来试试来吧。比如之前的数据库版本号是1,那么在onUpgrade()方法中就可以这样写:

public class MySQLiteHelper extends SQLiteOpenHelper {  
  
    ......  
  
    @Override  
    public void onCreate(SQLiteDatabase db) {  
        db.execSQL(CREATE_NEWS);  
        db.execSQL(CREATE_COMMENT);  
    }  
  
    @Override  
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
        switch (oldVersion) {  
        case 1:  
            db.execSQL(CREATE_COMMENT);  
        default:  
        }  
    }  
  
}

可以看到,这里在onUpgrade()方法中加入了一个switch判断,如果oldVersion等于1,就再创建一个comment表。现在只需要调用如下代码,表就可以得到创建或升级了:


SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 2);
SQLiteDatabase db = dbHelper.getWritableDatabase();

这里我们将版本号加1,如果用户是从旧版本升级过来的,就会新增一个comment表,而如果用户是直接安装的新版本,就会在onCreate()方法中把两个表一起创建了。

OK,现在软件的第二版本也发布出去了,可是就在发布不久之后,突然发现comment表中少了一个字段,我们并没有记录评论发布的时间。没办法,只好在第三版中修复这个问题了,那我们该怎么样去添加这个字段呢?主要需要修改comment表的建表语句,以及onUpgrade()方法中的逻辑,代码如下所示:


public class MySQLiteHelper extends SQLiteOpenHelper {

    ......

    public static final String CREATE_COMMENT = "create table comment ("
            + "id integer primary key autoincrement, " 
            + "content text, " 
            + "publishdate integer)";

    ......

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
        case 1:
            db.execSQL(CREATE_COMMENT);
            break;
        case 2:
            db.execSQL("alter table comment add column publishdate integer");
            break;
        default:
        }
    }

}

可以看到,在建表语句当中我们新增了publishdate这一列,这样当执行onCreate()方法去创建表的时候,comment表中就会有这一列了。那么如果是从旧版本升级过来的呢?也没有问题,我们在onUpgrade()方法中已经把升级逻辑都处理好了,当oldVersion等于2的时候,会执行alter语句来添加publishdate这一列。现在调用以下代码来创建或升级数据库:


SQLiteOpenHelper dbHelper = new MySQLiteHelper(this, "demo.db", null, 3);
SQLiteDatabase db = dbHelper.getWritableDatabase();

将数据库版本号设置成3,这样就可以保证数据库中的表又是最新的了。

现在我们已经学习了新增表和新增列这两种升级方式,那么如果是某张表中的某一列已经没有用了,我想把这一列删除掉该怎么写呢?很遗憾,SQLite并不支持删除列的功能,对于这情况,多数软件采取的作法是无视它,反正以后也用不到它了,留着也占不了什么空间,所以针对于这种需求,确实没什么简单的解决办法。

这大概就是传统开发当中升级数据库表的方式了,虽说能写出这样的代码表示你已经对数据库的升级操作理解的比较清楚了,但随着版本越来越多,onUpgrade()方法中的逻辑也会变得愈发复杂,稍微一不留神,也许就会产生错误。因此,如果能让代码自动控制升级逻辑,而不是由人工来管理,那就是再好不过了,那么下面我们就来学习一下怎样使用LitePal来进行升级表的操作。

使用LitePal升级表

通过上一篇文章的学习,我们已经知道LitePal是一款ORM模式的框架了,已经熟悉创建表流程的你,相信对于升级表也一定会轻车熟路的。那么为了模仿传统升级表方式中的需求,现在我们也需要创建一张comment表。第一步该怎么办呢?相信你也许早就已经猜到了,那当然是先创建一个Comment类了,如下所示:


package com.example.databasetest.model;

public class Comment {
    
    private int id;
    
    private String content;
    
    // 自动生成get、set方法 
    ...
}

OK,Comment类中有id和content这两个字段,也就意味着comment表中会有id和content这两列。

接着修改litepal.xml中的配置,在映射列表中新增Cooment类,并将版本号加1,如下所示:


<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="demo" ></dbname>

    <version value="2" ></version>

    <list>
        <mapping class="com.example.databasetest.model.News"></mapping>
        <mapping class="com.example.databasetest.model.Comment"></mapping>
    </list>
</litepal>

没错,就是这么简单,仅仅两步,升级的操作就已经完成了,现在我们只需要操作一下数据库,comment表就会自动生成了,如下所示:

SQLiteDatabase db = Connector.getDatabase();

那么我们还是通过.table命令来查看一下结果,如下图所示:

3012329-04552b7607ff8178

OK,comment表已经出来了,那么再通过pragma命令来查看一下它的表结构吧:

3012329-c92cd8b4ac292130

没有问题,comment表中目前有id和content这两列,和Comment模型类中的字段是保持一致的。

那么现在又来了新的需求,需要在comment表中添加一个publishdate列,该怎么办呢?不用怀疑,跟着你的直觉走,相信你已经猜到应该在Comment类中添加这样一个字段了吧,如下所示:

public class Comment {  
      
    private int id;  
      
    private String content;  
      
    private Date publishDate;  
      
    // 自动生成get、set方法   
    ...  
}  

然后呢?剩下的操作已经非常简单了,只需要在litepal.xml中对版本号加1就行了,如下所示:

<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="3" ></version>  
    ...  
</litepal>

这样当我们下一次操作数据库的时候,publishdate列就应该会自动添加到comment表中。调用Connector.getDatabase()方法,然后重新查询comment表结构,如下所示:

3012329-bbe314ca90618c77

可以看到,publishdate这一列确实已经成功添加到comment表中了。

通过这两种升级方式的对比,相信你已经充分体会到了使用LitePal进行升级表操作所带来的便利了吧。我们不需要去编写任何与升级相关的逻辑,也不需要关心程序是从哪个版本升级过来的,唯一要做的就是确定好最新的Model结构是什么样的,然后将litepal.xml中的版本号加1,所有的升级逻辑就都会自动完成了。LitePal确实将数据库表的升级操作变得极度简单,使很多程序员可以从维护数据库表升级的困扰中解脱出来。

然而,LitePal却明显做到了更好。前面我们提到过关于删除列的问题,最终的结论是无法解决,因为SQLite是不支持删除列的命令的。但是如果使用LitePal,这一问题就可以简单地解决掉,比如说publishdate这一列我们又不想要了,那么只需要在Comment类中把它删除掉,然后将版本号加1,下次操作数据库的时候这个列就会不见了。

那么有的朋友可能会问了,不是说SQLite不支持删除列的命令吗?那LitePal又是怎样做到的呢?其实LitePal并没有删除任何一列,它只是先将comment表重命名成一个临时表,然后根据最新的Comment类的结构生成一个新的comment表,再把临时表中除了publishdate之外的数据复制到新的表中,最后把临时表删掉。因此,看上去的效果好像是做到了删除列的功能。

这也是使用框架的好处,如果没有框架的帮助,我们显然不会为了删除一个列而大废周章地去写这么多的代码,而使用框架的话,具体的实现逻辑我们已经不用再关心,只需要控制好模型类的数据结构就可以了。

另外,如果你想删除某一张表的话,操作也很简单,在litepal.xml中的映射列表中将相应的类删除,表自然也就不存在了。其它的一些升级操作也都是类似的,相信你已经能举一反三,这里就不再赘述了。

关联关系的基础知识

喜欢把所有的代码都写在一个类里的程序员肯定是个新手。没错,任何一个像样的程序都不可能仅仅只有一个类的,同样地,任何一个像样的数据库也不可能仅仅只有一张表。我们都知道,在面向对象的编程语言中,多个类之间可以相互关联引用,共同完成某项功能。那么在数据库当中,多个表之间可以相互关联吗?当然可以!只不过表与表之间的关联关系要比对象之间的关联关系复杂一些,也更加难懂,但是作为数据库的基本功,还是应该了解清楚的,那么我们就先来学习一下数据库表关联的基础知识。

表与表之间的关联关系一共有三种类型,一对一、多对一、和多对多,下面我们分别对这三种类型展开进行讨论。

一对一

表示两个表中的数据必须是一一对应的关系。这种场景其实并不是很常见,我们还是通过例子来直观地体会一下,例子仍然是在之前文章的基础上展开的。

现在我们已经创建好了news这张表,里面主要记录了新闻的标题和内容,那么除了标题和内容之外,有些新闻还可能带有一些导语和摘要,我们把这两个字段放在一张introduction表中,作为新闻的简介。那么很显然,news表和introduction表就是一对一的关系了,因为一条新闻只能对应一个简介,一个简介也只能属于一条新闻。它们之间的对应关系大概如下图描述的一样:

3012329-c78fb02881be6ace

可以看到,News1对应了Introduction2,News2对应了Introduction3,News3对应了Introduction1,但不管怎么样,它们都是一对一的关系。

那么这种一对一的关系,在编程语言中该怎么体现出来呢?相信熟悉面向对象设计的你,一定很轻松就能想出来吧,只需要在News类中持有一个Introduction类的引用,然后在Introduction类中也持有一个News类的引用,这样它们之间自然就是一对一的关系了。

没错,对象之间的一对一关系非常简单易懂,那么难点就在于,如何在数据库表中建立这样的一对一关系了。由于数据库并不像面向对象的语言一样支持相互引用,如果想让两张表之间建立一对一的关系,一般就只能通过外键的方式来实现了。因此,一对一关系的表结构就可以这样设计:

3012329-965529a6a137fff8

请注意,introduction表中有一个news_id列,这是一个外键列,里面应该存放一个具体的新闻id,这样一条introduction就能对应一条news,也就实现一对一的关系了,如下图所示:

3012329-f1486c2dd0c186ad

由此我们就能够看出,id为1的introduction对应着id为2的news,id为2的introduction对应着id为3的news,id为3的introduction对应着id为1的news。需要注意的是,一对一的关系并没有强制要求外键必须加在哪一张表上,你可以在introduction表中加一个news_id作为外键,也可以在news表中加一个introduction_id作为外键,不管使用哪一种,都可以表示出它们是一对一的关联关系。

多对一

表示一张表中的数据可以对应另一张表中的多条数据。这种场景比起一对一关系就要常见太多了,在我们平时的开发工作中多对一关系真的是比比皆是。比如说现在我们的数据库中有一个news表,还有一个comment表,它们两个之间就是典型的多对一关系,一条新闻可以有很多条评论,但是一条评论只能是属于一条新闻的。它们的关系如下图所示:

3012329-97271dcc5b3ceeb8

而这种多对一的关系在编程语言中是非常容易体现出来的,比如Java中就有专门集合类,如List、Set等,使用它们的话就能轻松简单地在对象之间建立多对一的关系,我们稍后就会看到。那么,这里的难点仍然是在数据库表中如何建立这样的多对一关系。现在说难点其实已经不难了,因为前面我们已经学会了一对一关系的建立方法,而多对一也是类似的。没错,数据库表中多对一的关系仍然是通过外键来建立的,只不过一对一的时候外键加在哪一张表上都可以,但多对一的时候关键必须要加在多方的表中。因此,多对一关系的表结构就可以这样设计:

3012329-4c777af496e47a54

在comment表中有一个news_id列,这是一个外键列,里面应该存放一个具体的新闻id,并且允许多条comment都存放同一个新闻id,这样一条评论就只能对应一条新闻,但一条新闻却可以有多条评论,也就实现多对一的关系了,如下图所示:

3012329-9e0d609a1738428e

由此我们就可以看出,id为1、2、3的三条评论是属于第一条新闻的,而id为4、5的两条评论是属于第二条新闻的。

多对多

表示两张关联表中的数据都可以对应另一张表中的多条数据。这种场景也不算是很常见,但比一对一关系要稍微更加常用一些。举个例子,我们都知道新闻网站是会将新闻进行种类划分的,这样用户就可以选择自己喜欢的那一类新闻进行浏览,比如说网易新闻中就会有头条、科技、娱乐、手机等等种类。每个种类下面当然都会有许多条新闻,而一条新闻也可能是属于多个种类的,比如iPhone6发布的新闻既可以属于手机种类,也可以属于科技种类,甚至还可以上头条。因此,新闻和种类之间就是一种多对多的关系,如下图所示:

3012329-2ae6e270b6da0c98

可以看到,News1是属于Category1的,而News2和News3都是既属于Category1也属于Category2,如此复杂的关联关系该如何表示呢?在面向对象的编程语言中一切都是那么的简单,只需要在News类中使用集合类声明拥有多个Category,然后在Category类中也使用集合类声明拥有多个News就可以了,我们稍后就会看到。而难点仍然是留在了数据库上,两张表之间如何建立多对多的关联关系呢,还是用外键吗?肯定不行了,多对多的情况只能是借助中间表来完成了。也就是说,我们需要多建立一张表,这张表没什么其它作用,就是为了存放news表和category表之间的关联关系的,如下图所示:

3012329-d4e052119727f137

注意这里我们建立一张名为category_news的中间表,中间表的命名并没有什么强制性的约束,但一个良好的命名规范可以让你一眼就明白这张表是用来做什么的。中间表里面只有两列,而且也只需要有两列,分别是news表的外键和category表的外键,在这里存放新闻和种类相应的id,就可以让它们之间建立关联关系了,如下图所示:

3012329-8369d84b1d7b590e

由此我们就可以看出,第一条新闻是属于第一个种类的,而第二和第三条新闻,则既属于第一个种类,也属于第二个种类。反过来也可以这样看,第一个种类下面有第一、第二、第三这三条新闻,而第二个种类下面只有第二、第三这两条新闻。不管怎么看,多对多的关系都是成立的。

好了,三种关联关系都讲完了,那我们来简单总结一下吧。虽说上面介绍了花了很大的篇幅讲解数据库的表关联知识,但其实最后的结论是非常简单的,大家可以当成口诀一样背下来。即一对一关联的实现方式是用外键,多对一关联的实现方式也是用外键,多对多关联的实现方式是用中间表。记下了这个口诀,在很多数据库设计的时候,你都可以发挥得更加游刃有余。

使用LitePal建立表关联

虽说口诀就是这个样子,但牵扯到表关联的时候毕竟增加了建表的难度,建表语句会更加复杂,你也需要格外地小心以防止出现什么错误。因此,使用LitePal来自动建立表关联又是一个非常不错的选择,我们不需要关心什么外键、中间表等实现的细节,只需要在对象中声明好它们相互之间的引用关系,LitePal就会自动在数据库表之间建立好相应的关联关系了,下面我们就来尝试一下吧。

首先确定一下一共涉及到了哪些实体类,News和Comment,这两个类我们在前两篇文章中就已经建好了,然后还需要有Introduction和Category这两个类,新建Introduction类,代码如下所示:

public class Introduction {  
      
    private int id;  
      
    private String guide;  
      
    private String digest;  
      
    // 自动生成get、set方法  
} 

接着新建Category类,代码如下所示:

public class Category {  
      
    private int id;  
      
    private String name;  
      
    // 自动生成get、set方法  
}  

现在四个类都已经建好了,但目前它们都还是各自独立的,互相之间没有任何联系,那么我们现在就开始用极为简单易懂的方式来给它们建立关联吧。首先,News和Introduction是一对一的关系,那就可以在News类中添加如下引用:

public class News {  
    ...  
    private Introduction introduction;  
      
    // 自动生成get、set方法  
}  

就是这么简单,在News类中可以得到一个对应的Introduction的实例,那么它们之间就是一对一关系了。

接着Comment和News是多对一的关系,因此News中应该包含多个Comment,而Comment中应该只有一个News,所以就可以这样写:

public class News {  
    ...  
    private Introduction introduction;  
      
    private List<Comment> commentList = new ArrayList<Comment>();  
      
    // 自动生成get、set方法  
}  

先使用一个泛型为Comment的List集合来表示News中包含多个Comment,然后修改Comment类的代码,如下所示:

public class Comment {  
    ...  
    private News news;  
      
    // 自动生成get、set方法   
}  

在Comment类中声明了一个News的实例,这样就清楚地表示出了News中可以包含多个Comment,而Comment中只能有一个News,也就是多对一的关系了。

最后News和Category是多对多的关系,相信聪明的你一定已经知道该怎么写了。News中可以包含多个Category,所以仍然应该使用List集合来表示:

public class News {  
    ...  
    private Introduction introduction;  
      
    private List<Comment> commentList = new ArrayList<Comment>();  
      
    private List<Category> categoryList = new ArrayList<Category>();  
      
    // 自动生成get、set方法  
}  

而Category中也可以包含多个News,因此Category类也应该使用相同的写法,如下所示:

public class Category {  
    ...  
    private List<News> newsList = new ArrayList<News>();  
      
    // 自动生成get、set方法  
} 

这样就清楚地表达出它们之间是多对多的关联了。

关联关系都声明好了之后,我们只需要将所有的实体类都添加到映射列表当中,并将数据库版本号加1就可以了。修改litepal.xml的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>  
<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="4" ></version>  
  
    <list>  
        <mapping class="com.example.databasetest.model.News"></mapping>  
        <mapping class="com.example.databasetest.model.Comment"></mapping>  
        <mapping class="com.example.databasetest.model.Introduction"></mapping>  
        <mapping class="com.example.databasetest.model.Category"></mapping>  
    </list>  
</litepal>  

基本上到这里就可以轻松地说结束了,现在只需要任意操作一下数据库,表之间的关联关系就将会自动建立,比如说调用一下Connector.getDatabase()方法。

下面我们来验证一下吧,输入.table命令查看一下当前数据库中的表,如下所示:

3012329-2b7c6fd85d810e62

OK,news、comment、category、introduction这几张表全都有了,除此之外还有一张category_news中间表。那我们要来一一检查一下了,先查看一下introduction表的结构吧,如下所示:

3012329-0dcd392c81e78b46

可以看到,多了一个news_id列,说明introduction表和news表之间的一对一关系已经建立好了。

然后再检查一下comment表的结构,如下所示:

3012329-f31df196cf6165a2

OK,comment表中也有一个news_id的列,那么comment表和news表之间的多对一关系也已经建立好了。

最后检查一下category_news这张中间表的结构,如下所示:

3012329-f1651934c8312335

一共只有两列,一列是news_id,一列是category_id,分别对应着两张表的外键,这样news表和category表的多对多关系也建立好了。

传统的存储数据方式

其实最传统的存储数据方式肯定是通过SQL语句拼接字符串来进行存储的,不过这种方式有点过于“传统”了,今天我们在这里就不讨论这种情况。实际上,Android专门提供了一种用于存储数据的简便方法,使得我们不用编写SQL语句就可以执行存储操作。下面来看一下SQLiteDatabase中的insert()方法:

public long insert(String table, String nullColumnHack, ContentValues values)  

可以看到,insert方法接收三个参数,第一个参数是表名,第二个参数通常都用不到,直接传null,第三个参数则是一个封装了待存储数据的ContentValues对象。因此,比如说我们想往news表中插入一条新闻,就可以这样写:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
ContentValues values = new ContentValues();  
values.put("title", "这是一条新闻标题");  
values.put("content", "这是一条新闻内容");  
values.put("publishdate", System.currentTimeMillis());  
long id = db.insert("news", null, values);

其中,调用ContentValues的put()方法来添加待存储数据,put()方法接收两个参数,第一个参数是数据库表中对应的列名,第二个参数就是要存储的值,最后调用一下insert()方法,这条新闻就会插入到news表当中了,并且该数据行对应的id会作为返回值进行返回。

用法很简单是吗?确实,比起直接使用SQL语句,SQLiteDatabase中提供的insert()方法的确简单了很多。但insert()方法也并非是那么的完美,它还是有很多不方便的地方的,比如说没有考虑表关联的情况,我们需要手动对关联表的外键进行存储。再比如说,没有提供批量存储的功能,当我们有一个集合的数据需要存储时,需要通过循环来遍历这个集合,然后一次次地调用insert()方法来插入数据。

好了,那么关于传统存储数据的用法就简单介绍到这里,因为确实没什么的更多的用法了,并且它也不是我们今天的主角。接下来,就让我们看一看今天的惊喜,学习如何使用LitePal来进行数据库存储的操作。

使用LitePal存储数据

LitePal中与存储相关的API其实并不多,但用法还是颇为丰富的,而且比起传统的insert()方法,使用LitePal来存储数据可以简单到让你惊叹的地步,那么今天我们就来完整地学习一下LitePal存储数据的所有用法。

在前面几篇文章当中,我们在项目里已经建好了News、Comment、Introduction、Category这几个实体类,通过这些实体类,LitePal就可以把相应的表自动创建出来。现在来观察这几个实体类,我们发现这几个类都是没有继承结构的。没错,因为LitePal进行表管理操作时不需要这些实体类有任何的继承结构,当时为了简单起见就没有写。但是进行CRUD操作时就不行了,LitePal要求所有的实体类都要继承自DataSupport这个类,因此这里我们就要把继承结构给加上才行。修改News类的代码,如下所示:

public class News extends DataSupport{  
      
    ......  
      
    // 自动生成get、set方法  
}  

可以看到,这里只是让News类继承自了DataSupport,其它什么都没有改变。另外几个Comment、Introduction、Category类也使用同样的改法,这里就不一一演示了。

继承了DataSupport类之后,这些实体类就拥有了进行CRUD操作的能力,那么比如想要存储一条数据到news表当中,就可以这样写:

News news = new News();  
news.setTitle("这是一条新闻标题");  
news.setContent("这是一条新闻内容");  
news.setPublishDate(new Date());  
news.save();  

怎么样?是不是非常简单,不需要SQLiteDatabase,不需要ContentValues,不需要通过列名组装数据,甚至不需要指定表名,只需要new出一个News对象,然后把要存储的数据通过setter方法传入,最后调用一下save()方法就好了,而这个save()方法自然就是从DataSupport类中继承而来的了。

除此之外,save()方法还是有返回值的,我们可以根据返回值来判断存储是否成功,比如说这样写:

if (news.save()) {  
    Toast.makeText(context, "存储成功", Toast.LENGTH_SHORT).show();  
} else {  
    Toast.makeText(context, "存储失败", Toast.LENGTH_SHORT).show();  
}  

可以看出,save()方法返回的是一个布尔值,用于表示存储成功还是失败,但同时也说明这个方法是不会抛出异常的。有些朋友希望如果存储失败的话就抛出异常,而不是返回一个false,那就可以使用saveThrows()方法来代替,如下所示:

News news = new News();  
news.setTitle("这是一条新闻标题");  
news.setContent("这是一条新闻内容");  
news.setPublishDate(new Date());  
news.saveThrows();  

使用saveThrows()方法来存储数据,一旦存储失败就会抛出一个DataSupportException异常,我们可以通过对这个异常进行捕获来处理存储失败的情况。

那有些细心的朋友可能已经注意到,使用的insert()方法来存储数据时是有返回值的,返回的是插入行对应的id。但LitePal中的save()方法返回的是布尔值,那么我们怎样才能拿到存储成功之后这条数据对应的id呢?对此,LitePal使用了一种非常巧妙的做法,还记得我们在每个实体类中都定义了一个id字段吗?当调用save()方法或saveThrows()方法存储成功之后,LitePal会自动将该条数据对应的id赋值到实体类的id字段上。让我们来做个试验吧,代码如下所示:

News news = new News();  
news.setTitle("这是一条新闻标题");  
news.setContent("这是一条新闻内容");  
news.setPublishDate(new Date());  
Log.d("TAG", "news id is " + news.getId());  
news.save();  
Log.d("TAG", "news id is " + news.getId());  

在save之前打印一下news的id,在save之后再打印一次,现在运行一下,打印结果如下所示:

3012329-a2a0bbfe3c26c1ad

OK,在save之前打印的id是0,说明此时id这个字段还没有被赋值,在save之后打印的id是1,说明此时id已经被赋值了。那么我们再到数据库表中再查看一下这条记录到底有没有存储成功吧,如下图所示:

3012329-f2f71f92d2ccc895

可以看到,这条新闻确实已经存储成功了,并且对应的id正是1,和我们前面打印的结果是一致的。

不过LitePal的存储功能显示不仅仅只有这些用法,事实上,LitePal在存储数据的时候默默帮我们做了很多的事情,比如多个实体类之间有关联关系的话,我们不需要考虑在存储数据的时候怎么去建立数据与数据之间的关联,因为LitePal一切都帮我们做好了。

还是通过一个例子来看一下吧,Comment和News之间是多对一的关系,一条News中是可以包含多条评论的,因此我们就可以这样写:

Comment comment1 = new Comment();  
comment1.setContent("好评!");  
comment1.setPublishDate(new Date());  
comment1.save();  
Comment comment2 = new Comment();  
comment2.setContent("赞一个");  
comment2.setPublishDate(new Date());  
comment2.save();  
News news = new News();  
news.getCommentList().add(comment1);  
news.getCommentList().add(comment2);  
news.setTitle("第二条新闻标题");  
news.setContent("第二条新闻内容");  
news.setPublishDate(new Date());  
news.setCommentCount(news.getCommentList().size());  
news.save();  

可以看到,这里先是存储了一条comment1数据,然后存储一条comment2数据,接着在存储News之前先把刚才的两个Comment对象添加到了News的commentList列表当中,这样就表示这两条Comment是属于这个News对象的,最后再把News存储到数据库中,这样它们之间的关联关系就会自动建立了。让我们查看数据库表检查一下吧,首先看一下news表,如下所示:

3012329-19c4af2c399282fe

OK,第二条新闻已经成功存储到news表中了,这条新闻的id是2。那么从哪里可以看出来关联关系呢?我们在上一篇文章中学过,多对一关联的时候,外键是存放在多方的,因此关联关系我们要到comment表中去查看,如下所示:

3012329-01dfb8bda53f6fe7

可以看到,两条评论都已经成功存储到comment表中了,并且这两条评论的news_id都是2,说明它们是属于第二条新闻的。怎么样,仅仅是在存储数据之前建立好实体类之间的关系,再调用一下save()方法,那么数据之间的关联关系就会自动建立了,是不是非常简单?上面的代码只是多对一情况的一种用法,还有一对一和多对多的情况,其实用法都是差不多的,相信你已经能举一反三了。

另外,LitePal对集合数据的存储还专门提供了一个方法,比如说我们有一个News集合,那么应该怎样去存储这个集合中的每条News呢?传统情况下可以这样写:

List<News> newsList;  
...  
for (News news : newsList) {  
    news.save();  
}  

通过一个循环来遍历出这个集合中的每一个News对象,然后逐个调用save()方法。这样的写法当然是可以的,但是效率会比较低,因为调用save()方法的时候除了会执行存储操作之外,还会去分析News类的关联关系,那么每次循环都去重新分析一遍关联关系显然是比较耗时的。因此,LitePal提供了一个saveAll()方法,专门用于存储集合数据的,用法如下所示:

List<News> newsList;  
...  
DataSupport.saveAll(newsList);  

saveAll()方法接收一个Collection集合参数,只要把待存储的集合数据传入即可。这个方法可以完成和上面一段代码完全一样的功能,但效率却会高得多,而且写法也更加简单。

传统的修改和删除数据方式

上篇文章中我们已经得知,SQLiteDatabase类中提供了一个insert()方法用于插入数据,那么类似地,它还提供了update()和delete()这两个方法,分别用于修改和删除数据。先来看一下update()方法的方法定义:

public int update(String table, ContentValues values, String whereClause, String[] whereArgs)  

update()方法接收四个参数,第一个参数是表名,第二个参数是一个封装了待修改数据的ContentValues对象,第三和第四个参数用于指定修改哪些行,对应了SQL语句中的where部分。

那么比如说我们想把news表中id为2的记录的标题改成“今日iPhone6发布”,就可以这样写:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6发布");  
db.update("news", values, "id = ?", new String[] {"2"});  

其作用相当于如下SQL语句:

update news set title='今日iPhone6发布' where id=2;  

可以看出,比起直接使用SQL语句,update()方法的语义性明显更强,也更容易让人理解。
接下来再看一下delete()方法的方法定义:

public int delete(String table, String whereClause, String[] whereArgs)  

delete()方法接收三个参数,第一个参数同样是表名,第二和第三个参数用于指定删除哪些行,对应了SQL语句中的where部分。

那么比如说我们想把news表中所有没有评论的新闻都删除掉,就可以这样写:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
db.delete("news", "commentcount = ?", new String[] {"0"}); 

其作用相当于如下SQL语句:

delete from news where commentcount=0;  

由此可见,Android给我们提供的这些帮助方法,在很大程度上确实简化了不少数据库操作的复杂度。不过LitePal显然做到了更好,下面就让我们学习一下如何使用LitePal来进行修改和删除操作。

使用LitePal修改数据

LitePal修改数据的API比较简单,并没有什么太多的用法,也比较好理解,方法都是定义在DataSupport类中的,我们先来看一下方法定义:

public static int update(Class<?> modelClass, ContentValues values, long id)  

这个静态的update()方法接收三个参数,第一个参数是Class,传入我们要修改的那个类的Class就好,第二个参数是ContentValues对象,这三个参数是一个指定的id,表示我们要修改哪一行数据。

那么比如说我们想把news表中id为2的记录的标题改成“今日iPhone6发布”,就可以这样写:

ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6发布");  
DataSupport.update(News.class, values, 2);  

可以看出,总体来讲还是比原生的用法要简单一些的,首先我们避免掉了要去获取SQLiteDatabase对象的步骤,其次在指定修改某一条id记录的时候只需要传入这个id即可,语法更简练。

那么有的朋友可能会问了,也许我想修改的是某一个条件下的所有数据,而不是仅仅修改某个id的数据,那该怎么办呢?别担心,LitePal还提供了另外一个简便的方法,方法定义如下:

public static int updateAll(Class<?> modelClass, ContentValues values, String... conditions)  

updateAll()方法表示修改多行记录,其中第一个参数仍然是Class,第二个参数还是ContentValues对象,第三个参数是一个conditions数组,用于指定修改哪些行的约束条件,返回值表示此次修改影响了多少行数据。

那么比如说我们想把news表中标题为“今日iPhone6发布”的所有新闻的标题改成“今日iPhone6 Plus发布”,就可以这样写:

ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6 Plus发布");  
DataSupport.updateAll(News.class, values, "title = ?", "今日iPhone6发布");  

前面都没什么好说的,重点我们看一下最后的这个conditions数组,由于它的类型是一个String数组,我们可以在这里填入任意多个String参数,其中最前面一个String参数用于指定约束条件,后面所有的String参数用于填充约束条件中的占位符(即?号),比如约束条件中有一个占位符,那么后面就应该填写一个参数,如果有两个占位符,后面就应该填写两个参数,以此类推。

比如说我们想把news表中标题为“今日iPhone6发布”且评论数量大于0的所有新闻的标题改成“今日iPhone6 Plus发布”,就可以这样写:

ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6 Plus发布");  
DataSupport.updateAll(News.class, values, "title = ? and commentcount > ?", "今日iPhone6发布", "0");  

可以看出,通过占位符的方式来实现条件约束明显要比原生的API更加简单易用。

那么如果我们想把news表中所有新闻的标题都改成“今日iPhone6发布”,该怎么写呢?其实这就更简单了,只需要把最后的约束条件去掉就行了,如下所示:

ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6 Plus发布");  
DataSupport.updateAll(News.class, values); 

怎么样,这种写法是不是感觉语义性非常强?updateAll()方法在不指定约束条件的情况下就是修改所有行的数据,的的确确是update all了。

当然有些朋友可能会觉得这样用起来还是有点复杂,因为这个ContentValues对象很烦人,每次创建它的时候都要写很多繁琐的代码。没关系,LitePal也充分考虑了这种情况,提供了一种不需要ContentValues就能修改数据的方法,下面我们尝试使用这种新方法来完成上述同样的功能。

比如把news表中id为2的记录的标题改成“今日iPhone6发布”,就可以这样写:

News updateNews = new News();  
updateNews.setTitle("今日iPhone6发布");  
updateNews.update(2);  

这次我们并没有用ContentValues,而是new出了一个News对象,把要修改的数据直接set进去,最后调用一下update()方法并传入id就可以了。不仅不用创建ContentValues对象,连表名都不用指定了,因为News对象默认就是修改的news表。

这是其中一种用法,那么如果我们想把news表中标题为“今日iPhone6发布”且评论数量大于0的所有新闻的标题改成“今日iPhone6 Plus发布”,就可以这样写:

News updateNews = new News();  
updateNews.setTitle("今日iPhone6发布");  
updateNews.updateAll("title = ? and commentcount > ?", "今日iPhone6发布", "0");

还是非常好理解的,这里我就不再详细解释了。

但是这种用法有一点需要注意,就是如果我们想把某一条数据修改成默认值,比如说将评论数修改成0,只是调用updateNews.setCommentCount(0)这样是不能修改成功的,因为即使不调用这行代码,commentCount的值也默认是0。所以如果想要将某一列的数据修改成默认值的话,还需要借助setToDefault()方法。用法也很简单,在setToDefault()方法中传入要修改的字段名就可以了(类中的字段名),比如说我们想要把news表中所有新闻的评论数清零,就可以这样写:

News updateNews = new News();  
updateNews.setToDefault("commentCount");  
updateNews.updateAll();  

使用LitePal删除数据

LitePal删除数据的API和修改数据是比较类似的,但是更加的简单一些,我们先来看一下DataSupport类中的方法定义,如下所示:

public static int delete(Class<?> modelClass, long id)  

delete()方法接收两个参数,第一个参数是Class,传入我们要删除的那个类的Class就好,第二个参数是一个指定的id,表示我们要删除哪一行数据。

那么比如说我们想删除news表中id为2的记录,就可以这样写:

DataSupport.delete(News.class, 2);  

需要注意的是,这不仅仅会将news表中id为2的记录删除,同时还会将其它表中以news id为2的这条记录作为外键的数据一起删除掉,因为外键既然不存在了,那么这么数据也就没有保留的意义了。

说起来可能有点拗口,我们还是举例看一下。比如news表中目前有两条数据,如下图所示:

3012329-b1b8b5ed625bf72b

然后comment表中也有两条数据,如下图所示:

3012329-f17491cc296a2a35

其中comment表中两条数据的外键都是2,指向的news表中id为2的这条记录。那么下面我们执行如下删除语句:

int deleteCount = DataSupport.delete(News.class, 2);  
Log.d("TAG", "delete count is " + deleteCount); 

其中delete()方法的返回值表示被删除的记录数,打印结果如下所示:

3012329-b3c7f161559167dd

可以看到,有三条记录被删除了,那我们再到news表中查询一下:

3012329-e235ace49d409e86

OK,只剩下一条记录了,id为2的那条记录确实被删除了。那么再到comment表中看一下呢,如下图所示:

3012329-ba7d22d3c6bf50cb

数据全没了!为什么呢?因为comment表中的两条数据都是以news表中id为2的数据作为外键的,现在外键不存在了,那么这两条数据自然也没有存在的意义了,因此被删除的记录数一共是3条。这样是不是就好理解了很多呢?

除了删除指定id的数据之外,DataSupport中也提供了一个通过where语句来批量删除数据的方法,先看一下方法定义:

public static int deleteAll(Class<?> modelClass, String... conditions)  

看起来很眼熟吧?非常简单,deleteAll()方法接收两个参数,第一个参数是Class,传入我们要删除的那个类的Class就好,第二个参数是一个conditions数组,用于指定删除哪些行的约束条件,返回值表示此次删除了多少行数据,用法和updateAll()方法是基本相同的。

那么比如说我们想把news表中标题为“今日iPhone6发布”且评论数等于0的所有新闻都删除掉,就可以这样写:

DataSupport.deleteAll(News.class, "title = ? and commentcount = ?", "今日iPhone6发布", "0");  

而如果我们想把news表中所有的数据全部删除掉,就可以这样写:

DataSupport.deleteAll(News.class);  

在不指定约束条件的情况下,deleteAll()方法就会删除表中所有的数据了。

除了DataSupport类中提供的静态删除方法之外,还有一个删除方法是作用于对象上的,即任何一个继承自DataSupport类的实例都可以通过调用delete()这个实例方法来删除数据。但前提是这个对象一定是要持久化之后的,一个非持久化的对象如果调用了delete()方法则不会产生任何效果。

比如说下面这种写法:

News news = new News();  
news.delete();  

这里new出了一个News对象,这个对象明显是没有持久化的,那么此时调用delete()方法则不会删除任何数据。

但如果我们之前将这个对象持久化过了,那么再调用delete()方法就会把这个对象对应的数据删除掉了,比如:

News news = new News();  
news.setTitle("这是一条新闻标题");  
news.setContent("这是一条新闻内容");  
news.save();  
...  
news.delete(); 

一个对象如果save过了之后,那就是持久化的了。除了调用save()方法之外,通过DataSupport中提供的查询方法从数据库中查出来的对象也是经过持久化的,查询的功能我们会在下篇博客中讲解。

另外还有一个简单的办法可以帮助我们判断一个对象是否是持久化之后的,DataSupport类中提供了一个isSaved()方法,这个方法返回true就表示该对象是经过持久化的,返回false则表示该对象未经过持久化。那么删除一个对象对应的数据也就可以这样写了:

News news;  
...  
if (news.isSaved()) {  
    news.delete();  
}  

传统的查询数据方式

其实最传统的查询数据的方式当然是使用SQL语句了,Android当中也提供了直接使用原生SQL语句来查询数据库表的方法,即SQLiteDatabase中的rawQuery()方法,方法定义如下:

public Cursor rawQuery(String sql, String[] selectionArgs)  

其中,rawQuery()方法接收两个参数,第一个参数接收的就是一个SQL字符串,第二个参数是用于替换SQL语句中占位符(?)的字符串数组。rawQuery()方法返回一个Cursor对象,所有查询到的数据都是封闭在这个对象当中的,我们只要一一取出就可以了。

当然这种用法其实并不是很常用,因为相信大多数人都还是不喜欢编写SQL语句的。所以,Android专门提供了一种封装好的API,使得我们不用编写SQL语句也能查询出数据,即SQLiteDatabase中的query()方法。query()提供了三个方法重载,其中参数最少的一个也有七个参数,我们来看下方法定义:

public Cursor query(String table, String[] columns, String selection,  
            String[] selectionArgs, String groupBy, String having,  
            String orderBy)  

其中第一参数是表名,表示我们希望从哪张表中查询数据。第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于去约束查询某一行或某几行的数据,不指定则默认是查询所有行的数据。第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。

这个方法是query()方法最少的一个方法重载了,另外还有两个方法重载分别是八个和九个参数。虽说这个方法在Android数据库表查询的时候非常常用,但重多的参数让我们在理解这个方法的时候可能会很费力,另外使用起来的时候也会相当的不爽。比如说,我们想查询news表中的所有数据,就应该要这样写:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
Cursor cursor = db.query("news", null, null, null, null, null, null);  

可以看到,将第一个表名参数指定成news,然后后面的六个参数我们都用不到,就全部指定成null。

那如果是我们想查询news表中所有评论数大于零的新闻该怎么写呢?代码如下所示:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
Cursor cursor = db.query("news", null, "commentcount>?", new String[]{"0"}, null, null, null);  

由于第三和第四个参数是用于指定约束条件的,所以我们在第三个参数中指明了commentcount>?,然后在第四个参数中通过一个String数组来替换占位符,这样查到的结果就是news表中所有评论数大于零的新闻了。那么其它的几个参数呢?仍然用不到,所以还是只能传null。

然后我们可以看到,query()方法的返回值是一个Cursor对象,所有查询到的数据都是封装在这个对象中的,所以我们还需要将数据逐一从Cursor对象中取出,然后设置到News实体类当中,如下所示:

List<News> newsList = new ArrayList<News>();  
if (cursor != null && cursor.moveToFirst()) {  
    do {  
        int id = cursor.getInt(cursor.getColumnIndex("id"));  
        String title = cursor.getString(cursor.getColumnIndex("title"));  
        String content = cursor.getString(cursor.getColumnIndex("content"));  
        Date publishDate = new Date(cursor.getLong(cursor.getColumnIndex("publishdate")));  
        int commentCount = cursor.getInt(cursor.getColumnIndex("commentcount"));  
        News news = new News();  
        news.setId(id);  
        news.setTitle(title);  
        news.setContent(content);  
        news.setPublishDate(publishDate);  
        news.setCommentCount(commentCount);  
        newsList.add(news);  
    } while (cursor.moveToNext());  
}  

这大概就是传统查询数据方式的用法了,总体来看,用法确实非常不友好,尤其是query()方法冗长的参数列表,即使我们用不到那些参数,也必须要传入许多个null。另外,查询到的数据还都只是封装到了一个Cursor对象中,我们还需要将数据一一取出然后再set到实体类对象当中。麻烦吗?可能你觉得不麻烦,因为你已经习惯了这种用法。但是习惯总是可以改变的,也许当你体验了LitePal中查询API给我们带来的便利之后,就会有了新的看法了,那么下面我们就一起来体验一下LitePal的查询艺术。

使用LitePal查询数据

LitePal在查询方面提供了非常丰富的API,功能多种多样,基本上已经能够满足我们平时所有的查询需求了。不仅如此,LitePal在查询API的设计方面也是非常用心,摒弃了原生query()方法中繁琐的参数列表,而是改用了一种更为灵巧的方式——连缀查询。除此之外,LitePal查询的结果也不再返回Cursor对象,然后再由开发者自己去逐个取出,而是直接返回封装好的对象。这些改变都使得查询数据变得更加简单,也更加合理,那么下面我们就来完整地学习一下LitePal中查询数据的所有用法。

简单查询

比如说现在我们想实现一个最简单的功能,查询news表中id为1的这条记录,使用LitePal就可以这样写:

News news = DataSupport.find(News.class, 1);  

天呐!有没有觉得太轻松了?仅仅一行代码,就可以把news表中id为1的记录查出来了,而且结果还是自动封装到News对象里的,也不需要我们手动再从Cursor中去解析。如果是用原生的SQL语句,或者query()方法来写,至少要20行左右的代码才能完成同样的功能!

那我们先冷静一下,来分析分析这个find()方法。可以看到,它的参数列表也比较简单,只接收两个参数,第一个参数是一个泛型类,也就是说我们在这里指定什么类,返回的对象就是什么类,所以这里传入News.class,那么返回的对象也就是News了。第二个参数就更简单了,就是一个id值,如果想要查询id为1的记录就传1,想查id为2的记录就传2,以此类推。

本来一个还算颇为复杂的功能,通过LitePal之后就变得这么简单了!那么你可能已经迫不及待地想要学习更多LitePal中更多的查询用法了,别着急,我们一个个来看。

你也许遇到过以下场景,在某些情况下,你需要取出表中的第一条数据,那么传统的做法是怎么样的呢?在SQL语句中指定一个limit值,然后获取返回结果的第一条记录。但是在LitePal中不用这么麻烦,比如我们想要获取news表中的第一条数据,只需要这样写:

News firstNews = DataSupport.findFirst(News.class);  

OK,语义性非常强吧,让人一眼就看懂是什么意思了,只需调用findFirst()方法,然后传入News类,得到的就是news表中的第一条数据了。

那我们举一翻三,如果是想要获取News表中的最后一条数据该怎么写呢?同样简单,如下所示:

News lastNews = DataSupport.findLast(News.class);  

因为获取表中第一条或者是最后一条数据的场景比较常见,所以LitePal特意提供了这两个方法来方便我们的操作。

那么我们看到这里,目前都只是查询单条数据的功能,如果想要查询多条数据该怎么办呢?比如说,我们想把news表中id为1、3、5、7的数据都查出来,该怎么写呢?也许有的朋友会比较聪明,立马就想到可以一个个去查,就调用四次find()方法嘛,然后把1、3、5、7这四个id分别传进去不就可以了。没错,这样做完全是可以的,而且效率也并不低,但是LitePal给我们提供了一个更简便的方法——findAll()。这个方法的用法和find()方法是非常类似的,只不过它可以指定多个id,并且返回值也不再是一个泛型类对象,而是一个泛型类集合,如下所示:

List<News> newsList = DataSupport.findAll(News.class, 1, 3, 5, 7);  

可以看到,首先我们是调用的findAll()方法,然后这个方法的第一个参数仍然是指定的泛型类,但是后面的参数就很随意了,你可以传入任意个id进去,findAll()方法会把所有传入的id所对应的数据全部查出来,然后一起返回到List<News>这个泛型集合当中。

虽说这个语法设计算是相当人性化,但是在有些场景或许不太适用,因为可能要你要查询的多个id已经封装到一个数组里了。那么没关系,findAll()方法也是接收数组参数的,所以说同样的功能你也可以这样写:

long[] ids = new long[] { 1, 3, 5, 7 };  
List<News> newsList = DataSupport.findAll(News.class, ids);  

看到这里,那有的朋友可能会奇怪了,说findAll()方法不应该是查询所有数据的意思吗?怎么总是查询几个id所对应数据呢?哈!这个问题问得好,因为findAll()方法也是可以查询所有数据的,而且查询所有数据的写法更简单,只需要这样写:

List<News> allNews = DataSupport.findAll(News.class);  

看到没有,我们只需要把后面的参数都去掉,在不指定具体id的情况下,findAll()方法查询出的就是news表中的所有数据了,是不是语义性非常强?

而且大家不要以为刚才这些都只是findAll()的几个方法重载而已,实际上刚才我们的这几种用法都是调用的同一个findAll()方法!一个方法却能够实现多种不同的查询效果,并且语义性也很强,让人一看就能理解,这就是LitePal的查询艺术!

连缀查询

当然了,LitePal给我们提供的查询功能还远远不只这些,好戏还在后头。相信大家现在也已经发现了,我们目前的查询功能都是基于id来进行查询的,并不能随意地指定查询条件。那么怎样才能指定查询条件呢?让我们回想一下传统情况应该怎么做,query()方法中接收七个参数,其中第三和第四个参数就是用于指定查询条件的,然后其它几个参数都填null就可以了。但是呢,前面我们已经痛批过了这种写法,因为冗长的参数列表太过繁琐,那么LitePal又是怎么解决这个问题的呢?我们现在就来学习一下。

为了避免冗长的参数列表,LitePal采用了一种非常巧妙的解决方案,叫作连缀查询,这种查询很灵活,可以根据我们实际的查询需求来动态配置查询参数。 那这里举个简单的例子,比如我们想查询news表中所有评论数大于零的新闻,就可以这样写:

List<News> newsList = DataSupport.where("commentcount > ?", "0").find(News.class);  

可以看到,首先是调用了DataSupport的where()方法,在这里指定了查询条件。where()方法接收任意个字符串参数,其中第一个参数用于进行条件约束,从第二个参数开始,都是用于替换第一个参数中的占位符的。那这个where()方法就对应了一条SQL语句中的where部分。

接着我们在where()方法之后直接连缀了一个find()方法,然后在这里指定一个泛型类,表示用于查询哪张表。那么上面的一段代码,查询出的结果和如下SQL语句是相同的:

select * from users where commentcount > 0;  

但是这样会将news表中所有的列都查询出来,也许你并不需要那么多的数据,而是只要title和content这两列数据。那么也很简单,我们只要再增加一个连缀就行了,如下所示:

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0").find(News.class);  

可以看到,这里我们新增了一个select()方法,这个方法接收任意个字符串参数,每个参数要求对应一个列名,这样就只会把相应列的数据查询出来了,因此select()方法对应了一条SQL语句中的select部分。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:

select title,content from users where commentcount > 0;  

很好玩吧?不过这还不算完呢,我们还可以继续连缀更多的东西。比如说,我希望将查询出的新闻按照发布的时间倒序排列,即最新发布的新闻放在最前面,那就可以这样写:

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").find(News.class);  

order()方法中接收一个字符串参数,用于指定查询出的结果按照哪一列进行排序,asc表示正序排序,desc表示倒序排序,因此order()方法对应了一条SQL语句中的order by部分。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:

select title,content from users where commentcount > 0 order by publishdate desc;  

然后呢,也许你并不希望将所有条件匹配的结果一次性全部查询出来,因为这样数据量可能会有点太大了,而是希望只查询出前10条数据,那么使用连缀同样可以轻松解决这个问题,代码如下所示:

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").limit(10).find(News.class); 

这里我们又连缀了一个limit()方法,这个方法接收一个整型参数,用于指定查询前几条数据,这里指定成10,意思就是查询所有匹配结果中的前10条数据。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:

select title,content from users where commentcount > 0 order by publishdate desc limit 10;  

刚才我们查询到的是所有匹配条件的前10条新闻,那么现在我想对新闻进行分页展示,翻到第二页时,展示第11到第20条新闻,这又该怎么实现呢?没关系,在LitePal的帮助下,这些功能都是十分简单的,只需要再连缀一个偏移量就可以了,如下所示:

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").limit(10).offset(10)  
        .find(News.class);  

可以看到,这里我们又添加了一个offset()方法,用于指定查询结果的偏移量,这里指定成10,就表示偏移十个位置,那么原来是查询前10条新闻的,偏移了十个位置之后,就变成了查询第11到第20条新闻了,如果偏移量是20,那就表示查询第21到第30条新闻,以此类推。因此,limit()方法和offset()方法共同对应了一条SQL语句中的limit部分。

那么上面的一段代码,查询出的结果和如下SQL语句是相同的:

select title,content from users where commentcount > 0 order by publishdate desc limit 10,10;  

这大概就是LitePal中连缀查询的所有用法了。看出区别了吧?这种查询的好处就在于,我们可以随意地组合各种查询参数,需要用到的时候就把它们连缀到一起,不需要用到的时候不用指定就可以了。对比一下query()方法中那冗长的参数列表,即使我们用不到那些参数,也必须要传null,是不是明显感觉LitePal中的查询更加人性化?

激进查询

不过,上述我们的所有用法中,都只能是查询到指定表中的数据而已,关联表中数据是无法查到的,因为LitePal默认的模式就是懒查询,当然这也是推荐的查询方式。那么,如果你真的非常想要一次性将关联表中的数据也一起查询出来,当然也是可以的,LitePal中也支持激进查询的方式,下面我们就来一起看一下。

不知道你有没有发现,刚才我们所学的每一个类型的find()方法,都对应了一个带有isEager参数的方法重载,这个参数相信大家一看就明白是什么意思了,设置成true就表示激进查询,这样就会把关联表中的数据一起查询出来了。

比如说,我们想要查询news表中id为1的新闻,并且把这条新闻所对应的评论也一起查询出来,就可以这样写:

News news = DataSupport.find(News.class, 1, true);  
List<Comment> commentList = news.getCommentList();

可以看到,这里并没有什么复杂的用法,也就是在find()方法的最后多加了一个true参数,就表示使用激进查询了。这会将和news表关联的所有表中的数据也一起查出来,那么comment表和news表是多对一的关联,所以使用激进查询一条新闻的时候,那么该新闻所对应的评论也就一起被查询出来了。

激进查询的用法非常简单,就只有这么多,其它find()方法也都是同样的用法,就不再重复介绍了。但是这种查询方式LitePal并不推荐,因为如果一旦关联表中的数据很多,查询速度可能就会非常慢。而且激进查询只能查询出指定表的关联表数据,但是没法继续迭代查询关联表的关联表数据。因此,这里我建议大家还是使用默认的懒加载更加合适,至于如何查询出关联表中的数据,其实只需要在模型类中做一点小修改就可以了。修改News类中的代码,如下所示:

public class News extends DataSupport{  
      
    ...  
  
    public List<Comment> getComments() {  
        return DataSupport.where("news_id = ?", String.valueOf(id)).find(Comment.class);  
    }  
      
}  

可以看到,我们在News类中添加了一个getComments()方法,而这个方法的内部就是使用了一句连缀查询,查出了当前这条新闻对应的所有评论。改成这种写法之后,我们就可以将关联表数据的查询延迟,当我们需要去获取新闻所对应的评论时,再去调用News的getComments()方法,这时才会去查询关联数据。这种写法会比激进查询更加高效也更加合理。

原生查询

相信你已经体会到,LitePal在查询方面提供的API已经相当丰富了。但是,也许你总会遇到一些千奇百怪的需求,可能使用LitePal提供的查询API无法完成这些需求。没有关系,因为即使使用了LitePal,你仍然可以使用原生的查询方式(SQL语句)来去查询数据。DataSuppport类中还提供了一个findBySQL()方法,使用这个方法就能通过原生的SQL语句方式来查询数据了,如下所示:

Cursor cursor = DataSupport.findBySQL("select * from news where commentcount>?", "0");  

findBySQL()方法接收任意个字符串参数,其中第一个参数就是SQL语句,后面的参数都是用于替换SQL语句中的占位符的,用法非常简单。另外,findBySQL()方法返回的是一个Cursor对象,这和原生SQL语句的用法返回的结果也是相同的。

传统的聚合函数用法

虽说是聚合函数,但它的用法其实和传统的查询还是差不多的,即仍然使用的是select语句。但是在select语句当中我们通常不会再去指定列名,而是将需要统计的列名传入到聚合函数当中,那么执行select语句使用的还是SQLiteDatabase中的rawQuery()方法。下面我们来尝试一下,比如说想要统计news表中一共有多少行,就可以这样写:

SQLiteDatabase db = dbHelper.getWritableDatabase();  
Cursor c = db.rawQuery("select count(1) from news", null);  
if (c != null && c.moveToFirst()) {  
    int count = c.getInt(0);  
    Log.d("TAG", "result is " + count);  
}  
c.close();  

可以看到,在rawQuery()方法中我们指定了一个聚合查询语句,其中count(1)就是用于去统计一共有多少行的。当然这里并不一定要用count(1),使用count(*)或者count(主键)都可以。然后rawQuery()方法返回的是一个Cursor对象,我们从这个Cursor当中取出第一行第一列的数据,这也就是统计出的结果了。

那如果我们想要统计出news表中评论的总数量该怎么写呢?代码如下所示:


SQLiteDatabase db = dbHelper.getWritableDatabase();  
Cursor c = db.rawQuery("select sum(commentcount) from news", null);  
if (c != null && c.moveToFirst()) {  
    int count = c.getInt(0);  
    Log.d("TAG", "result is " + count);  
}  
c.close();  

我们发现,代码基本是非常相似的,只不过查询语句当中count()函数替换成了sum()函数。当然了,sum()函数要求传入一个指定的列名,表示我们要汇总这一列的总合,因此这里我们传入了commentcount这一列。

其它聚合函数的用法也是类似的,就不一一列举了。由此我们可以总结出一些结论,聚合函数都是要使用rawQuery()方法进行SQL查询,然后结果会封装到Cursor对象当中,接着我们再从Cursor中将结果取出。虽说你可能觉得上面的用法已经足够简单了,因为总共也就只写了六七行代码,但是你有没有想过更简单的写法,比如说只用一行代码就完成聚合查询操作。你没有看错,就是一行代码,LitePal让这些都成为了可能,那么下面我们就来学习一下LitePal中聚合函数的用法。

使用LitePal的聚合函数

LitePal中一共提供了count()、sum()、average()、max()和min()这五种聚合函数,基本上已经将SQL语句当中最常用的几种聚合函数都覆盖了,那么下面我们就来对这五种聚合函数的用法一一进行学习。
count()

count()方法主要是用于统计行数的,刚才演示了如何通过SQL语句来统计news表中一共有多少行,那么下面我们来看一下如何通过LitePal来实现同样的功能,代码如下所示:

int result = DataSupport.count(News.class);  

你没有看错!就是这样一行代码就可以了。调用DataSupport类当中的count()方法,count()方法接收一个Class参数,用于指定去统计哪张表当中的数据,然后返回值是一个整型数据,也就是统计出的结果了。

除此之外,LitePal中所有的聚合函数都是支持连缀的,也就是说我们可以在统计的时候加入条件语句。比如说想要统计一共有多少条新闻是零评论的,就可以这样写:

int result = DataSupport.where("commentcount = ?", "0").count(News.class);  

这个用法和我们在上一篇文章当中学到的连缀查询是比较像的,在DataSupport类中首先指定一个where语句用于条件约束,然后连缀一个count()方法,这样统计出的就是满足条件语句的结果了。连缀不仅适用于count()方法,也同样适用于下面我们将要介绍的所有方法,但由于用法都是相同的,后面就不再重复介绍了。

sum()

看完了count()方法应该是觉得非常简单吧,剩下的几个聚合函数也是同样简单的,我们继续来学习一下。

sum()方法主要是用于对结果进行求合的,比如说我们想要统计news表中评论的总数量,就可以这样写:

int result = DataSupport.sum(News.class, "commentcount", int.class);  

sum()方法的参数要稍微多一点,我们来一一看下。第一个参数很简单,还是传入的Class,用于指定去统计哪张表当中的数据。第二个参数是列名,表示我们希望对哪一个列中的数据进行求合。第三个参数用于指定结果的类型,这里我们指定成int型,因此返回结果也是int型。

需要注意的是,sum()方法只能对具有运算能力的列进行求合,比如说整型列或者浮点型列,如果你传入一个字符串类型的列去求合,肯定是得不到任何结果的,这时只会返回一个0作为结果。

average()

average()方法主要是用于统计平均数的,比如说我们想要统计news表中平均每条新闻有多少评论,就可以这样写:

double result = DataSupport.average(News.class, "commentcount");  

其中average()方法接收两个参数,第一个参数不用说,仍然是Class。第二个参数用于指定列名的,表示我们想要统计哪一列的平均数。需要注意的是,这里返回值的类型是double型,因为平均数基本上都是会带有小数的,用double类型可以最大程序保留小数位的精度。

同样地,average()方法也只能对具有运算能力的列进行求平均值,如果你传入了一个字符串类型的列,也是无法得到任何结果的,这时同样只会返回一个0作为结果。

max()

max()方法主要用于求出某个列中最大的数值,比如我们想要知道news表中所有新闻里面最高的评论数是多少,就可以这样写:

int result = DataSupport.max(News.class, "commentcount", int.class);  

可以看到,max()方法接收三个参数,第一个参数同样还是Class,用于指定去统计哪张表当中的数据。第二个参数是列名,表示我们希望统计哪个列中的最大值。第三个参数用于指定结果的类型,根据实际情况来选择传入哪种类型就行了。

那么不用多说,max()方法也只能对具有运算能力的列进行求最大值的,希望你在使用的时候能够谨记这一点。

min()

min()方法主要用于求出某个列中最小的数值,比如我们想要知道news表中所有新闻里面最少的评论数是多少,就可以这样写:

int result = DataSupport.min(News.class, "commentcount", int.class);  

min()方法和max()方法的用法基本上是一模一样的,参数也是完全相同,只是方法名变了一下。它们一个是求出某一列中的最大值,一个是求出某一列中的最小值,仅此而已。

现在我们已经将LitePal中所有聚合函数的用法全部都学习完了,怎么样,是不是感觉非常的简单?学完之后相信大家也意识到我在开篇的时候并不是在吹牛皮了,确确实实只需要一行代码就可以完成各种聚合查询操作了,上面任何一个统计操作我们都没有写到第二行代码。

好了,经过上面的学习,我们已经将LitePal中最主要的功能基本都学习完了,相信你从头看到这里,也是经历了一个对LitePal零认识,到目前可以熟练使用LitePal的一个过程。那么我们的这个Android数据库高手秘籍教程到这里也就要暂停了,这个系列短期不会再更新,而是等到LitePal发布了新版本,有了新功能之后才会对它继续进行讲解。

猜你喜欢

转载自blog.csdn.net/qq_24671941/article/details/82458063