Android Realm数据库使用指南

Realm数据库, 目前有Java, Objective‑C, React Native, Swift, Xamarin的几种实现, 是一套用来取代SQLite的解决方案.

本文面向Android开发, 所以只讨论Java实现.

目前Realm Java的最新版本是2.3.1.

官方文档在此: realm java doc, 花一个下午就可以基本过一遍, 之后随时查用.

我写了一个小程序TodoRealm, 使用Realm做数据库实现的一个To-do应用, 在实际使用的过程中也有一些发现.

本文是我自己看文档的时候的一些记录, 有一些实际使用时的发现也穿插在对应的章节了.

Setup

在项目的根build.gradle的文件中添加:

buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:2.3.0"
}
}

然后在app的build.gradle文件中添加:

apply plugin: 'realm-android'

Done.

Models

Model类只要继承RealmObject即可.

public class User extends RealmObject {

    private String          name;
private int age; @Ignore
private int sessionId; // Standard getters & setters generated by your IDE…
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getSessionId() { return sessionId; }
public void setSessionId(int sessionId) { this.sessionId = sessionId; }
}

字段类型

Model类中可以包含的字段类型包括基本数据类型(及它们的装箱类型)和Date类, 另外也可以包含RealmObject的子类或者是RealmList<? extends RealmObject>.

字段性质

在字段上加注解可以定义字段的性质:

@Required表明字段非null.

原生类型和RealmList类型默认是非null的.

RealmObject字段永远是可以为null的.

@Ignore表示字段不会被存储.

@Index加索引.

@PrimaryKey加主键, 主键只能有一个, 主键默认加索引.

但是注意主键默认没有加@Required, 如果主键要求非null, 需要显式添加@Required.

主键使用

有主键才能使用copyToRealmOrUpdate()这个方法.

主键类型必须是String或者整型(byte, short, int, long)或者它们的装箱类型(Byte, Short, Integer, Long).

有主键的对象创建的时候不能使用createObject(Class<E> clazz)方法, 而应该使用createObject(Class<E> clazz, Object primaryKeyValue)附上主键.

或者用

copyToRealm(obj)copyToRealmOrUpdate(obj), 前者遇到主键冲突时会崩溃, 后者遇到主键冲突会更新已有对象.

自动更新的对象

Realm中的数据对象是自动更新(Auto-Updating)的, 对象一旦被查询出来, 后续发生的任何数据改变也会立即反映在结果中, 不需要刷新对象.

这是一个非常有用的特性, 结合数据变化的通知可以很方便地刷新UI.

关系

Realm model对象间可以很方便地建立关系.

你可以在Model中存储另一个对象的引用, 建立多对一的关系; 也可以存储一组对象RealmList<T>, 建立一对多或多对多的关系.

RealmList<T>的getter永远也不会返回null, 它只会返回一个为空的list.

把这个字段设置为null可以清空这个list.

初始化

Realm在使用之前需要调用初始化:

Realm.init(context);

建议把它放在Application的onCreate()里.

配置

配置类: RealmConfiguration定义了Realm的创建配置.

最基本的配置:

RealmConfiguration config = new RealmConfiguration.Builder().build();

它会创建一个叫default.realm的文件, 放在Context.getFilesDir()的目录下.

如果我们想自定义一个配置, 可以这样写:

// The RealmConfiguration is created using the builder pattern.
// The Realm file will be located in Context.getFilesDir() with name "myrealm.realm"
RealmConfiguration config = new RealmConfiguration.Builder()
.name("myrealm.realm")
.encryptionKey(getKey())
.schemaVersion(42)
.modules(new MySchemaModule())
.migration(new MyMigration())
.build();
// Use the config
Realm realm = Realm.getInstance(config);

所以我们是可以有多个配置, 访问多个Realm实例的.

我们可以把配置设置为默认配置:

Realm.init(this);
RealmConfiguration config = new RealmConfiguration.Builder().build();
Realm.setDefaultConfiguration(config);

之后用Realm.getDefaultInstance()取到的就是这个默认配置对应的实例.

数据库迁移

迁移的策略是通过config指定的:

