为什么我不再使用Realm
也许你并没有听说过Realm,这是一个面向安卓(亦面相iOS)的移动端数据库技术。和SQLite不同,它允许你在持久层直接和数据对象工作。在它之上是一个函数式风格的查询api,众多的努力让它比传统的SQLite操作更快。基于这些原因让我决定试试Realm。
blob.png
大约一年前,当我第一次使用Realm的时候,给我的第一印象非常不错。我需要保持一些用户数据在手机本地,但是SharedPreferences用起来有点复杂了。Realm允许我用快速干净的代码里完成这件事情。它完全不需要像SQLite那样自己手动写额外的代码。
我接下来的一个项目在缺少网络连接的时候需要一个比较复杂的离线模式。从网络抓取的数据必须保存在手机本地。我决定完全使用Realm并观察它随项目增大是如何扩大的。
而我很快发现Realm让数据模型的工作成了一种负担。它有几个限制以至于我必须在整个代码基础上做处理。结果我尝试在Realm之上抽象出另外一层来减轻这种限制。
定义对象
为了说明这些限制,让我们从简单的Person对象开始:
public class Person extends RealmObject { private String name; private int age; 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; } }
注意我们必须直接继承自RealmObject。这阻碍我们利用数据模型中的任意类型的继承。
并且我们还不能定义除setters和getters之外的实例方法。如果你想重写equals或者toString`那么你就别想了。这样导致的另外一个后果就是我们只能局限于使用标记接口模式(markerinterfaces)(注解也是可以的)。
不仅仅被限制于setters和getters,实际上我们还必须提供它们。因此我们的数据对象是不可变的!另外,setters和getters方法只是为Realm替换自己实现的代理方法。它不能操作数据,跑出异常,或者打印日志。
虽然我们可以提供一个非默认的构造函数,但是我们必须保证存在一个空的构造函数。如果你想用一个builder或者工厂方法来作为实例化的唯一途径,那么这种限制就成了一个问题。稍后我们将看看如何用Realm创建对象。
在我们能持有的field类型方面,也有一些限制。所有的基本数据类型以及它们的封装类型都能支持,包括String,Date,和byte[]`。但是对于其它类型,为了被持久化,必须继承自RealmObject。Lists可以用RealmList来支持。
但是也仅此而已。如果我们想使用枚举而不是int,是没有办法的(找到一个使用@IntDef的理由了)。我们还不能使用集合类型,比如Set和Map。
创建和更新对象
为了创建一个Person类的实例,我们必须做如下事情:
Realm realm = Realm.getInstance(context); realm.beginTransaction(); Person person = realm.createObject(Person.class); person.setName("John"); person.setAge(25); realm.commitTransaction();
你会注意到我们必须包裹一下Person对象,同时任何对它的修改都在一个transaction中。如果我们能在transaction之外做这件事情并在我们准备好的时候持久化它,就要灵活得多。而现在我们在想要创建或者更新我们对象的任何时候都要卡在写额外的Realm代码上面。
之前我提过我们可以定义一个非默认的构造函数。比如,对于Person我们可能有一个带有name和age的构造函数:
public class Person extends RealmObject { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Person() { // required empty public constructor } // setters and getters ... }
我们不再需要直接调用setters:
Person person = new Person("John", 25); realm.beginTransaction(); Person realmPerson = realm.copyToRealm(person); realm.commitTransaction();
这让我们省去了一些写额外代码的时间,但是仍然受限于transaction。
MitigatingTheseIssues
为了避免在基础代码中处理这些限制,我为数据对象定义了两套类:POJOs(普通对象)和Realm对象。然后我们创建了一个能在两者之间映射的abstraction。
这是可行的,但是有两个主要的问题。第一个是当你持有许多不同类型的对象时,你需要许多代码来映射这些类。管理这些是很痛苦的而且这也很容易产生bug。对象映射的概念以及它存在的问题都不是什么新东西了。
第二个就是我觉得这有违最初使用Realm的目的。能在持久层直接使用对象是它的主要好处。如果我们为了使用POJO而必须在Realm之上创建抽象,那么它相比SQLite或者像DBFlow一样的ORM的优势在哪里呢?
值得一提的是Realm的维护者已经意识到了这些限制,而且在一定程度上,许多问题都可能被解决(见这里和这里))。Realm也的确具有一些其它的优势,比如性能以及在iOS和安卓之间共享数据的能力。