Realm 数据库基础使用和注意事项(1)

Realm is a mobile database: a replacement for SQLite & ORMs

目前 Realm-Java 最新版本是 10.8.0,安卓项目最开始使用的是从 5 开始,然后是 6 到目前的版本 10,从 5 到 10 版本使用上面的变化差异不大。因此只要熟悉 10 版本即可

一、RealmConfiguration

Realm DB 配置参数对于使用 Realm 很重要

val builder = RealmConfiguration.Builder()
builder.modules(ExampleModule())
    .assetFile("example.realm")
    .directory(File(context.dataDir, "xxx"))
    .allowWritesOnUiThread(true)
    .allowQueriesOnUiThread(true)
    .name("example.realm")
    .schemaVersion(10)
val configuration = builder.build()

1.1Modules

Realm 比较重要基础概念 Modules,Modules 用来指定 Realm DB 中的表,对应 java 或者 kotlin 里面就是 class,这个 class 通过继承 RealmObject 来实现,当然也可以通过接口实现,具体可以参看 Android 官方文档

@RealmModule(classes = [TargetTags::class])
class ExampleModule

以上就一个很简单的 Module 指定,这个 DB 包含一张表,表明叫做 TargetTags

public class TargetTags extends RealmObject {
@PrimaryKey
private String objectId;
@Index
private String targetId;
@Index
private String tag;

public String getObjectId() {
return objectId;
}

public void setObjectId(String objectId) {
this.objectId = objectId;
}

public String getTargetId() {
return targetId;
}

public void setTargetId(String targetId) {
this.targetId = targetId;
}

public String getTag() {
return tag;
}

public void setTag(String tag) {
this.tag = tag;
}
}

这个类实现了 RealmObject,对外提供了基础方法,并且使其一个字段生命为 primaryKey。当生成Realm DB 文件时,里面就会含有 TargetTags 的表,其中字段有 objectId、targetId、tag

1.2 schemaVersion

数据库版本号,类似于 Sqlite 的版本号,主要目的是针对字段变更、表的增加或者删除的。
对于字段的变更安卓不像 ios 那么方便,即使版本号增加也还是需要手动迁移,因此需要特别注意。
Realm DB在使用的时候就会对字段进行相应检查,如果检查不通过就会对外抛出异常,并且提示使用者哪里出问题,一般表现为该有的字段没有做对应迁移

1.3 directory

数据库目录,如果不指定的话默认就会在应用的 data 目录下面,最好指定对应目录,方便数据库文件的管理及迁移操作,尤其 Realm 打开 DB 文件时候会生成多个辅助文件,为了方便管理务必指定文件路径

1.4 name

DB 在数据库目录下面的文件名称,为了方便管理也最好指定

1.5 assetFile

可以让 Realm 直接指定读取 asset 目录下面准备好的 realm 文件,其实现原理也是通过拷贝 asset 目录下面的资源文件到指定目录。如果需要做比较特殊的迁移操作,或者说废弃掉当前目录下面的 realm 文件,需要提前自行删除或者移动

1.6 migration

数据库迁移操作,用户实现 RealmMigration 接口,在这个接口的

public void migrate(@NotNull DynamicRealm realm, long oldVersion, long newVersion) {
}

方法中做相应表的迁移,有增加字段,删除字段,修改字段,为某个字段添加额外索引,另外增加表删除表也需要在这里面声明。其本身实现原理是 Realm 另外一种使用方法,这种使用方式效率较低,但是对扩充字段比较方便,有兴趣的可以去查看文档 DynamicRealm 使用

1.7 其它

.allowWritesOnUiThread(true)
.allowQueriesOnUiThread(true)
.compactOnLaunch()

protected RealmConfiguration(File realmPath,
        @Nullable String assetFilePath,
        @Nullable byte[] key,
        long schemaVersion,
        @Nullable RealmMigration migration,
        boolean deleteRealmIfMigrationNeeded,
        OsRealmConfig.Durability durability,
        RealmProxyMediator schemaMediator,
        @Nullable RxObservableFactory rxObservableFactory,
        @Nullable FlowFactory flowFactory,
        @Nullable Realm.Transaction initialDataTransaction,
        boolean readOnly,
        @Nullable CompactOnLaunchCallback compactOnLaunch,
        boolean isRecoveryConfiguration,
        long maxNumberOfActiveVersions,
        boolean allowWritesOnUiThread,
        boolean allowQueriesOnUiThread) {}

realm 使用配置地方有很多,重要的还有能否在主线程写入和读取等等。可以在有需要的时候再去查看

二 Realm 使用

使用 RealmConfiguration 可以创建空的 Realm DB 文件,也可以通过拷贝方式拷贝现成 Realm 到指定目录,然后通过

Realm.getInstance(configuration)

直接就可以打开 Realm 对其中的表做数据库标准操作:增、删、改、查

不同于一般的关系型数据库,面向对象型数据库操作都是以对象为基本单位,尤其是修改某个属性时操作比较特殊,直接操作对象本身,这些操作可以去查看官方安卓文档
Realm 还有其他不少,例如删除数据库、数据库数据变更监听等,官方文档写得更加详细

三、transaction

transaction 中文为事务,数据库常用的操作,是旨在保证插入安全,尤其是数据插入失败时,保证数据库数据安全,Realm 的所有写入都需要放在 transaction 中,并且 transaction 中不能套 transaction

在特殊情况时,如果不确定当前写入操作是否在 transaction 需要进行额外判断

realm.isInTransaction()


因此需要进行比较复杂操作时,最好使用封装好的工具方法进行插入,防止出现事务操作中套事务的方式出现异常问题

四、Realm Thread

MongoDB Realm enables simple and safe multithreaded code when you follow these three rules:
Avoid writes on the UI thread if you write on a background thread:
You can write to a realm from any thread, but there can be only one writer at a time. Consequently, write transactions block each other. A write on the UI thread may result in your app appearing unresponsive while it waits for a write on a background thread to complete. If you are using Realm Sync, avoid writing on the UI thread as Sync writes on a background thread.
Don’t pass live objects, collections, or realms to other threads:
Live objects, collections, and realm instances are thread-confined: that is, they are only valid on the thread on which they were created. Practically speaking, this means you cannot pass live instances to other threads. However, Realm Database offers several mechanisms for sharing objects across threads.
Don’t lock to read:
Realm Database’s Multiversion Concurrency Control (MVCC) architecture eliminates the need to lock for read operations. The values you read will never be corrupted or in a partially-modified state. You can freely read from realms on any thread without the need for locks or mutexes. Unnecessarily locking would be a performance bottleneck since each thread might need to wait its turn before reading.

4.1.Realm 对象目前是不能跨线程

在某个线程创建,如果某个线程退出就需要把 Realm 对象关闭,通过调用

realm.close();

Realm 本身实现了一套比较比较复杂的缓存机制,因此每次调用 Realm.getInstance(configuration) 都首先从缓存里面去查找,如果没有才会创建新的,当然不同线程缓存不同,在主线程操作 Realm 最好保证 Realm 存活不要轻易关闭,非主 UI 线程如果线程不是永久存活,就需要考虑 Realm 关闭,否则一是会出现 Ream 不能完全被关闭一直在占用内存,另外一种情况就是 Realm 缓存数目一直在增加,可能会超过上限造成崩溃。

4.2.Realm 查询得到 RealmObject 不能跨线程

通过 Realm 查询到的 RealmObject 不能跨线程,当然目前 Realm SDK 提供了几种跨线程操作

  • To modify the data on two threads, query for the object on both threads using a primary key.
  • To send a fast, read-only view of an object to other threads, freeze the object.
  • To keep and share many read-only views of the object in your app, copy the object from the realm.
  • To react to changes made on any thread, use notifications.
  • To see changes from other threads in the realm on the current thread, refresh your realm instance (event loop threads refresh automatically).

比较重要的思路,一个是通过 id 查找,另外通过 realm 拷贝方式,还可以通过比较高级方式 freeze 方式得到的查询结果,realm 拷贝对内存和 cpu 消耗会比较大,不适合那些高频操作,freeze 会对 realm DB 文件增长影响有点大,因此根据具体使用场景决定使用方向

五、Refreshing Realms

刷新 realm 算是比较高阶操作了,尤其是牵涉到后台线程插入 DB 的时候,如果希望主线程立马能够取到刚刚插入或者更新的数据,那需要在主线程刷新 realm

realm.refresh();

当然 Realm 系统提供了一套自动刷新的机制,前提是当前线程有 looper,如果没有 looper 直接调用刷新逻辑会抛异常。

不管在后台大批量插入数据,又或者小批量插入数据,如果发现主 UI 界面有概率性部分数据显示异常,就需要考虑刷新 realm 数据库了,当然刷新会消耗系统性能,不能频繁操作。

六、Encrypt a Realm

加密 DB,这种操作防止数据轻易外泄,只需要在 RealmConfiguration 中指定即可,当然已经加密的 DB 不能通过没有指定秘钥的 RealmConfiguration 打开,切记。

七、其它

Realm 自从被 MongoDB 收购后增加了不少数据同步操作,尤其是对写入和读取在异步线程中操作判断越来越严格。不管是写入还是读取都是比较消耗性能的,写入更加耗性能。
不建议把大批量的写入和读取操作放在主 UI 线程,这会造成界面卡顿,可以考虑把重量级写入放在后台,回到主 UI 线程直接刷新数据库,又或者可以通过 realm 提供的回调方式

RealmObjectChangeListener

单纯的 query 对象基本不怎么消耗手机性能, realm 实现了一套 lazy load 方式,但是如果还想要去读取对象里面字段,尤其是比较大批量读取字段的时候,这个时候是非常耗 CPU 时间,有兴趣的可以去查看 realm 官方文档。

总结:Realm 为我们提供了比较便利的面向对象级别的数据库操作,好处是上手简单,基本只要稍微看过文档,知道基本使用方式就可以开发,坏处是,随着开发量越来越多,realm 到后期使用会越来越复杂,会牵涉到线程,插入读取,性能损耗之类的,是一种易上手但很难精的数据库。使用过程中一定要谨慎。

官方安卓 Realm: https://docs.mongodb.com/realm/sdk/android/
MongoDB Realm:https://www.mongodb.com/zh-cn/realm
旧有 Realm:https://docs.mongodb.com/realm-legacy/docs/java/latest/index.html#getting-started

不同的编程思想

一、面向对象编程(OOP)

简述

面向对象的特点就是:封装、多态、继承。通过封装数据和其他方法,面向对象的编程使软件开发更加以人为中心,符合人类的直觉。在面向对象编程世界里,一切皆为对象,对象是程序的基本单元,对象把程序与数据封装起来提供对外访问的能力,提高软件的重用性,灵活性和扩展性

继承与多态都是后来不断发展的过程中加入进去的。从某种意义上说,多态性是继承性的泛化,因为并不是原始方法或对象的所有属性都需要传输到新实体。相反,还可以选择重写一些属性。

与此同时,继承性和多态性并不是面向对象编程所特有的。真正的区别在于封装数据及其包含的方法。

【扩展】:面向对象编程的基本原则「开闭原则」具有两个主要特征:

(1)对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。

2)对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。

问题

面向对象变成是我们非常容易理解的编程方式,继承和多态更丰富了其内容,但是随之而来也造成了不少的困扰,面向对象编程的问题大多都是由此而来的。

  1. 香蕉猴子丛林问题

    这个问题可以简述为:这个类可能是另一个类的子类,因此你需要将它的父类也包含在内。然后你会发现,这个父类可能也是另一个类的子类,以此类推,最后要面对一堆代码。由此产生大量的多余内容也会造成性能问题,特别是在系统规模变得越来越大时,问题也越发严重。
  2. 脆弱的基类

    你的子类越多,继承的越多,你的基类越难以修改。
  3. 菱形继承问题

    利用继承可以将一类中的属性传递给其他类,但是你无法混合两个不同类的属性,起码常规方法是做不到的。
  4. 层级问题

    类上堆积的属性越多,建立适当的层次结构就越困难。在你所处理的属性集群中,可能A类共享了B类的一些属性,但不是全部属性,反之亦然。在大型复杂项目中,层次结构的问题会导致很大的混乱。
    对于层级问题,我们可能会想到进行没有层次结构的面向对象编程。我们可以使用属性集群,并根据需要继承、扩展或重写属性。也许这有点混乱,但的确可以解决一些问题。
    但是这里还存在一个问题:封装的全部目的是使数据片段彼此之间保持安全,从而使计算效率更高,但没有严格的层次结构,这是行不通的。
  5. 引用问题

    引用问题更像是香蕉猴子问题的延伸,当你只需要一个香蕉,但是却拿到了整片森林时,这片森林已经不再安全了,封装已经被破坏,你可以任意处置其中的数据。
  6. 类库庞大

    当系统越来越庞大时,我们的类库越来越多,且单个库也会越来越大,越来越不易阅读和掌握,潜在隐患增多,我们无法保证类库中的每个类在各种环境中百分之百的正确。

