Android数据库操作(三)——增、删、改、查、聚合函数

前言

本文参考转发摘自:郭霖博客http://blog.csdn.net/guolin_blog?viewmode=contents 强烈建议进入原博客查看学习

1、数据存储操作

  • 通过学习,学会了创建表、升级表、以及建立表关联,接下来就是对数据库的CRUD操作了。其中LitePal提供的CRUD操作的API还是颇为丰富的。

1.1 传统的储存数据方式

  • 其实最传统的存储数据方式肯定是通过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来进行数据库存储的操作。

1.2 使用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());
 //调用sava方法保存(没有继承DataSupport是没有这个方法的)
 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之后再打印一次,现在运行一下,打印结果如下所示:


可以看到,这条新闻确实已经存储成功了,并且对应的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表,如下所示:

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


可以看到,两条评论都已经成功存储到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集合参数,只要把待存储的集合数据传入即可。这个方法可以完成和上面一段代码完全一样的功能,但效率却会高得多,而且写法也更加简单。

  • 好了,这样我们就把LitePal中提供的存储操作的用法全部都学完了,下一节文章当中会开始讲解更新和删除操作的用法。

2、修改操作

  • 上文中,在 增删改查四种操作中,我们已经把“增”学完了*,紧接着我们来学学修改操作。

2.1 传统的方式修改操作

  • 上文中我们已经得知,SQLiteDatabase类中提供了一个 insert()方法用于插入数据,那么类似地,它还提供了 update()这个方法,用于修改数据。先来看一下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()方法的语义性明显更强,也更容易让人理解。

2.2 使用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();  

3、删除操作

3.1 传统的删除数据方式

  • 上文中我们已经得知,SQLiteDatabase类中提供了 insert()和update()方法,分别用于插入和修改数据,那么类似地,它还提供了 delete()这个方法,用于删除数据

  • 接下来再看一下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来进行删除操作。

3.2 使用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表中目前有两条数据,如下图所示:


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


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

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

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


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


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


  • 数据全没了!为什么呢?因为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();  
}  
  • 好了,这样我们就把LitePal中提供的删除数据操作的用法基本都学习完了

4、查询数据

  • 在所有的数据库操作当中,查询操作肯定是最复杂的,用法也是最多的,因此LitePal在查询方面提供的API也是比较丰富,而且LitePal在查询方面的API设计也是颇为艺术的。

4.1 传统的查询数据方式

  • 其实最传统的查询数据的方式当然是使用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的查询艺术。

4.2 通过LitePal查询数据

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

4.2.1 简单查询

  • 比如说现在我们想实现一个最简单的功能,查询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的查询艺术!

4.2.2 连缀查询

  • 当然了,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中的查询更加人性化?

4.2.3 激进查询

  • 不过,上述我们的所有用法中,都只能是查询到指定表中的数据而已,关联表中数据是无法查到的,因为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()方法,这时才会去查询关联数据。这种写法会比激进查询更加高效也更加合理。

4.2.4 原生查询

  • 相信你已经体会到,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语句的用法返回的结果也是相同的。

  • 好了,这样我们就把LitePal中提供的查询数据的方法全部都学完了,

5、聚合函数

  • 在上文当中,我们已经把LitePal查询操作的所有用法都学习完了,很显然,LitePal帮我们提供了非常强大的查询API,使得我们可以极度轻松地完成各种类型的查询。但是呢,在SQL语句当中,有一种查询是比较特殊的,就是聚合函数查询,它不像传统查询一样是将表中的某些列的数据查询出来,而是将查询结果进行聚合和统计,最终将统计后的结果进行返回。因此,任何一个关系型数据库中都会提供像count()、sum()等聚合函数。那么不出你所料,LitePal当中也是对这些聚合函数都进行了封装,让我们的操作可以变得更加简单。

5.1 传统的聚合函数用法

  • 虽说是聚合函数,但它的用法其实和传统的查询还是差不多的,即 仍然使用的是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中聚合函数的用法。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容