LitePal - Android开源数据库框架的CRUD操作笔记

LitePal

LitePal是GitHub上一款开源的Android数据库框架,简介易用并且已支持kotlin,这里对数据库操作做一个笔记,并记录郭霖大神每次的升级带来了哪些功能。

准备

在项目的 build.gradle 文件添加依赖

dependencies {
    implementation 'org.litepal.android:java:3.0.0'
}

Kotlin

dependencies {
    implementation 'org.litepal.android:kotlin:3.0.0'
}

1. 使用LitePal建表

Define the models first. For example you have two models, Album and Song. The models can be defined as below:

  • unique:唯一性
  • defaultValue:默认值
  • nullable:是否允许空
  • ignore:只关注主键对应记录是不存在,无则添加,有则忽略
public class Album extends LitePalSupport {

    @Column(unique = true, defaultValue = "unknown")
    private String name;

    private float price;

    private byte[] cover;

    private List<Song> songs = new ArrayList<Song>();

    // generated getters and setters.
    ...
}
public class Song extends LitePalSupport {

    @Column(nullable = false)
    private String name;

    private int duration;

    @Column(ignore = true)
    private String uselessField;

    private Album album;

    // generated getters and setters.
    ...
}

Then add these models into the mapping list in litepal.xml:

<list>
    <mapping class="org.litepal.litepalsample.model.Album"></mapping>
    <mapping class="org.litepal.litepalsample.model.Song"></mapping>
</list>

OK! The tables will be generated next time you operate database. For example, gets the SQLiteDatabase with following codes:

SQLiteDatabase db = Connector.getDatabase();

2. 使用LitePal存储数据

继承了LitePalSupport类之后,这些实体类就拥有了进行CRUD操作的能力,

  • 存储一条数据到news表当中
News news = new News();  
news.setTitle("这是一条新闻标题");  
news.setContent("这是一条新闻内容");  
news.setPublishDate(new Date());  
news.save();  

save()方法还是有返回值的,我们可以根据返回值来判断存储是否成功

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

如果存储失败的话就抛出异常,而不是返回一个false,那就可以使用saveThrows()方法来代替

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

当调用save()方法或saveThrows()方法存储成功之后,LitePal会自动将该条数据对应的id赋值到实体类的id字段上。
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(); 
  • LitePal提供了一个saveAll()方法,专门用于存储集合数据
List<News> newsList;  
...  
LitePal.saveAll(newsList); 
  • LitePal 1.5.0版本中新增了一个saveOrUpdate()方法,专门用来处理这种不存在就存储,已存在就更新的需求。
Person p = new Person();
p.setName("小明");
p.setAge(16);
p.saveOrUpdate("name=?", p.getName());

调用saveOrUpdate()方法后,LitePal内部会自动判断,如果表中已经存在小明这条记录了,就会自动更新,如果不存在的话,就会自动插入。

  • 集合数据存储

比如说现在我们的Album实体类中有一个集合字段:

public class Album extends DataSupport {
    String name;
    List<String> titles = new ArrayList<>;
    // 生成get set方法
}

下面我们将这个Album存储到数据库中,如下所示:

Album album = new Album();
album.setName("范特西");
album.getTitles().add("爱在西元前");
album.getTitles().add("双截棍");
album.getTitles().add("安静");
album.save();

LitePal会额外进行一个操作,就是创建一个album_titles表,并将集合中的数据存储在这里

当我们去查询album数据的时候,会自动将它所关联的集合数据一起查出来:

Album album = DataSupport.findFirst(Album.class);
List<String> titles = album.getTitles();
for (String title : titles) {
    Log.d(TAG, "title is " + title);
}

除了支持List<String>集合之外,还有List<Integer>、List<Boolean>、List<Long>、List<Float>、List<Double>、List<Character>这几种类型的集合也是支持的。

如果你不希望你的集合数据被存储到数据库中的话,可以使用注解的方式将它忽略掉:

public class Album extends DataSupport {
    String name;
    
    @Column(ignore = true)
    List<String> titles = new ArrayList<>;
    // 生成get set方法
}
  • 如果这只是一个独立的Model,和其它Model没有任何关联,那么就可以调用saveFast()方法,从而大大提升存储效率,saveFast()方法的调用方式和save()方法是完全一样的:
