You are reading the documentation for an older version of Realm. You can view the latest documentation instead.

Realm Java 让你能够高效地编写 app 的模型层代码,保证你的数据被安全、快速地存储。参考下列示例来开始你的 Realm 之旅:

// Define you model class by extending RealmObject
public class Dog extends RealmObject {
    private String name;
    private int age;

    // ... Generated getters and setters ...
}

public class Person extends RealmObject {
    @PrimaryKey
    private long id;
    private String name;
    private RealmList<Dog> dogs; // Declare one-to-many relationships

    public Person(long id, String name) {
        this.id = id;
        this.name = name;
    }

    // ... Generated getters and setters ...
}

// Use them like regular java objects
Dog dog = new Dog();
dog.setName("Rex");
dog.setAge(1);

// Initialize Realm
Realm.init(context);

// Get a Realm instance for this thread
Realm realm = Realm.getDefaultInstance();

// Query Realm for all dogs younger than 2 years old
final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => 0 because no dogs have been added to the Realm yet

// Persist your data in a transaction
realm.beginTransaction();
final managedDog = realm.copyToRealm(dog); // Persist unmanaged objects
Person person = realm.createObject(Person); // Create managed objects directly
person.getDogs().add(managedDog);
realm.commitTransaction();

// Listeners will be notified when data changes
puppies.addChangeListener(new RealmChangeListener<RealmResults<Dog>>() {
    @Override
    public void onChange(RealmResults<Dog> results) {
        // Query results are updated in real time
        puppies.size(); // => 1
    }
});

// Asynchronously update objects on a background thread
realm.executeTransactionAsync(new Realm.Transaction() {
    @Override
    public void execute(Realm bgRealm) {
        Dog dog = bgRealm.where(Dog.class).equals("age", 1).findFirst();
        dog.setAge(3);
    }
}, new Realm.Transaction.OnSuccess() {
    @Override
    public void onSuccess() {
    	// Original queries and Realm objects are automatically updated.
    	puppies.size(); // => 0 because there are no more puppies younger than 2 years old
    	managedDog.getAge();   // => 3 the dogs age is updated
    }
});

快速入门

前提

  • Android Studio 1.5.1 或者更高版本;
  • JDK 版本 >=7;
  • 较新的 Android SDK 版本;
  • 我们支持 Android API 9 以上的所有版本(Android 2.3 Gingerbread 及以上)。

Note: 目前我们还不支持 Android 以外的 Java 环境;我们不再支持 Eclipse IDE,请迁移到 Android Studio。

安装

第一步: 在项目的 build.gradle 文件中添加如下 class path 依赖。

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

项目的 build.gradle 文件在如下位置: Project level build.gradle file

第二步: 在 app 的 build.gradle 文件中应用 realm-android 插件。

apply plugin: 'realm-android'

app的 build.gradle 文件在如下位置:

Application level build.gradle file

请参考以下示例工程对两个 build.gradle 文件的修改:

其它构建系统

构建工具 Maven 和 Ant 已不再被支持。如果你仍然有充足的理由需要得到这些构建工具的支持,请在如下两个链接留言说明你的理由以便我们决定是否应该添加对它们的支持。

ProGuard

Realm 已经集成了 ProGuard 的配置。你不需要针对 Realm 做 ProGuard 的改动。

Realm 浏览器

我们提供了一个独立的应用程序 Realm Browser 来读写 .realm 数据库。

你可以通过 Tools > Generate demo database 生成一个测试数据库。

查阅 StackOverflow 上的这个答案 获得有关你的应用程序 Realm 文件路径的详细说明。

Realm Browser 已经上架Map App Store。你也可以从我们 github 页面下载。

Realm Browser 目前不能运行在 Windows 或者 Linux 环境中。但你可以使用Stetho-RealmStetho 是 Facebook 提供的桥接安卓调试和 Chrome 浏览器的组件。

API 手册

你可以查阅 API 手册获得所有关于类、方法及其它细节的信息。

示例

请查看我们的示例来获得有关 Realm 的常用功能。有关编译和运行示例的细节可以参考这里

introExample 包含了如何使用当前的API的简单例子。

gridViewExample 用来展示如何使用 Realm 作为 GridView 的后端存储。它同时也展示了如何用 JSON 来填充数据库。另外还有怎么通过 ABI splits 来缩小 APK 体积。

threadExample 展示了如何在多线程环境中使用 Realm。

The adapterExample 展示了如何便捷地使用 RealmBaseAdapterRealmRecyclerViewAdapter 绑定 RealmResults 到安卓的 ListViewRecyclerView

jsonExample 展示了 Realm 与 JSON 相关的功能。

encryptionExample 向你展示如何使用加密的 Realm。

rxJavaExamples 展示了如何与 RxJava 结合使用 Realm。

unitTestExample 展示了如何写与 Realm 相关的单元测试。

获得帮助

模型

Realm 数据模型定义需要继承自 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; }
}

Realm 数据模型不仅仅支持 private 成员变量,你还可以使用 publicprotected 以及自定义的成员方法。

public class User extends RealmObject {

    public String name;

    public boolean hasLongName() {
      return name.length() > 7;
    }

    @Override
    public boolean equals(Object o) {
      // Custom equals comparison
    }
}

字段类型

Realm 支持以下字段类型:booleanbyteshortintlongfloatdoubleStringDatebyte []。整数类型 shortintlong 都被映射到 Realm 内的相同类型(实际上为 long )。再者,还可以使用 RealmObject 的子类和 RealmList<? extends RealmObject> 来表示模型关系。

Realm 对象中还可以声明包装类型(boxed type)属性,包括:BooleanByteShortIntegerLongFloatDouble。通过使用包装类型,可以使这些属性存取空值(null)。

@Required修饰类型和空值(null)

某些时候,空值(null)对于属性并不合适。这时可以使用注解 @Required 告诉 Realm 强制禁止空值(null)被存储。只有 BooleanByteShortIntegerLongFloatDoubleStringbyte[] 以及 Date 可以被 @Required 修饰。在其它类型属性上使用 @Required 修饰会导致编译失败。基本数据类型(primitive types)不需要使用注解 @Required,因为他们本身就不可为空。RealmObject 属性永远可以为空。

忽略的属性

注解 @Ignore 意味着一个字段不应该被保存到 Realm。某些时候输入的信息包含比模型更多的字段,而你不希望处理这些未使用的数据字段,你可以用 @Ignore 来标识这些你希望被 Realm 忽略的字段。

Auto-Updating Objects

RealmObject 是实时的、自动更新的底层数据的映射视图。你不需要去重新获得对象已取得其最新版本。对于数据的改动会即时反应到相关的对象或者查询结果。

final Dog myDog;
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        myDog = realm.createObject(Dog.class);
        myDog.setName("Fido");
        myDog.setAge(1);
    }
});

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Dog myPuppy = realm.where(Dog.class).equalTo("age", 1).findFirst();
        myPuppy.setAge(2);
    }
});

myDog.getAge(); // => 2

RealmObjectRealmResults 的设计不仅仅是为了更快和更有效率,它们同时也让代码更加简洁以及反应性更强。举例来说,假设你的 Activity 或者 Fragment 依赖于某个 RealmObject 或者 RealmResults,你无需担心何时去刷新或者重新获取它们以更新 UI——它们会自动更新。

你可以通过订阅 Realm notifications 来得知 Realm 的数据在何时被更新从而刷新你的 UI。

索引(Index)属性

注解 @Index 会为字段增加搜索索引。这会导致插入速度变慢,同时数据文件体积有所增加,但能加速查询。因此建议仅在需要加速查询时才添加索引。目前仅支持索引的属性类型包括:StringbyteshortintlongbooleanDate

主键 (primary keys)

@PrimaryKey 可以用来定义字段为主键,该字段类型必须为字符串(String)或整数(shortintlong)以及它们的包装类型(ShortIntLong)。不可以存在多个主键。使用支持索引的属性类型作为主键同时意味着为该字段建立索引。

当创建 Realm 对象时,所有字段会被设置为默认值。为了避免与具有相同主键的另一个对象冲突,建议创建一个 unmanaged 对象,为字段的赋值,然后用 copyToRealm() 方法将该对象复制到 Realm。

主键的存在意味着可以使用 copyToRealmOrUpdate() 方法,它会用此主键尝试寻找一个已存在的对象,如果对象存在,就更新该对象;反之,它会创建一个新的对象。当 copyToRealmOrUpdate() 的调用对象没有主键时,会抛出异常。

使用主键会对性能产生影响。创建和更新对象将会慢一点,而查询则会变快。很难量化这些性能的差异,因为性能的改变跟你数据库的大小息息相关。

Realm.createObject()会返回一个所有字段被设置为默认值的新对象。如果该模型类存在主键,那么有可能返回对象的主键的默认值与其它已存在的对象冲突。建议创建一个非托管(unmanaged)Realm 对象,并给其主键赋值,然后调用 copyToRealm() 来避免冲突。

final MyObject obj = new MyObject();
obj.setId(42);
obj.setName("Fish");
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // This will create a new object in Realm or throw an exception if the
        // object already exists (same primary key)
        // realm.copyToRealm(obj);

        // This will update an existing object with the same primary key
        // or create a new object if an object with no primary key = 42
        realm.copyToRealmOrUpdate(obj);
    }
});

字符串(String)和包装类型(ShortIntLong)的主键可以被赋予空值(null);除非它们同时被 @Required 修饰。

定制对象(Customizing Objects)

你几乎可以把 RealmObject 当作 POJO 使用。只需扩展 RealmObject,将相应属性声明为 public。不需要 setter 和 getter, 而是直接访问属性。

public class Dog extends RealmObject {
    public String name;
    public int age;
}

你可以像使用其他类一样直接使用 Dog 类。createObject()copyToRealm() 可以帮助你创建一个托管给 Realm 的 Dog 对象。

realm.executeTransaction(new Realm.Transaction() {
    @Overrride
    public void execute(Realm realm) {
        Dog dog = realm.createObject(Dog.class);
        dog.name = "Fido";
        dog.age  = 5;
    }
};

如果需要的话,你可以在 setter 和 getter 中添加你自己的逻辑。例如你可能需要在保存之前验证值的合法性。另外,你还可以为你的 RealmObject 添加自定义方法。

RealmModel 接口

除直接继承于 RealmObject 来声明 Realm 数据模型之外,还可以通过实现 RealmModel 接口并添加 @RealmClass 修饰符来声明。

@RealmClass
public class User implements RealmModel {

}

RealmObject 的所有方法都有其相对应的静态方法。

// With RealmObject
user.isValid();
user.addChangeListener(listener);

// With RealmModel
RealmObject.isValid(user);
RealmObject.addChangeListener(user, listener);

关系

任意两个 RealmObject 可以相互关联。

public class Email extends RealmObject {
    private String address;
    private boolean active;
    // ... setters and getters left out
}

public class Contact extends RealmObject {
    private String name;
    private Email email;
    // ... setters and getters left out
}

RealmObject 之间的关联总体来说并不怎么消耗系统开销。Realm 对关系对象的处理非常高效并且节约内存。

多对一

你只需要简单地声明一个 Realm 模型类的属性即可:

public class Contact extends RealmObject {
    private Email email;
    // Other fields…
}

每个 Contact 对象都有 0 或 1 个 Email 对象。在 Realm 中,你可以任意在多个 Contact 对象中使用同一个 Email 对象。同理,这个例子也解释了怎样实现一对一关系。

设置一个类型为 RealmObject 的属性为空值(null)会清除该属性的引用,但并不会删除对应的 RealmObject

多对多

你可以通过使用 RealmList<T> 为一个对象关联0或多个其它对象。设想一个通讯录成员拥有多个 email:

public class Contact extends RealmObject {
    public String name;
    public RealmList<Email> emails;
}

public class Email extends RealmObject {
    public String address;
    public boolean active;
}

RealmList 是 Realm 模型对象的容器,其行为与 Java 的普通 List 近乎一样。同一个 Realm 模型对象可以存在于多个 RealmList 中。同一个 Realm 模型对象可以在同一个 RealmList 中存在多次。你可以使用 RealmList 来表现一对多和多对多的数据关系。

你可以创建对象,然后使用 RealmList.add() 来添加 Email 对象到 Contact

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Contact contact = realm.createObject(Contact.class);
        contact.name = "John Doe";

        Email email1 = realm.createObject(Email.class);
        email1.address = "[email protected]";
        email1.active = true;
        contact.emails.add(email1);

        Email email2 = realm.createObject(Email.class);
        email2.address = "[email protected]";
        email2.active = false;
        contact.emails.add(email2);
    }
});

