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

快速入门

下载Realm Android 或者在GitHub的realm-java页面查看源代码.

前提

  • 目前我们还不支持Android以外的Java环境;
  • Android Studio >= 0.8.6;如果您要在Eclipse中使用Realm,见下文;
  • 较新的 Android SDK 版本;
  • JDK版本>=7;
  • 我们支持Android API 9以上的所有版本(Android 2.3 Gingerbread及以上)。

安装

您可以使用Maven或手动添加一个Jar包到项目中。

Maven

  1. 确保您的项目使用 jcenter 作为依赖仓库 (最新版本Android Gradle插件的默认设置);
  2. compile 'io.realm:realm-android:0.82.2' 添加到您的项目依赖;
  3. 在Android Studio菜单中选择:Tools->Android->Sync Project with Gradle Files。

Jar

  1. 下载发布包,并解压;
  2. 使用Android Studio创建一个新项目;
  3. realm-VERSION.jar文件夹复制到 app/libs
  4. 在Android Studio菜单中选择:Tools->Android->Sync Project with Gradle Files。
  1. 下载发布包,并解压;
  2. distribution/eclipse/目录中复制jar文件和文件夹(包含 librealm-jni.so 文件)到app的libs目录;
  3. libs文件夹中右键单击realm jar文件,选择“Build Path” -> “Add to Build path”;
  4. 右键单击您的项目,选择“Properties”,进入“Java Compiler” -> “Annotation Processing”,勾选“Enable project specific settings”,然后选择“Apply”;
  5. 继续选择“Annotation Processing” -> “Factory Path”并勾选“Enable project specific settings”; “Click Add JARs”,然后在“libs”中选择realm jar文件,单击”OK”,应用,并开始编译;
  6. 为了触发注解处理器(annotation processor),您必须在每一个RealmObject子类声明前添加@RealmClass

ProGuard

在编译时,Realm为各RealmObject生成一个代理类。为了确保这些类在经过代码混淆工具如ProGuard处理后仍然可见,添加下面的配置到您的ProGuard配置文件中:

-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-dontwarn javax.**
-dontwarn io.realm.**

Realm浏览器

目前仅支持Mac OS X。
Windows和Linux版本正在开发中。

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

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

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

Realm Browser已经上架Map App Store

API手册

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

示例

根目录包含有关Realm入门的几个例子。您只需在Android Studio中导入运行即可。

根目录中的RealmIntroExample包含了如何使用当前的API的简单例子。请查阅源代码来获取相关信息。

RealmGridViewExample用来展示如何使用Realm作为GridView的后端存储。它同时也展示了如何用JSON来填充数据库。

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

RealmAdapterExample展示了如何以一个非常便捷的方式使用RealmBaseAdapter绑定RealmResults到安卓的ListView。

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

RealmEncryptionExample向您展示如何使用加密的Realm。

##获得帮助

模型

Realm数据模型定义非常类似于传统的Java Bean。只需要继承自RealmObject类,Realm的注解处理器(Annotation processor)会自动生成代理类。

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

请注意,RealmObject子类的getter和setter会被生成的代理类重载,您添加到getter和setter的任何自定义逻辑实际上并不会被执行。

字段类型

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

忽略的属性

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

索引(Index)属性

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

主键

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

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

主键的存在意味着可以使用createOrUpdate()方法,它会用此主键尝试寻找一个已存在的对象,如果对象存在,就更新该对象;反之,它会创建一个新的对象。

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

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

MyObject obj = new MyObject();
obj.setId(42);
obj.setName("Fish");
realm.beginTransaction();
// This will create a new one in Realm
// realm.copyToRealm(obj);
// This will update a existing one with the same id or create a new one instead
realm.copyToRealmOrUpdate(obj);
realm.commitTransaction();

限制

由于Realm会使用代理类重载模型类中的getter和setter,模型类中允许的内容会有一些限制

写入

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

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

// Obtain a Realm instance
Realm realm = Realm.getInstance(this);

realm.beginTransaction();

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

realm.commitTransaction();

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

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

//  ... 

realm.cancelTransaction();

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

由但得益于 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管理的,这非常重要。对原始对象(Standalone 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]");
	}
});