二、面向协议编程(POP)

简述

简单来说,协议就是一张代码实现蓝图,我们可以在这张蓝图上勾勒出可能需要实现的方法、属性和其他满足特定任务的功能模块。而类、结构或枚举都可以通过这张蓝图(协议)来提供对这些需求的实际实现。而任何满足协议要求的类型都被认为符合该协议,都需要实现该协议规定必须实现的方法和属性。

其实我们使用面向协议编程的方式,很大程度上就是为了弥补上述面向对象编程中遇到的问题。

抽象一点,协议仅仅是实现多态的代码的一种方式,其他很多方式也都可以实现:继承、泛型、值、函数等等。值是最简单的方法;函数比值的方式复杂一点,但是仍然很简单;泛型(无约束)也比协议简单。但完整地说,协议通常比类的继承结构要简单。

这里有一点启发性的建议:仔细考虑你的协议是塑造数据模型还是行为模型。对于数据,结构类型可能更容易。对于复杂的行为(例如具有多个方法的委托),协议通常更容易。(标准库中的集合协议有点特殊:它们并不真的描述数据,而是在描述数据处理。)

也就是说,尽管协议非常有用,但是不要为了面向协议而使用协议。首先检视你的问题,并且尽可能地尝试用最简单的方法解决。通过问题顺藤摸瓜找到解决办法,不要背道而驰。面向协议编程并没有好坏之分,跟其他的技术(函数编程、面向对象、依赖注入、类的继承)一样,它能解决一些问题,但我们不应盲目,要择其善者而从之。有时候使用协议,但通常还有更简单的方法。

参考【为什么我们应该批评使用协议】

关于面向协议编程的示例

三、面向过程编程(POP)

简述

面向过程编程就是一种线性思想,按照步骤一步步的去解决问题,也就是面向过程的。对应到代码层面就是一个步骤一个函数。特点是简单,适用于逻辑比较简单的业务。

这里不多做赘述。

四、面向切面编程(AOP)

简述

主要是因为面向对象编程的主要思想就是封装,而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用,但是同时也增加了代码的重复性,两个分装好的内容再想去做同一件事,只能加入重复的代码,而如果不这么做,选择再封装一个独立的方法让他们去调用,就产生了藕合,会影响原有的两个类。因此就希望有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。不够除了运行时,还可以在编译期、类加载期织入如前文所述也可以通过协议的方式来解决此问题

AOP像OOP一样,只是一种编程范式,AOP并没有规定说,实现AOP协议的代码,要用什么方式去实现。不过从技术上来说,面向切面编程基本上都是通过代理机制实现的,对面向对象编程来说是一种十分有益的补充。

关于AOP面向切面编程的详细讲解

五、链式编程

简述

链式编程是指将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好。例如:a(1).b(2).c(3),

  • 代表:Masonry框架

链式编程其实就两个要点:

  • Block 作为当前对象的属性。
  • Block 返回值是当前对象。

关于链式编程的详细

链式编程的一个简单示例

六、响应式编程

响应式编程是指不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆是流。

  • 代表:KVO运用

响应式编程的详细

七、函数式编程

函数式编程是把操作尽量写成一系列嵌套的函数或者方法调用。

  • 函数式编程特点:1、可以把函数作为参数传递给另一个函数,也就是所谓的高阶函数。2、可以返回一个函数,这样就可以实现闭包或者惰性计算
  • 代表:AF的网络回调
  • 以上两个特点还仅仅是简化了代码。从代码的可维护性上讲,函数式编程最大的好处是引用透明,即函数运行的结果只依赖于输入的参数,而不依赖于外部状态,因此,我们常常说函数式编程没有副作用。没有副作用有个巨大的好处,就是函数内部无状态,即输入确定,输出就是确定的,容易测试和维护。

以上我们看到,函数式和链式编程实现原理是一样的,都是在调用方法的时候返回一个对象,利用这个返回的对象去调用下一个方法。只不过链式编程在定义的时候是返回一个 【返回值为对象的block

函数式编程初探

函数式编程入门

总结:

我们要知道, 编程语言不过就是工具,编程思想就是操作工具的方法,没有最好的编程语言和最好的编程思想能够解决所有场景的问题,哪门编程思想解决现实问题越快,代码越容易维护,就去使用它,重要的不是用什么编程思想,而是锻炼自己理解现实需求和抽象代码的能力,这才是最优的方式