Android 进阶之第三方库的介绍 Realm [一] 基础用法
对于Realm的学习主要分成篇博文,这篇如果只是想要单纯简单应用的话只要学会本篇博文的内容即可上手在项目中使用,如果对于Realm想要进一步深入了解它的高级用法可以查看Android 进阶之第三方库的介绍 Realm [二] 高级用法:
注意本篇博文大多数的内容都来自Realm官网,只不过是通过自己的认知规律重新组织下,来加深这些内容的理解。读大学那会儿老师说过看的东西永远是别人的,所以需要自己动手实践,哪怕梳理下内容也会转化成自己的。觉得非常有道理。
官网地址:https://realm.io
为什么选用Realm?
官网给出了如下的原因
- 易用
Ream 不是在SQLite基础上的ORM,它有自己的数据查询引擎。并且十分容易使用。 - 快速
由于它是完全重新开始开发的数据库实现,所以它比任何的ORM速度都快很多,甚至比SLite速度都要快。 - 跨平台
Realm 支持 iOS & OS X (Objective‑C & Swift) & Android. 我们可以在这些平台上共享Realm数据库文件,并且上层逻辑可以不用任何改动的情况下实现移植。 - 高级
Ream支持加密,格式化查询,易于移植,支持JSON,流式api,数据变更通知等高级特性 - 可信赖的
这个就不用解释了吧,每个库都这么说。 - 社区支持
有什么限制?
目前Realm还不支持 Android 以外的 Java 环境;
Android Studio >= 1.5.1 ;
较新的 Android SDK 版本;
JDK 版本 >=7;
支持 Android API 9 以上的所有版本(Android 2.3 Gingerbread 及以上)。
1. 添加Realm到工程
在module的build.gradle文件中添加realm依赖:
compile 'io.realm:realm-android:0.84.1' |
2. 创建一个Realm数据库
- 创建一个持久化Realm数据库
和SQLite数据库类似,每个Realm对应一个数据库文件。但是它的数据库后缀不是.db而是.realm
可以调用Realm.getInstance方法来创建一个Realm数据库:注意这里只是传入一个context,并没有传入数据库文件的文件名,这时候会创建一个叫做 default.realm的Realm文件,一般来说,这个文件位于/data/data/Realm musicRealmDB = Realm.getInstance(context);
/files/,可以通过realm.getPath()来获得该Realm的绝对路径。
需要注意的是Realm的实例是线程单例化的,也就是说,在同一个线程内多次调用静态方法获得针对同路径的Realm,会返回同一个Realm实例。
但是如果在这个基础上还想创建一个新的数据库,或者你不想用default.realm这个名字,那么就需要使用如下的方式创建了:
Realm musicRealmDB = |
除了上述的方法还可以使用:
Realm realm = Realm.getDefaultInstance(); |
来获得默认的Realm实例
默认RealmConfiguration:
RealmConfiguration可以保存为默认配置。通过在自定义的Application设置默认的Realm配置,可以使您在代码中的其他地方更加方便地创建针对该默认配置的Realm。
public class MyApplication extends Application { |
创建非持久化的Realm
RealmConfiguration myConfig = new RealmConfiguration.Builder(context)
.name("myrealm.realm")
.inMemory()
.build();这样就可以创建一个存在于“内存中的” Realm。“内存中的”Realm 在内存紧张的情况下仍有可能使用到磁盘存储,但是这些磁盘空间都会在Realm实例完全关闭的时候被释放。
请注意使用同样的名称同时创建“内存中的”Realm 和常规的(持久化)Realm 是不允许的。
当某个“内存中的”Realm 的所有实例引用都被释放,该Realm 下的数据也同时会被清除。建议在您的应用生命周期中保持对“内存中的” Realm 实例的引用以避免非期望的数据丢失。关闭Realm实例
Realm 实现了 Closeable 接口以便与释放 native 内存和文件描述符,请务必在使用完毕后关闭 Realm 实例。
Realm 实例是基于引用计数的, 也就是说假设您在同一个线程中调用了 getInstance() 两次,您需要同样调用 close() 两次以关闭该实例。
需要实现 Runnable,简单地在函数开始的时候调用 getInstance(),在函数结束的时候调用 close() 即可!
对于UI线程,您可以选择在 onDestroy() 方法内调用 realm.close()。
对于 AsyncTask,可以参照如下方式,在退出的时候关闭protected Void doInBackground(Void... params) {
Realm realm = null;
try {
realm = Realm.getDefaultInstance();
// ... Use the Realm instance ...
} finally {
if (realm != null) {
realm.close();
}
}
return null;
}如果需要创建一个包含 Looper 的线程,可以参考如下的用法:
public class MyThread extends Thread {
private Realm realm;
public void run() {
Looper.prepare();
try {
realm = Realm.getDefaultInstance();
//... Setup the handlers using the Realm instance ...
Lopper.loop();
} finally {
if (realm != null) {
realm.close();
}
}
}
}如果minSdkVersion >= 19,可以使用try-with-resources,这种情况下就不需要手动来关闭了
try (Realm realm = Realm.getDefaultInstance()) {
// No need to close the Realm instance manually
}3. 数据模型定义
Define you model class by extending the RealmObject
要想将某个JavaBean对象存储到Ream数据库中必须继承RealmObject类,但是需要注意的是Realm 数据模型不可以继承自除了 RealmObject 以外的其它对象
public class MusicItemBean extends RealmObject { |
Realm 数据模型不仅仅支持 private 成员变量,您还可以使用 public、protected 以及自定义的成员方法。目前不支持 final、transient 和 volatile 修饰的成员变量
Realm 支持boolean、byte、short、int、long、float、double、String、Date和byte [] 等类型字段。还可以使用 RealmObject 的子类和 RealmList<? extends RealmObject> 来表示模型关系。
Realm 对象中还可以声明包装类型(boxed type)属性,包括:Boolean、Byte、Short、Integer、Long、Float和Double。Realm常用的注释:
@PrimaryKey 属性:
如果想让RealmObject的一个成员变量作为主键,可以使用@PrimaryKey注解。需要注意的是该字段类型必须为字符串(String)或整数(short、int 或 long)以及它们的包装类型(Short、Int 或 Long)。@Required 属性:
@Required 表示该字段不能为null,只有 Boolean,Byte,Short,Integer,Long,Float,Double,String,byte[] 以及 Date 可以被 @Required 修饰。
在其它类型属性上使用 @Required 修饰会导致编译失败。基本数据类型(primitive types)不需要使用注解 @Required,因为他们本身就不可为空。RealmObject 属性永远可以为空。
主键的存在意味着可以使用 createOrUpdate() 方法,它会用此主键尝试寻找一个已存在的对象,如果对象存在,就更新该对象;反之,它会创建一个新的对象。
使用主键会对性能产生影响。创建和更新对象将会慢一点,而查询则会变快。很难量化这些性能的差异,因为性能的改变跟您数据库的大小息息相关。@Ignore 属性:
注解 @Ignore 意味着一个字段不应该被保存到 Realm。@Index 属性:
注解 @Index 会为字段增加搜索索引。这会导致插入速度变慢,同时数据文件体积有所增加,但能加速查询。因此建议仅在需要加速查询时才添加索引。目前仅支持索引的属性类型包括:String、byte、short、int、long、boolean和Date。
4. 存储数据项目:
Realm强制所有的写操作(添加、修改和删除对象)都在一个事务中执行从而确保数据的一致性。
要开始一个事务可以使用beginTransaction方法。反之要结束当前事务,可以使用commitTransaction方法。要取消事务可以调用cancelTransaction()方法:如下所示:
写入事务可以提交或取消。在提交期间,所有更改都将被写入磁盘,并且,只有当所有更改可以被持久化时,提交才会成功。通过取消一个写入事务,所有更改将被丢弃。
musicRealmDB.beginTransaction(); |
注意这里并没有调用MusicItemBean构造方法创建新的对象,如果你真的需要使用构造方法也可以,下面是使用构造方法的示例,它调用copyToRealm写入,Realm对象支持多个构造函数,只要其中之一是公共无参数构造函数即可。
MusicItemBean music = new MusicItemBean(); |
MyObject obj = new MyObject(); |
需要注意的是,写入事务之间会互相阻塞,如果一个写入事务正在进行,那么其他的线程的写入事务就会阻塞它们所在的线程。同时在 UI线程和后台线程使用写入事务有可能导致ANR问题。可以使用异步事务(async transactions)以避免阻塞UI线程。
当正在进行一个写入事务时读取操作并不会被阻塞,这意味着,除非需要从多个线程进行并发写入操作,否则,可以尽量使用更大的写入事务来做更多的事情而不是使用多个更小的写入事务。
当写入事务被提交到 Realm 时,该Realm的所有其他实例都将被通知,读入隐式事务将自动刷新您每个Realm对象。
5 Transaction 事务
除了上述的beginTransectiion,commitTransection外还有事务执行块以及异步事务这两种:
- 事务执行块(Transaction blocks)
异步执行块会自动处理写入事物的开始和提交,并在错误发生时取消写入事物。
realm.executeTransaction(new Realm.Transaction() { |
- 异步事务(Asynchronous Transactions)
事务会相互阻塞其所在的线程,在后台线程中开启事务进行写入操作可以有效避免 UI 线程被阻塞。通过使用异步事务,Realm 会在后台线程中进行写入操作,并在事务完成时将结果传回调用线程。OnSuccess 和 OnError 并不是必须重载的,重载了的回调函数会在事务成功或者失败时在被调用发生的线程执行。回调函数是通过 Looper 被执行的,所以在非 Looper 线程中只有空(null)回调函数被允许使用。realm.executeTransactionAsync(new Realm.Transaction() {
public void execute(Realm bgRealm) {
User user = bgRealm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
}, new Realm.Transaction.OnSuccess() {
public void onSuccess() {
// Transaction was a success.
}
}, new Realm.Transaction.OnError() {
public void onError(Throwable error) {
// Transaction failed and was automatically canceled.
}
});RealmAsyncTask transaction = realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
User user = bgRealm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
}, null);
异步事务调用会返回一个 RealmAsyncTask 对象。当你退出 Activity 或者 Fragment 时可以使用该对象取消异步事务。如果你在回调函数中更新UI,那么忘记取消异步事务可能会造成你的应用崩溃。
public void onStop () { |
6. 查询数据条目:
要创建一个查询,使用相关Realm对象的where方法并传入相关的类。创建完查询之后,将会返回一个RealmResults。可以通过RealmResults对象的findAll方法获取所有的结果。
当查询没有任何匹配时,返回的 RealmResults 对象将不会为 null,取而代之的是它的 size() 方法将返回 0。
RealmResults<MusicItemBean> results1 = |
- Realm 支持如下的查询条件:
between()、greaterThan()、lessThan()、greaterThanOrEqualTo() 和 lessThanOrEqualTo() |
RealmResults<MusicItemBean> qury2 = |
- 查询条件的组合:
字符串查询条件可以通过使用 Case.INSENSITIVE 修饰符来忽略字母 A-Z 和 a-z 的大小写。
并且可以使用 beginGroup()(相当于左括号)和 endGroup()(相当于右括号)来将查询条件组合起来:
此外,也可以用 not() 否定一个条件。该 not() 运算符可以与 beginGroup()/endGroup() 一起使用来否定子条件。
RealmResults<User> r = realm.where(User.class) |
- 对查询结果进行排序:
如果想要查询的结果按照某项进行排序,那么可以使用findAllSorted,并传入一个boolean指定归类顺序,true表示升序排序,false表示降序排序。
RealmResults<MusicItemBean> qury2 = |
还可以通过sort来进行排序:
RealmResults<User> result = realm.where(User.class).findAll(); |
- 为某个查询添加监听事件:
下面是来自官网的,它将会监控age小于2的查询结果,如果我们往这里面添加了数据那么这个接口将会被回调:
final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll(); |
- 聚合
RealmResult 自带一些聚合方法:RealmResults<User> results = realm.where(User.class).findAll();
long sum = results.sum("age").longValue();
long min = results.min("age").longValue();
long max = results.max("age").longValue();
double average = results.average("age");
long matches = results.size(); - 遍历查询结果:
RealmResults<User> results = realm.where(User.class).findAll();
for (User u : results) {
// ... do something with the object ...
}RealmResults<User> results = realm.where(User.class).findAll();
for (int i = 0; i < results.size(); i++) {
User u = results.get(i);
// ... do something with the object ...
} - 异步查询
声明并注册回调方法:
private RealmChangeListener callback = new RealmChangeListener<RealmResults<User>>() { |
在退出 Activity 或者 Fragment 时移除监听器的注册以避免内存泄漏。
public void onStop () { |
主动检查查询是否完成
RealmResults<User> result = realm.where(User.class).findAllAsync(); |
注意同步查询返回的 RealmResults 实例的 isLoaded 方法会永远返回 true。可以在 Looper线程中使用异步查询异步查询需要使用Handler来传递查询结果在没有Looper的线程中使用异步查询会导致IllegalStateException 异常被抛出。
7. 自动更新数据条目:
在Realm中只要调用set方法就会自动更新对应的条目,而不用像其他的数据库那样调用update方法:
realm.beginTransaction(); |
8. 异步查询和更新数据
下面的示例是通过异步方式查询小狗的年龄等于1的狗并将其年龄设置为3.如果设置成功onSuccess方法将会被调用:
realm.executeTransactionAsync(new Realm.Transaction() { |
9. 删除数据
// obtain the results of a query |
10. 多表关系
任意两个 RealmObject 可以相互关联。
下面的例子中Contact与Email建立了关联:
public class Email extends RealmObject { |
- 多对一 关系
要实现多对一的关联只需要简单地声明一个 Realm 模型类的属性即可:
public class Contact extends RealmObject { |
设置一个类型为 RealmObject 的属性为空值(null)会清除该属性的引用,但并不会删除对应的 RealmObject。
- 多对多 关系
要实现多对多的关联可以通过使用 RealmList
public class Contact extends RealmObject { |
RealmList 是 Realm 模型对象的容器,其行为与 Java 的普通 List 近乎一样。
同一个 Realm 模型对象可以存在于多个 RealmList 中。
同一个 Realm 模型对象可以在同一个 RealmList 中存在多次。
使用 Contact 和 Email 类
public class Email extends RealmObject { |
您可以通过标准的 getter 和 setter 来访问 RealmList.
realm.beginTransaction(); |
Realm支持关联查询。以如下模型举例:
public class Person extends RealmObject { |
每个 User 对象都与多个 Dog 对象相关联,如下图所示:
// users => [U1,U2] |
以上的查询含义为“所有至少含有一个 color 为 Brown 的 User”。请务必注意,这里的返回的 User 中,有可能包含 color 不为 Brown 的 Dog 对象,因为在其 RealmList 列表中,其它的 Dog 对象满足查询条件:
// r1 => [U1,U2] |
请注意第一个查询返回两个 User 对象,因为它们都满足查询条件。每个 User 对象都包含一个 Dog 对象列表——列表中至少有一个 Dog 对象满足查询条件。谨记我们是在寻找其拥有的 Dog 对象满足条件(name 和 color)的 User,不是在针对 Dog 对象进行查询。
因此第二个查询建立在第一个的 User 结果(r1)以及 r1 的每个 User 的 Dog 列表之上。两个 User 仍然满足第二个查询,但这次是 color 满足查询条件。
我们再深入了解下这个概念,请看以下代码:
// r1 => [u1,u2] |
第一个查询表示找到所有的User他至少有一个Dog的名字为fluffy并且找到所有User他至少有一个Dog的颜色是brown 然后返回这两个结果的交集。
第二个查询表示找到所有的User他至少有一个Dog的名字为fluffy;然后在这个结果之上找到所有的User他至少有一个Dog的颜色为brown;
最后在之前的结果之上找到所有的 User 他至少有一个 Dog 的颜色为 yellow。
我们来解释一下第一个查询以深入了解下这个行为。两个条件分别是equalto(“dogs.name”, “fluffy”) 和 equalto(“dogs.color”, “brown”)。
u1和u2 完全满足第一个条件 ——我们称其c1集合。u1和u2也同时完全满足第二个条件——我们称其 c2 集合。查询中的逻辑与即是c1与c2的交集c1与c2的交集就是u1和u2。因此r1就包含u1和u2。
第二个查询不一样。我们来分别讲解。该查询第一部分看起来是这样的:realmresults
它的结果包含 u1 和 u2。然后 r2b = r2a.where().equalto(“dogs.color”, “brown”).findall();
的结果仍然包含 u1 和 u2 (两个 User 都有颜色为 brown 的 Dog)。最后的查询 r2 = r2b.where().equalto(“dogs.color”, “yellow”).findall();
结果只包含 u2,因为只有 u2 同时有一个颜色为 brown 的 Dog 和一个颜色为 yellow 的 Dog。
11 自动更新(Auto-Refresh)
如果 Realm 实例存在于一个带有 Looper 的线程,那么这个 Realm 实例即具有自动更新的功能。这意味这如果发生了 Realm 数据库的变化,那么该 Realm 实例会在下一个事件循环(event loop)中自动更新。这个便捷的功能使您不必花费太多的精力就能保证的UI与数据的实时同步。
如果 Realm 的实例所在线程没有绑定 Looper,那么该实例不会被更新直到您手动调用 waitForChange() 方法。请注意,不更新 Realm 以保持对旧数据的引用会造成而外的磁盘和内存开销。这也是为什么要在线程结束时调用 close() 关闭 Realm 实例的一个重要原因。
如果您想确定当前 Realm 实例是否有自动更新功能,可以通过调用 isAutoRefresh() 方法查询。
12 实际例子
下面是官网给出的一些学习Demo建议大家通过这些代码来学习会比较快点:
introExample 包含了如何使用当前的API的简单例子。
gridViewExample 用来展示如何使用 Realm 作为 GridView 的后端存储。它同时也展示了如何用 JSON 来填充数据库。另外还有怎么通过 ABI splits 来缩小 APK 体积。
threadExample 展示了如何在多线程环境中使用 Realm。
adapterExample 展示了如何以一个非常便捷的方式使用 RealmBaseAdapter 绑定 RealmResults 到安卓的 ListView。
jsonExample 展示了 Realm 与 JSON 相关的功能。
encryptionExample 向您展示如何使用加密的 Realm。
rxJavaExamples 展示了如何与 RxJava 结合使用 Realm。
unitTestExample 展示了如何写与 Realm 相关的单元测试。
下面是API文档链接:
!Realm API