Product product = new Product();
product.setName("Android Phone");
product.setPrice(1999.99);
product.saveFast();  
  • byte[]类型的字段灵活性非常高,它可以用来存储图片,但又不仅限于存储图片,任何二进制的数据都是可以存储的,比如一段小语音,或者是小视频,但不建议在手机数据库中存储较大的二进制数据。添加一个byte[]类型的字段:
public class Product extends DataSupport {    
        
    private String name;    
          
    private double price;    
        
    private byte[] image; 
    // generated getters and setters.
    ...
}

存储一张图片时就可以这样写:

byte[] imageBytes = getImageBytesFromSomewhere();
Product product = new Product();
product.setName("Android Phone");
product.setPrice(1999.99);
product.setImage(imageBytes);
product.saveFast();

在查询的时候byte数据会影响效率,只查询name和price这两列,image这一列数据是不会被查询出来的,因此就完全不会影响效率了

Product product = DataSupport.select("name", "price").where("id = ?", id).find(Product.class);

3. 使用LitePal修改数据

  • 把news表中id为2的记录的标题改成“今日iPhone6发布”
ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6发布");  
LitePal.update(News.class, values, 2);  
News updateNews = new News();  
updateNews.setTitle("今日iPhone6发布");  
updateNews.update(2); 
  • 把news表中标题为“今日iPhone6发布”的所有新闻的标题改成“今日iPhone6 Plus发布”
ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6 Plus发布");  
LitePal.updateAll(News.class, values, "title = ?", "今日iPhone6发布"); 
  • 把news表中标题为“今日iPhone6发布”且评论数量大于0的所有新闻的标题改成“今日iPhone6 Plus发布”
ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6 Plus发布");  
LitePal.updateAll(News.class, values, "title = ? and commentcount > ?", "今日iPhone6发布", "0");  
News updateNews = new News();  
updateNews.setTitle("今日iPhone6发布");  
updateNews.updateAll("title = ? and commentcount > ?", "今日iPhone6发布", "0");  
  • 把news表中所有新闻的标题都改成“今日iPhone6发布”
ContentValues values = new ContentValues();  
values.put("title", "今日iPhone6 Plus发布");  
LitePal.updateAll(News.class, values);  
  • 把news表中所有新闻的评论数清零
News updateNews = new News();  
updateNews.setToDefault("commentCount");  
updateNews.updateAll();  

4. 使用LitePal删除数据

  • 删除news表中id为2的记录
LitePal.delete(News.class, 2);  

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

  • 把news表中标题为“今日iPhone6发布”且评论数等于0的所有新闻都删除掉
LitePal.deleteAll(News.class, "title = ? and commentcount = ?", "今日iPhone6发布", "0");  
  • 把news表中所有的数据全部删除掉
LitePal.deleteAll(News.class);  

5. 使用LitePal查询数据

  • 查询news表中id为1的这条记录
News news = LitePal.find(News.class, 1);  
  • 获取news表中的第一条数据
News firstNews = LitePal.findFirst(News.class);
  • 获取News表中的最后一条数据
News lastNews = LitePal.findLast(News.class);  
  • 获取news表中id为1、3、5、7的数据
List<News> newsList = LitePal.findAll(News.class, 1, 3, 5, 7);  
long[] ids = new long[] { 1, 3, 5, 7 };  
List<News> newsList = LitePal.findAll(News.class, ids);  
  • 查询所有数据
List<News> allNews = LitePal.findAll(News.class);  
  • 查询news表中所有评论数大于零的新闻
    where()方法接收任意个字符串参数,其中第一个参数用于进行条件约束,从第二个参数开始,都是用于替换第一个参数中的占位符的。
List<News> newsList = LitePal.where("commentcount > ?", "0").find(News.class); 

这样会将news表中所有的列都查询出来,如果只要title和content这两列数据

List<News> newsList = LitePal.select("title", "content")  
        .where("commentcount > ?", "0").find(News.class);  
  • 查询出的新闻按照发布的时间倒序排列
List<News> newsList = LitePal.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").find(News.class);  
  • 只查询出前10条数据
