本篇博客为数据库框架Realm的调研笔记

一、Realm介绍
Realm为一套数据库和ORM的轻量级解决方案[1],相比SQLite,Realm更快,并拥有更多现代数据库的特性,比如关系和关联查询、流式api、数据变更通知、加密、跨平台[2]。此外,Realm还和JSON、Gson、Retrofit提供了支持,Realm自带RealmBaseAdapter,能够完成自动更新,因此ui就能自动刷新,但目前只能和ListView配合使用[3]

二、Realm使用[2][4]
1.引入Realm
compile ‘io.realm:realm-android:0.87.5’

2.创建数据库

1
Realm realm = Realm.getInstance(context);

getInstance中会调用Realm.getInstance(new RealmConfiguration.Builder(context).name(DEFAULT_REALM_NAME).build()); 创建默认叫做default.realm的Realm文件

或者使用配置文件

1
2
3
4
5
6
7
Realm otherRealm =
Realm.getInstance(
new RealmConfiguration.Builder(context)
.name("myOtherRealm.realm”)//设置数据库文件名称
.encryptionKey(key)//加密数据库
.build()
);

创建名为myOtherRealm的realm文件
这里使用构造器模式,在Builder的构造函数中完成配置文件的名称设置,路径设置(默认为Context.getFilesDir()位置),数据版本号默认设置,以及modules的设置,这里的modules理解为不同的数据库,同时,使用流式布局完成属性的设置
build方法则调用RealmConfiguration的构造函数,该函数除了设置构造器设置的属性,还会调用createSchemaMediator完成schemaMediator的创建,该schemaMediator会根据设置的module来查找moduleMediator,如果我们没有通过setModules设置,那么会使用Realm中的默认Module:DefaultRealmModule ,它使用了注解allClasses=true代表包含了所有的Model,该Realm对应Mediator为RealmDefaultModuleMediator。

3.创建Model
必须使用继承自RealmObject的类,并且其中的变量必须为private,必须定义Getter/Setter方法,Realm注解处理器会生成代理类,并会覆盖getter/setter方法,所以在getter/setter方法中不能对变量进行处理,同时,我们还不能定义除getter/setter之外的实例方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Country extends RealmObject {

@PrimaryKey
private String code;//设置主键
@Index
private String name;//设置索引
private int population;
@Ignore
private int sessionId;//不存入数据库

public Country() { }

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getPopulation() {
return population;
}

public void setPopulation(int population) {
this.population = population;
}
}

关于Realm的类型,其支持所有基本数据类型boolean, byte, short, ìnt, long, float, double, String, Date and byte[]以及它们的封装类型,但对于其他类型,为了被持久化,必须继承自RealmObject,Lists则可以用RealmList,但无法使用Set和Map

4.插入数据
Realm遵循ACID规范,为了保持原子性和一致性,强制所有写入操作在一个事务中,并且需要在getInstance的那个线程中完成插入

1
2
3
4
5
realm.beginTransaction();
User user = realm.createObject(User.class); // Create a new object
user.setName("John");
user.setEmail("john@corporation.com");
realm.commitTransaction();

将已存在对象拷贝进realm

1
2
3
4
5
6
7
Country country2 = new Country();
country2.setName("Russia");
country2.setPopulation(146430430);
country2.setCode("RU");
realm.beginTransaction();
Country copyOfCountry2 = myRealm.copyToRealm(country2);
realm.commitTransaction();

或者并不立即commit[4]

1
2
3
4
5
realm.beginWriteTransaction();
User user = realm.createObject(User.class); // Create a new object
user.setName("John");
user.setEmail("john@corporation.com")
realm.cancelTransaction();

或者

1
2
3
4
5
6
7
8
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {

User user = realm.createObject(User.class);
user.setName("Cuber");
user.setAge(26);
}
});

以及update

1
2
3
4
5
6
7
8
9
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();

注意上面的createObject方法

1
2
3
4
5
6
public <E extends RealmObject> E createObject(Class<E> clazz) {
checkIfValid();
Table table = getTable(clazz);
long rowIndex = table.addEmptyRow();
return get(clazz, rowIndex);
}

其中会调用到getTable,

1
2
3
4
5
6
7
8
9
public Table getTable(Class<? extends RealmObject> clazz) {
Table table = classToTable.get(clazz);
if (table == null) {
clazz = Util.getOriginalModelClass(clazz);
table = sharedGroupManager.getTable(configuration.getSchemaMediator().getTableName(clazz));
classToTable.put(clazz, table);
}
return table;
}

其中使用了在RealmConfiguration中初始化的schemaMediator得到clazz的tableName,若为默认的RealmDefaultModuleMediator,那么无论传入什么clazz,都会调用AllTypesRealmProxy.getTableName();返回字符串”class_AllTypes”,并会调用Group的getTable方法,其中会调用new table方法完成数据表的创建。

5.查询数据
在某个线程上执行query后,得到的数据包括queries、results 是不能被另一个线程访问的。所以我们一般在UI线程上完成数据库的查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
RealmQuery<User> query = realm.where(User.class);
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();

除了equalTo 还有between() greaterThan() beginsWith()等

realm.where()方法调用了

1
RealmQuery.createQuery(this, clazz);

随后调用

1
2
3
4
5
6
7
8
9
10
public RealmResults<E> findAll() {
checkQueryIsNotReused();
RealmResults<E> realmResults;
if (isDynamicQuery()) {
realmResults = (RealmResults<E>) RealmResults.createFromDynamicTableOrView(realm, query.findAll(), className);
} else {
realmResults = RealmResults.createFromTableOrView(realm, query.findAll(), clazz);
}
return realmResults;
}

其中还会调用到TableQuery的findAll方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public TableView findAll() {
validateQuery();

// Execute the disposal of abandoned realm objects each time a new realm object is created
context.executeDelayedDisposal();
long nativeViewPtr = nativeFindAll(nativePtr, 0, Table.INFINITE, Table.INFINITE);
try {
return new TableView(this.context, this.table, nativeViewPtr, this);
} catch (RuntimeException e) {
TableView.nativeClose(nativeViewPtr);
throw e;
}
}

这里底层使用了c++来完成数据的查找。然后会生成一个tableview,可以理解为视图,视图是查询的结果集,但是不保存真实数据,而是匹配对象引用的列表。最后将视图作为参数传递给RealmResults。RealmResults 可以理解为数据库中的动态视图的一个包装,如果该RealmResults在具有Looper的线程上,那么当我们调用commitTransaction完成某个表的update操作后,该表对应的RealmResults会自动更新。如果是非looper的线程,则需要我们调用realm.refresh方法完成RealmResults的更新。

RealmResults就是Realm性能优于ormlite的原因之一,RealmResults,可以看做是一个类型安全的Cursor,但不像cursor,我们不需要拷贝数据到cursorWindow中,因此没有pagination effect,同时我们不需要担心cursorWindow的limit问题。大多数的orm框架都是将数据赋值到java的堆内存中,这是十分耗时耗内存的操作,并且会一次性加载进很多我们不需要的数据库字段,Realm的做法就是只加载一次,并且是只加载引用,当我们需要某个字段时,才去加载需要的数据[8]

异步查询findAllAsync:类似Java中的Feature
查询不会被阻塞,而是直接返回RealmResults,当后台完成查询后会之前返回的RealmResults会被更新

查询结果可以使用一些排序和聚合方法

1
2
3
RealmResults<User> result = realm.where(User.class).findAll();
result.sort("age"); // Sort ascending
result.sort("age", Sort.DESCENDING);
1
2
3
4
5
6
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();

6.删除数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// All changes to data must happen in a transaction
realm.beginTransaction();

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

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

// Delete all matches
result.clear();

realm.commitTransaction();

7.数据库update[6]
给realm配置设置schemaVersion和migration文件即可

1
2
3
4
5
6
7
8
9
10
RealmConfiguration config1 = new RealmConfiguration.Builder(this)
.name("default1")
.schemaVersion(3)
.migration(new Migration())
.build();

realm = Realm.getInstance(config1); // Automatically run migration if needed
showStatus("Default1");
showStatus(realm);
realm.close();//关闭realm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Migration implements RealmMigration {

@Override
public void migrate(final DynamicRealm realm, long oldVersion, long newVersion) {
// During a migration, a DynamicRealm is exposed. A DynamicRealm is an untyped variant of a normal Realm, but
// with the same object creation and query capabilities.
// A DynamicRealm uses Strings instead of Class references because the Classes might not even exist or have been
// renamed.

// Access the Realm schema in order to create, modify or delete classes and their fields.
RealmSchema schema = realm.getSchema();

/************************************************
// Version 0
class Person
@Required
String firstName;
@Required
String lastName;
int age;

// Version 1
class Person
@Required
String fullName; // combine firstName and lastName into single field.
int age;
************************************************/
// Migrate from version 0 to version 1
if (oldVersion == 0) {
RealmObjectSchema personSchema = schema.get("Person");

// Combine 'firstName' and 'lastName' in a new field called 'fullName'
personSchema
.addField("fullName", String.class, FieldAttribute.REQUIRED)
.transform(new RealmObjectSchema.Function() {
@Override
public void apply(DynamicRealmObject obj) {
obj.set("fullName", obj.getString("firstName") + " " + obj.getString("lastName"));
}
})
.removeField("firstName")
.removeField("lastName");
oldVersion++;
}
}
}