有时递归关系很有用,这在 Realm 是允许的。

public class Person extends RealmObject {
    public String name;
    public RealmList<Person> friends;
    // Other fields…
}

当然,在使用递归关系的时候,你要注意死循环的问题。Realm 并不会检查 RealmList 的循环嵌套。

设置一个类型为 RealmList 的属性为空值(null)会清空该列表,即列表长度变为 0。但并不会删除列表中的任何 RealmObjectRealmList 的获取器(getter)永不会返回 null。其返回对象永远是一个 RealmList 实例,但其长度有可能为0。

Realm支持关联查询。以如下模型举例:

public class Person extends RealmObject {
  private String id;
  private String name;
  private RealmList<Dog> dogs;
  // getters and setters
}

public class Dog extends RealmObject {
  private String id;
  private String name;
  private String color;
  // getters and setters
}

每个 Person 对象都与多个 Dog 对象相关联,如下图所示:

Table Diagram

让我们通过关联查询来得到一些 Person 对象 ——

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

首先,请注意equalTofield名称包含关联的路径,以“.”分隔。

以上的查询含义为“所有至少含有一个 color 为 Brown 的 Person”。请务必注意,这里的返回的 Person 中,有可能包含 color 不为 Brown 的 Dog 对象,因为在其 RealmList 列表中,其它的 Dog 对象满足查询条件:

persons.get(0).getDogs(); // => [A,B]
persons.get(1).getDogs(); // => [B,C,D]

我们来看看另外两个查询:

// r1 => [U1,U2]
RealmResults<Person> r1 = realm.where(Person.class)
                             .equalTo("dogs.name", "Fluffy")
                             .findAll();

// r2 => [U1,U2]
RealmResults<Person> r2 = r1.where()
                          .equalTo("dogs.color", "Brown")
                          .findAll();

请注意第一个查询返回两个 Person 对象,因为它们都满足查询条件。每个 Person 对象都包含一个 Dog 对象列表——列表中至少有一个 Dog 对象满足查询条件。谨记我们是在寻找其拥有的 Dog 对象满足条件(namecolor)的 Person,不是在针对 Dog 对象进行查询。因此第二个查询建立在第一个的 Person 结果(r1)以及 r1 的每个 PersonDog 列表之上。两个 Person 仍然满足第二个查询,但这次是 color 满足查询条件。

我们再深入了解下这个概念,请看以下代码:

// r1 => [u1,u2]
realmresults<user> r1 = realm.where(user.class)
                             .equalto("dogs.name", "fluffy")
                             .equalto("dogs.color", "brown")
                             .findall();

// r2 => [u2]
realmresults<user> r2 = realm.where(user.class)
                             .equalto("dogs.name", "fluffy")
                             .findall()
                             .where()
                             .equalto("dogs.color", "brown")
                             .findall();
                             .where()
                             .equalto("dogs.color", "yellow")
                             .findall();

第一个查询表示找到所有的 Person 他至少有一个 Dog 的名字为 fluffy 并且找到所有 Person 他至少有一个 Dog 的颜色是 brown 然后返回这两个结果的交集。第二个查询表示找到所有的 Person 他至少有一个 Dog 的名字为 fluffy;然后在这个结果之上找到所有的 Person 他至少有一个 Dog 的颜色为 brown;最后在之前的结果之上找到所有的 Person 他至少有一个 Dog 的颜色为 yellow

我们来解释一下第一个查询以深入了解下这个行为。两个条件分别是equalto("dogs.name", "fluffy")equalto("dogs.color", "brown")u1u2 完全满足第一个条件 —— 我们称其 c1 集合。u1u2 也同时完全满足第二个条件 —— 我们称其 c2 集合。查询中的逻辑与即是 c1c2 的交集。c1c2 的交集就是 u1u2。因此,r1 就包含 u1u2

第二个查询不一样。我们来分别讲解。该查询第一部分看起来是这样的:realmresults<user> r2a = realm.where(user.class).equalto("dogs.name", "fluffy").findall()。它的结果包含 u1u2。然后 r2b = r2a.where().equalto("dogs.color", "brown").findall(); 的结果仍然包含 u1u2 (两个 Person 都有颜色为 brownDog)。最后的查询 r2 = r2b.where().equalto("dogs.color", "yellow").findall(); 结果只包含 u2,因为只有 u2 同时有一个颜色为 brownDog 和一个颜色为 yellowDog

写入

在任何时间都可以对对象进行访问和查询(读取事务是隐式的)。 所有的写操作(添加、修改和删除对象),必须包含在写入事务(transaction)中。写入事务可以提交或取消。在提交期间,所有更改都将被写入磁盘,并且,只有当所有更改可以被持久化时,提交才会成功。通过取消一个写入事务,所有更改将被丢弃。使用写入事务,会确保你的数据的一致性。

写入事务也用于确保线程安全:

// Obtain a Realm instance
Realm realm = Realm.getDefaultInstance();

realm.beginTransaction();

//... add or update objects here ...

realm.commitTransaction();

当你在写入事务内处理 Realm 对象时,你可能会遇到想要放弃更改的情况。你可以简单地取消写入事务:

realm.beginTransaction();
User user = realm.createObject(User.class);

//  ...

realm.cancelTransaction();

请注意,写入事务之间会互相阻塞,如果一个写入事务正在进行,那么其他的线程的写入事务就会阻塞它们所在的线程。同时在 U I线程和后台线程使用写入事务有可能导致 ANR 问题。可以使用 异步事务(async transactions)以避免阻塞 UI 线程。

Realm 数据文件不会因为程序崩溃而损坏。当有异常在事务块中被抛出时,当前事务中所做出的数据修改会被丢弃。如果在该情况下程序需要继续运行,那么请调用 cancelTransaction() 来中止事务,或者使用 executeTransaction() 来执行事务。

由但得益于 Realm 的 MVCC 架构,当正在进行一个写入事务时读取操作并不会被阻塞!这意味着,除非你需要从多个线程进行并发写入操作,否则,你可以尽量使用更大的写入事务来做更多的事情而不是使用多个更小的写入事务。当写入事务被提交到 Realm 时,该 Realm 的所有其他实例都将被通知,读入隐式事务将自动刷新你每个 Realm 对象。

在 Realm 中的读写访问是符合 ACID 的.

创建对象

由于 Realm 对象都强依赖于 Realm,它们应该直接通过 Realm 被实例化:

realm.beginTransaction();
User user = realm.createObject(User.class); // Create a new object
user.setName("John");
user.setEmail("[email protected]");
realm.commitTransaction();

或者你可以先创建一个对象的实例,并在之后使用 realm.copyToRealm() 添加。Realm 对象支持多个构造函数,只要其中之一是公共无参数构造函数即可。

User user = new User("John");
user.setEmail("[email protected]");

// Copy the object to Realm. Any further changes must happen on realmUser
realm.beginTransaction();
User realmUser = realm.copyToRealm(user);
realm.commitTransaction();

当使用 realm.copyToRealm() 时,请注意只有返回的对象是由 Realm 管理的,这非常重要。对原始对象(umanaged Object)的任何改变都不会写入 Realm.

事务执行块(Transaction blocks)

除手动调用 realm.beginTransaction()realm.commitTransaction()realm.cancelTransaction() 之外你可以使用 realm.executeTransaction() 方法,它会自动处理写入事物的开始和提交,并在错误发生时取消写入事物。

realm.executeTransaction(new Realm.Transaction() {
	@Override
	public void execute(Realm realm) {
		User user = realm.createObject(User.class);
		user.setName("John");
		user.setEmail("[email protected]");
	}
});

异步事务(Asynchronous Transactions)

事务会相互阻塞其所在的线程,在后台线程中开启事务进行写入操作可以有效避免 UI 线程被阻塞。通过使用异步事务,Realm 会在后台线程中进行写入操作,并在事务完成时将结果传回调用线程。

realm.executeTransactionAsync(new Realm.Transaction() {
    @Override
    public void execute(Realm bgRealm) {
        User user = bgRealm.createObject(User.class);
        user.setName("John");
        user.setEmail("[email protected]");
    }
}, 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.
    }
});

OnSuccessOnError 并不是必须重载的,重载了的回调函数会在事务成功或者失败时在被调用发生的线程执行。回调函数是通过 Looper 被执行的,所以在非 Looper 线程中只有空(null)回调函数被允许使用。

RealmAsyncTask transaction = realm.executeTransactionAsync(new Realm.Transaction() {
            @Override
            public void execute(Realm bgRealm) {
                User user = bgRealm.createObject(User.class);
                user.setName("John");
                user.setEmail("[email protected]");
            }
        }, null);

异步事务调用会返回一个 RealmAsyncTask 对象。当你退出 Activity 或者 Fragment 时可以使用该对象取消异步事务。如果你在回调函数中更新 UI,那么忘记取消异步事务可能会造成你的应用崩溃。

public void onStop () {
    if (transaction != null && !transaction.isCancelled()) {
        transaction.cancel();
    }
}

更新字符串和 byte 数组

Realm 的写操作针对的是整个字符串或 byte 数组属性而非该属性中的单独元素。假设你需要修改某 byte 数组中的第五个字符,你需要:

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        bytes[] bytes = realmObject.binary;
        bytes[4] = 'a';
        realm.binary = bytes;
    }
});

原因是 Realm 的 MVCC 架构需要在确定旧版本数据可以被舍弃之前仍然保留旧版本的数据。

快照(Snapshots)

所有的 Realm 集合都会自动更新。它们总会被更新到最新的数据。这在大多数情况下是符合预期的。但是在遍历并且同时修改集合元素时,自动更新的特性会给你带来麻烦。例如:

RealmResults<Person> guests = realm.where(Person.class).equalTo("invited", false).findAll();
realm.beginTransaction();
for (int i = 0; guests.size(); i++) {
    guests.get(i).setInvited(true);
}
realm.commitTransaction();

这段代码预期通过一个简单循环来邀请所有的 Guest 对象。因为 RealmResults 在每次循环都会被更新,你会发现最终的运行结果是只有一半的 Guest 对象收到了邀请。当 Guest 对象收到邀请以后,它会被立即从集合中移除,因为它不再满足查询条件,集合的大小在此刻发生了改变,所以当 i 增加时,循环会错过一个集合元素。

你可以通过使用集合数据的 快照 来解决这个问题。集合快照保证其中的元素及其顺序不会改变,即使在元素被修改甚至删除的情况下。

Realm 集合的迭代子(Iterator)会自动使用快照。你也可以通过 RealmResultsRealmListcreateSnapshot() 方法来创建一个快照。

RealmResults<Person> guests = realm.where(Person.class).equalTo("invited", false).findAll();

// Use an iterator to invite all guests
realm.beginTransaction();
for (Person guest : guests) {
    guest.setInvited(true);
}
realm.commitTransaction();

// Use a snapshot to invite all guests
realm.beginTransaction();
OrderedRealmCollectionSnapshot<Person> guestsSnapshot = guests.createSnapshot();
for (int i = 0; guestsSnapshot.size(); i++) {
    guestsSnapshot.get(i).setInvited(true);
}
realm.commitTransaction();

查询

Realm 中的所有读取(包括查询)操作都是延迟执行的,且数据绝不会被拷贝。

Realm 的查询引擎使用 Fluent interface 来构造多条件查询。

使用 User 类 -

public class User extends RealmObject {

    @PrimaryKey
    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; }
}

比如查找所有叫做 John 或 Peter 的用户,你可以这么写:

// Build the query looking at all users:
RealmQuery<User> query = realm.where(User.class);

// Add query conditions:
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");

// Execute the query:
RealmResults<User> result1 = query.findAll();

// 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();

查询返回一个 RealmResults 实例,其中包含名叫 John 和 Peter 的用户。在 findAll 方法家族中还有别的类似方法,例如 findAllSorted() 会返回一个排序后的结果集,findAllAsync() 会在后台线程异步进行查询。请参阅 API 文档 获得更多信息。

这些对象并不会被拷贝到集合中,也就是说你得到的是一个匹配对象引用的列表,你对匹配对象所有的操作都是直接施加于它的原始对象。RealmResults 继承自 Java 的 AbstractList,行为类似。例如你可以通过 index 来访问其中的某个对象。

当查询没有任何匹配时,返回的 RealmResults 对象将不会为 null,取而代之的是它的 size() 方法将返回 0。

修改或删除 RealmResults 中任何一个对象都必须在写入事务中完成。

你还可以进行关联查询

查询条件

通过 where() 方法你可以得到一个 RealmQuery 对象并通过该对象创建各种查询条件。

查询的第一个参数是字段名称。假如字段不支持这个查询条件异常会被抛出。

所有数据类型均支持如下查询条件:

  • equalTo()
  • notEqualTo()

in() 可以用来匹配一组数据中的某个成员。例如来查询 name 为 ”Jill“、”William” 或者 “Trillian” 的对象,你可以使用 in("name", new String[]{"Jill", "William", "Trillian"})in() 支持字符串、二进制数据和数值型字段。

数值型字段,包括 Date 还支持如下这些查询条件:

  • between()
  • greaterThan()
  • lessThan()
  • greaterThanOrEqualTo()
  • lessThanOrEqualTo()

字符串类型字段额外支持如下查询条件:

  • contains()
  • beginsWith()
  • endsWith()
  • like()

你还可以针对这四种字符串查询条件设置大小写敏感参数。Case.INSENSITIVE 会忽略大小写,而 Case.SENSITIVE 只匹配大小写完全相同的字符串。默认参数是 Case.SENSITIVE

逻辑运算符

每个查询条件都会被被隐式地被逻辑和(&)组合在一起,而逻辑或(or)需要显式地去执行 or()

使用 User 类 -

public class User extends RealmObject {

    @PrimaryKey
    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; }
}

你也可以将查询条件组合在一起,使用 beginGroup()(相当于左括号)和 endGroup()(相当于右括号):

RealmResults<User> r = realm.where(User.class)
                            .greaterThan("age", 10)  //implicit AND
                            .beginGroup()
                                .equalTo("name", "Peter")
                                .or()
                                .contains("name", "Jo")
                            .endGroup()
                            .findAll();

此外,也可以用 not() 否定一个条件。该 not() 运算符可以与 beginGroup()/endGroup() 一起使用来否定子条件。例如你想查找所有名字不为 “Peter” 或 “Jo” 的 User 对象:

RealmResults<User> r = realm.where(User.class)
                           .not()
                           .beginGroup()
                                .equalTo("name", "Peter")
                                .or()
                                .contains("name", "Jo")
                            .endGroup()
                            .findAll();

当然,针对这个特定的查询,in() 会更加的方便:

RealmResults<User> r = realm.where(User.class)
                           .not()
                           .in("name", new String[]{"Peter", "Jo"})
                           .findAll();

排序(Sorting)

当你执行完查询获得结果后,可以对它进行排序:

RealmResults<User> result = realm.where(User.class).findAll();
result = result.sort("age"); // Sort ascending
result = result.sort("age", Sort.DESCENDING);

排序默认为升序。你可以使用 Sort.DESCENDING 来做降序排序。你还可以同时针对不同字段进行排序。

唯一值(Unique values)

你可以使用 distinct() 来查找唯一值。例如查找所有不同的姓名:

RealmResults<Person> unique = realm.where(Person.class).distinct("name");

只有数值型和字符串字段支持该查询条件。和排序类似,你也可以针对多个字段使用该查询条件。

链式查询

因为查询结果并不会被复制,且在查询提交时并不会被执行,你可以链式串起查询并逐步进行分类筛选:

RealmResults<Person> teenagers = realm.where(Person.class).between("age", 13, 20).findAll();
Person firstJohn = teenagers.where().equalTo("name", "John").findFirst();

你也可以在子对象上使用链式查询。假设以上 Person 对象包含一个 Dog 对象列表:

public class Dog extends RealmObject {
    private int age;
    // getters & setters ...
}

public class Person extends RealmObject {
    private int age;
    private RealmList<Dog> dogs;
    // getters & setters ...
}

你可以查询找出所有年龄在 13 和 20 之间的 Person 并且他至少拥有一个 1 岁的 Dog

RealmResults<Person> teensWithPups = realm.where(Person.class).between("age", 13, 20).equalTo("dogs.age", 1).findAll();

请注意,查询链最终是建立在 RealmResults 上而非 RealmQuery。如果你在某存在的 RealmQuery 上添加更多的查询条件,那么你在修改查询本身,而非查询链。请参考关联查询

查询结果的自动更新(Auto-Updating Results)

RealmResults 是对其所包含数据的自动更新视图,这意味着它永远不需要被重新查询获取。数据对象的改变会立刻被反映到相应的查询结果。

final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => 0

realm.executeTransaction(new Realm.Transaction() {
    @Override
    void public execute(Realm realm) {
        Dog dog = realm.createObject(Dog.class);
        dog.setName("Fido");
        dog.setAge(1);
    }
});

puppies.addChangeListener(new RealmChangeListener() {
    @Override
    public void onChange(RealmResults<Dog> results) {
      // results and puppies point are both up to date
      results.size(); // => 1
      puppies.size(); // => 1
    }
});

这对所有的 RealmResults 有效 —— 无论是否有过滤条件、是否是链式查询。

RealmResults 的这个特性不仅使得 Realm 快速高效,而且让你的代码更简洁。举例来说,假设你的 Activity 或者 Fragment 依赖于某个查询结果,你可以将相应的 Realm 对象或者 RealmResults 保存为一个属性,你不需要在每次访问时确定其是否被更新 —— Realm 会保证这些。

你可以通过订阅 Realm notifications 来得知 Realm 数据更新了,进而刷新 UI 而不必重新查询获得 RealmResults

因为查询结果的自动更新特性,请不要依赖于固定的索引(indices)、不变的条目数。

按类型检索对象

从 Realm 中检索对象的最基本方法是 realm.where(Foo.class).findAll(),它返回了包含被查询模型类的所有对象的 RealmResults

另外还有提供排序功能的 findAll()。参见 realm.where(Foo.class).findAllSorted() 了解详情。

聚合

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

RealmResults<User> results = realm.where(User.class).findAll();
for (User u : results) {
    // ... do something with the object ...
}

或者使用 for 循环:

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 ...
}

RealmResults 的自动更新会通过 looper 事件触发,但在事件到来之前,某些元素有可能不再满足查询条件或者其已被删除。

final RealmResults<User> users = getUsers();
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        users.get(0).deleteFromRealm(); // indirectly delete object
    }
});

for (User user : users) {
    showUser(user); // Will crash for the deleted user
}

为避免该问题,可以使用 RealmResultsdeleteFromRealm() 方法:

final RealmResults<User> users = getUsers();
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        users.deleteFromRealm(0); // Delete and remove object directly
    }
});

for (User user : users) {
    showUser(user); // Deleted user will not be shown
}

删除

你可以从查询结果中删除数据:

// obtain the results of a query
final RealmResults<Dog> results = realm.where(Dog.class).findAll();

// All changes to data must happen in a transaction
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // remove single match
        results.deleteFirstFromRealm();
        results.deleteLastFromRealm();

        // remove a single object
        Dog dog = results.get(5);
        dog.deleteFromRealm();

        // Delete all matches
        results.deleteAllFromRealm();
    }
});

异步查询(Asynchronous Queries)

可以使用后台线程进行查询。

Realm 的大部分查询都非常快——快到可以使用在UI线程中而感觉不到延迟。但如果需要进行非常复杂的查询或者在大量数据中进行查询,那么使用后台线程进行查询将会是一个不错的主意。

示例:查找名字为 “John” 或者 “Peter” 的用户。

创建异步查询

RealmResults<User> result = realm.where(User.class)
                              .equalTo("name", "John")
                              .or()
                              .equalTo("name", "Peter")
                              .findAllAsync();

请注意,这里的调用并不会阻塞,而是立即返回一个 RealmResults<User>。这很类似于标准 Java 中 Future 的概念。查询将会在后台线程中被执行,当其完成时,之前返回的 RealmResults 实例会被更新。

如果你希望当查询完成、RealmResults 被更新时获得通知,你可以注册一个 RealmChangeListener。这个监听器会在 RealmResults 被更新时被调用(通常是在事务被提交后)。

注册回调

private RealmChangeListener callback = new RealmChangeListener<RealmResults<User>>() {
    @Override
    public void onChange(RealmResults<User> results) {
        // called once the query complete and on every update
    }
};

public void onStart() {
    RealmResults<User> result = realm.where(User.class).findAllAsync();
    result.addChangeListener(callback);
}

请在退出 Activity 或者 Fragment 时移除监听器的注册以避免内存泄漏。

public void onStop () {
    result.removeChangeListener(callback); // remove a particular listener
    // or
    result.removeChangeListeners(); // remove all registered listeners
}

检查查询是否完成

RealmResults<User> result = realm.where(User.class).findAllAsync();
if (result.isLoaded()) {
  // Results are now available
}

同步查询返回的 RealmResults 实例的 isLoaded 方法会永远返回 true

强制装载异步查询

你可以选择性地等待异步查询完成,而这将会阻塞当前线程,使查询变成同步(与 Future.get() 类似的概念)。

RealmResults<User> result = realm.where(User.class).findAllAsync();
result.load() // be careful, this will block the current thread until it returns

非 Looper 线程

你可以在 Looper 线程中使用异步查询。异步查询需要使用 Handler 来传递查询结果。在没有 Looper 的线程中使用异步查询会导致 IllegalStateException 异常被抛出。

Realms

Realm(s) 是我们对数据库的称谓:它包含多个不同的对象,并对应磁盘中的一个文件。在使用之前,需要对 Realm 库进行初始化操作:

Realm.init(context);

你需要提供一个安卓的 Context 对象来对 Realm 进行初始化。初始化操作只要进行一次。继承 Application 并在重载的 onCreate() 方法中进行初始化是个不错的主意。

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    Realm.init(this);
  }
}

请别忘了修改 AndroidManifest.xml:

<application
  android:name=".MyApplication"
  ...
/>

默认的 Realm

你可能已经注意到,我们总是通过Realm.getInstance(this)来访问我们已初始化的realm变量。该静态方法会为你的当前线程返回一个Realm实例,它对应了你Context.getFilesDir()目录中的default.realm文件。

该文件位于你应用的可写根目录中。默认情况下的Realm使用内部存储(internal storage),你的应用并不需要取得任何读写权限。一般来说,这个文件位于/data/data/<packagename>/files/

你可以通过realm.getPath()来获得该Realm的绝对路径。

请务必注意到Realm的实例是线程单例化的,也就是说,在同一个线程内多次调用静态方法获得针对同路径的Realm,会返回同一个Realm实例。

配置 Realm

RealmConfiguration 用来配置要被创建的 Realm 的各种特性。最简配置如下所示:

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

如上配置的 Realm 会被存储在 Context.getFilesDir() 并且命名为 default.realm

一个典型的配置如下所示:

// 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);

你还可以有多个RealmConfiguration。如此,你便可以控制Realm的版本、结构(schema)和路径。

RealmConfiguration myConfig = new RealmConfiguration.Builder()
  .name("myrealm.realm")
  .schemaVersion(2)
  .modules(new MyCustomSchema())
  .build();

RealmConfiguration otherConfig = new RealmConfiguration.Builder()
  .name("otherrealm.realm")
  .schemaVersion(5)
  .modules(new MyOtherSchema())
  .build();

Realm myRealm = Realm.getInstance(myConfig);
Realm otherRealm = Realm.getInstance(otherConfig);

你可以使用 Realm.getPath() 来获取 Realm 文件的绝对路径。

很重要的一点是 Realm 实例是线程单例化的,也就是说多次在同一线程调用静态构建器会返回同一 Realm 实例。

默认 RealmConfiguration

RealmConfiguration可以保存为默认配置。通过在自定义的Application设置默认的Realm配置,可以使你在代码中的其他地方更加方便地创建针对该默认配置的Realm。

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // The Realm file will be located in Context.getFilesDir() with name "default.realm"
    RealmConfiguration config = new RealmConfiguration.Builder().build();
    Realm.setDefaultConfiguration(config);
  }
}

public class MyActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Realm realm = Realm.getDefaultInstance();
    try {
        // ... Do something ...
    } finally {
        realm.close();
    }
  }
}

In-Memory Realm

定义一个非持久化的、存在于内存中的 Realm 实例:

RealmConfiguration myConfig = new RealmConfiguration.Builder()
    .name("myrealm.realm")
    .inMemory()
    .build();

这样就可以创建一个存在于“内存中的” Realm。“内存中的”Realm 在内存紧张的情况下仍有可能使用到磁盘存储,但是这些磁盘空间都会在Realm实例完全关闭的时候被释放。

请注意使用同样的名称同时创建“内存中的”Realm 和常规的(持久化)Realm 是不允许的。

当某个“内存中的”Realm 的所有实例引用都被释放,该 Realm 下的数据也同时会被清除。建议在你的应用生命周期中保持对“内存中的” Realm 实例的引用以避免非期望的数据丢失。

Dynamic Realm

对于普通的 Realm 来说,数据模型被定义成了 RealmObject 的子类。这样做保证了类型安全,但有时候某些数据模型在编译期是无法获得的。例如在处理数据迁移(migration)或CSV文件的时候。

DynamicRealm 是普通 Realm 的一个变种。它可以在没有 RealmObject 子类的情况下操作 Realm 数据。其对数据的访问是基于字符串而非 RealmObject 的定义。

创建 Dynamic Realm 使用与创建普通 Realm 相同的RealmConfiguration,但是它的创建过程会忽略对 schema、migration以及 schema 版本的检查。

RealmConfiguration realmConfig = new RealmConfiguration.Builder().build();
DynamicRealm realm = DynamicRealm.getInstance(realmConfig);

// In a DynamicRealm all objects are DynamicRealmObjects
DynamicRealmObject person = realm.createObject("Person");

// All fields are accessed using strings
String name = person.getString("name");
int age = person.getInt("age");

// An underlying schema still exists, so accessing a field that does not exist
// will throw an exception
person.getString("I don't exist");

// Queries still work normally
RealmResults<DynamicRealmObject> persons = realm.where("Person")
    .equalTo("name", "John")
    .findAll();

DynamicRealm 以类型安全和性能为代价,换来了更多的灵活性。请在你确实需要这些灵活性的情况下使用。

关闭 Realm 实例

Realm 实现了 Closeable 接口以便与释放 native 内存和文件描述符,请务必在使用完毕后关闭 Realm 实例。

Realm 实例是基于引用计数的, 也就是说假设你在同一个线程中调用了 getInstance() 两次,你需要同样调用 close() 两次以关闭该实例。举例来说,如果你需要实现 Runnable,简单地在函数开始的时候调用 getInstance(),在函数结束的时候调用 close() 即可!

对于UI线程,你可以选择在 onDestroy() 方法内调用 realm.close()

如果你想使用除 UI 线程外的 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();
            }
        }
    }
}

对于 AsyncTask,这里有个不错的例子可以参考:

protected Void doInBackground(Void... params) {
    Realm realm = Realm.getDefaultInstance();
    try {
        // ... Use the Realm instance ...
    } finally {
        realm.close();
    }

    return null;
}

如果你想使用 ThreadRunnable 进行一些短期任务,参考如下代码:

// Run a non-Looper thread with a Realm instance.
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        Realm realm = null;
        try {
            realm = Realm.getDefaultInstance();
            // ... Use the Realm instance ...
        } finally {
            if (realm != null) {
                realm.close();
            }
        }
    }
});

thread.start();

如果你很幸运地工作在 minSdkVersion >= 19Java >= 7 之下,可以使用try-with-resources

try (Realm realm = Realm.getDefaultInstance()) {
    // No need to close the Realm instance manually
}

自动更新(Auto-Refresh)

如果 Realm 实例存在于一个带有 Looper 的线程,那么这个 Realm 实例即具有自动更新的功能。这意味这如果发生了 Realm 数据库的变化,那么该 Realm 实例会在下一个事件循环(event loop)中自动更新。这个便捷的功能使你不必花费太多的精力就能保证的UI与数据的实时同步。

如果 Realm 的实例所在线程没有绑定 Looper,那么该实例不会被更新直到你手动调用 waitForChange() 方法。请注意,不更新 Realm 以保持对旧数据的引用会造成而外的磁盘和内存开销。这也是为什么要在线程结束时调用 close() 关闭 Realm 实例的一个重要原因。

如果你想确定当前 Realm 实例是否有自动更新功能,可以通过调用 isAutoRefresh() 方法查询。

查找 Realm 数据库文件

如果你想知道你应用的 Realm 文件的具体路径,请参见这个 StackOverflow上的答案

线程(Threading)

其实对于跨线程使用 Realm,你需要知道的事情并不多。关键点是得益于对象和查询的即时更新特性,你不需要担心数据在多线程时的一致性和效率问题。

你可以实时在不同线程中读取和写入 Realm 对象,不用担心其它线程会对同一对象进行操作。你需要在改变对象时使用事务,在另一线程中指向同一对象的数据会被即时更新(更新会在下一次事件循环时进行)。

唯一局限是你不能随意跨线程传递 Realm 对象。如果你在另一线程使用同一对象,请在哪个线程使用查询重新获得该对象。请谨记所有的 Realm 对象都会在不同线程中保持更新——Realm 会在数据改变时通知你。

参考如下实例。

Realm 线程实例

假设我们的应用要展示一个用户列表。我们在一个后台线程中(一个安卓 IntentService)从远端获取新用户并将它们存储到 Realm 中。但后台线程存储新用户时,UI 线程中的数据会被自动更新。UI 线程会通过 RealmChangeListener 得到通知,这时 UI 线程应刷新相应的控件。因为 Realm 的自动更新特性,无需重新查询数据。

// in a Fragment or Activity, etc
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    // ... boilerplate omitted for brevity
    realm = Realm.getDefaultInstance();
    // get all the customers
    RealmResults<Customer> customers = realm.where(Customer.class).findAllAsync();
    // ... build a list adapter and set it to the ListView/RecyclerView/etc

    // set up a Realm change listener
    changeListener = new RealmChangeListener<RealmResults<Customer>>() {
        @Override
        public void onChange(RealmResults<Customer> results) {
            // This is called anytime the Realm database changes on any thread.
            // Please note, change listeners only work on Looper threads.
            // For non-looper threads, you manually have to use Realm.waitForChange() instead.
            listAdapter.notifyDataSetChanged(); // Update the UI
        }
    };
    // Tell Realm to notify our listener when the customers results
    // have changed (items added, removed, updated, anything of the sort).
    customers.addChangeListener(changeListener);
}

// In a background service, in another thread
public class PollingService extends IntentService {
    @Override
    public void onHandleIntent(Intent intent) {
        Realm realm = Realm.getDefaultInstance();
        try {
            // go do some network calls/etc and get some data and stuff it into a 'json' var
            String json = customerApi.getCustomers();
            realm.beginTransaction();
            realm.createObjectFromJson(Customer.class, json); // Save a bunch of new Customer objects
            realm.commitTransaction();
            // At this point, the data in the UI thread is already up to date.
            // ...
        } finally {
            realm.close();
        }
    }
    // ...
}

一旦后台服务添加了新用户,customer 列表会被自动更新,不需要你的任何动作。对于单个的 RealmObject 也是同样。假设你需要管理一个 Realm 对象,只需要在一个线程中更新它的数据,UI 线程会自动得到更新后的数据。如果你需要对数据更新作出回应,只需要添加一个 listener,就像我们在以上代码中所作的一样。

这就是所有啦。

跨线程使用 Realm

请谨记:Realm、RealmObject 和RealmResults 实例都不可以跨线程使用。但是你可以使用异步查询异步事务来将部分操作放入后台线程进行,待完成时调用线程被通知以获取结果。

当你需要跨线程访问同一部分数据时,只需简单地在该线程重新获取一个 Realm 实例(例如:Realm.getInstance(RealmConfiguration config) 或是其他类似方法),然后通过这个 Realm 实例来查询获得你需要的数据。查询获得的对象会映射到 Realm 中的相同数据,由此方法获得对象在其线程中任何地方都可读写!

Schemas

Realm 使用所有项目中的 Realm 模型类来创建 schema。但这个行为是可以改变的,例如,你可以通过使用 RealmModule 让 Realm 只包含所有模型类的一个子集。

// Create the module
@RealmModule(classes = { Person.class, Dog.class })
public class MyModule {
}

// Set the module in the RealmConfiguration to allow only classes defined by the module.
RealmConfiguration config = new RealmConfiguration.Builder()
  .modules(new MyModule())
  .build();

// It is possible to combine multiple modules to one schema.
RealmConfiguration config = new RealmConfiguration.Builder()
  .modules(new MyModule(), new MyOtherModule())
  .build();

共享 schemas

库(library)开发者请注意: 在库中使用到的 Realm 必须通过 RealmModule 来暴露和使用其 schema。

这样可以防止库项目自动生成默认 RealmModule 从而避免和 app 生成的默认 RealmModule 冲突。库项目也是通过 RealmModule 来向 app 项目暴露自己的 Realm 模型类。