查询

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

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

比如查找所有叫做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 的用户。这些对象并非拷贝,也就是说你得到的是一个匹配对象引用的列表,你对匹配对象所有的操作都是直接施加于它的原始对象。RealmResults继承自Java的AbstractList,行为类似。例如你可以通过index来访问其中的某个对象。

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

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

按类型检索对象

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

另外还有提供排序功能的allObjects()。参见 realm.allObjectsSorted()了解详情。

查询条件

Realm支持以下查询条件:

  • betweengreaterThan()lessThan()greaterThanOrEqualTo()lessThanOrEqualTo()
  • equalTo()notEqualTo()
  • contains()beginsWith()endsWith()

并非所有条件都适用于所有数据类型,具体请参考RealmQuery API

修饰符

字符串查询条件可以通过使用CASE_INSENSITIVE修饰符来忽略字母A-Z和a-z的大小写。

逻辑运算符

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

你也可以将查询条件组合在一起,使用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() 一起使用来否定子条件。

排序

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

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

链式查询

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

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

聚合

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

删除

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

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

// All changes to data must happen in a transaction
realm.beginTransaction();

// remove single match
results.remove(0);
results.removeLast();

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

// Delete all matches
results.clear();

realm.commitTransaction()

Realms

Realm(s)是我们对数据库的称谓:它包含多个不同的对象,并对应磁盘中的一个文件。

默认的Realm

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

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

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

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

配置Realm

您可以通过Realm.getInstance(context)非常方便地开始使用Realm。但如果您有更多的需求,通过创建一个RealmConfiguration,您可以控制有关Realm的更多细节。

// The RealmConfiguration is created using the builder pattern.
RealmConfiguration config = new RealmConfiguration.Builder(context)
  .name("myrealm.realm")
  .encryptionKey(getKey())
  .schemaVersion(42)
  .setModules(new MySchemaModule())
  .migration(new MyMigration())
  .build();

// These two are equivalent
Realm realm = Realm.getInstance(context);
Realm realm = Realm.getInstance(new RealmConfiguration.Builder(context).build());

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

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    RealmConfiguration config = new RealmConfiguration.Builder(context).build();
    Realm.setDefaultConfiguration(config);
  }
}

public class MyActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Realm realm = Realm.getDefaultInstance();
  }
}

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

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

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

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

In-Memory Realm

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

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

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

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

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

跨线程使用Realm

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

关闭Realm实例

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

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

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

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

protected Long doInBackground(Context... contexts) {
    Realm realm = null;
    try {
        realm = Realm.getInstance(contexts[0]);

        // ... Use the Realm instance
    } finally {
        if (realm != null) {
            realm.close();
        }
    }
}

如果您需要创建一个包含Looper的线程,可以参考这个:

public class MyThread extends Thread {
    private final Context;

    public MyThread(Context context) {
        this.context = context;
    }

    public void run() {
        Looper.prepare();
        Realm realm = null;
        try {
            realm = Realm.getInstance(context);

            //... Setup the handlers using the Realm instance
            Lopper.loop();
        } finally {
            if (realm != null) {
                realm.close();
            }
        }
    }
}

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

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

自动更新(Auto-Refresh)

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

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

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

查找Realm数据库文件

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

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(context)
  .setModules(new MyModule())
  .build();

// It is possible to combine multiple modules to one schema.
RealmConfiguration config = new RealmConfiguration.Builder(context)
  .setModules(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(context)
  .name("library.realm")
  .setModules(new MyLibraryModule())
  .build();

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

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

关系

任意两个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对象。同理,这个例子也解释了怎样实现一对一关系。

多对多

您可以通过使用RealmList为一个对象关联0或多个其它对象。

public class Contact extends RealmObject {
    private RealmList<Email> emails;
    // Other fields…
}

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

您可以通过标准的getter和setter来访问RealmList.

realm.beginTransaction();
Contact contact = realm.createObject(Contact.class);
contact.setName("John Doe");

Email email1 = realm.createObject(Email.class);
email1.setAddress("[email protected]");
email1.setActive(true);
contact.getEmails().add(email1);

Email email2 = realm.createObject(Email.class);
email2.setNumber("[email protected]");
email2.setActive(false);
contact.getEmails().add(email2);

realm.commitTransaction();

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

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

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

Realm支持关联查询。继续使用之前的例子,如果您希望找到所有Email的active状态为真的Contact对象,您可以:

RealmResults<Contact> contacts = realm.where(Contact.class).equalTo("emails.active", true).findAll();

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

以上的查询含义为“所有至少含有一个email为active状态的contact”。请务必注意,这里的返回的contact中,有可能包含active为假的email对象,因为在其RealmList列表中,其它的email的active为真。

另外,关联查询中的每个条件是单独评估的!查询的最终结果是多个关联查询结果的交集。如下这个例子返回的最终结果的每个contact都至少有1个active为真的email和一个active为假的email。

RealmResults<Contact> contacts = realm.where(Contact.class).equalTo("emails.active", true).equalTo("emails.active", false).findAll();

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.beginTransaction();
realm.createObjectFromJson(City.class, "{ city: \"Copenhagen\", id: 1 }");
realm.commitTransaction();

// Insert multiple items using a InputStream
InputStream is = new FileInputStream(new File("path_to_file"));
realm.beginTransaction();
try {
    realm.createAllFromJson(City.class, is);
    realm.commitTransaction();
} catch (IOException e) {
    realm.cancelTransaction();
}

通知(Notifications)

从0.80.3开始,Realm内部使用弱引用来管理RealmChangeListener以避免可能的内存泄漏。请不要使用匿名RealmChangeListener当作addListener的传入参数。请自行维护对RealmChangeListener的引用直至您不再需要更新通知。

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

public class MyActivity extends Activity {
    private Realm realm;
    // A reference to RealmChangeListener needs to be held to avoid being
    // removed by the garbage collector.
    private RealmChangeListener realmListener;

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

您可以轻松移除所有监听器。

realm.removeAllChangeListeners();

迁移(Migrations)

迁移功能仍然还在开发中。目前功能性完整,但是接口相对复杂。不久我们会重写迁移接口。

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

如果没有旧Realm数据文件存在,那么代码的改变即会反应到相应的Realm数据文件改变。但如果已经有旧版本的Realm数据文件存在,Realm会抛出异常提示数据库文件需要迁移。

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

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

加密

Please take note of the Export Compliance section of our LICENSE, as it places restrictions against the usage of Realm if you are located in countries with an export restriction or embargo from the United States.

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

byte[] key = new byte[64];
new SecureRandom().nextBytes(key);
Realm realm = Realm.getInstance(this, key);

// ... use the Realm as normal ...

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

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

适配器(Adapter)

Realm提供了一个抽象的工具类来方便地将RealmResult展示到UI控件上。RealmBaseAdapter类帮您处理了大部分的工作,您只需要实现getView()方法。

public class Person extends RealmObject {
    private String name;
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

public class MyAdapter extends RealmBaseAdapter<Person> implements ListAdapter {

    private static class MyViewHolder {
        TextView name;
    }

    public MyAdapter(Context context, int resId,
                     RealmResults<Person> realmResults,
                     boolean automaticUpdate) {
        super(context, realmResults, automaticUpdate);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = inflater.inflate(android.R.layout.simple_list_item_1, 
                                           parent, false);
            viewHolder = new ViewHolder();
            viewHolder.name = (TextView) convertView.findViewById(android.R.id.text1);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        Person item = realmResults.get(position);
        viewHolder.name.setText(item.getName());
        return convertView;
    }

    public RealmResults<Person> getRealmResults() {
        return realmResults;
    }    
}

对其它库的支持

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

GSON

GSON 是Google开发的JSON处理库. 当使用Realm配合GSON 2.3.1(最新版)时,您需要指定ExclusionStrategy

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

Gson gson = new GsonBuilder()
        .setExclusionStrategies(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return f.getDeclaringClass().equals(RealmObject.class);
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return false;
            }
        })
        .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。类似地,您可以用这个方法来处理其它原始数据类型数组。

Otto

Otto是一个由Square开发的事件总线(event bus)。Otto与Realm搭配使用非常容易,但请注意时序和并发性的问题。

Otto通常使用同一个线程来收发事件,这意味着您可以将RealmObject作为事件参数并在接收器方法中处理。但您如果使用这个方法让Otto讲所有事件发送到UI线程执行,您就不可以再使用RealmObject做为事件参数了(RealmObject不能跨线程使用)。

RealmObject在一个线程中被改变,Realm会在另一个线程中通过Handler更新Realm的数据。而Otto.post(event)会立刻调用事件处理。因此,当您发送Realm数据改变事件到其它线程,您需要手工调用realm.refresh()来得到最新的数据。

@Subscribe
public void handleEvent(OttoEvent event) {
    realm.refresh();
    // Continue working with Realm data loaded in this thread
}

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

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

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

Retrofit

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

由于Retrofit内部使用GSON,您需要定义一个GsonConverter以转换JSON数据到RealmObject

Gson gson = new GsonBuilder()
        .setExclusionStrategies(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return f.getDeclaringClass().equals(RealmObject.class);
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return false;
            }
        })
        .create();

