翻了一下推送的历史记录,距离LitePal上一个版本发布已经过去整整五个月时间了。其实在上次发布LitePal 1.3.2版本的时候我就跟大家承诺过,1.4.0版本中一定会加入支持多数据库的功能,只是我没想到这个版本来得这么迟。
上个版本发布的时候我推送了一篇《我为什么能将效率提升了800%》,是6月14号推送的。当时我盘算着,7月份应该就可以推出1.4.0版本,加入大家呼吁已久的多数据库功能,只是没想到后来书稿的进度变得那么紧。
一开始我和出版社约定好的时间是9月份将《第二行代码》全书完稿,但是越写到后面改动的内容就越多,像第12章的内容就是完全重写的,第14、15章的内容也基本是全部重写了。再加上我当时还一直在等Android 7.0和Android Studio 2.2的发布,发布了之后又对书中所有已写的内容进行了一遍7.0系统的适配修订,最终才在10月初将全书完稿。因此,LitePal新版本的事情也就只能一推再推。
不过认真的我在写完新书之后立刻就开始忙起了LitePal新版本开发的事情,花了将近一个月的时间终于将所有功能点都开发完了,那么今天也是迫不及待地推荐给大家。
由于我的公众号每天都会有很多新朋友关注,因此肯定有不少人并不知道LitePal到底是什么,那么这里我还是先给大家做个普及。LitePal是一个由我开发的Android端数据库ORM框架,极度简化了数据库的使用方式,我们甚至不用写一行SQL语句就能完成绝大部分的数据库操作,大大降低了开发的成本。
而最新的1.4.0版本中更是加入了许多令人期待的功能,那么今天这篇文章就来介绍一下这些新增的功能,以及我做开源框架时候的一些设计思路。
支持多数据库功能是LitePal 1.4.0版本中的重头戏,在这之前,使用LitePal只能创建一个数据库,而从1.4.0版本开始,使用LitePal可以创建任意多个数据库。
其实我在刚开始做程序的时候学的是服务器开发,对服务器数据库了解得还算比较多。而服务器程序一般都只会有一个数据库,因此我在最初设计LitePal的时候也只考虑了单数据库功能。
但是后来很多朋友就提出了疑问,像QQ这样的应用都是有多个数据库文件的,每登录了一个不同的账号就会单独创建一个数据库文件,这样各个账号之间的数据就不会搞混乱。其实在服务器开发当中,是不可能为每一个不同的账号都单独创建一个数据库的,这样的话一台服务器当中可能会存在几千万、甚至是上亿个数据库。通常情况下,服务器程序会为每一个账号的数据加上一个外键关联,查询的时候使用外键过滤一下,只查当前账号的数据,这样就不会导致各个账号之间的数据混乱了。但是在客户端开发当中,要求每个程序员还要懂外键的设计,这个要求可能确实就有点高了。因此,最简单的办法就是创建多个数据库。
那么,由于支持多数据库属于新增功能,我在开发这个功能的时候还要考虑向下兼容的问题。因此不能将现有接口的用法全部摒弃,重新开发,而是要在完全支持现有接口的情况下添加新功能。
我们都知道,LitePal会自动管理项目中的所有表的创建和升级,但需要依赖一个litepal.xml配置文件,如下所示:
litepal>
dbname value="demo" />
version value="1" />
list>
mapping class="com.test.model.Singer" />
mapping class="com.test.model.Album" />
list>
litepal>
我们在这个配置文件中管理数据库名、数据库版本、以及要映射哪些类。
但是大家仔细思考一下,这种用法是无法支持多数据库的。因为litepal.xml作为项目中的资源,一旦打完包之后就不能再更改了,而我们显然不可能在还没打包之前就确定下来要创建哪些数据库(比如说根据用户登录不同的账号创建不同的数据库)。
因此,之前这种资源配置文件的用法只能适用于单数据库的情况,想要实现多数据库功能的话就必须另辟蹊径了。
经过长时间的设计思考,我新增了一个LitePalDB类,这个类中加入了和litepal.xml文件中一一对应的字段,相当于把资源配置文件的功能可以放到代码中去完成了。比如说我们可以这样创建一个LitePalDB对象:
LitePalDB litePalDB = new LitePalDB("demo2", 1);
litePalDB.addClassName(Singer.class.getName());
litePalDB.addClassName(Album.class.getName());
这其实和上面的配置文件实现了同样的效果,我们创建了一个名为demo2的数据库,将它的版本号指定成1,然后将Singer和Album这两个实体类映射成表。
接下来的工作就简单了,想要切换到这个数据库,只需要调用一下如下方法即可:
LitePal.use(litePalDB);
调用use()方法之后就会将当前工作的数据库切换到demo2,数据库和表将会在你下次进行任何数据库操作的时候创建。
使用这种动态创建数据库的方式,我们就摆脱了配置文件的限制,从而可以自由地创建任意多个数据库了。
不过接口设计到这里还不算合理,因为如果你的数据库中有很多张表,比如说需要映射几十个实体类,当我创建多个数据库的时候,每次都要new一个LitePalDB对象,然后add几十个实体类进去,显然非常繁琐。大部分人创建多个数据库可能都是用的完全一模一样的配置,只是为不同的用户创建一个不同名字的数据库而已。
针对于这种情况,我专门另外设计了一个接口,如下所示:
LitePalDB litePalDB = LitePalDB.fromDefault("demo3");
LitePal.use(litePalDB);
可以看到,这里我们不再使用new的方式创建LitePalDB的实例了,而是使用了一个LitePalDB.fromDefault()方法,这个方法中只要求传入一个数据库名。这样就会创建一个名为demo3数据库,而它的所有配置都会直接使用litepal.xml文件中配置的内容。
也就是说,我们可以将繁琐的配置仍然放在litepal.xml文件中,然后动态创建数据库时只需要指定一个数据库名就可以了。
那么当你切换到动态创建的数据库之后,如果还想切换回litepal.xml中指定的默认数据库该怎么办呢?很简单,调用如下方法即可:
LitePal.useDefault();
另外,由于现在允许了动态创建数据库,那么项目中的数据库文件就可能会非常多,因此我又提供了一个用于删除数据库的接口:
LitePal.deleteDatabase("demo3");
将想要删除的数据库名作为参数传入到deleteDatabase()方法即可。
经过以上几个接口的设计,LitePal的多数据库功能就算是比较完整了。大家看上去虽然最终用法非常简单,但是在一开始什么都没有的时候,我去设计这些接口真的是花了很多时间来思考。
当然,1.4.0版本中不可能就只加了这一个功能而已,接下来我们看看新版LitePal中还有哪些新的功能。
之前有不少朋友跟我反馈过,说他的实体类中有一个List字段,结果用LitePal存储不了这部分数据。其实对于ORM映射来说,集合数据真的是非常难处理的,但是既然有很多人都提出了这个问题,我还是要想办法解决一下的。
先来看一下为什么说集合数据非常难处理吧。首先最基本的ORM映射规则就是将Java中的类映射成数据库中的表,将Java中的字段映射成数据库中的列,然后每一个Java对象就对应着数据库表中的一行记录。那么Java中的8种基本数据类型以及String类型当然是非常好处理的,将它们直接存储到对应的列中就可以了,但集合数据不行。因为集合数据中可能是有任意多条记录的,而每个Java对象就只能对应数据库表中的一行记录而已,因此根本没有地方可以去存放集合数据。
我也不知道其他一些Android数据库框架到底支不支持这种集数数据的存储,因为我从来没有去学习过别的数据库框架的用法,主要是怕学了之后会影响到我自己的设计思路。但是LitePal将从1.4.0版本开始支持集数数据的存储了。
那么刚才都已经说了很难处理,LitePal到底是如何实现的呢?这里和大家分享一下我的实现思路,其实主要就是模仿数据库关联表的方式来实现的。
比如说现在我们的Album实体类中有一个集合字段:
public class Album extends DataSupport {
String name;
List titles = new ArrayList;
// 生成get set方法
}
这个titles集合记录了这张专辑里面有哪些歌名。
下面我们将这个Album存储到数据库中,如下所示:
Album album = new Album();
album.setName("范特西");
album.getTitles().add("爱在西元前");
album.getTitles().add("双截棍");
album.getTitles().add("安静");
album.save();
然后我们去查看album表,你会发现里面就只有id和name这两列:
这也是之前版本LitePal表现的行为。而在1.4.0版本中,LitePal会额外进行一个操作,就是创建一个album_titles表,并将集合中的数据存储在这里,如下图所示:
可以看到,这里记录了所有集合中的数据,并将这些数据和album的id进行了关联,从而可以区分出每条数据到底是属于哪一个Album对象的。
当然了,这些都是LitePal底层的实现原理,我们在使用的时候即使不了解这些原理也完全没问题,因为LitePal都将这些功能封装好了。
这样当我们去查询album数据的时候,会自动将它所关联的集合数据一起查出来:
Album album = DataSupport.findFirst(Album.class);
List titles = album.getTitles();
for (String title : titles) {
Log.d(TAG, "title is " + title);
}
打印结果如下图所示:
除了支持List集合之外,还有List、List、List、List、List、List这几种类型的集合也是支持的。
当然,如果你不希望你的集合数据被存储到数据库中的话,可以使用注解的方式将它忽略掉:
public class Album extends DataSupport {
String name;
@Column(ignore = true)
List titles = new ArrayList;
// 生成get set方法
}
这个功能可能并不是那么的重要,但是为了提升LitePal各方面的细节,我还是做了,没想到这个功能比前面两个加起来还要复杂。
已经数不清有多少次了,有人在使用LitePal的时候会遇到一个这样的错误,然后问我是怎么回事:
观察一下上面的错误日志,你会发现这里提醒我们SQL语句的语法出错了。但是LitePal帮我们将SQL语句都封装好了,我们一行SQL都不需要写,怎么会出错呢?
仔细看一下上面的建表语句,你会发现里面有一个index列,这个列的名字就是实体类中定义的字段的名字。在Java中index并不是关键字,因此这个字段的名字是合法的,但是在数据库当中,index是索引的关键字,是不能用来作为列名的,因此在建表的时候就报错了。
由于遇到这个问题的人实在是太多了,因此我决定在1.4.0版本中解决掉这个问题。解决的思路也很简单,将这种使用数据库中的关键字的字段名进行一下转义就可以了。
那么首先我需要统计出SQLite中到底有哪些关键字。真是不查不知道,一查吓一跳,SQLite中的关键字数量有上百个,其中有不少我自己都没听说过,如下表所示:
因此,除了刚才的index之外,还有很多坑都是可能被大家踩到的。
在建表的时候对关键字进行转义还算比较简单,就是检查一下字段名,如果是数据库关键字的话,就给它添加一个_lpcolumn后缀。也就是说,如果是刚才的index字段,那么就会将它转义成index_lpcolumn,从而避免了和数据库关键字冲突。
而真正复杂的地方在于增删改查,比如说我要查询一条数据可能会这么写:
DataSupport
.select("index")
.where("index = ?", "0")
.order("index")
.find(Album.class);
其中,select()、where()、order()方法中都可能会传入列名,而我显然不可能让开发者在调用的时候就自己先将index转换成index_lpcolumn,因此只能由LitePal在内部来进行转换。在这几个方法中,where()方法尤其让我头疼,因为它里面可能会写出各种复杂的查询语句,比如可能会包含between、not between、in、not in、exists、not exists等情况,还可能会有==、!=、=、、like等等用法。
SQL语句的复杂性使我想要从中准确地解析出所有的列名然后再判断是否需要转义变得十分困难,可以说我大量的时间都花在这个上面了。像更新、删除等其他情况也都类似,总之,输入源既然不可控,那么就对我内部解析算法的健壮性要求非常高。
就目前的测试结果来看,我做得应该还算是不错的,暂时还没发现什么解析的bug。当然,对于开发者来说,最好的做法还是尽量避免使用数据库的关键字,而不是依靠LitePal的转义机制来避过问题。
升级方式一如既往的简单,如果你使用的是Android Studio,只需要在build.gradle中修改一下配置即可:dependencies {
compile 'org.litepal.android:core:1.4.0'
}
1.4.0版本中的所有的功能都是向下兼容的,因此你的升级不用付出任何成本。
如果你使用的还是Eclipse,那么就需要到LitePal的项目主页去下载最新版的jar包了,项目主页地址是:
https://github.com/LitePalFramework/LitePal
另外,对于那些之前没有了解过LitePal,但是想从头去学习的朋友们,可以去参考我之前写过的一个专栏,里面非常详细地介绍了LitePal从零到精通的所有用法,点击下方 阅读原文 将会直接跳转到LitePal的专栏页面。
每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: