[Swift] Làm quen với Realm trong lập trình ứng dụng iOS
1. Giới thiệu:
Các bạn khi lập trình ứng dụng mobile cho iOS lưu trữ database, chắc không lạ gì với cơ sở dữ liệu Sqlite và Core Data. Mỗi cái có các ưu điểm và khuyết điểm riêng. Trong bài viết này mình không đi sâu vào giới thiệu sự khác nhau giữa các cơ sở dữ liệu này. Mình sẽ giới thiệu về Realm như một lựa chọn thay thế cho Sqlite và Core Data, giới thiệu về cách cài đặt và sử dụng trong ứng dụng cho các bạn khi lần đầu làm quen với cơ sở dữ liệu này.
2. Cài đặt:
Có 3 cách cài đặt Realm
– Cài trực tiếp từ file framework
Bạn có thể tải realm framework qua đường link sau: Link
Sau khi giải nén bạn sẽ được 2 file RealmSwift.framework và Realm.framework
Bạn hãy kéo thả 2 file này vào Project → Target của dự án → General → Embedded Binaries (nhớ bấm vào Copy items if needed)→ Finish
Thêm mới Run Script Phase→ Project →Target của dự án → Build Phases → New Run Script Phase và paste đoạn script này vào
1
|
bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"
|
– Cài thông qua CocoaPods
Cách này yêu cầu máy tính của bạn đã cài đặt CocoaPods (Bạn có thể xem tại đây)
Thêm đoạn code sau vào file Podfile
1
2
|
use_frameworks!
pod 'RealmSwift'
|
Sau đó chạy lệnh để cài đặt Realm
1
|
pod install
|
Sử dụng file .xcworkspace được tạo bởi CocoaPods để làm việc. (Chú ý không sử dụng file mặc định .xcodeproj)
– Cài thông qua Carthage
Cách này cũng yêu cầu máy tính của bạn đã cài đặt Carthage (Bạn có thể xem tại đây)
Thêm dòng này vào file Cartfile của bạn
1
|
github "realm/realm-cocoa"
|
Sau đó chạy lệnh
1
|
carthage update
|
Khi chạy xong trong thư mục build của Carthage sẽ xuất hiện 2 file là RealmSwift.framework và Realm.framework tại đường dẫn Carthage/Build/
Bạn hãy kéo thả 2 file này vào Project → Target của dự án → General → Linked Frameworks and Libraries (nhớ bấm vào Copy items if needed)→ Finish
3. Các thao tác với Realm:
a. Định nghĩa đối tượng lưu trữ dữ liệu trong Realm.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import RealmSwift
class Customer: Object {
@objc dynamic var id: String
@objc dynamic var customerName = “”
@objc dynamic var createdDate: Date? = nil
let options: List<OptionModel> = List()
override static func primaryKey() -> String? {
return "id"
}
override static func indexedProperties() -> [String] {
return ["customerName"]
}
}
|
Đối tượng Customer được kế thừa từ Object, Object là một đối tượng đặc biệt của Realm. Mọi đối tượng khi thao tác trên Realm đều phải kế thừa từ Object này.
Realm hỗ trợ hầu hết các kiểu dữ liệu như Bool, Int, Int8, Int16, Int32, Int64, Float, String, Date, Data.
Một lưu ý nhỏ là Realm chỉ hỗ trợ String, Date và Data có thể khai báo là Optional.
Như ví dụ trên thì createdDate là một giá trị Optional.
Nếu như bạn khai báo
1
|
@objc dynamic var number: Int? = 0
|
thì sẽ bị lỗi khi biên dịch.
Để hỗ trợ Optional Int ta phải khai báo như sau:
1
|
let number = RealmOptional<Int>()
|
Và khi gán giá trị cho RealmOptional ta gán thông qua thuộc tính value của RealmOptional.
Ngoài ra Realm còn hỗ trợ khai báo Primary key và Index thông qua việc khai báo 2 hàm override của đối tượng Realm Object. Với việc khai báo này khi truy vấn cơ sở dữ liệu từ Realm, kết quả sẽ được xuất ra nhanh hơn khi làm việc với cơ sở dữ liệu vài chục nghìn records trở lên.
Realm cũng hỗ trợ kiểu List để lưu giữ giá trị mảng. Chú ý đối tượng lưu trữ trong List cũng phải là kiểu đối tượng được kế thừa từ Realm Object.
Khi thêm giá trị vào List ta chỉ việc append giá trị vào list thông qua method append.
b. Cấu hình cho Realm
Ta tạo mới 1 class để quản lý các business liên quan tới truy cập cơ sở dữ liệu.
1
2
3
4
5
6
7
8
9
10
11
12
|
class DatabaseManager {
static let instance = DatabaseManager()
public let customerDA: CustomerDA
init() {
let config = Realm.Configuration(
schemaVersion: 1
)
self.customerDA = CustomerDA(config: config)
if let realmUrl = Realm.Configuration.defaultConfiguration.fileURL {
Logger.log("Realm URL: " + realmUrl.absoluteString) //Xem đường dẫn file lưu cơ sở dữ liệu
}
}
|
Như khai báo ở trên ta tạo 1 class DatabaseManager dạng Singletone (Chỉ tạo 1 lần và có thể truy cập ở mọi nơi thông qua biến instance khai báo ở trên) để quản lý toàn bộ database.
Trong hàm khởi tạo, ta gán thuộc tính schemeVersion (chú ý giá trị được khai báo tăng dần, sẽ nói ở phần Migration) làm config chung cho toàn bộ đối tượng của Realm trong dự án.
Tạo mới class Customer Data Access để xử lý toàn bộ chức năng truy vấn cơ sở dữ liệu của customer.
1
2
3
4
5
6
|
class CustomerDA {
private var config: Realm.Configuration
init(config: Realm.Configuration) {
self.config = config
}
}
|
c. Truy cập dữ liệu trong Realm
-
Lấy customer đầu tiên trong database
1
2
3
|
func getCurrentCustomer() -> Customer? {
return try! Realm(configuration: self.config).objects(Customer.self).first
}
|
Đoạn code này giống như ta sử dụng 1 đối tượng truy cập cơ sở dữ liệu (ở đây là Realm), quét toàn bộ record table Customer và trả về giá trị đầu tiên.
-
Filter, Sort kết quả
Để filter giá trị cần lấy ta tạo mới 1 mệnh đề sử dụng NSPredicate và filter với predicate đó.
1
2
|
let predicate = NSPredicate(format: "name = %@ AND name BEGINSWITH %@", "ten", "B")
customers = realm.objects(Customer.self).filter(predicate)
|
Hoặc để sort kết quả
1
|
let sortedCustomers = realm.objects(Customer.self).filter("name = ten AND name BEGINSWITH 'B'").sorted(byKeyPath: "name")
|
Một lưu ý nhỏ, trong Realm mọi đối tượng đều được lưu trữ dưới dạng là lazy object, dù cho trả ra bao nhiêu đối tượng đi nữa thì bộ nhớ cũng sẽ không được tăng lên trừ khi ta trực tiếp lấy nó ra.
Ví dụ ở đoạn code này sẽ trả ra toàn bộ record của customer.
1
|
let allCustomer = try! Realm(configuration: self.config).objects(Customer.self)
|
Giả sử như database của chúng ta có 5000 records của customer thì giá trị của allCustomer đang chứa 5000 records. Nhưng nó chỉ là 1 biến reference tới 5000 records đó chứ không phải là đối tượng được lưu trong bộ nhớ ram. Chỉ khi chúng ta lấy trực tiếp nó ra thì nó mới được lưu vào ram (chiếm bộ nhớ)
-
Thêm mới một record
1
2
3
4
5
6
7
8
9
10
11
12
|
func saveCustomer(customer: Customer?){
if let customer = customer {
let realm = try! Realm(configuration: self.config)
if realm.isInWriteTransaction {
realm.add(customer, update: true)
} else {
try! realm.write {
realm.add(customer, update: true)
}
}
}
}
|
-
Cập nhật nội dung của record
1
2
3
4
5
6
7
8
9
10
11
12
|
func updateInfo(name: String) {
if let currentCustomer = self.getCurrentCustomer() {
let realm = try! Realm(configuration: self.config)
if realm.isInWriteTransaction {
currentCustomer.name = name
} else {
try! realm.write {
currentCustomer.name = name
}
}
}
}
|
Như ở trên khi thêm mới hoặc cập nhật thông tin 1 đối tượng Realm Object, ta phải kiểm tra đối tượng Realm này có đang ở trên cùng 1 transaction write hay không?
Vì trong Realm khi ta thêm mới, chỉnh sửa, hoặc xoá bắt buộc phải viết ở trong 1 transaction.
d. Migrations trong Realm như thế nào?
Ta cùng xem xét ví dụ sau:
1
2
3
4
|
class Customer: Object {
@objc dynamic var id: String
@objc dynamic var customerName = “”
}
|
và
1
2
3
4
|
class Customer: Object {
@objc dynamic var id: String
@objc dynamic var customerFullName = “” //Đổi customerName thành customerFullName
}
|
Với ví dụ trên ta để ý rằng, đối tượng customer đã thay đổi thuộc tính customerName thành thuộc tính customerFullName, hoặc ta có thể hiểu thuộc tính customerName đã bị xoá bỏ và customerFullName đã được thêm vào.
Do đó với đoạn code trên, khi khởi chạy ứng dụng thì sẽ bị lỗi crash thông báo rằng properties của Customer đã bị thay đổi. Vậy để giải quyết vấn đề này ta sẽ làm như thế nào.
Ta cùng quay lại chỗ config của Realm ở trên. Ta chỉ cần sửa schemaVersion lớn hơn giá trị cũ thì Realm sẽ tự detect ra properties nào bị xoá và được thêm vào để chỉnh sửa lại thông tin của cơ sở dữ liệu. Do đó khi khởi chạy ứng dụng ta sẽ không bị crash vì lỗi migration nữa.
1
2
3
|
let config = Realm.Configuration(
schemaVersion: 2
)
|
e. Notifications trong Realm.
Realm cung cấp sẵn cho ta một đối tượng là NotificationToken để có thể lắng nghe các thay đổi trong cơ sở dữ liệu như :
- Khi nào được khởi tạo xong?
- Khi nào có dữ liệu mới thêm vào?
- Khi nào có dữ liệu được chỉnh sửa?
- Khi nào có dữ liệu bị xoá đi?
Ta cùng tham khảo đoạn code dưới
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
|
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Message.self).filter("id > 5") // Observe Results Notifications
notificationToken = results.observe {[weak self] (changes: RealmCollectionChange) in
guard let strongSelf = self else { return }
switch changes {
case .initial(let messages):
strongSelf.holdMessages = Array(messages)
break
case .update(let messages, deletions: let deletions, insertions: let insertions, modifications: let modifications):
let _holdMessage = strongSelf.holdMessages
strongSelf.holdMessages = Array(messages)
if insertions.count > 0 {
let obj = [messages, insertions] as [Any]
strongSelf.insertMessage(obj: obj)
}
if modifications.count > 0 {
let obj = [messages, modifications] as [Any]
strongSelf.modifyMessage(obj: obj)
}
if (deletions.count > 0) {
let obj = [_holdMessage, deletions] as [Any]
strongSelf.deleteMessage(obj: obj)
}
break
case .error(let error):
Logger.log(error.localizedDescription)
break
}
}
deinit {
notificationToken?.invalidate()
}
}
|
Ở ví dụ trên biến notificationToken phải được khai báo toàn cục, và nên invalidate nó khi không sử dụng tới nữa.
Có một chú ý là trong trường hợp update xoá record khỏi cơ sở dữ liệu RealmCollectionChange mặc định sẽ trả về giá trị mới nhất của toàn bộ records trong database, có nghĩa là nó sẽ không trả về toàn bộ records trước khi delete mà chỉ trả về toàn bộ records sau khi đã deleted. Vì vậy nếu cần lấy giá trị trước khi deleted ta cần phải lưu giữ nó lại thông qua biến holdMessages ở trên.
f. Xem nội dung cơ sở dữ liệu của Realm như thế nào?
Khi chúng ta xây dựng xong ứng dụng điều chúng ta muốn biết nhất là cơ sở dữ liệu hiện có đang lưu trữ đúng giá trị ta cần hay không? Realm đã cung cấp sẵn cho chúng ta Realm Studio để có thể quản lý dễ dàng cơ sở dữ liệu. Với Realm Studio ta có thể xem cấu trúc của database, chỉnh sửa trực tiếp dữ liệu của database (Thêm, Sửa, Xoá).
Công cụ này hỗ trợ cả Mac, Windows và Linux bạn có thể download tại đây.
4. Các điểm cần chú ý
- Không chạy nhiều thể hiện của Realm trên nhiều thread. (Realm có cơ chế tự kiểm tra khi đang được truy cập trên cùng 1 thread hay không, nên khi muốn chạy multithread bắt buộc phải truy cập thông qua ThreadSafeReference)
- Không đọc Realm object từ thread này rồi chỉnh sửa trên thread khác.
- Phải chỉnh sửa dữ liệu trên 1 write transaction của Realm instance.
5. Tổng kết:
Như mình đã giới thiệu qua ở trên. Thao tác với Realm cực kỳ đơn giản và dễ dàng sử dụng.
Vì được phát triển từ C++ nên các vấn đề liên quan tới performance được tối ưu tối đa phù hợp cho các dự án vừa và lớn liên quan tới cơ sở dữ liệu.
Nếu bạn thích thú với Realm có thể áp dụng ngay vào dự án của mình để nhận thấy sự khác biệt.
Link tham khảo: