LitaPal
LitaPal简介
那么先来简单介绍一下吧,LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发时最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表、増删改查的操作。并且LitePal很“轻”,jar包只有100k不到,而且近乎零配置。
配置创建LitePal
添加依赖
dependencies {
implementation 'org.litepal.android:java:3.0.0'
}
main目录下创建assets目录
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname vaule="People" />
<version value="1" />
<list>
</list>
<!--<dbname>标签用于指定数据库名 <version>标签用于指定版本号 <list>标签用于指定所有的映射模型-->
</litepal>
配置LitePalApplication,修改AndroidMainfest.xml
中代码
<application
android:name="org.litepal.LitePalApplication"/>
- 新建类并继承LitePalSupport(DataSupport已被弃用)
- 在litepal.xml文件中的
<list>
标签下用<mapping>
将新建的类添加到映射模型列表当中。 - 任意进行一次数据库操作,数据库和你想要创建的表格就会自动创建成功
- 升级数据库,只需要写好我们要升级的类,把版本号加1并添加映射关系即可。
package com.example.litepaltest;
import org.litepal.crud.LitePalSupport;
public class Book extends LitePalSupport {
private int id;
private String author;
private double price;
private int pages;
private String name;
private String press;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPress() {
return press;
}
public void setPress(String press) {
this.press = press;
}
}
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" ></dbname>
<version value="1" ></version>
<list>
<mapping class = "com.example.litepaltest.Book"></mapping>
</list>
</litepal>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Connector.getDatabase();//最简单的数据库操作
}
});
使用LitePal建立表关联
关联关系的基础知识
表与表之间的关联关系一共有三种类型,一对一、多对一、和多对多,下面我们分别对这三种类型展开进行讨论。
###一对一
我们已经创建好了news这张表,里面主要记录了新闻的标题和内容,那么除了标题和内容之外,有些新闻还可能带有一些导语和摘要,我们把这两个字段放在一张introduction表中,作为新闻的简介。那么很显然,news表和introduction表就是一对一的关系了,因为一条新闻只能对应一个简介,一个简介也只能属于一条新闻。它们之间的对应关系大概如下图描述的一样:
可以看到,News1对应了Introduction2,News2对应了Introduction3,News3对应了Introduction1,但不管怎么样,它们都是一对一的关系。
这种一对一的关系需要在News类中持有一个Introduction类的引用,然后在Introduction类中也持有一个News类的引用,这样它们之间自然就是一对一的关系了。
对象之间的一对一关系非常简单易懂,那么难点就在于,如何在数据库表中建立这样的一对一关系了。由于数据库并不像面向对象的语言一样支持相互引用,如果想让两张表之间建立一对一的关系,一般就只能通过外键的方式来实现了。因此,一对一关系的表结构就可以这样设计:
对象之间的一对一关系非常简单易懂,那么难点就在于,如何在数据库表中建立这样的一对一关系了。由于数据库并不像面向对象的语言一样支持相互引用,如果想让两张表之间建立一对一的关系,一般就只能通过外键的方式来实现了。因此,一对一关系的表结构就可以这样设计:
introduction表中有一个news_id列,这是一个外键列,里面应该存放一个具体的新闻id,这样一条introduction就能对应一条news,也就实现一对一的关系了,如下图所示:
由此我们就能够看出,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表,它们两个之间就是典型的多对一关系,一条新闻可以有很多条评论,但是一条评论只能是属于一条新闻的。它们的关系如下图所示:
而这种多对一的关系在编程语言中是非常容易体现出来的,比如Java中就有专门集合类,如List、Set等,使用它们的话就能轻松简单地在对象之间建立多对一的关系,我们稍后就会看到。那么,这里的难点仍然是在数据库表中如何建立这样的多对一关系。现在说难点其实已经不难了,因为前面我们已经学会了一对一关系的建立方法,而多对一也是类似的。没错,数据库表中多对一的关系仍然是通过外键来建立的,只不过一对一的时候外键加在哪一张表上都可以,但多对一的时候关键必须要加在多方的表中。因此,多对一关系的表结构就可以这样设计:
在comment表中有一个news_id列,这是一个外键列,里面应该存放一个具体的新闻id,并且允许多条comment都存放同一个新闻id,这样一条评论就只能对应一条新闻,但一条新闻却可以有多条评论,也就实现多对一的关系了,如下图所示:
多对多
表示两张关联表中的数据都可以对应另一张表中的多条数据。这种场景也不算是很常见,但比一对一关系要稍微更加常用一些。举个例子,我们都知道新闻网站是会将新闻进行种类划分的,这样用户就可以选择自己喜欢的那一类新闻进行浏览,比如说网易新闻中就会有头条、科技、娱乐、手机等等种类。每个种类下面当然都会有许多条新闻,而一条新闻也可能是属于多个种类的,比如iPhone6发布的新闻既可以属于手机种类,也可以属于科技种类,甚至还可以上头条。因此,新闻和种类之间就是一种多对多的关系,如下图所示:
可以看到,News1是属于Category1的,而News2和News3都是既属于Category1也属于Category2,如此复杂的关联关系该如何表示呢?在面向对象的编程语言中一切都是那么的简单,只需要在News类中使用集合类声明拥有多个Category,然后在Category类中也使用集合类声明拥有多个News就可以了,我们稍后就会看到。而难点仍然是留在了数据库上,两张表之间如何建立多对多的关联关系呢,还是用外键吗?肯定不行了,多对多的情况只能是借助中间表来完成了。也就是说,我们需要多建立一张表,这张表没什么其它作用,就是为了存放news表和category表之间的关联关系的,如下图所示:
注意这里我们建立一张名为category_news的中间表,中间表的命名并没有什么强制性的约束,但一个良好的命名规范可以让你一眼就明白这张表是用来做什么的。中间表里面只有两列,而且也只需要有两列,分别是news表的外键和category表的外键,在这里存放新闻和种类相应的id,就可以让它们之间建立关联关系了,如下图所示:
由此我们就可以看出,第一条新闻是属于第一个种类的,而第二和第三条新闻,则既属于第一个种类,也属于第二个种类。反过来也可以这样看,第一个种类下面有第一、第二、第三这三条新闻,而第二个种类下面只有第二、第三这两条新闻。不管怎么看,多对多的关系都是成立的。
口诀:
-
一对一关联的实现方式是用外键
-
多对一关联的实现方式也是用外键
-
多对多关联的实现方式是用中间表
使用LitePal建立表关联
使用LitePal来自动建立表关联我们不需要关心什么外键、中间表等实现的细节,只需要在对象中声明好它们相互之间的引用关系,LitePal就会自动在数据库表之间建立好相应的关联关系了。
一对一
首先确定一下一共涉及到了哪些实体类,News和Comment,然后还需要有Introduction和Category这两个类,新建Introduction类,代码如下所示:
public class News extends LitePalSupport {
private int id;
private String title;
private String content;
private Introduction introduction;
private List<Comment> commentlist = new ArrayList<>();
private List<Category> categoryList = new ArrayList<Category>();
public List<Category> getCategoryList() {
return categoryList;
}
public void setCategoryList(List<Category> categoryList) {
this.categoryList = categoryList;
}
public List<Comment> getCommentlist() {
return commentlist;
}
public void setCommentlist(List<Comment> commentlist) {
this.commentlist = commentlist;
}
public Introduction getIntroduction() {
return introduction;
}
public void setIntroduction(Introduction introduction) {
this.introduction = introduction;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getPublishDate() {
return publishDate;
}
public void setPublishDate(Date publishDate) {
this.publishDate = publishDate;
}
public int getCommentCount() {
return commentCount;
}
public void setCommentCount(int commentCount) {
this.commentCount = commentCount;
}
public List<Comment> getComments() {
return LitePal.where("news_id=?", String.valueOf(id)).find(Comment.class);
}
private Date publishDate;
private int commentCount;
}
public class Comment extends LitePalSupport {
private int id;
private String content;
private News news;
public News getNews() {
return news;
}
public void setNews(News news) {
this.news = news;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
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命令查看一下当前数据库中的表,如下所示:
OK,news、comment、category、introduction这几张表全都有了,除此之外还有一张category_news中间表。那我们要来一一检查一下了,先查看一下introduction表的结构吧,如下所示:
可以看到,多了一个news_id列,说明introduction表和news表之间的一对一关系已经建立好了。
然后再检查一下comment表的结构,如下所示:
OK,comment表中也有一个news_id的列,那么comment表和news表之间的多对一关系也已经建立好了。
最后检查一下category_news这张中间表的结构,如下所示:
一共只有两列,一列是news_id,一列是category_id,分别对应着两张表的外键,这样news表和category表的多对多关系也建立好了。
使用LitePal查询数据
简单查询
比如说现在我们想实现一个最简单的功能,查询news表中id为1的这条记录,使用LitePal就可以这样写:
News news = LitePal.find(News.class, 1);
它的参数列表也比较简单,只接收两个参数,第一个参数是一个泛型类,也就是说我们在这里指定什么类,返回的对象就是什么类,所以这里传入News.class,那么返回的对象也就是News了。第二个参数就更简单了,就是一个id值,如果想要查询id为1的记录就传1,想查id为2的记录就传2,以此类推。
比如我们想要获取news表中的第一条数据,只需要这样写:
News firstNews = LitePal.findFirst(News.class);
只需调用findFirst()方法,然后传入News类,得到的就是news表中的第一条数据了。
如果是想要获取News表中的最后一条数据该怎么写呢?同样简单,如下所示:
News lastNews = LitePal.findLast(News.class);
如果想要查询多条数据该怎么办呢?比如说,我们想把news表中id为1、3、5、7的数据都查出来,该怎么写呢?
findAll()
。这个方法的用法和find()方法是非常类似的,只不过它可以指定多个id,并且返回值也不再是一个泛型类对象,而是一个泛型类集合,如下所示:
List<News> newsList = LitePal.findAll(News.class, 1, 3, 5, 7);
虽说这个语法设计算是相当人性化,但是在有些场景或许不太适用,因为可能要你要查询的多个id已经封装到一个数组里了。那么没关系,findAll()方法也是接收数组参数的,所以说同样的功能你也可以这样写:
long[] ids = new long[] { 1, 3, 5, 7 };
List<News> newsList = LitePal.findAll(News.class, ids);
连缀查询
比如我们想查询news表中所有评论数大于零的新闻,就可以这样写:
List<News> newsList = LitePal.where("commentcount > ?", "0").find(News.class);
首先是调用了LitePal的where()
方法,在这里指定了查询条件。where()
方法接收任意个字符串参数,其中第一个参数用于进行条件约束,从第二个参数开始,都是用于替换第一个参数中的占位符的。那这个where()
方法就对应了一条SQL语句中的where部分。
但是这样会将news表中所有的列都查询出来,也许你并不需要那么多的数据,而是只要title和content这两列数据。那么也很简单,我们只要再增加一个连缀就行了,如下所示:
List<News> newsList = DataSupport.select("title", "content")
.where("commentcount > ?", "0").find(News.class);
比如说,我希望将查询出的新闻按照发布的时间倒序排列,即最新发布的新闻放在最前面,那就可以这样写:
List<News> newsList = DataSupport.select("title", "content")
.where("commentcount > ?", "0")
.order("publishdate desc").find(News.class);
order()
方法中接收一个字符串参数,用于指定查询出的结果按照哪一列进行排序,asc表示正序排序,desc表示倒序排序,因此order()方法对应了一条SQL语句中的order by部分。
也许你并不希望将所有条件匹配的结果一次性全部查询出来,因为这样数据量可能会有点太大了,而是希望只查询出前10条数据,那么使用连缀同样可以轻松解决这个问题,代码如下所示:
List<News> newsList = DataSupport.select("title", "content")
.where("commentcount > ?", "0")
.order("publishdate desc").limit(10).find(News.class);
这里我们又连缀了一个limit()方法,这个方法接收一个整型参数,用于指定查询前几条数据,这里指定成10,意思就是查询所有匹配结果中的前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部分。
激进查询
比如说,我们想要查询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 LitePalSupport{
...
public List<Comment> getComments() {
return LitePal.where("news_id = ?", String.valueOf(id)).find(Comment.class);
}
}
可以看到,我们在News类中添加了一个getComments()
方法,而这个方法的内部就是使用了一句连缀查询,查出了当前这条新闻对应的所有评论。改成这种写法之后,我们就可以将关联表数据的查询延迟,当我们需要去获取新闻所对应的评论时,再去调用News的getComments()
方法,这时才会去查询关联数据。这种写法会比激进查询更加高效也更加合理。
原生查询
LitepalSupport类中还提供了一个findBySQL()
方法,使用这个方法就能通过原生的SQL语句方式来查询数据了,如下所示:
Cursor cursor = DataSupport.findBySQL("select * from news where commentcount>?", "0");
使用LitePal的聚合函数
传统的聚合函数使用
在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()
count()方法主要是用于统计行数的,刚才演示了如何通过SQL语句来统计news表中一共有多少行,那么下面我们来看一下如何通过LitePal来实现同样的功能,代码如下所示:
int result = LitePal.count(News.class);
count()方法接收一个Class参数,用于指定去统计哪张表当中的数据,然后返回值是一个整型数据,也就是统计出的结果了。
除此之外,LitePal中所有的聚合函数都是支持连缀的,也就是说我们可以在统计的时候加入条件语句。比如说想要统计一共有多少条新闻是零评论的,就可以这样写:
int result = LitePal.where("commentcount = ?", "0").count(News.class);
首先指定一个where语句用于条件约束,然后连缀一个count()方法,这样统计出的就是满足条件语句的结果了。连缀不仅适用于count()方法,也同样适用于下面我们将要介绍的所有方法,但由于用法都是相同的,后面就不再重复介绍了。
sum()
sum()方法主要是用于对结果进行求合的,比如说我们想要统计news表中评论的总数量,就可以这样写:
int result = LitePal.sum(News.class, "commentcount", int.class);
sum()方法的参数要稍微多一点,我们来一一看下。第一个参数很简单,还是传入的Class,用于指定去统计哪张表当中的数据。第二个参数是列名,表示我们希望对哪一个列中的数据进行求合。第三个参数用于指定结果的类型,这里我们指定成int型,因此返回结果也是int型。
需要注意的是,sum()方法只能对具有运算能力的列进行求合,比如说整型列或者浮点型列,如果你传入一个字符串类型的列去求合,肯定是得不到任何结果的,这时只会返回一个0作为结果。
average()
average()方法主要是用于统计平均数的,比如说我们想要统计news表中平均每条新闻有多少评论,就可以这样写:
double result = LitPal.average(News.class, "commentcount");
average()
法接收两个参数,第一个参数不用说,仍然是Class。第二个参数用于指定列名的,表示我们想要统计哪一列的平均数。需要注意的是,这里返回值的类型是double型,因为平均数基本上都是会带有小数的,用double类型可以最大程序保留小数位的精度。
max()
max()方法主要用于求出某个列中最大的数值,比如我们想要知道news表中所有新闻里面最高的评论数是多少,就可以这样写:
int result = LitaPal.max(News.class, "commentcount", int.class);
max()
方法接收三个参数,第一个参数同样还是Class,用于指定去统计哪张表当中的数据。第二个参数是列名,表示我们希望统计哪个列中的最大值。第三个参数用于指定结果的类型,根据实际情况来选择传入哪种类型就行了。
min()
min()方法主要用于求出某个列中最小的数值,比如我们想要知道news表中所有新闻里面最少的评论数是多少,就可以这样写:
int result = Litapal.min(News.class, "commentcount", int.class);
转自:Android数据库高手秘籍(四)——使用LitePal建立表关联
Android数据库高手秘籍(八)——使用LitePal的聚合函数