原文出处: HOSSAM GHAREEB 译文出处:Prayer’s blog(@EclipsePrayer)
智能手机的快速发展的同时,涌现出了很多对开发者友好的开发工具,这些工具不仅使得开发变得更加容易,同时也保证了性能和产品质量。如今,想要在 App Store 占据一席之地,并非易事。想要使得应用易于扩展就更加困难了。当你成功获得百万量级的用户时,应用中的每一个细节都不能放过,并且需要在很短的时间完成对细节的打磨。和数据库打交道,是很多开发者都会面临的一个问题。相信我们每个人都会因为数据库引起的各种问题而头疼不已,对于数据库,我想如今我们只有两种选择:SQLite 和 Core Data。我是 Core Data 的忠实粉丝,它对记录(records)的处理和持久化数据的能力非常强大,但是我意识到,在开发应用的过程中,我在 Core Data 上浪费了太多的时间。最近,我无意中发现了 Realm,一个可以替代 SQLite 和 Core Data 的更好的解决方案。
Realm 是一个跨平台的移动终端数据库,支持 iOS(Swift 和 Objective-C 语言都支持)和 Android。Realm 的目的就是提供比 SQLite 和 Core Data 更好更快的数据库支持。它不仅仅是更好和更快,而且更加易于使用,短短几行代码就可以完成很多操作。Realm 完全免费,你可以随意使用它。Realm 是为移动设备而生的,因为在过去的十年中,移动终端的数据库技术没有任何的革新。现在如果和移动终端的数据库打交道,你只有一种选择,使用 SQLite 或者是底层封装了 SQLite 的技术比如 Core Data。Realm 的目的是更加易用,它并不是一个建立在 SQLite 之上的 ORM,而是一个基于自己的持久化引擎,简单并且快速的面向对象移动数据库。
Realm拥有令人难以置信的速度并且使用起来非常简单,你会发现,无论是想完成数据库的读还是写操作,都只需要短短的几行代码。下面我会列出它的所有优势,并说明为什么 Realm 是你在移动应用上数据库的不二选择:
让我们开始学习 Realm,使用它来构建一个使用 Swift 语言的简单的 iPhone 应用。所完成的 demo 程序是一个简单的 Todo 应用。用户可以增加任务清单,每个任务清单都能够包含多个任务。任务拥有名称,备注,到期日期,可以添加图片,并且拥有一个布尔值来表示该任务是否已经完成。在开始创建 Xcode project 之前,我们需要先配置好 Xcode,安装使用 Realm 所需的工具。
请注意需要满足下列要求
在开始配置 Xcode 项目之前,请确保运行环境已经正确安装了 CocoaPods,我们将使用它来为项目安装 Realm。如果对 CocoaPods 不熟悉,你可以查看在线的教程,这些教程的材料足够让你明白如何开始使用它。
现在,使用 Single View Application
项目模板创建一个 Xcode 工程,命名为 RealmTasks
或者其他你喜欢的名字。请确保选择 Swift 作为开发语言。之后使用终端进入该工程目录,使用如下命令来初始化 CocoaPods:
1
|
pod init
|
然后使用 Xcode 打开生成的 Podfile 文件,在 target 之后,添加 pod 'RealmSwift'
,修改完之后,应该是下面这个样子:
下面运行 pod install
命令来把 Realm 下载到项目中。完成之后,你会在你的工程文件目录发现一个新生成的 Xcode 工作空间。请确保打开 RealmTasks.xcworkspace 而不是 xcodeproj。打开工作空间后,你会得到下面的情形:
现在就可以在 Xcode 中使用 Realm 啦,但是我们将安装下面的小工具来让使用 Realm 的过程更加容易。
Realm 开发团队提供了一个生成 Realm 模型非常有用的工具。为了安装这个插件,我们要使用 Alcatraz。如果你不知道 Alcatraz 是什么的话,这里解释一下,它是一个简单有用的开源包管理工具,它可以让你无需任何配置,自动地为 Xcode 安装插件,文件模板和颜色主题。为了安装 Alcatraz,你只需将以下代码复制到终端中执行,之后重启 Xcode:
1
|
curl -fsSL https://raw.githubusercontent.com/supermarin/Alcatraz/master/Scripts/install.sh | sh
|
然后在 Xcode 中,选择 Window
->Package Manager
,如下:
然后在弹出的窗口中,你可以选择安装你喜欢的插件或者文件模板,在搜索框中你可以搜索你喜欢的插件、配色。在搜索框输入“Realm”,在出现的结果中选择“RealmPlugin”,点击INSTALL
按钮
给大家介绍的最后一个工具是 Realm 浏览器。这个浏览器可以帮助你阅读和编辑你的.realm
数据库文件。这些文件在应用程序中被创建,在数据库表中保存了所有的数据实体(entities),属性(attributes)和记录(records)。之前我们说过,这些数据库文件可以在不同的平台像 iOS 和 Android 平台间共享。想要下载 最新版本的 Realm 浏览器,请访问 iTunes store。打开 Realm 浏览器,选择 Tools
->Generate demo database
。它会帮你生成 realm 数据库测试文件,你可以使用该浏览器打开和编辑它的内容。当你打开的时候,你会看到像下面的内容:
在 RealmTestClass1 中,它拥有 1000 条记录,显示了多种不同的参数类型。我们会在下节介绍支持的属性类型。
现在,Realm 的所有准备工作都已就绪,那我们开始动手把。
好戏才刚刚开始。首先我们来创建模型类或者说我们的数据库。为了创建 Realm 数据模型类,你只需要简单地新建一个普通的 Swift 类继承自 Object 就可以了。因为 Realm 数据模型类的基类是 Object,所以 Object 的子类都可以扩展为 Realm 的模型类。在创建类之后,就可以添加属性了。Realm 支持以下多种数据类型:
Realm 中的 List 可以包含多个 Object 实例,参考上面 demo 数据库的截图,最后一列表示在其他数据表中的存在的一组引用。在和 Realm 模型类打交道的时候,使用的方式和其他 Swift 类一样。例如,你可以添加方法或者遵循指定的协议。
多说无益,来看代码
现在让我们使用之前在 Xcode 中安装的 Realm 插件来新建一个 Realm 类。打开 Xcode,新建文件,选择 Realm:
然后选择 Swift 语言,类名我们输入 Task。会得到如下结果:
现在,可以向 Task 数据模型中添加属性了。
我们需要在数据模型中添加需要的属性。该例子中,Task 需要有 name (String), createdAt (NSDate), notes (String), 和 isCompleted (Bool)这些属性。添加这些之后,代码应该像下面酱紫:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Task: Object {
dynamic var name = ""
dynamic var createdAt = NSDate()
dynamic var notes = ""
dynamic var isCompleted = false
// Specify properties to ignore (Realm won't persist these)
// override static func ignoredProperties() -> [String] {
// return []
// }
}
|
我们已经为 Task 添加了属性,所有的属性前面都加了 dynamic var
,这使得属性可以被数据库读写。
接下来,我们要创建 TaskList 模型类,用来存储 Task 实例:
1
2
3
4
5
6
7
8
9
10
11
12
|
class TaskList: Object {
dynamic var name = ""
dynamic var createdAt = NSDate()
let tasks = List<Task>()
// Specify properties to ignore (Realm won't persist these)
// override static func ignoredProperties() -> [String] {
// return []
// }
}
|
TaskList 模型类拥有 name,createAt 和一个包含 Task 的 List 属性。需要注意的是:
在 Realm 中创建数据关系非常直观,就像在之前实现中你看到的一对多关系那样。在使用一对一的关系时,我们将使用 Object 类型,来看下面的例子:
1
2
3
4
5
6
7
|
class Person: Object{
dynamic var name = ""
}
class Car: Object{
dynamic var owner:Person?
}
|
上面的例子中,owner 属性表示 Car 和 Person 之间的一对一数据关系。
现在基本的数据模型都已经创建好了。接下来我们会在做一个 ToDo 应用的同时,来讨论 Realm。首先,从这里下载 app,在 Xcode 7 或更高的版本中运行,就像下面这样:
在项目中,我添加了两个视图控制器: TasksViewController 和 TaskListViewController。第一个视图控制器用来显示单个 task,第二个视图控制器用来显示所有的 TaskList。在 list 视图中,点击 + 按钮来添加一个任务清单。选择一个任务清单将会详情视图。你可以在这里添加多个 task。
了解了 demo 的大体思路之后,现在让我们来看看如何向 Realm 数据库中添加一个新的任务清单。为了实现这个功能,需要如下处理:
为了在 Realm 中保存数据,你只需要实例化继承自 Object 的数据模型类,然后将对象写入到 Realm 中,下面是示例代码:
1
2
3
4
5
6
7
8
9
10
11
|
let taskListA = TaskList()
taskListA.name = "Wishlist"
let wish1 = Task()
wish1.name = "iPhone6s"
wish1.notes = "64 GB, Gold"
let wish2 = Task(value: ["name": "Game Console", "notes": "Playstation 4, 1 TB"])
let wish3 = Task(value: ["Car", NSDate(), "Auto R8", false])
taskListA.tasks.appendContentsOf([wish1, wish2, wish3])
|
通过实例化 TaskList 类,我们创建了一个任务清单,之后设置了它的属性。随后我们创建了 3 个 Task 类型的对象。这里我们演示了创建 Realm 对象的三种途径:
在 Realm 中还可以是使用嵌套的方式来创建对象。在一对一关系和一对多关系的时候,你可以使用这种方式,这时候,一个类型对象的初始化需要一个或多个另一个类型的对象。面临这种情况的时候,你可以选择上面的第二或者第三种方法,使用一个字典或者一个数组来表示一个对象:
1
|
let taskListB = TaskList(value: ["MoviesList", NSDate(), [["The Martian", NSDate(), "", false], ["The Maze Runner", NSDate(), "", true]]])
|
在上面的代码中,我们创建了一个电影清单,并设置了清单名称、创建时间和清单内容,清单内容包括多个 task。每个 task 使用数组的方式来创建,例如 [“The Maze Runner”, NSDate(), “”, true]
表示一个task,内容分别对应了名称,创建时间,备注和是否已经完成。
现在你应该知道了如何在 Realm 中创建和使用对象,但是为了能够在应用程序重新启动的时候使用这些数据,需要使用写事务将它们持久化到 Realm 的数据库中。当使用 Realm 来持久化数据的时候,只要这些对象已经存储成功,你可以在任何线程中获取这些对象。一个 Realm 实例表示一个 Realm 数据库。可以像下面一样实例化它:
1
|
let uiRealm = try! Realm()
|
我们常常将上面这行代码写在 AppDelegate.swift
文件的顶端(译者注:类之外,全局变量),这样就可以在整个项目中获得该对象的引用。之后便可以很方便地调用它的读和写方法:
1
2
3
|
uiRealm.write { () -> Void in
uiRealm.add([taskListA, taskListB])
}
|
首先,uiRealm 对象已经在 AppDelegate 中创建,在整个 app 中都可以使用。Realm 对象在每个线程中都应该只被创建一次,因为它不是线程安全的,不能在不同的线程中共享。如果你想要在另一个线程中执行写操作,那么就需要创建一个新的 Realm 对象。我将这个实例命名为 uiRealm
,就是因为它应该只在 UI 线程中被使用。
现在让我们回到我们的 app 中,我们需要在用户点击 Create 按钮的时候保存任务列表。在TasksViewController
的displayAlertToAddTask
方法中,我们有一个 createAction
对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
let createAction = UIAlertAction(title: doneTitle, style: UIAlertActionStyle.Default) { (action) -> Void in
let taskName = alertController.textFields?.first?.text
if updatedTask != nil{
// update mode
uiRealm.write({ () -> Void in
updatedTask.name = taskName!
self.readTasksAndUpateUI()
})
}
else{
let newTask = Task()
newTask.name = taskName!
uiRealm.write({ () -> Void in
self.selectedList.tasks.append(newTask)
self.readTasksAndUpateUI()
})
}
}
|
在上面的代码中,我们从 TextField 中获取到任务名称,调用 Realm 的鞋方法来保存任务列表。
请注意,当你同时进行多个写操作的时候,他们会相互阻塞,阻塞住他们所运行的线程。所以应当考虑在 UI 之外的线程中来进行操作,另外需要注意的是,在进行写事务的时候,读操作并不会造成阻塞。这非常有用,尤其是当你在后台进行写操作的时候,用户可能会在不同界面切换,而这时候可以进行读操作。
现在你已经学会了如何在 Realm 中写数据,下面我们来看看如何检索数据。在 Realm 中检索数据的方式非常直观。Realm 提供了很多选项来过滤出你想要的数据。在 Realm 中进行查找操作的时候,它将会返回一个 Results 对象。可以把 Results 简单地当做是 Swift 的数组,因为它们的接口非常类似。
当得到 Results 实例的时候,已经从磁盘中直接获取到了数据。对这些数据的任何操作(使用事务)将会影响到磁盘上的数据。在 Realm 中来检索数据,只需要调用对象的方法,并将类名作为参数传进去。让我们看看如何使用这种方式来读取 TaskLists 并更新 UI:
我们在 TasksListsViewController
中定义了该属性:
1
|
var lists : Results<TaskList>!
|
然后实现了 readTasksAndUpdateUI
方法:
1
2
3
4
5
|
func readTasksAndUpdateUI(){
lists = uiRealm.objects(TaskList)
self.taskListsTableView.setEditing(false, animated: true)
self.taskListsTableView.reloadData()
}
|
在 tableView(_:cellForRowAtIndexPath:_)
方法中,我们将显示列表的名称,还有每个列表内的任务个数:
1
2
3
4
5
6
7
8
9
10
|
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCellWithIdentifier("listCell")
let list = lists[indexPath.row]
cell?.textLabel?.text = list.name
cell?.detailTextLabel?.text = "\(list.tasks.count) Tasks"
return cell!
}
|
代码逻辑非常直观吧。最后需要做的就是在 viewWillAppear
中调用 readTasksAndUpdateUI
方法,来确保总是显示数据更新之后的视图。
1
2
3
|
override func viewWillAppear(animated: Bool) {
readTasksAndUpdateUI()
}
|
上面展示了如何使用 Realm 来进行任务列表的读写操作。接下来,我们来看看如何进行数据更新和删除操作。在开始之前,先来看看项目中的修改和删除不分的相关代码。
首先在 TaskListsViewController
中,我们用一个布尔值 isEditingMode
来表示是在正常状态还是编辑状态:
1
|
var isEditingMode = false
|
当 Edit 按钮被点击的时候,将会调用 didClickOnEditButton
方法:
1
2
3
4
|
@IBAction func didClickOnEditButton(sender: UIBarButtonItem) {
isEditingMode = !isEditingMode
self.taskListsTableView.setEditing(isEditingMode, animated: true)
}
|
这个方法会使用 table view 的 setEditing
方法来开启或禁用 UITableView 的编辑模式。在表格视图中,默认的编辑操作是删除,但是从 iOS 8.0 开始,增加了一个 editActionsForRowAtIndexPath
方法来自定义一些操作,这些操作在在用户滑动表格 cell 的时候出现。
我们将使用该方法来添加两个功能,删除和编辑,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let deleteAction = UITableViewRowAction(style: UITableViewRowActionStyle.Destructive, title: "Delete") { (deleteAction, indexPath) -> Void in
//Deletion will go here
let listToBeDeleted = self.lists[indexPath.row]
uiRealm.write({ () -> Void in
uiRealm.delete(listToBeDeleted)
self.readTasksAndUpdateUI()
})
}
let editAction = UITableViewRowAction(style: UITableViewRowActionStyle.Normal, title: "Edit") { (editAction, indexPath) -> Void in
// Editing will go here
let listToBeUpdated = self.lists[indexPath.row]
self.displayAlertToAddTaskList(listToBeUpdated)
}
return [deleteAction, editAction]
}
|
这里我们使用 UITableViewRowAction
添加了两个操作,方法中定义了操作的 style
,title
和 handler
。当在滑动 cell 或者以其他方式进入编辑模式的时候,会像下面这样:
以上就是在进行删除和更新操作时候的 UI 代码逻辑。
想要从 Realm 数据库中删除对象或者数据,你只需要调用 Realm 对象的 delete
方法,同时将该对象作为参数传入。当然,这些操作会在写事务中完成。来看一下下面的代码的工作方式,我们从 Realm 数据库中删除了一个任务列表:
1
2
3
4
5
|
let listToBeDeleted = self.lists[indexPath.row]
uiRealm.write({ () -> Void in
uiRealm.delete(listToBeDeleted)
self.readTasksAndUpdateUI()
})
|
在删除之后,我们调用了 readTasksAndUpdateUI
方法来读取数据并更新 UI。
在 Realm 中,还有一个方法叫做 deleteAll
,它允许你删除数据库中所有 class 的所有数据。如果你想为当前用户持久化数据,但是在他退出登录的时候抹掉所有的相关数据,这个方法将会十分有用。
1
2
3
|
uiRealm.write({ () -> Void in
uiRealm.deleteAll()
})
|
在 Realm 中有多种方式可以来更新 object,但是这些方法都应该在写事务中完成。下面我们来看一些更新对象的方式。
使用 properties
你可以通过直接在 Realm 的写闭包中设置 property 的值来更新数据。例如,在 TasksViewController
中,我们可以简单地设置属性值来更新任务的状态信息:
1
2
3
|
uiRealm.write({ () -> Void in
task.isCompleted = true
})
|
使用主键
Realm 支持将某个 string 或 int 类型的属性设置为主键。当使用 add()
方法来创建 Realm 对象时,如果有相同主键的对象存在,就会更新这个对象的值。下面是示例代码:
1
2
3
4
5
6
7
8
9
|
let user = User()
user.firstName = "John"
user.lastName = "Smith"
user.email = "example@example.com"
user.id = 1
// Updating User with id = 1
realm.write {
realm.add(user, update: true)
}
|
这里的 id 属性为主键。如果 id 为 1 的用户存在,Realm 会更新相应的对象。如果不存在,Realm 将会把该对象存入数据库中。
使用 KVC (Key-Value Coding)
如果你是 iOS 开发的老手,那么对 Key-Value Coding 肯定不会陌生。Realm 的类型,像 Object,Results 和 List,都可以使用 Key-Value Coding。该特性可以帮助你在运行时设置或更新属性值。另外一个支持 KVC 的好处是,可以在无需遍历每个对象的情况下,批量更新对象数据。这么说你可能不是很理解,我们来看个例子:
1
2
3
4
|
let tasks = uiRealm.objects(Task)
uiRealm.write { () -> Void in
tasks.setValue(true, forKeyPath: "isCompleted")
}
|
在上面的代码中,使用查询语句来请求所有的 Task 对象,之后将所有得到的对象的 isCompleted
属性设置为 true。可以看出,将 Realm 中的所有 tasks 标记为已完成,仅仅只用了一行代码。
让我们回过头来看看我们的 ToDo 应用。如果仔细观察 displayAlertToAddTaskList
方法,你会看到如下代码片段:
1
2
3
4
5
|
// update mode
uiRealm.write({ () -> Void in
updatedList.name = listName!
elf.readTasksAndUpdateUI()
})
|
当用户遍历列表名称的时候,上面的代码会被调用。通过设置 name 属性的方式,就更新了数据库中的内容。
我们将一起看看 TaskListViewController
的大部分代码。现在让我们打开 TasksViewController
来看看,这个视图控制器用来显示任务清单中的任务项。视图控制器拥有一个 UITableView, 该视图有两个 section:完成的任务和未完成的任务。在 TasksViewController
中,有如下属性:
1
2
3
|
var selectedList : TaskList!
var openTasks : Results<Task>!
var completedTasks : Results<Task>!
|
selectedList
用来存储 TaskListsViewController
传递过来的选中的任务列表。为了将完成和未完成的任务分开,声明了两个属性,openTasks
和 completedTasks
。为了过滤出不同的任务完成状态,我们将使用 Realm 的方法 filter()
。在解释该函数如何工作之前,让我们先来看看如何在代码中使用它:
1
2
3
4
5
6
7
|
func readTasksAndUpateUI(){
completedTasks = self.selectedList.tasks.filter("isCompleted = true")
openTasks = self.selectedList.tasks.filter("isCompleted = false")
self.tasksTableView.reloadData()
}
|
在上面的方法中,我们使用 filter
方法来过滤 results
。Realm 提供了 filter()
方法来过滤数据。该方法可以被 List、Result 和 Object 对象调用。方法会返回过满足滤条件参数的特定对象。你可以把 filter
当做 NSPredicate
。基本上来说,你可以认为这两者差不多。就像上面的代码一样,你同样可以使用 string 作为参数创建一个 NSPredicate
对象,并把它作为参数传给 filter
方法。
让我们来看另外一个例子:
1
2
3
4
5
6
|
// using predicate string
var redCars = realm.objects(Car).filter("color = 'red' AND name BEGINSWITH 'BMW'")
// using NSPredicate
let aPredicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "red", "BMW")
redCars = realm.objects(Car).filter(aPredicate)
|
在上面的代码中,我们使用 filter
方法来过滤 color
为 red,并且 name
以 “BMW” 开头的对象。第一行代码使用 string 作为参数来进行过滤。另外,你也可以使用 NSPredicate 获得同样的效果。下面的表格总结了 filter 方法的大部分常用操作:
既然我们谈到了 Realm 数据库的基本操作,在本教程结束之前,我还想给大家介绍另外一个特性。排序功能,这是 Realm 提供了另一个特别有用的特性。对于 List 和 Result 对象,你可以调用方法 sorted(“排序标准”)
来将一组数据进行排序。让我们来看看如何在任务列表中使用该方法让任务列表以字母表或者创建时间先后顺序排序。首先,在 UI 中,我们增加了一个 segment control,将会根据选择的情况来进行对应的排序。
根据不同的选择来进行排序,代码逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
@IBAction func didSelectSortCriteria(sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 0{
// A-Z
self.lists = self.lists.sorted("name")
}
else{
// date
self.lists = self.lists.sorted("createdAt", ascending:false)
}
self.taskListsTableView.reloadData()
}
|
Realm 是一个非常简单易用,直观的本地数据库解决方案。Realm 提供了很好的可扩展性,只用很少的几行代码就可完成操作。对于大部分的应用甚至是游戏来说,我觉得如果需要使用数据库的话,Realm 值得尝试。
学习了本教程,你应该可以在项目中使用 Realm 来进行增删改查等基本的操作。Realm 还具有一些高阶特性,值得深入学习。最好的学习资料就是 Realm 网站的官方文档,Realm 的小伙伴把文档写得非常赞!
如果想要教程 ToDo 应用的的完整代码,你可以在这里下载
如果对代码有任何的问题,都欢迎留言反馈,我们会很乐意帮助到您。QQ技术交流群290551701