三、一些高级特性
1.Dynamic Realms[7]: 使用String 而不是Class,DynamicRealmObject person = realm.createObject(“Person”); 他会忽略schema,migration,和数据版本号,这种写法会放弃安全性和性能但会提高灵活性。在编译或者数据库升级时我们无法通过Class操作数据库等场景时可以考虑使用该种方式

2.和JSON结合使用

loadJsonFromStream

1
2
3
4
5
6
7
8
9
10
11
12
13
InputStream stream = getAssets().open("cities.json");
realm.beginTransaction();
try {
realm.createAllFromJson(City.class, stream);
realm.commitTransaction();
} catch (IOException e) {
// Remember to cancel the transaction if anything goes wrong.
realm.cancelTransaction();
} finally {
if (stream != null) {
stream.close();
}
}

或者
loadJsonFromJsonObject

1
2
3
4
5
6
7
8
Map<String, String> city = new HashMap<String, String>();
city.put("name", "København");
city.put("votes", "9");
JSONObject json = new JSONObject(city);

realm.beginTransaction();
realm.createObjectFromJson(City.class, json);
realm.commitTransaction();

loadJsonFromString

1
2
3
4
String json = "{ name: \"Aarhus\", votes: 99 }";
realm.beginTransaction();
realm.createObjectFromJson(City.class, json);
realm.commitTransaction();

3.和Gson结合使用
Realm也可以和Gson结合使用,但是在GSON1.7.1以上的版本中,需要设置ExclusionStrategies,否则会引起崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 : 'john@corporation.com' }";
User user = gson.fromJson(json, User.class);

4.和ListView结合使用
Realm自带RealmBaseAdapter,我们只需要将查询结果RealmResults直接作为参数传递给RealmBaseAdapter的构造函数,若列表数据有update,在我们提交完数据库事务以后Realm会自动更新RealmResults

1
2
3
4
RealmResults<TimeStamp> timeStamps = realm.where(TimeStamp.class).findAll();
final MyAdapter adapter = new MyAdapter(this, R.id.listView, timeStamps, true);
ListView listView = (ListView) findViewById(R.id.listView);
listView.setAdapter(adapter);

5.Realm数据查看
RealmBrowser

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
}
}

避免使用匿名类加入到listener中,自行维护listener的销毁。

7.关系和关联查询
任意两个RealmObject可以相互关联

1
2
3
4
5
6
7
8
9
public class Contact extends RealmObject {
private Email email;
// Other fields…
}

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

也可以使用递归

1
2
3
4
5
public class Person extends RealmObject {
private String name;
private RealmList<Person> friends;
// Other fields…
}

关联查询

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

四、Realm缺点[3][10]
1.Realm最终并不是使用Model,而是使用基于Model的代理类,该代理类会重载getter和setter方法以便访问数据库存取数据,所以我们不能在Getter和Setter中自定义数据的处理。并且只支持私有成员变量。为了解决不能在Model中自定义非getter和setter的问题,我们需要为数据对象定义两套类:POJOs和Realm对象,然后创建一个能在两者之间映射的abstraction[5]。
2.由于 Realm 极度线程安全,虽然在不同线程里,都能去访问 Realm “数据库”,但一个线程里的 Realms、RealmObject、queries、results 等等,是不能被另一个线程访问的。(比如在一个线程里,我从网络获取了数据,然后以 RealmObject 缓存。然后回到 UI 主线程,此时我要更新刚获得的网络数据,就需要再做一次本地查询。 )
3.忽略了大小写查询

参考文献
[1]https://realm.io/
[2]http://code.tutsplus.com/tutorials/up-and-running-with-realm-for-android--cms-25241
[3]http://ycz.im/2015/07/10/using-realm-to-replace-sqlite-in-android/
[4]https://bng86.gitbooks.io/android-third-party-/content/realm.html
[5]http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1203/3743.html?utm_source=tuicool&utm_medium=referral
[6]https://realm.io/docs/java/latest/#migrations
[7]https://realm.io/docs/java/latest/#dynamic-realms
[8]http://stackoverflow.com/questions/35219165/realm-lazy-queries-are-they-faster-than-ormlite
[9]https://realm.io/cn/docs/java/latest/#notifications
[10]https://realm.io/cn/docs/java/latest/#section-32