Giới thiệu về Realm Mobile Database

I. Realm Mobile Database là gì?

Ngày nay, việc phát triển ứng dụng di động đang trở thành xu hướng và phổ biến khắp mọi nơi. Hiện nay đang có khá nhiều hệ điều hành di động phổ biến và đi kèm đó là ngôn ngữ riêng cho từng hệ điều hành riêng biệt (Java cho Android; Swift hoặc Objective-C dành cho iOS,...).

Trong mỗi ứng dụng thì phần quan trọng không kém chính là Cơ sở dữ liệu. CSDL phổ biến nhất được sử dụng hiện nay trên hầu hết các thiết bị là SQLite bởi vì nó khá quen thuộc với đại đa số các lập trình viên do sử dụng câu truy vấn SQL. Tuy nhiên, SQLite cũng có những mặt hạn chế nhất định như tốc độ truy vấn khá chậm khi mà dữ liệu phình to ra cũng như khi mà thực hiện phép JOIN. Hơn thế nữa, với mỗi ngôn ngữ khác nhau thì việc thiết lập SQLite có thể tốn khá nhiều công sức.

Trên cơ sở đó, Realm Mobible Database ra đời với mục đích cung cấp cho lập trình viên một lựa chọn có thể thay thế cho SQLite hiện nay nhưng vẫn đảm bảo mọi chức năng cần thiết của một CSDL thông thường:

  • Realm Moblie Database ( gọi tắt là RMD) là một NoSQL ( Not Only SQL). Nó hướng tới việc xây dựng một ứng dụng theo hướng **Offline database first. **Điều này có nghĩa là ứng dụng vẫn có thể hoạt động dù cho không có kết nối mạng, dữ liệu sẽ được lưu trực tiếp trên thiết bị, người dùng vẫn có thể tiến hành mọi việc một cách thuận lợi.
  • RMD lưu trữ dữ liệu dưới dạng Object và nó cũng cung cấp các hàm và phương thức để có thể truy vấn dữ liệu mà không cần thông qua câu truy vấn SQL.
  • Phần core của RMD được viết bằng C++ và là mã nguồn mở, người dùng có thể tùy chỉnh lại theo ý muốn cá nhân.
  • Cross-flatform và  đã có phiên bản cho các ngôn ngữ sau: Swift, Java, Objective – C, Xamarin, React Native.
  • Cung cấp miễn phí.

(Trong nội dung bài viết dưới đây, tác giả sẽ nói về sử dụng Realm dành cho Swift)

II. Điểm mạnh của Realm Mobile Database

1. Dễ cài đặt và sử dụng

  • RMD khá dễ cài đặt và sử dụng. Đối với IOS, chúng ta có thể sử dụng thư viện quản lý CocoaPods để cài đặt và sử dụng.
  • Để sử dụng RMD, chúng ta chỉ cần tạo một class như bình thường, và kế thừa từ class “Object” của RMD.
import Foundation
import RealmSwift

class Message: Object {
    dynamic var id: String = ""
    dynamic var message: String?
    dynamic var name: String?
}

2. Tốc độ truy vấn nhanh.

RMD được tối ưu hóa để sử dụng bộ nhớ một cách ít nhất nhưng hiệu suất vẫn vượt trội so với các CSDL khác.

Dưới đây là bảng so sánh tốc độ của Realm so với các CSDL khác.

3. Realm Browser

Đối với hệ điều hành MacOS, Realm cung cấp công cụ Realm Browser giúp chúng ta có thể dễ dàng quản lý dữ liệu. Ngoài ra, chúng ta còn có thể trực tiếp thay đổi dữ liệu ở trên công cụ này.

4. Cross – Platform

Realm hiện nay đã có phiên bản hỗ trợ trên các ngôn ngữ lập trình di động phổ biến. Chúng ta có thể sử dụng một database duy nhất cho tất cả các phiên bản trên các hệ điều hành di động khác nhau. Hiện nay Realm đã có hỗ trợ cho các ngôn ngữ sau: Java, Swift, Objective – C, Xamarin, React Native.

III. Một số chức năng chính của Realm Mobile Database