// Library must create a module and set library = true. This will prevent the default
// module from being created.
// allClasses = true can be used instead of listing all classes in the library.
@RealmModule(library = true, allClasses = true)
public class MyLibraryModule {
}

// Library projects are therefore required to explicitly set their own module.
RealmConfiguration libraryConfig = new RealmConfiguration.Builder()
  .name("library.realm")
  .modules(new MyLibraryModule())
  .build();

// Apps can add the library RealmModule to their own schema.
RealmConfiguration config = new RealmConfiguration.Builder()
  .name("app.realm")
  .modules(Realm.getDefaultModule(), new MyLibraryModule())
  .build();

目前你不可以在一个 Realm 文件里声明多个 RealmModule。如果你有多个 RealmModule,你需要将它们声明在多个 Realm 文件中以确保每个文件只有一个 RealmModule 声明。

这里有一个如何使用在库和 app 项目间使用 RealmModule 的完整例子

JSON

你可以直接将 JSON 对象添加到 Realm 中,这些 JSON 对象可以是一个 String、一个 JSONObject 或者是一个 InputStream。Realm 会忽略 JSON 中存在但未定义在 Realm 模型类里的字段。单独对象可以通过 Realm.createObjectFromJson() 添加。对象列表可以通过 Realm.createAllFromJson() 添加。

// A RealmObject that represents a city
public class City extends RealmObject {
    private String city;
    private int id;
    // getters and setters left out ...
}

// Insert from a string
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        realm.createObjectFromJson(City.class, "{ city: \"Copenhagen\", id: 1 }");
    }
});

// Insert multiple items using an InputStream
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        try {
            InputStream is = new FileInputStream(new File("path_to_file"));
            realm.createAllFromJson(City.class, is);
        } catch (IOException e) {
            throw new RuntimeException();
        }
    }
});

Realm 解析 JSON 时遵循如下规则: * 使用包含空值(null)的 JSON 创建对象: * 对于非必须(可为空值的属性),设置其值为 null; * 对于必须(不可为空值的属性),抛出异常; * 使用包含空值(null)的 JSON 更新对象: * 对于非必须(可为空值的属性),设置其值为 null; * 对于必须(不可为空值的属性),抛出异常; * 使用不包含对应属性的 JSON: * 该属性保持不变

通知(Notifications)

你可以通过针对 RealmRealmResults 或者 RealmList 注册监听器来获取数据更新通知。

你可以通过调用 removeChangeListener()removeAllChangeListeners() 来停止通知。侦听器所注册的相应对象被垃圾回收时,通知也会停止。请在你所期望的通知生命周期内,保持对侦听器注册的相应对象的强引用。

通知的传递机制(How Notifications are Delivered)

通知回调函数永远只会在注册通知的对应线程上进行调用,并且该线程需要一个正在运行的 Looper

如果相关的写操作发生在一个另外的线程之上,那么侦听器会在事务提交后被异步调用。

如果相关写操作发生在相同线程之上,那么侦听器会在事务提交时被同步调用。如果当本线程中的 Realm 数据不是最新版本,且最新版本的数据改变与当前注册的监听器相关,那么当事务开始(beginTransaction())的时候,侦听器也会被同步调用。这些情况里侦听器会被 beginTransaction()commitTransaction() 同步调用,这意味着这时在回调函数中开启事务(beginTransaction())会由于事务嵌套的原因抛出异常。假如你的应用架构有可能触发这种情况,请通过 Realm.isIntransaction() 来判断是否当前 Realm 实例的事务状态。

因为异步的通知是通过 looper 事件来传递的,队列中的其他无关事件有可能因为通知送达延迟。这意味着多个事务中改变的数据不一定总是通过多个通知来送达侦听器,它们有可能被合并为一个通知送达。

Realm 通知(Realm Notifications)

当后台线程向 Realm 添加数据,你的 UI 线程或者其它线程可以添加一个监听器来获取数据改变的通知。监听器在 Realm 数据改变的时候会被触发。

public class MyActivity extends Activity {
    private Realm realm;
    private RealmChangeListener realmListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      realm = Realm.getDefaultInstance();
      reamlListener = new RealmChangeListener<Realm>() {
        @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();
    }
}

集合通知(Collection Notifications)

集合通知与 Realm 通知不同,它会包含针对那些集合数据改变的信息。这些信息会指明从上次通知到达以来,集合中的插入、删除以及改变的元素索引。

对于异步查询(findAllAsync())来说通知第一次到达时改变集合为空,表明了这是异步查询完成后的第一次通知,之后的通知则包含相应的集合改变信息。

你可以通过传递给监听器的 OrderedCollectionChangeSet 对象来获取集合中删除(deletions)、插入(insertions)和修改(changes)的元素索引。

插入(insertions)和删除(deletions)指有元素被添加进或移除出集合从而引起集合元素构成的改变。这些改变有可能在你创建或者删除相关 Realm 对象时发生。对于 RealmResults 来说,当你修改了某个 Realm 对象的属性,而这个属性修改会导致查询结果的不同,这时集合的插入和修改通知也会被触发。

修改(changes)通知会当集合中的某对象的属性改变时被触发,当然这个属性的改变需能保证该元素仍然满足查询条件从而使其仍然存在于集合当中。

public class Dog extends RealmObject {
  public String name;
  public int age;
}

public class Person exteds RealmObject {
  public String name;
  public RealmList<Dog> dogs;
}

让我们假设你在监听如上一个 Person 对象集合,你会在如下情况收到通知:

  • 集合中某 Person 对象姓名改变;
  • 你在 dogs 中添加或者移除了一个 dog 对象。

这使得我们可以分别控制 UI 不同部分的动画和更新而不需要在数据改变时刷新整个 UI。

private final OrderedRealmCollectionChangeListener<RealmResults<Person>> changeListener = new OrderedRealmCollectionChangeListener() {
    @Override
    public void onChange(RealmResults<Person> collection, OrderedCollectionChangeSet changeSet) {
        // `null`  means the async query returns the first time.
        if (changeSet == null) {
            notifyDataSetChanged();
            return;
        }
        // For deletions, the adapter has to be notified in reverse order.
        OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges();
        for (int i = deletions.length - 1; i >= 0; i--) {
            OrderedCollectionChangeSet.Range range = deletions[i];
            notifyItemRangeRemoved(range.startIndex, range.length);
        }

        OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges();
        for (OrderedCollectionChangeSet.Range range : insertions) {
            notifyItemRangeInserted(range.startIndex, range.length);
        }

        OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges();
        for (OrderedCollectionChangeSet.Range range : modifications) {
            notifyItemRangeChanged(range.startIndex, range.length);
        }
    }
};

请参考 RealmRecyclerViewAdapter

对象通知(Object notifications)

你可以为某个 Realm 对象注册监听器以获得针对对象属性修改的细粒度通知。

只有托管 RealmObject 可以注册监听器。

你可以通过监听器传递的 ObjectChangeSet 来获取对象属性改变的详细信息,包括哪些属性被修改以及该对象已被删除。

在监听对象被删除时 ObjectChangeSet.isDeleted() 会返回 true

ObjectChangeSet.getChangedFields() 会返回哪些字段有所改变。你也可以通过 ObjectChangeSet.isFieldChanged() 来判断一个指定字段是否改变。

private final RealmObjectChangeListener<Dog> listener = new RealmObjectChangeListener<Dog>() {
    @Override
    public void onChange(Dog dog, ObjectChangeSet changeSet) {
        if (changeSet.isDeleted()) {
            Log.i(TAG, "The dog was deleted");
            return;
        }

        for (String fieldName : changeSet.getChangedFields()) {
            Log.i(TAG, "Field " + fieldName + " was changed.");
        }
    }
};

迁移(Migrations)

所有数据库都要处理模型改变的情况。Realm 的数据模型用标准 Java 对象来定义,改变数据模型只需要改变数据对象定义即可。

如果没有旧 Realm 数据文件存在,那么代码的改变即会反应到相应的 Realm 数据文件改变。但如果已经有旧版本的 Realm 数据文件存在,Realm 会抛出异常提示数据库文件需要迁移。请在相应的 RealmConfiguration 设置 schema 版本和 migration 代码来正确处理并避免该异常抛出。

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()

如上示例使得相应的 migration 代码在迁移需要的时候被自动执行。我们提供了相关 API 用来升级已保存的 schema 以及对应之前 schema 的数据。

// Example migration adding a new class
RealmMigration migration = new RealmMigration() {
  @Override
  public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {

     // DynamicRealm exposes an editable schema
     RealmSchema schema = realm.getSchema();

     // Migrate to version 1: Add a new class
     // Example:
     // public Person extends RealmObject {
     //     private String name;
     //     private int age;
     //     // getters and setters left out for brevity
     // }
     if (oldVersion == 0) {
        schema.create("Person")
            .addField("name", String.class)
            .addField("age", int.class);
        oldVersion++;
     }

     // Migrate to version 2: Add a primary key + object references
     // Example:
     // public Person extends RealmObject {
     //     private String name;
     //     @PrimaryKey
     //     private int age;
     //     private Dog favoriteDog;
     //     private RealmList<Dog> dogs;
     //     // getters and setters left out for brevity
     // }
     if (oldVersion == 1) {
        schema.get("Person")
            .addField("id", long.class, FieldAttribute.PRIMARY_KEY)
            .addRealmObjectField("favoriteDog", schema.get("Dog"))
            .addRealmListField("dogs", schema.get("Dog"));
        oldVersion++;
     }
  }
}

请查阅这个数据迁移例子来获取这方面的细节。

如果没有旧 Realm 数据文件存在,那么迁移并不需要,在这种情况下,Realm 会创建一个新的以 .realm 为后缀,基于新的对象模型的数据文件。在开发和调试过程中,假如你需要频繁改变数据模型,并且不介意损失旧数据,你可以直接删除 .realm 文件(这里包含所有的数据!)而不用关心迁移的问题。这在你应用的开发早期阶段非常有用。

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

加密

translation missing: cn.documentation.cocoa.alert

Realm 文件可以通过传递一个512位(64字节)的密钥参数给 Realm.getInstance().encryptionKey() 来加密存储在磁盘上。

byte[] key = new byte[64];
new SecureRandom().nextBytes(key);
RealmConfiguration config = new RealmConfiguration.Builder()
  .encryptionKey(key)
  .build();

Realm realm = Realm.getInstance(config);

这保证了所有永久性存储在磁盘上的数据都是通过标准 AES-256 加密的。每次创建新的 Realm 实例的时候,都需要提供相同的密钥。

参考 examples/encryptionExample。这个例子演示了如何通过 Android KeyStore 来安全地存储密钥。

适配器(Adapter)

Realm提供了一些抽象的工具类来方便地将 OrderedRealmCollectionRealmResultsRealmList 都实现了这个借口)展示到UI控件上。

你需要在 app 的 build.gradle 中添加额外的依赖以使用这些适配器。

dependencies {
	compile 'io.realm:android-adapters:2.0.0'
}

Intents

因为你不可以直接通过 intent 传递 RealmObject,我们建议你只传递 RealmObject 的标识符。举个常用例子,假如你的对象拥有一个主键,请通过 intent 的 bundle 来传递这个主键的值。

// Assuming we had a person class with a @PrimaryKey on the 'id' field ...
Intent intent = new Intent(getActivity(), ReceivingService.class);
intent.putExtra("person_id", person.getId());
getActivity().startService(intent);

在接受方(Activty、Service、IntentService、BroadcastReceiver 及其它)从 bundle 中解析出这个主键然后打开 Realm 查询得到这个 RealmObject

// in onCreate(), onHandleIntent(), etc.
String personId = intent.getStringExtra("person_id");
Realm realm = Realm.getDefaultInstance();
try {
    Person person = realm.where(Person.class).equalTo("id", personId).findFirst();
    // do something with the person ...
} finally {
    realm.close();
}

可以参考 threading example 中的 Object Passing 部分。该示例展示了在安卓开发中常用的如何传递 id 并且得到对应的 RealmObject

Android Framework 多线程 API 相关

当你使用下列 API 时请小心:

AsyncTaskdoInBackground() 方法会运行在一个后台线程。IntentServiceonHandleIntent(Intent intent) 方法会运行在一个后台工作线程。

如果你需要在这些方法中使用 Realm,请在对 Realm 的调用结束后关闭 Realm 实例。见如下例子。

AsyncTask

doInBackground 方法中打开并关闭 Realm,如下所示:

private class DownloadOrders extends AsyncTask<Void, Void, Long> {
    protected Long doInBackground(Void... voids) {
        // Now in a background thread.

        // Open the Realm
        Realm realm = Realm.getDefaultInstance();
        try {
            // Work with Realm
            realm.createAllFromJson(Order.class, api.getNewOrders());
            Order firstOrder = realm.where(Order.class).findFirst();
            long orderId = firstOrder.getId(); // Id of order
            return orderId;
        } finally {
            realm.close();
        }
    }

    protected void onPostExecute(Long orderId) {
        // Back on the Android mainThread
        // do something with orderId such as query Realm
        // for the order and perform some operation with it.
    }
}

IntentService

ChangeListener 在 IntentService 中不会工作。尽管 IntentService 本身是一个 Looper 线程,但每次 onHandleIntent 的调用是独立的事件。你可以注册监听器的调用不会返回失败,但他们永远不会被触发。

onHandleIntent() 方法中打开并关闭 Realm,如下所示:

public class OrdersIntentService extends IntentService {
    public OrdersIntentService(String name) {
        super("OrdersIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // Now in a background thread.

        // Open the Realm
        Realm realm = Realm.getDefaultInstance();
        try {
            // Work with Realm
            realm.createAllFromJson(Order.class, api.getNewOrders());
            Order firstOrder = realm.where(Order.class).findFirst();
            long orderId = firstOrder.getId(); // Id of order
        } finally {
            realm.close();
        }
    }
}

对其它库的支持

这个章节描述了怎样与其它 Android 流行库搭配使用 Realm。

GSON

GSON 是 Google 开发的 JSON 处理库。GSON 与 Realm 可以无缝配合使用。

// Using the User class
public class User extends RealmObject {
    private String name;
    private String email;
    // getters and setters left out ...
}

Gson gson = new GsonBuilder().create();
String json = "{ name : 'John', email : '[email protected]' }";
User user = gson.fromJson(json, User.class);

GridViewExample 展示了如何配合 GSON 使用 Realm。

序列化(Serialization)

你有时需要序列化与反序列化一个 Realm 对象以便与其它库(比如 Retrofit)相配合。因为 GSON使用成员变量值而非 getter 和 setter ,所以你无法通过 GSON 的一般方法来序列化 Realm 对象。

你需要为 Realm 模型对象自定义一个 JsonSerializer 并且将其注册为一个 TypeAdapter

请参考这个 Gist

数组(Primitive lists)

某些 JSON API 会以数组的形式返回原始数据类型(例如 String 和 integer),Realm 暂时不支持对这种数组的处理。但你可以通过自定义 TypeAdapter来处理这种情况。

这个 Gist 展示了如何将 JSON 中的整型数组存入 Realm。类似地,你可以用这个方法来处理其它原始数据类型数组。

Troubleshooting

Realm 对象属性可能会包含循环引用。在这种情况下,GSON 会抛出 StackOverflowError。例如如下 Realm 对象拥有一个 Drawable 属性:

public class Person extends RealmObject {
    @Ignore
    Drawable avatar;
    // other fields, etc
}

Person 类含有一个 Android Drawable 并且被 @Ignore 修饰。当 GSON 序列化时,Drawable 被读取并且造成了堆栈溢出。(GitHub Issue)。添加如下代码以避免类似问题:

public boolean shouldSkipField(FieldAttributes f) {
  return f.getDeclaringClass().equals(RealmObject.class) || f.getDeclaringClass().equals(Drawable.class);
}

请注意对 Drawable.class 的判定语句,它告诉 GSON 跳过这个属性的序列化以避免堆栈溢出错误。

Jackson-databind

Jackson-databind 用来绑定 JSON 数据和其对应的 Java 类。

Jackson 需要使用发射进行工作。这与 Realm 的 RxJava 支持有冲突,因为作为可选功能,RxJava 有可能不再工程依赖库中。这会导致类似如下的异常抛出:

java.lang.NoClassDefFoundError: rx.Observable
at libcore.reflect.InternalNames.getClass(InternalNames.java:55)

为了修正这个问题你可以添加 RxJava 依赖到工程中或者创建一个假类文件如下所示:

package rx;

public class Observable {
    // Dummy class required for Jackson-Databind support if
    // RxJava is not a project dependency.
}

这个问题也同时报告给了 Jackson 项目

Kotlin

Realm 完全兼容 Kotlin 语言,但有些地方需要注意:

  • 你的模型类需要是开放的(open)
  • 你可能需要在某些情况下添加注解 @RealmCLass 以保证编译通过。这是由于当前 Kotlin 注解处理器的一个限制
  • 很多 Realm API 引用了 Java 类。你必须在编译依赖中添加 org.jetbrains.kotlin:kotlin-reflect:${kotlin_version}

参见示例

Parceler

Parceler 可以帮助对象自动生成支持 Parcelable 接口的样板代码。因为 Realm 的代理类,你需要以下设置以便应用 Parceler 到 Realm 对象。

// All classes that extend RealmObject will have a matching RealmProxy class created
// by the annotation processor. Parceler must be made aware of this class. Note that
// the class is not available until the project has been compiled at least once.
@Parcel(implementations = { PersonRealmProxy.class },
        value = Parcel.Serialization.BEAN,
        analyze = { Person.class })
public class Person extends RealmObject {
	// ...
}

如果你使用 Gradle 来获取 Parceler,请确保存在以下配置代码:

compile "org.parceler:parceler-api:1.0.3"
apt "org.parceler:parceler:1.0.3"

请注意目前在使用 Parceler 的时候有如下的限制:

  1. 如果你的模型包含 RealmList,那么你需要注册一个特殊 adapter
  2. 一旦对象被打包(parcelled),它将变为一个有当前数据快照,不再被 Realm 管理的一个 unmanaged 对象。之后该对象的数据变化不会被 Realm 写入。

Retrofit

Retrofit 是一个由 Square 开发,保证类型安全(typesafe)的 REST API 处理工具。

Realm 可以与 Retrofit 1.x 和 2.x 无缝配合工作。但请注意 Retrofit 不会自动将对象存入 Realm。你需要通过调用 Realm.copyToRealm()Realm.copyToRealmOrUpdate() 来将它们存入 Realm。

GitHubService service = restAdapter.create(GitHubService.class);
List<Repo> repos = service.listRepos("octocat");

// Copy elements from Retrofit to Realm to persist them.
realm.beginTransaction();
List<Repo> realmRepos = realm.copyToRealmOrUpdate(repos);
realm.commitTransaction();

Robolectric

通过 Robolectric 库可以让你在不使用真实设备或者模拟器的情况下直接在 Java 虚拟机上进行 JUnit 测试。但目前 Roboletrics 不支持带有原生库的测试。而 Realm 包含使用 C++ 的原生库,所以你目前不可以通过 Roboletrics 测试使用 Realm 的项目。

你可以关注这个 Robolectric 的新功能请求

RxJava

RxJava 是 Netflix 发布的一个 Reactive 的扩展 库以支持 观察者模式

Realm 包含了对 RxJava 的原生支持。如下类可以被暴露为一个 ObservableRealm, RealmResults, RealmObject, DynamicRealm and DynamicRealmObject

// Combining Realm, Retrofit and RxJava (Using Retrolambda syntax for brevity)
// Load all persons and merge them with their latest stats from GitHub (if they have any)
Realm realm = Realm.getDefaultInstance();
GitHubService api = retrofit.create(GitHubService.class);
realm.where(Person.class).isNotNull("username").findAllAsync().asObservable()
    .filter(persons.isLoaded)
    .flatMap(persons -> Observable.from(persons))
    .flatMap(person -> api.user(person.getGithubUserName())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(user -> showUser(user));

请注意异步查询不会阻塞当前线程,如上代码会立即返回一个 RealmResults 实例。如果你想确定该 RealmResults 已经加载完成请使用 filter operatorRealmResults<E>.isLoaded() 方法。通过判断 RealmResults 是否已经加载可以得知查询是否已经完成。

参考 RxJava sample project

配置

RxJava 是可选依赖,这意味着 Realm 不会自动包含它。这样做的好处是你可以选择需要的 RxJava 版本以及防止过多的无用方法被打包。如果你要使用相关功能,请手动添加 RxJava 到 build.gradle 文件。

dependencies {
  compile 'io.reactivex:rxjava:1.1.0'
}

你也可以通过继承 RxObservableFactory 来决定 Observable 的生成方式,然后通过 RealmConfiguration 进行配置。

RealmConfiguration config = new RealmConfiguration.Builder()
  .rxFactory(new MyRxFactory())
  .build()

如果没有 RxObservableFactory 被定义,RealmObservableFactory 会被默认使用,它支持 RxJava <= 1.1.*。

测试和调试

参考 unitTestExample 来获得与 JUnit3、JUnit4、Robolectric、Mockito 和 PowerMock 结合使用 Realm 的示例。

Happy hacking!欢迎在realm-java与我们真实存在的人类程序员交流!

在 Android Studio 中调试

当你使用 Android Studio 或者 IntelliJ 调试的时候请留神:调试视图中显示的变量值可能会造成误导。

举个例子,在 Android Studio 中查看一个RealmObject的所有属性。你会发现这些属性与期望不符。这时因为 Realm 为每个 RealmObject 创建了代理类,通过使用代理类的 getter 和 setter 方法来存取数据,从而原始对象的属性不会被赋值。请参考(更多信息)。请查看访问器的返回以获得正确数据,入下图所示:

Android Studio, IntelliJ Debug RealmObject

上图断点在113行。三个值被观察:person 变量,访问器person.getName()person.getAge()。代码107行到111行改变了 person 实例的 name 和 age,这些值在事务中被持久化。当运行暂停在113行时,可以观察到,调试器中显示的 person 属性是不正确的,但是访问器person.getName()person.getAge()的返回正确。请注意,.toString() 方法同样会输出正确的结果。

NDK Debugging

Realm 包含 native 代码。我们建议你使用崩溃报告工具如 Crashlytics 来记录 native 错误。更多信息意味着我们能帮你更快地解决问题。

默认的 stack trace 信息量较小,不利于解决 NDK 崩溃问题。 Crashlytics 允许你收集更多的崩溃信息。请参考 steps outlined in this guide

添加如下代码到你工程根目录的 build.gradle 文件。请注意不需要添加 androidNdkOutandroidNdkLibsOut

crashlytics {
  enableNdk true
}

目前的限制

我们尽可能在减少使用 Realm 时的限制,但目前为止,仍有如下这些限制。

请参阅我们的 GitHub issues 获取更多关于已知问题的信息。

概要

Realm 的目标是在可扩展性和运行效率之间取得一个平衡,因此在保存数据方面有一些客观存在的限制,例如:

  1. 类名长度的上限是 57 个字符。Realm-java 在存储时会为对象名添加 class_ 前缀,你可以通过 Realm browser 看到;
  2. 成员变量名长度上限是 63 个字符;
  3. 数据模型类即使在不同包中也不可以有相同的类名;
  4. 不支持嵌套事务(transaction),使用嵌套事务会导致抛出异常;
  5. Stringbyte [] 大小不能超过 16MB;

字符串排序与查询(Sorting and querying on String)

针对字符串的排序和忽略大小写匹配只支持字符集 ‘Latin Basic’、’Latin Supplement’、’Latin Extended A’、’Latin Extended B’ (UTF-8 range 0-591)。另外在使用 equalTo()notEqualTo()contains()endsWith()beginsWith() 或者 like() 进行忽略大小写查询时,它们仅在字符为英文环境(English locale)时起效。

针对字符串的排序,只支持字符集 ‘Latin Basic’、’Latin Supplement’、’Latin Extended A’、’Latin Extended B’ (UTF-8 range 0-591)。如果排序对象不在这些字符集的范围内,那么 RealmResults 的顺序并不会被改变。

多线程(Threads)

Realm数据文件本身支持多线程并发访问。但是Realm实例、RealmObjectRealmQuery以及RealmResults不可以跨线程使用。请参考多线程示例获取更多在多线程环境中使用Realm的信息。更多信息

Realm 文件不支持多进程访问

尽管 Realm 文件支持多线程访问,但还不支持多进程访问。不同进程请使用不同的 Realm 文件拷贝。我们很快会提供对于多进程的支持。

RealmObject’s hashCode

RealmObject是实时更新的对象,它们有可能因为其它线程对同对象的改动而更新。尽管两个 Realm 对象在调用 RealmObject.equals() 返回 true 时会有相同的哈希值,但它们的哈希值并不是不变的。因此,不应该将它们放入 HashMapHashSet 等依赖于不变哈希值的容器中。

最佳实践

与 Android 相关

Realm 可以无缝地引入安卓开发。你需要谨记 RealmObject线程限制。当你需要跨 activity、se rvice或者 broadcast receiver 传递 Realm 对象的时候,请牢记这一点。

防止出现 ANR

一般来说 Realm 的读写是足够快的,甚至在 UI 线程中读写也不是问题。但是,写事务是互相阻塞的,所以为了避免 ANR 的出现,我们建议你在后台线程中执行写操作。参考异步事务获得如何在后台线程进行写入相关的信息。

控制 Realm 实例的生命周期

请仔细考虑 Realm 实例的生命周期控制。RealmObjectsRealmResults 在访问其引用数据时都是懒加载的。因为这个原因,如果你仍然需要访问其中的 Realm 对象或者查询结果时,请不要关闭你的 Realm 实例。但从另一角度来说,Realm 实例以及针对该实例创建的 Realm 对象和查询结果都会占用一定的资源,所以在你不再需要该实例的时候也应该即时调用 Realm.close() 来释放它们。

为了避免不必要的 Realm 数据连接的打开和关闭,Realm 内部有一个基于引用计数的缓存。这表示在同一线程内调用 Realm.getDefaultInstance() 多次是基本没有开销的,并且底层资源会在所有实例都关闭的时候才被释放。

一个合理的选择是通过 UI 组件的创建和销毁来控制 Realm 的生命周期。以下代码展示了如何在包含 RecycleViewActivityFragment 中控制 Realm 的生命周期。在两个例子中,Realm 实例和 RecycleView 适配器均在 UI 组件初始化时创建,在相应的销毁函数中关闭。请注意即使这里 Activty.onDestory() 没有被调用,Realm.close() 未被执行,Realm 的数据仍然是安全的。

当然,假如你的多个 Fragment 需要访问同样一份数据,那么你完全可以用 Activity 来控制 Realm 实例的生命中期而不是单独在每个 Fragement 中来控制它。

// Setup Realm in your Application
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Realm.init(this);
        RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().build();
        Realm.setDefaultConfiguration(realmConfiguration);
    }
}