RealmConfiguration config = new RealmConfiguration.Builder()
.schemaVersion(2) // Must be bumped when the schema changes
.migration(new MyMigration()) // Migration to run instead of throwing an exception
.build()

其中MyMigration实现了RealmMigration接口, 在migrate()方法中根据新旧版本号进行一步一步地升级.

具体例子见Migration.

开发的时候为了方便我用的是.deleteRealmIfMigrationNeeded(), 这样在需要数据库迁移的时候直接就删了数据重新开始了.

关于Realm的close()

一个打开的Realm实例会持有一些资源, 有一些是Java不能自动管理的, 所以就需要打开实例的代码负责在不需要的时候将其关闭.

Realm的instance是引用计数的(reference counted cache), 在同一个线程中获取后续实例是免费的, 但是底层的资源只有当所有实例被释放了之后才能释放. 也即你调用了多少次getInstance(), 就需要调用相应次数的close()方法.

比较建议的方法是在Activity或Fragment的生命周期中处理Realm实例的开启和释放:

  • 在Activity的onCreate()getInstance(), onDestroy()close().
  • 在Fragment的onCreateView()getInstance(), onDestroyView()close().

如果多个Fragment相关的都是同一个数据库实例, 那么在Activity中处理更好一些.

写操作一般的流程是这样:

// Obtain a Realm instance
Realm realm = Realm.getDefaultInstance(); realm.beginTransaction(); //... add or update objects here ... realm.commitTransaction();

这里创建对象可以用createObject()方法或者copyToRealm()方法.

前者是先创建再set值, 后者是先new对象再更新数据库.

如果不想自己处理beginTransaction(), cancelTransaction()commitTransaction(), 可以直接调用realm.executeTransaction()方法:

realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
User user = realm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
});

异步

因为transactions之间是互相阻塞的.

异步执行可以用这个方法:

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");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// Transaction was a success.
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
// Transaction failed and was automatically canceled.
}
});

这两个回调是Optional的, 它们只能在有Looper的线程调用.

注意: 这个方法的返回值对象可以用于在Activity/Fragment生命周期结束的时候取消未完的操作.

删除和更新

所有的写操作都要放在transaction中进行, 如上, 不同的操作只是其中具体方法不同.

删除操作:

final RealmResults<User> users = getUsers();
// method 1:
users.get(0).deleteFromRealm();
// method 2:
users.deleteFromRealm(0); // delete all
users.deleteAllFromRealm();

更新操作:

realm.copyToRealmOrUpdate(obj);

注意: 这个方法需要Model有主键, 会更新obj的主键对应的对象, 如果不存在则新建对象.

查询

查询可以流式地写:

// Or alternatively do the same all at once (the "Fluent interface"):
RealmResults<User> result2 = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAll();

查询条件默认是and的关系, or则需要显式指定.

这个RealmResults是继承Java的AbstractList的, 是有序的集合, 可以通过索引访问.

RealmResults永远不会为null, 当查不到结果时, 它的size()返回0.

查询的线程

基本上所有的查询都是很快进行的, 足够在UI线程上同步进行.

所以绝大多数情况在UI线程上使用findAll()是没有问题的.

如果你要进行非常复杂的查询, 或者你的查询是在非常大的数据集上进行的, 你可以选择异步查询, 使用findAllAsync().

查询条件是一个集合 -> in()

如果想要查询的某一个字段的值是在一个集合中, 比如我有一个id的集合, 我现在想把id在这个集合中的项目全都查出来, 这就可以使用in操作符:

RealmResults<TodoList> toDeleteLists = realm.where(TodoList.class).in("id", ids).findAll();

链式查询

查询的时候可以利用link或关系来查询, 比如一个Person类中含有一个RealmList<Dog> dogs的字段.

查询的时候可以这样:

RealmResults<Person> persons = realm.where(Person.class)
.equalTo("dogs.color", "Brown")
.findAll();

利用字段名dogs.来查询一个dog的属性, 再查出拥有这种特定属性dog的人.

但是反向地, 我们能不能查询主人是满足特定属性的人的所有dogs呢? 目前(2017.2.17)这种查询仍是不支持的. 这里有讨论: realm-java-issue-607.