1. Models

  • Để sử dụng RMD, ta chỉ cần tạo một class model như bình thường và kế thừa lớp “Object” của Realm. Đối với ngôn ngữ Swift, ta cần thêm “dynamic” vào phía trước các thuộc tính mà ta cần trong DB.
  • Realm hỗ trợ đầy đủ các dịnh dạng thông thường như String, Int8, Int16, Int32, Int64, Double, Float, NSDate, NSData.
  • Đối với loại dữ liệu String, NSDate, NSData thì có thể là thuộc tính optional. Kiểu dữ liệu Object thì bắt buộc phải là optional; còn đối với kiểu dữ liệu số thì có thể sử dụng RealmOptional().
  • Chúng ta có thể đặt Primary Key, Indexed cho một class bằng cách override lại các hàm như primaryKey(); indexedProperties().
import Foundation
import RealmSwift

class Dog: Object {
    dynamic var id:Int = 0
    dynamic var name: String?
    dynamic var createdAt: NSDate?
    let age = RealmOptional<Int>()
    // Relationship
    dynamic var owner: Person? //To - One Relationship
    let friends = List<Dogs>() //To-Many relationships

    //Set primary key
    override static func primaryKey() -> String? {
        return "name"
    }
    //Set index properties
    override static func indexedProperties() -> [String] {
        return ["name"]
    }
    
}

2. Writes

Create: Có gì cách để khởi tạo 1 Object, ta có thể sử dụng nhiều cách khác nhau để khởi tạo.

// (1) Create a Dog object and then set its properties
var myDog = Dog()
myDog.name = "Rex"
myDog.age = 10

// (2) Create a Dog object from a dictionary
let myOtherDog = Dog(value: ["name" : "Pluto", "age": 3])

// (3) Create a Dog object from an array
let myThirdDog = Dog(value: ["Fido", 5])

Object sau khi được khởi tạo thì có thể sử dụng một cách như bình thường. Object này chỉ được lưu xuống bộ nhớ khi mà ta thực hiện hàm write. Sau khi Object đã được write rồi thì mọi thay đổi trên nó sẽ ngay lập tức được lưu lại xuống DB một cách tự động.

// Create Object
var myDog = Dog()
myDog.name = "Hachi"
myDog.age = 10 //age = 10

// Get the default Realm
let realm = try! Realm()
// You only need to do this once (per thread)

// Add to the Realm inside a transaction
try! realm.write {
  realm.add(myDog) //Object is saved in DB
}

// Update age, value is saved automatically
myDog.age = 20

Update: Có nhiều cách để update một object như là: update theo PrimaryKey (Realm sẽ tự động tìm kiếm theo primaryKey và update object), update theo kiểu Key-Value (phù hợp cho việc update dữ liệu trong lúc Run-time)

// Creating a dog with the same primary key as a previously saved dog
let shibuDog = Book()
shibuDog .name= "Nobi"
shibuDog .age= 4
shibuDog .id = 1

// Update using Primary Key
// Updating dog with id = 1
try! realm.write {
  realm.add(shibuDog , update: true)
}

try! realm.write {
  realm.create(Dog.self, value: ["id": 1, "age": 4], update: true)
  // the dog's `name` property will remain unchanged.
}

// Update Object using Key - Value code
let persons = realm.objects(Person.self)
try! realm.write {
  persons.first?.setValue(true, forKeyPath: "isFirst")
  persons.setValue("name", forKeyPath: "Jane")
}

Delete: Để xóa một Object hoặc xóa tất cả Object trong Realm

// let dog = ....

// Delete an object in transaction
try! realm.write {
  realm.delete(dog)
}

// Delete all objects from the realm
try! realm.write {
  realm.deleteAll()
}

3. Queries

  • Trong Realm, câu Queries sẽ trả về một Results  instance có thuộc tính duy nhất là Object của class được truy vấn.
  • Ở trong Realm, câu truy vấn thuộc dạng lazy, tức là dữ liệu chỉ thực sự được đọc khi mà nó được truy cập. Hơn thế nữa, câu truy vấn trong Realm không tạo 1 bản sao của dữ liệu mà chính là dữ liệu thật sự đang lưu ở dưới đĩa, do đó mọi thay đổi của chúng ta trên các thuộc tính này sẽ ngay lập tức được lưu lại.