// Configure Retrofit to use the proper GSON converter
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.github.com")
    .setConverter(new GsonConverter(gson))
    .build();

GitHubService service = restAdapter.create(GitHubService.class);

Retrofit不会自动将对象添加到Realm,您必须通过realm.copyToRealm()realm.copyToRealmOrUpdate() 方法来手动添加。

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的新功能请求

下一步

请参考我们的应用示例获取更多信息。

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

目前的限制

Realm还处于beta阶段,我们一直在不断地修改bug及添加功能。在1.0版本的发布之前,Realm存在如下这些限制。

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

概要

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

  1. 类名长度的上限是57个字符。Realm-java在存储时会为对象名添加class_前缀,你可以通过Realm browser看到;
  2. 成员变量名长度上限是63个字符;
  3. Date的精度是1秒。为了32位和64位系统的兼容性考虑,目前能存储的Date必须在1900-12-13和2038-01-19范围之内;
  4. 不支持嵌套事务(transaction),使用嵌套事务会导致抛出异常;
  5. Stringbyte []大小不能超过16MB;
  6. 忽略大小写查询所支持的字符集仅限于’Latin Basic’、’Latin Supplement’、’Latin Extended A’、’Latin Extended B’ (UTF-8 range 0-591);

对象

因为Realm生成的代理类会重载setter和getter,所以针对Realm对象有如下限制:

  • 只支持私有成员变量;
  • 只支持默认getter和setter方法;
  • 支持公有和私有静态变量;
  • 支持静态方法;
  • 支持接口实现。

这意味着您目前只能直接继承于RealmObject,不能重载其方法例如toString()或者equals()。您只可以实现接口。我们正在努力移除这些限制

排序

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

忽略大小写查询

当使用equalTocontainendsWithbeginsWithcaseSensitive设置只针对英文环境(English locale)有效。参见这个问题

多线程

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

null值

Realm暂时不支持存储null值。但对于它的支持很快就会开发完毕。在这之前,我们建议您使用一个额外的布尔字段来标记null与否。

迁移(migration)

对于迁移的支持目前并不成熟,您得依赖部分内部接口来实现迁移。更加易用的迁移支持将在不远的未来提供。目前您可以查看迁移示例来实现相关功能。

Realm文件不支持多进程访问

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

FAQ

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

StackOverflow上的这个答案介绍了怎么找到您的Realm文件。

Realm库有多大?

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

Realm能应用在生产环境中吗?

Realm从2012年起就已经开始应用在商业生产环境中了。

请注意:我们会针对社区的反馈来调整接口,当然我们也会针对反馈来添加新的功能以及问题修正。

Realm是免费的吗?

是的!Realm安卓版本是完全免费的,包括使用在商业项目中。

那你们怎么赚钱呢?

我们已经通过销售围绕我们的核心技术的产品和支持服务赚钱啦。如果您有额外的需求,欢迎通过邮件与我们取得联系。我们会一如既往地在Apache 2.0许可下开发并支持realm-java项目,它将永远作为一个开源项目存在。

我经常在代码中看到core相关的字样,它是什么?