所以两种解决办法: 一是做两次查询; 二是在Dog类的model里加入对Person的引用.

Notifications

可以添加一个listener, 在数据改变的时候收到更新.

public class MyActivity extends Activity {
private Realm realm;
private RealmChangeListener realmListener; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
realmListener = new RealmChangeListener() {
@Override
public void onChange(Realm realm) {
// ... do something with the updates (UI, etc.) ...
}};
realm.addChangeListener(realmListener);
} @Override
protected void onDestroy() {
super.onDestroy();
// Remove the listener.
realm.removeChangeListener(realmListener);
// Close the Realm instance.
realm.close();
}
}

注意listener需要在不用的时候删除掉.

可以用这样删除所有的listeners:

realm.removeAllChangeListeners();

Listener不一定要和Realm绑定, 也可以和具体的RealmObject或者RealmResults绑定.

当Listener被调用的时候, 它绑定的对象是自动更新的, 不需要手动刷新.

查看数据库的工具

用Stetho不能直接查看Realm的数据库, 看不到.

需要用这个工具配置一下: stetho-realm.

之后就可以在浏览器中查看Realm的数据库了.

(但是感觉这个工具不是很好用, 有时候不显示数据, 有时候显示的是旧数据.)

也可以用官方提供的Realm Browser来查看, 但是只有Mac版.

如何查看看这里: StackOverflow answer.

实际使用的感想和遇到的问题

优点

  • 建立Model之间的关系很方便也很直接, 查询的时候自动关联了其中的关系.
  • 自动更新(Auto-Updating)的特性很有用, 不用再关心数据的刷新, 只用关心UI的刷新.

比如一旦给Adapter绑定了数据, 之后的数据更新只需要在onChange()里面通知Adapter调用notifyDataSetChanged()即可.

当然我并没有用RealmBaseAdapterRealmRecyclerViewAdapter, 估计这两个更好用, 官方有例子, 这里不再赘述.

缺点

这里有的也不能说是缺点, 只是使用起来觉得不方便的地方.

  • 限制了创建对象和操作对象必须在同一个线程.

    违反了这条会报错: java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created. 比如我们在UI线程查询出来的对象, 想要异步地删除或者更新, 我们必须在新的线程重新查询.
  • 没有主键自增的功能, 见Issue #469, 需要自己控制主键自增.
  • 从List中删除了一项之后, 最后的一项会移动过来补到被删除的那一项原来的位置. 这是因为人家就是这么设计的stackoverflow. 默认情况下是没有排序的, 数据按照添加的顺序返回, 但是这并不是一种保证, 所以当删除了中间的元素, 后面的会补上这个位置, 以保证底层的数据是放在一起的. 解决办法就是指定一个排序规则.
  • 查询出来的对象不可以临时改变其数据, 否则会报错: java.lang.IllegalStateException: Changing Realm data can only be done from inside a transaction.
  • 不支持反向link的查询. (见前面链式查询部分的介绍).
  • 不支持级联删除. 即从数据库中删除一个对象的时候, 不会删除其中RealmObject子类或RealmList类型的字段在数据库中对应的数据. Issue #1104, Issue #2717. 这点也可以理解, 因为model之间的关系可能是多对多的. 所以需要实现级联删除的地方需要手动处理.
  • 测试不方便: RealmResults对象即不能被mock也不能被new; 所有的Model对象也不能被mock. 因为Mockito can only mock non-private & non-final classes.

资源

我的练习Demo:

最后, 欢迎关注微信公众号: 圣骑士Wind