let dogs = realm.objects(Dog.self) // retrieves all Dogs from the default Realm
  • **Filtering: **với hàm filter, chúng ta có thể lọc và chọn ra các object cụ thể thỏa mãn điều kiện. Hàm filter sử dụng giống với NSPredicate và có cấu trúc tương tự với SQL.
  • Hàm Filter hỗ trợ đầy đủ các phép so sánh như ==, !=, >, <, >=, <=. Đối với dữ liệu kiểu String hoặc NSData thì nó cũng hỗ trợ các phép tìm kiếm như ==, !=, BEGINSWITH, CONTAINS, ENDSWITH.
// Query using a predicate string
var tanDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'")

// Query using an NSPredicate
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog.self).filter(predicate)
  • **Chaining: **điểm đặc biệt của Realm là nó cho phép tạo các chain queries, tức là câu queries sau có thể thực hiện dựa vào giá trị của câu queries trước đó..
let tanDogs = realm.objects(Dog.self).filter("color = 'tan'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH 'B'")
  • Sorting: Realm cho phép sort giá trị trả về theo một hay nhiều thuộc tính.
// Sort tan dogs with names starting with "B" by name
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byProperty: "name")
  • Limited Results: do cách lấy dữ liệu của Realm là lazy nên việc giới hạn số lượng object trả về là không cần thiết.

4. Notifications

Điểm mạnh của Realm là có thể đăng ký một listener để tiếp nhận thông tin bất cứ khi nào dữ liệu trên Collections, Result, List bị thay đổi. Khi bất kì commit transaction nào thực hiện thì nó đều gửi đi 1 notifications, chúng ta chỉ cần đăng ký để lắng nghe noti đó thì sẽ biết được khi nào dữ liệu thay đổi và thực hiện việc cập nhật lại giao diện. Có 2 loại notifications: Realm notifications và Collections Notifications.

Realm Notifications: đây là notifications của realm instance, nó sẽ được gọi bất kì khi nào có 1 commited transaction được thực thi.

// Observe Realm Notifications
let token = realm.addNotificationBlock { notification, realm in
    viewController.updateUI()
}

// later
token.stop()

Collections Notifications: notifications này chỉ gọi khi mà dữ liệu trên 1 object cụ thể mà ta đăng ký có sự thay đổi. Nó cũng xác định cho ta biết dữ liệu bị thay đổi như thế nào (insert, update, delete).

class ViewController: UITableViewController {
  var notificationToken: NotificationToken? = nil

  override func viewDidLoad() {
    super.viewDidLoad()
    let realm = try! Realm()
    let results = realm.objects(Person.self).filter("age > 5")

    // Observe Results Notifications
    notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
      guard let tableView = self?.tableView else { return }
      switch changes {
      case .initial:
        // Results are now populated and can be accessed without blocking the UI
        tableView.reloadData()
        break
      case .update(_, let deletions, let insertions, let modifications):
        // Query results have changed, so apply them to the UITableView
        tableView.beginUpdates()
        tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                           with: .automatic)
        tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                           with: .automatic)
        tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                           with: .automatic)
        tableView.endUpdates()
        break
      case .error(let error):
        // An error occurred while opening the Realm file on the background worker thread
        fatalError("\(error)")
        break
      }
    }
  }

  deinit {
    notificationToken?.stop()
  }
}

IV. Đánh giá chung

1. Điểm mạnh

  • Tốc độ truy vấn nhanh, cài đặt dễ dàng.
  • Hỗ trợ đầy đủ các nhu cầu cần thiết của một CSDL.
  • Có thể xác định được lúc dữ liệu thay đổi để thực hiện cập nhật giao diện.
  • Cross – flatform nên có thể sử dụng cho nhiều ngôn ngữ.
  • Tài liệu hướng dẫn đầy đủ.

2. Điểm yếu

  • Không hỗ trợ trường giá trị tự tăng để làm khóa chính.
  • Không thể custom initializer.
  • Không thể override hàm Setter và Getter.