core是我们对于Realm的C++存储引擎的称谓。这部分代码目前并没有开放。但基于Apache 2.0许可开源这部分代码在我们的计划之中。在我们清理并完善core的核心功能之前,这部分的二进制版本发布会基于该许可

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

主要区别在于普通Java对象本身会包含其数据,但Realm对象不会。Realm对象通过get或者set方法来直接从Realm数据库中存取数据。 也就是说:Realm对象总体上比普通Java对象更轻量;Realm对象在数据改变的时候会自动更新,而普通Java对象不会。

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

原因是我们需要针对模型类添加一些易用的接口,例如removeFromRealm()。另外它让我们比较容易在内部使用范型(generic)从而提高代码的易读性和易用性。

RealmProxy类是做什么用的?

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

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

我为什么要为每个字段生成getter和setter方法?

代理类依赖于getter和setter。

代理类会重载getter和setter方法以便直接访问数据库存取数据。对于Java中私有成员变量来说这是唯一可行的方法。

这同时意味着当您在实现接口的时候(例如Comparable)也需要使用getter和setter来存取私有成员变量。

这并不是一个十分理想的方案。通过AspectJ和Javassist等其它一些途径有可能实现我们需要的功能,但我们还在研究这些可行性。

使用代理类使得您不可以在getter和setter中添加自己的代码逻辑。一个替代方案是您可以使用@Ignore来让Realm忽略某个字段,同时使用这个字段的getter和setter来添加您想要的代码逻辑。参考如下的例子:

package io.realm.entities;

import io.realm.RealmObject;
import io.realm.annotations.Ignore;

public class StringOnly extends RealmObject {

    private String name;

    @Ignore
    private String kingName;

    // custom setter
    public void setKingName(String kingName) { setName("King " + kingName); }

    // custom getter
    public String getKingName() { return getName(); }

    // setter and getter for 'name'
}

您可以使用setKingName()取代setName()来实现自己想要的功能。请注意,这里调用的setName()并不会直接对成员变量赋值。

我为什么不能自己创建RealmList?

RealmList看起来很像一个普通的List,但它并不是。我们只是用它来表现Realm管理的数据之间的关系。所以它的构造器是受保护的(protected),并且它会在您调用Realm.createObject()时被自动创建。

我们在考虑怎样能更容易地将普通List映射到到RealmList上。

我为什么不能自己创建RealmObject?

与RealmList类似的原因。Realm需要使用RealmObject的代理类来管理数据。

我为什么需要使用事务(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来释放这部分磁盘空间)。

异常Annotation processor may not have been executed.是什么意思?

在编译过程中,Realm会针对模型类处理并生成相应的代理类。如果这个注解处理过程失败,模型类或者其方法在运行时无法被找到,那么就会抛出该异常。由于Java 6不支持注解(annotation)的继承,您需要在模型类之前添加@RealmClass。另外,请尝试删除或者清空编译结果,重新编译来解决这个问题。

Android Chromium WebView

注意: 这个问题已经在WebView的v43版本中修正。

从5.0 Lollipop开始,安卓的Chromium WebView可以通过Play Store升级。但不幸的是在WebView v40版本中有一个bug会导致加密的Realm崩溃。

现有的解决方案包括降级WebView到版本v39,或者在打开加密Realm之前调用CookieManager.getInstance()

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_webview);
    CookieManager.getInstance(); // Prevent Realm from crashing
    Realm realm = Realm.getInstance(this, getKey())
}

参考这里获得更多细节。

Encryption is not supported on this device 异常

部分旧型号的设备(例如HTC One X)的信号处理(signal handler)工作不正常。而Realm的加解密功能依赖于Unix信号(signal),加密Realm在这些设备上将无法使用。Firefox Android版本也曾出现同样的问题,请见On-demand decompression startup failure with Firefox on the HTC One X (Android 4.2.2)

v0.82.2版本开始,Realm会在打开或创建加密Realm时尝试探测运行其的设备是否存在这个问题。如果探测结果表明该设备无法使用加密Realm,RealmEncryptionNotSupportedException会被抛出。

请考虑捕获RealmEncryptionNotSupportedException并在这些设备上使用不加密的Realm。

我们同时也在开发替代方案以确保加密的Realm在这些设备上也可以使用。