Android Realm数据库使用指南的更多相关文章

  1. 【我的Android进阶之旅】Realm数据库学习资料汇总(持续更新)

    介绍 realm是一个跨平台移动数据库引擎,支持iOS.OS X(Objective-C和Swift)以及Android. 2014年7月发布.由YCombinator孵化的创业团队历时几年打造,是第 ...

  2. 玩转Android之数据库框架greenDAO3.0使用指南

    用过ActiveAndroid.玩过ORMLite,穿过千山万水,最终还是发现greenDAO好用,ActiveAndroid我之前有一篇文章介绍过 玩转Android之数据库框架ActiveAndr ...

  3. 优于CoreData的Realm数据库基础教程

    Realm 是一个跨平台的移动数据库引擎,于 2014 年 7 月发布,准确来说,它是专门为移动应用所设计的数据持久化解决方案之一. Realm 可以轻松地移植到您的项目当中,并且绝大部分常用的功能( ...

  4. Realm数据库的简单介绍和使用

    给大家介绍一个数据库操作的框架Realm,本文主要以iOS 平台的使用场景为例. realm是一个跨平台移动数据库引擎,支持iOS.OS X(Objective-C和Swift)以及Android: ...

  5. iOS中Realm数据库的基本用法

      原文  http://git.devzeng.com/blog/simple-usage-of-realm-in-ios.html 主题 RealmiOS开发 Realm是由 Y Combinat ...

  6. iOS开发-Realm数据库

    Realm Realm-Object-c,见:https://realm.io/cn/docs/objc/latest/Realm官网:https://realm.io 使用流程 导入头文件#impo ...

  7. Android Realm初试

    Realm is a mobile database that runs directly inside phones, tablets or wearables. This repository h ...

  8. [RN] React Native 使用 realm 数据库

    React Native 使用 realm  数据库 realm 是一款专为移动 ​ 端开发的高性能数据库,其宣称自己是最快的 react-native 数据库. realm 整体的优点有这么四点: ...

  9. Android Sqlite 数据库版本更新

      Android Sqlite 数据库版本更新 http://87426628.blog.163.com/blog/static/6069361820131069485844/ 1.自己写一个类继承 ...

随机推荐

  1. OmniGraffle-新手指南

    OmniGraffle是一款不错的可视化软件,通过它你可以把你想要展现的数据简介.直观的展现在图.表上,这是我在数据可视化工具这篇随笔中说过的功能.但是当我真正用它时,我发现它可以做的事情还有很多. ...

  2. UVa 10137 & ZOJ 1874 The Trip

    题目大意:n个学生去旅行,旅行中每个学生先垫付,最后平摊所有费用,多退少补,并且支出差距控制在1分钱以内,求最小的交易金额. @2013-8-16 以前在zoj做过,把原来的代码直接提交了,虽然AC了 ...

  3. Mysql中主从复制的原理、配置过程以及实际案例

    Mysql中主从复制的原理.配置过程以及实际案例1.什么是主从复制?原理:主从分离,什么意思呢?我们不妨画个图看看.如图1所示: 2.准备工作:预备两台服务器,我这里使用虚拟机安装了两个Centos6 ...

  4. ajax请求获取到数据,但是仍然不能触发success方法

    这个问题消耗了我的很多时间. 原来是因为.php文件中的 echo echo json_encode($k);  后面少加了个exit; 因为echo echo json_encode($k); 之后 ...

  5. jmeter接口测试实践

    一.什么是接口测试? 接口测试是测试系统组件间接口的一种测试.接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点.测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻 ...

  6. NodeMCU之旅(一):构建、刷入固件,上传代码

    扬帆起航 本系列文章将试图实现,使用Web页面远程点亮led.具体包括: 在NodeMCU上搭建HTTP服务器,使其可以通过Web页面配置要接入的网络. 在配置页面可以显示附近中英网络名与信号强度. ...

  7. [ Android 五种数据存储方式之一 ] —— SharedPreferences存储数据

    SharedPreferences类,它是一个轻量级的存储类,特别适合用于保存软件配置参数. 主要是保存一些常用的配置比如窗口状态,一般在Activity中 重载窗口状态onSaveInstanceS ...

  8. #DP# ----- OpenJudge最大子矩阵

    OpenJudge 1768:最大子矩阵 总时间限制: 1000ms   内存限制: 65536kB 描述 已知矩阵的大小定义为矩阵中所有元素的和.给定一个矩阵,你的任务是找到最大的非空(大小至少是1 ...

  9. sql server停止和重启命令

    http://www.ynpxrz.com/n822732c2024.aspx 我们知道:sql server重启分分两步走 1.停止 net stop mssqlserver 2.重启 net st ...

  10. spring mvc 异常处理和session添加

    在controller中进行设置 package com.stone.controller; import java.util.HashMap; import java.util.Map; impor ...