// onCreate()/onDestroy() overlap when switching between activities.
// Activity2.onCreate() will be called before Activity1.onDestroy()
// so the call to getDefaultInstance in Activity2 will be fast.
public class MyActivity extends Activity {
    private Realm realm;
    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        realm = Realm.getDefaultInstance();

        setContentView(R.layout.activity_main);

        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        recyclerView.setAdapter(
            new MyRecyclerViewAdapter(this, realm.where(MyModel.class).findAllAsync()));

        // ...
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        realm.close();
    }
}

// Use onCreateView()/onDestroyView() for Fragments.
// Note that if the db is large, getting the Realm instance may, briefly, block rendering.
// In that case it may be preferable to manage the Realm instance and RecyclerView from
// onStart/onStop instead. Returning a view, immediately, from onCreateView allows the
// fragment frame to be rendered while the instance is initialized and the view loaded.
public class MyFragment extends Fragment {
    private Realm realm;
    private RecyclerView recyclerView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        realm = Realm.getDefaultInstance();

        View root = inflater.inflate(R.layout.fragment_view, container, false);

        recyclerView = (RecyclerView) root.findViewById(R.id.recycler_view);
        recyclerView.setAdapter(
            new MyRecyclerViewAdapter(getActivity(), realm.where(MyModel.class).findAllAsync()));

        // ...

        return root;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        realm.close();
    }
}

重用 RealmResults 和 RealmObjects

在 UI 线程和其它拥有 Looper 的线程中,RealmObjectRealmResults 都会在 Realm 数据改变时自动刷新。这意味着你不需要在 RealmChangeListener 中重新获取这些对象。它们已经被更新并且准备好被重绘在屏幕上了。

public class MyActivity extends Activity {

    private Realm realm;
    private RealmResults<Person> allPersons;
    private RealmChangeListener realmListener = new RealmChangeListener<Realm>() {
        @Override
        public void onChange(Realm realm) {
            // Just redraw the views. `allPersons` already contain the
            // latest data.
            invalidateView();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realm = Realm.getDefaultInstance();
        realm.addRealmChangeListener(listener);
        allPerson = realm.where(Person.class).findAll(); // Create the "live" query result
        setupViews(); // Initial setup of views
        invalidateView(); // Redraw views with data
    }

    // ...
}

配方

我们特别制作了些特别的 Realm 配方,经常去这里找找看,没准有你需要的。如果你需要这里没有的配方,请在GitHub创建一个问题。

同步(Sync)

Realm 移动端平台(RMP)扩展了 Realm 移动端数据库,实现了在设备间的 Realm 数据同步。它们的基本 API 是完全相同的,RMP 增加了一些 API 来实现相关的同步功能。

开启 Realm 移动端平台支持(Enabling Realm Mobile Platform)

请添加如下代码到你的 app 的 build.gradle 文件中:

realm {
  syncEnabled = true;
}

完成!

创建用户并登录(Creating and Logging in Users)

Realm SyncUser 对象是 Realm 对象服务器的核心组件,它与每个需要同步的 Realm 数据库相关联。SyncUser 对象支持的登录方式包括常规的用户名/密码登录和第三方的登录。

创建用户并登录需要如下两个信息:

  • 相应的 Realm 登录服务器的 URL;
  • 相关的校验信息(例如用户名/密码或者访问秘钥等)。

  • A URL (as a string) of a Realm Authentication Server to connect to.
  • Credentials for an authentication mechanism that describes the user as appropriate for that mechanism (i.e., username/password, access key, etc).

Realm 通过这些信息来创建 SyncUser 对象。

创建校验信息(Creating a Credential)

参考 Realm Object Server Authentication 来获得第三方登录相关信息。

用户名/密码
SyncCredentials myCredentials = SyncCredentials.usernamePassword(username, password, true);

第三个参数为 true 表示在登录之前首先创建用户。请注意,如果用户已存在,该参数应设置为 false

Google
String token = "..."; // a string representation of a token obtained by Google Login API
SyncCredentials myCredentials = SyncCredentials.google(token);
Facebook
String token = "..."; // a string representation of a token obtained by Facebook Login API
SyncCredentials myCredentials = SyncCredentials.facebook(token);
自定义用户验证(Custom Auth)
String token = "..."; // a string representation of a token obtained from your authentication server
Map<String, Object> customData = new HashMap<>();
SyncCredentials myCredentials = SyncCredentials.custom(
  token,
  'myauth',
  customData,
);

Note: 你可以把自定义登录信息作为第三个参数传递给校验方法,请参考 API reference

登录(Logging in the User)

有了校验信息以后我们就可以登录 Realm 对象服务了。

String authURL = "http://my.realm-auth-server.com:9080/auth";
SyncUser user = SyncUser.login(myCredentials, authURL);

用户相关的操作(Working with Users)

对于普通 Realm,我们使用 RealmConfiguration 来进行配置。安卓 RMP 使用 SyncConfiguration 来对其独有的配置进行扩展。这些独有配置包括一个已验证的用户和一个 Realm 对象服务的 URL。同步服务器的 URL 中的波浪号(~)会被替换为相关用户的唯一标识符。Realm 默认会自动配置同步 Realm 文件的存储路径,当然你也可以指定路径。

SyncUser user = getUserFromLogin();
String serverURL = "realm://my.realm-server.com/~/default";
SyncConfiguration configuration = new SyncConfiguration.Builder(user, serverURL).build();

你也可以得到当前已登录并且登录状态仍然有效的用户。

SyncUser user = SyncUser.getCurrentUser();

得到用 JSON 表示的用户与其校验信息。

String userJson = user.toJson();

你可以通过这个 JSON 串来获取一个 SyncUser 实例而无需从新经过第三方登录服务:

SyncUser user = SyncUser.fromJson(userJson);

Realm saves the current user using a UserStore. The default UserStore is backed by a private Shared Preference file, but this behaviour can be overridden using SyncManager.setUserStore(userStore).

It is important to remember that users - and their credentials - are considered sensitive data.

退出登录(Logging Out)

退出登录很简单:

user.logout();

当用户退出登录,同步将会停止。只有在相关所有的 Realm 实例都被关闭的前提下,才可以退出登录。当用户退出登录后,SyncConfigurtion 将会失效,无法在使用它打开 Realm。

打开一个同步 Realm 实例(Opening a Synchronized Realm)

一旦 SyncConfiguration 被创建,就可以像打开一个普通 Realm 实例一样打开一个同步 Realm 实例:

Realm realm = Realm.getInstance(syncConfiguration);

访问控制(Access Control)

RMP 弹性化的访问控制使得您可以针对不同用户同一个 Sync Realm 配置不同的访问权限。例如在一个实施写作的应用中,你可以允许所有用户对同一个 Realm 有写权限。再比如在一个常见的发布/订阅架构的应用中,可以只有一个特定用户拥有写权限,而其他用户只有读权限。

目前有以下三种权限可供配置:

  • mayRead 表示用户拥有读权限;
  • mayWrite 表示用户拥有写权限;
  • mayManage 表示用户拥有管理员权限。

如果没有显式配置权限,默认情况下只有 Realm 的拥有者有权限访问该 Realm。管理员用户是个例外,他们会拥有该服务器上所有 Realm 的所有权限。

更多信息请参考 Access Control

管理用 Realm(Management Realm)

所有权限配置都是通过写入管理用 Realm 实现的。管理用 Realm 会被 Realm 对象服务(Realm Object Server)自动创建,它就和一个普通的 Sync Realm 一样。你可以通过添加 Permission Change 对象来改变针对某个 Realm 的权限。

你可以通过调用 SyncUser.getManagementRealm() 来获取管理用 Realm 实例。就跟操作普通 Realm 实例一样,请在使用完毕后关闭实例。

修改权限(Modifying Permissions)

你可以通过添加 PermissionChange 对象到管理用 Realm 来针对某个 Realm 文件进行权限控制。

SyncUser user = SyncUser.currentUser();
String realmURL = "realm://ros.example.com/~/default"; // The remote Realm URL on which to apply the changes
String anotherUserID = "other-user-id"; // The user ID for which these permission changes should be applied

Realm realm = user.getManagementRealm();
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Boolean mayRead = true; // Grant read access
        Boolean mayWrite = null; // Keep current permission
        boolean mayManage = false; // Revoke management access
        PermissionChange change = new PermissionChange(realmUrl,
                                                       anotherUserID,
                                                       mayRead,
                                                       mayWrite,
                                                       mayManage);
        realm.insert(change);
    }
});

你可以在 realmURL 中使用通配符 * 来改变某个用户的所有 Realm 权限。

你可以在 userID 中使用通配符 * 来改变某个对象服务器中所用用户的权限。

当对象服务器处理完 PermissionChange 对象中传递的权限更改信息后,它会设置该对象中的 statusstatusMessage 字段来返回处理结果。

你可以通过订阅 PermissionChange 对象的更改通知来获取权限更改操作的结果。请参考通知 及以下示例。

SyncUser user = SyncUser.currentUser();
Realm realm = user.getManagementRealm();
final String permissionId = "permission-id";
PermissionChange change = realm.where(PermissionChange.class).equalTo("id", permissionId).findFirst();
change.addChangeListener(new RealmChangeListener<PermissionChange>() {
    @Override
    public void onChange(PermissionChange change) {
        if (change.getId().equals(permissionId)) {
            Integer status = change.getStatusCode();
            if (status == 0) {
                // Handle success
                realm.close();
            } else if (status > 0) {
                // Handle error
            }
        }
    }
});

日志(Logging)

有时候调试同步 Realm 会比较麻烦,获取更多的日志信息会对调试有很大的帮助。使用如下代码来允许 Realm 输出更多的调试信息:

RealmLog.setLevel(Log.VERBOSE);

错误处理(Error reporting)

你可以为每个 SyncConfiguration 注册一个错误处理器来处理相关错误:

SyncConfiguration configuration = new SyncConfigurtion.Builder(user, serverURL)
  .errorHandler(new Session.ErrorHandler() {
    void onError(Session session, ObjectServerError error) {
        // do some error handling
    }
  })
  .build();

你也可以为所有 SyncConfiguration 注册一个全局的错误处理器:

SyncManager.setDefaultSessionErrorHandler(myErrorHandler);

数据迁移(Migrations)

同步 Realm 支持自动数据迁移。但目前只支持增加性的数据改动,例如新的 schema 增加了一个类,或是某个类中添加了一个字段。非增加性的 schema 变动会导致异常被抛出。

数据迁移会被自动出发,无须添加自定义 migration 块或是 schema 版本。

冲突处理(Conflict Resolution)

请参考 Realm Object Server documentation

限制

目前不支持 finalvolatile 修饰的成员变量,这主要是为了 Realm 对象在非托管(unmanaged RealmObject)时候的行为差异化。

Realm 数据模型不可以继承自除了 RealmObject 以外的其它对象。你可以选择不声明默认无参数构造器,但是如果 你声明了,那么该构造器必须为空构造器。这是目前 Realm 的一个限制。但你可以自由添加任意其它的构造器。

FAQ

我怎么才能查看我的Realm文件的数据?

StackOverflow 上的这个答案介绍了怎么找到你的 Realm 文件。你可以通过 Realm Browser 来查看数据内容

Realm 基础库有多大?

大部分情况下,在你 release 版本的 apk 文件里,Realm 只占用 800KB 空间。我们发布的版本支持的处理器架构包括 ARM7、ARMv7、ARM64、x86、MIPS。所以发布的库文件本身看起来会稍微大一些。但安卓系统在安装 apk 时只会安装针对该设备处理器架构的原生库,安装后占用空间会比 apk 文件本身还要小一些。

你可以通过将 APK 针对不同平台分离打包以减少其大小。 添加如下代码到 build.gradle

android {
    splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
        }
    }
}

根据你设置的每个 ABI,会生成一个独立 APK 文件。参考Android Tools documentation

我们的这个实例也演示了同样的功能 GitHub

即使你不想针对不同 ABI 打包 APK,你仍然可以选择不在 APK 中包含某几个 ABI 的 so 库文件。请在 build.gradle 中指定你的 abiFilters

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
        }
    }
}

Realm 是开源的吗?

是的!Realm 的底层 C++ 引擎和上层 SDK 都是在 Apache 2.0 协议下的开源软件。Realm 还包括一个闭源的 Realm 移动端平台扩展,但如果你只需要 Realm 移动端数据库功能,你讲不要需要它。

普通 Java 对象和 Realm 对象之间有什么区别?

主要区别在于普通 Java 对象本身会包含其数据,但 Realm 对象不会。Realm 对象通过 get 或者 set 方法来直接从 Realm 数据库中存取数据。

Realm 对象实例可以是托管(managed)或是非托管(unmanaged)的。

  • 托管对象 是被 Realm 持久化的,会自动更新且被限制在了指定线程。因为它会在 Java 堆上占用更少空间,所以总体上它比非托管对象更轻量。

  • 非托管对象 就和普通的 Java 对象一样。它们没有被持久化且不会自动更新。它们没有线程限制。

托管对象和非托管对象之间可以通过 Realm.copyToRealm()Realm.copyFromRealm() 来相互转换。

为什么模型类需要继承于 RealmObject?

原因是我们需要针对模型类添加一些易用的接口。另外它让我们比较容易在内部使用范型(generic)从而提高代码的易读性和易用性。如果你因为种种原因不想使用继承,你也可以通过实现 RealmModel 接口来定义对象。

RealmProxy类是做什么用的?

我们使用 RealmProxy 类来保证 Realm 对象本身不存储任何实际的数据,进而通过直接访问 Realm 数据来存取数据。

对于你项目中的模型类,Realm 的注解处理器(Annotation processor)会生成相应的 RealmProxy 类。该代理类继承于你的模型类,代理类是你在调用 Realm.createObject() 时实际返回的类型。

我为什么需要使用事务(transaction)才能写入Realm数据库?

Transactions 是为了保证对数据的原子操作。它使得你可以保证多个数据被一次改变,或者保持不变。通过事务你可以精确控制你对每次需要改变多少数据(例如一次改变多个对象)。

基于 SQL 的数据库如 SQLite,插入多条数据通常是被自动隐性包含在事务中一次完成的。但在 Realm 中,写入事务永远是显性的。

怎么处理 out-of-memory 异常?

Realm 基于我们的原生嵌入式存储引擎。该引擎会在原生内存堆(native memory)上而不是Java虚拟机的内存堆分配内存。如果你的应用在内存管理上的存在问题导致 Realm 无法分配内存,io.realm.internal.OutOfMemoryError 异常会被抛出。请一定不要忽略这个异常!请不要使用空 catch 语句块来忽略这个异常!这么做有可能导致你的 Realm 数据库文件损坏!在该异常抛出时终止应用程序是不会损坏数据库的。如果你遇到了该异常,请检查应用是否有内存泄漏,或者其它内存使用异常的问题。

Realm 数据库文件大小

一般来说,Realm 数据文件比同等情况下的 SQLite 数据文件更小。

为了你数据的连续性,Realm 会同时保存数据的不同版本。如果你在一个线程中读取了 Realm 数据之后阻塞了该线程,同时又在其它线程中写入了 Realm,那么在第一个线程中的 Realm 不会被更新,并且因此一个非最新的 Realm 数据版本会被保存(这可能并不是你需要的数据)。以上会导致你的 Realm 文件体积增大(当然,Realm 会重用这些额外的磁盘空间占用,或者你可以通过调用 compactRealmFile 来释放这部分磁盘空间)。

但我运行我的 app 时,看到有指向 Mixpanel 的网络连接,那是什么?

Realm 在字节码处理器运行的时候会收集匿名数据。我们保证这个过程是完全匿名的。它通过告诉我们你在使用哪个 Realm 版本、你的操作系统是什么帮助我们提高Realm的质量。它不会在你的生产环境中执行,也不会在你的用户设备上执行。它只会在注解处理器运行的时候执行。你可以在我们的源代码中看到它具体收集了哪些信息。

Couldn’t load “librealm-jni.so”

如果你的 app 还包含其它原生库,并且它们没有提供对 arm64 平台的支持,arm64 的安卓设备在加载 Realm 的 librealm-jni.so 时可能会失败。这是因为安卓无法同时加载32位和64位的原生库。最佳解决方案是为所有原生库提供 arm64 版本。但在你使用第三方闭源库的时候这不太好实现。参考 VLC and Realm Library conflicts

一个解决方案是在编译期通过配置 gradle 将 Realm 的 arm64 库排除在外:

android {
    //...
    packagingOptions {
        exclude "lib/arm64-v8a/librealm-jni.so"
    }
    //...
}

更多信息请参考 Mixing 32- and 64-bit Dependencies in Android

据我们所知如下这些第三方库没有提供 64 位原生库支持:

怎样备份和恢复 Realm?(How to back up and restore Realm?)

Realm 作为文件存储在文件系统中。你可以通过调用 getPath() 得到 Realm 文件的路径。你只需备份或者恢复该文件即可。但请注意,备份恢复前应务必保证所有 Realm 实例关闭,否则可能损坏数据。

你还可以通过realm.writeCopyTo(File)来备份一个已有实例被打开的 Realm 数据库。

这篇文章描述了怎样用 Google Drive 保存 Realm 文件:第一部分 第二部分 第三部分

黑莓设备(Blackberry devices)

部分黑莓设备支持运行安卓应用,但它们提供的运行环境并不完整。在这种情况下我们无法保证 realm-java 对于这些设备的兼容性。已知问题包括:

io.realm.exceptions.RealmFileException: Function not implemented in io_realm_internal_SharedRealm.cpp line 81 Kind: ACCESS_ERROR.

如果你在黑莓设备上发现问题,你试着自行修复并提交 patch 给 realm-java 或者 realm-core。这两个项目都是完全开源的。

怎样保存和获取 Realm 秘钥(How to store and retrieve the encryption key used by Realm)

请使用安卓的 KeyStore 来保存 Realm 秘钥。参考以下步骤:

  1. 使用安卓 KeyStore 来生成一个 RSA 秘钥,安卓系统保证了存储和获取秘钥时的安全性。在安卓 M 版本及以上,系统会要求用户输入密码(或指纹验证)以解锁 KeyStore。

  2. 生成一个 AES 秘钥用来加密 Realm。

  3. 使用第一步生成的 RSA 秘钥来加密你的 AES 秘钥。

  4. 现在可以安全地将这个加密后的 AES 秘钥存储在文件系统中了(例如 SharedPreferences)。

  5. 当需要时,你可以首先获得加密后的 AES 秘钥,然后用 RSA 公钥来解密并将解密后的秘钥传入 RealmConfiguration 以打开加密 Realm。

你可以参考我们的这个示例: https://github.com/realm/realm-android-user-store

这个示例 使用到了指纹 API。