List<News> newsList = LitePal.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").limit(10).find(News.class);  
  • 对新闻进行分页展示,翻到第二页时,展示第11到第20条新闻
List<News> newsList = LitePal.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").limit(10).offset(10)  
        .find(News.class);  

offset()方法,用于指定查询结果的偏移量,这里指定成10,就表示偏移十个位置,那么原来是查询前10条新闻的,偏移了十个位置之后,就变成了查询第11到第20条新闻了,如果偏移量是20,那就表示查询第21到第30条新闻,以此类推。
查询出的结果和如下SQL语句是相同的

select title,content from users where commentcount > 0 order by publishdate desc limit 10,10; 
  • 判断某条数据存不存在
if (DataSupport.isExist(Student.class, "name = ?", "Jimmy")) {
  // 存在名叫Jimmy的学生
} else {
  // 不存在名叫Jimmy的学生
}

  • 当目标数据不存在的时候才将数据存入到数据库,比如user表中要求用户名必须唯一,那么就可以这样写:
User user = new User();
user.setUsername("Tom");
user.setPassword("123456")
user.saveIfNotExist("username = ?", "Tom")

激进查询

查询news表中id为1的新闻,并且把这条新闻所对应的评论也一起查询出来

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

建议使用默认的懒加载更加合适,至于如何查询出关联表中的数据,其实只需要在模型类中做一点小修改就可以了。修改News类中的代码,如下所示:

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

这种写法会比激进查询更加高效也更加合理

原生查询

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

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

6. 使用LitePal的聚合函数

统计news表中一共有多少行

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

统计一共有多少条新闻是零评论的

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

统计news表中评论的总数量
第一个参数很简单,还是传入的Class,用于指定去统计哪张表当中的数据。第二个参数是列名,表示我们希望对哪一个列中的数据进行求合。第三个参数用于指定结果的类型,这里我们指定成int型,因此返回结果也是int型。

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

统计news表中平均每条新闻有多少评论

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

某个列中最大的数值

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

某个列中最小的数值

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

7. 异步操作数据库

所有的CRUD方法都加入了一个Async的副本方法,比如说原来有一个find()方法,现在就会多出一个findAsycn()方法,原来有一个save()方法,现在就会多出一个saveAsync()方法。如果你想要进行异步数据库操作的时候,只要去调用原API相对应的Async副本方法就可以了。
每一个Async副本方法的后面添加了一个listen()方法,专门用于监听异步操作的结果。

  • 异步保存
Album album = new Album();
album.setName("album");
album.setPrice(10.99f);
album.setCover(getCoverImageBytes());
album.saveAsync().listen(new SaveCallback() {
    @Override
    public void onFinish(boolean success) {
    }
});
  • 异步查询
LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
    @Override
    public void onFinish(Song song) {

    }
});
  • 异步查询多条
LitePal.where("duration > ?", "100").findAsync(Song.class).listen(new FindMultiCallback<Song>() {
    @Override
    public void onFinish(List<Song> list) {

    }
});
  • 泛型优化(3.0.0引入)
LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
    @Override
    public void onFinish(Song song) {

    }
});

8. 加密(1.6.0版本加入)

比如我们有一个Book类,类中有一个name字段和一个page字段,现在我们希望将name字段的值进行加密,那么只需要这样写:

public class Book extends LitePalSupport {
    @Encrypt(algorithm = AES)
    private String name;
    private int page;
    
    // getter and setter
}

只需要在name字段的上方加上@Encrypt(algorithm = AES)这样一行注解即可,其他的任何操作都无需改变。
更加方便的是,这种AES加密只是针对于破解者的一种防护措施,但是对于开发者而言,加解密操作是完全透明化的。也就是说,作为开发者我们并不用考虑某个字段有没有被加密,然后要不要进行解密等等,我们只需要仍然使用标准的LitePal API来查询数据即可。

不过除了上面这些基本功能之外,还有一些细节可能也是你需要知道的。

  • 第一点细节,你可以为AES算法来指定一个你自己的加密密钥。使用不同的密钥,加密出来的结果也是不一样的。如果你没有指定密钥,LitePal会使用一个默认的密钥来进行加密。因此,尽可以地调用LitePal.aesKey()方法来指定一个你自己的加密密钥,这样会让你的数据更加安全。
  • 第二点细节,AES算法包括还有下面即将要介绍的MD5算法都只对String类型的字段有效,如果你尝试给其他类型的字段(比如说int字段)指定@Encrypt注解,LitePal并不会执行任何加密操作。
  • 第三点细节,加密后的数据字段不能再通过where语句来进行查询、修改或删除。也就是说,执行类似于 where("name = ?", "第一行代码") 这样的语句将无法查到任何数据,因为在数据库中存储的真实值已经不是第一行代码了。(可以用其他的条件来查,如果你必须要用加密的这个字段来查,那么可以把你要查询的内容先用litepal的接口加密一下,使用CipherUtil这个类,里面有加解密的所有方法)

MD5加密的使用场景,如下所示:

public class User extends LitePalSupport {
    @Encrypt(algorithm = MD5)
    private String password;
    private String username;
    
    // getter and setter
}

MD5加密是不能被解密的。

9. 将数据库保存到SD卡(1.6.0版本加入)

假如我们希望将数据库文件保存到SD卡的 guolin/database目录下,只需要修改litepal.xml中的配置即可,如下所示:

<litepal>
    ...
    <storage value="guolin/database" />

</litepal>

没错,就是这么简单。注意不需要将SD卡的完整路径配置进去,只需要配置相对路径即可。
另外还有非常重要的一点需要注意,由于从Android 6.0开始访问SD卡需要申请运行时权限,而LitePal是不会去帮你申请运行时权限的(因为LitePal中既没有Activity也没有Fragment),因此如果你选择将数据库文件存储在SD卡上,那么请一定要确保你的应用程序已经对访问SD卡权限进行了运行时权限处理,否则LitePal的所有操作都将会失败。

10. 多数据库及数据库初始化和更新

  • 新增了一个LitePalDB类,这个类中加入了和litepal.xml文件中一一对应的字段,相当于把资源配置文件的功能可以放到代码中去完成了。比如说我们可以这样创建一个LitePalDB对象:
User user = new User();
user.setUsername("Tom");
user.setPassword("123456")
user.saveIfNotExist("username = ?", "Tom")

这其实和上面的配置文件实现了同样的效果,我们创建了一个名为demo2的数据库,将它的版本号指定成1,然后将Singer和Album这两个实体类映射成表。要切换到这个数据库,只需要调用一下如下方法即可:

LitePal.use(litePalDB);

调用use()方法之后就会将当前工作的数据库切换到demo2,数据库和表将会在你下次进行任何数据库操作的时候创建。

大部分人创建多个数据库可能都是用的完全一模一样的配置,只是为不同的用户创建一个不同名字的数据库而已。

针对于这种情况使用另一个接口,如下所示:

LitePalDB litePalDB = LitePalDB.fromDefault("demo3");
LitePal.use(litePalDB);

这样就会创建一个名为demo3数据库,而它的所有配置都会直接使用litepal.xml文件中配置的内容。

  • 切换回litepal.xml中指定的默认数据库
LitePal.useDefault();
  • 删除数据库的接口:
LitePal.deleteDatabase("demo3");
  • 监听数据库的创建和升级(3.0.0版本)

If you need to listen database create or upgrade events and fill some initial data in the callbacks, you can do it like this:

LitePal.registerDatabaseListener(new DatabaseListener() {
    @Override
    public void onCreate() {
        // fill some initial data
    }

    @Override
    public void onUpgrade(int oldVersion, int newVersion) {
        // upgrade data in db 必须开子线程操作,否则会出现Litepal getDatabase called recursively。
    }
});

11. 支持kotlin(升级到2.0.0)

val book = Book("第一行代码", 552)
val result = book.save()
val cv = ContentValues()
cv.put("name", "第二行代码")
cv.put("page", 570)
LitePal.update(Book::class.java, cv, 1)
LitePal.delete(Book::class.java, 1)
LitePal.where("name like ?", "第_行代码")
       .order("page desc")
       .limit(5)
       .find(Book::class.java)

注:以上笔记均来自专栏

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

推荐阅读更多精彩内容