12/08/2018, 16:34

Một vài kinh nghiệm khi dùng Realm trong android

Trong bài viết này, mình xin chia sẻ 1 vài kinh nghiệm nhỏ khi sử dụng Realm trong android. Có thể bạn đã đọc nó trong Realm documents hoặc gặp đâu đó trong lúc phát triển ứng dụng android. Realm Transaction Không giống như read operation (thao tác đọc), write operation (thao tác ghi) trong ...

Trong bài viết này, mình xin chia sẻ 1 vài kinh nghiệm nhỏ khi sử dụng Realm trong android. Có thể bạn đã đọc nó trong Realm documents hoặc gặp đâu đó trong lúc phát triển ứng dụng android.

Realm Transaction

Không giống như read operation (thao tác đọc), write operation (thao tác ghi) trong Realm phải được bao bọc trong các transaction. Khi kết thúc một thao tác ghi, bạn có thể thực hiện transaction hoặc hủy bỏ nó. Thực hiện một transaction tức là ghi tất cả các thay đổi vào đĩa (và nếu dùng synced Realm , xếp nó vào queues để đồng bộ với Realm Object Server). Nếu bạn hủy một thao tác ghi, tất cả các thay đổi sẽ bị loại bỏ. Transaction có nghĩa là tất cả hoặc không có gì , hoặc tất cả các thao tác ghi trong một transaction thành công, hoặc không gì trong số chúng có hiệu lực. Điều này giúp đảm bảo tính nhất quán dữ liệu, cũng như cung cấp an toàn cho thread.

// Obtain a Realm instance
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
//... add or update objects here ...
realm.commitTransaction();

Write transaction sẽ block các transaction khác, vì thế nó có thể gây lỗi ANR (App not responding) nếu như bạn tạo write transaction trong cả UI va background thread cùng lúc. Để tránh điều này, nên dùng async transaction khi bạn muốn tạo các write transaction trên UI thread.

Ngược lại, read transaction không bị block khi write transaction được mở. Trừ khi bạn cần thực hiện các transaction đồng thời từ nhiều thread cùng một lúc, bạn có thể ưu tiên các transaction lớn hơn làm nhiều công việc hơn nhiều transaction khác. Khi bạn thực hiện một write transaction cho Realm, tất cả các instance khác của Realm đó sẽ được thông báo và được cập nhật tự động.

Read & write access in Realm is ACID.

Một lưu ý khác khi làm việc với transaction là nếu một exception xảy ra bên trong một transaction, bạn sẽ mất tất cả các thay đổi trong transaction đó, nhưng bản thân Realm sẽ không bị ảnh hưởng (hoặc corrupted). Nếu bạn catch exception và ứng dụng được chạy tiếp, bạn cần phải hủy transaction đó. Nếu thực hiện transaction bằng lời gọi excuteTransaction, điều này sẽ được tự động.

Ex:

// SHOULD NOT
Realm realm = Realm.getDefaultInstance();
try {
    realm.beginTransaction();
    realm.copyToRealm(dog);
    realm.commitTransaction();
} catch (Exception ex) {
    realm.cancelTransaction();
}

Nên để Realm handle việc hủy transaction một cách tự động:

// SHOULD 
Realm realm = null;
try { // I could use try-with-resources here
    realm = Realm.getDefaultInstance();
    realm.executeTransaction(new Realm.Transaction() {
        @Override
        public void execute(Realm realm) {
            realm.insertOrUpdate(dog);
        }
    });
} finally {
  if(realm != null) {
    realm.close();
  }
}

// OR IN SHORT (Retrolambda), try-with-resources
try(Realm realmInstance = Realm.getDefaultInstance()) {
    realmInstance.executeTransaction((realm) -> realm.insertOrUpdate(dog));
}

Close all realm instances

Realm sử dụng một tham chiếu đến counted thread local cache và thực thi optimized schema validation. Có nghĩa là chừng nào bạn có ít nhất một instance mở trên một thread, gọi Realm.getInstance() chỉ là một tra cứu HashMap.

Nếu bạn có một instance mở trên bất kỳ thread nào, Realm sẽ bỏ qua schema validation trên các thread khác, mặc dù đây là instance đầu tiên được mở ở đó.

Nếu bạn close tất cả các trường hợp trên một Thread nhất định, chúng ta sẽ giải phóng bộ nhớ cục bộ của thread và nó sẽ cần phải được phân bổ lại cho instance tiếp theo của thread đó.

Nếu bạn close tất cả các instance trên tất cả các thread, bạn sẽ có một cold boot tốn kém vì chúng ta cần phải phân bổ bộ nhớ và tiến hành schema validation.

Best practice ở đây là giữ cho Realm instance mở cho đến khi thread của bạn còn sống. Đối với UI thread được thực hiện dễ dàng bằng cách sử dụng pattern được mô tả ở đây: https://realm.io/docs/java/latest/#controlling-the-lifecycle-of-realm-instances

Đối với các background thread, mở instance của Realm ở đầu và đóng nó khi thoát thì sẽ là tối ưu nhất

new Thread(new Runnable() {
  public void run() {
    Realm realm = Realm.getDefaultInstance();
    doWork(realm); // work with realm
    realm.close();
  }
}).start();

// With minSdkVersion >= 19 and Java >= 7,  you can do this:
try (Realm realm = Realm.getDefaultInstance()) {
    // No need to close the Realm instance manually
}

Prevent open too much transactions

Tránh việc thực thi quá nhiều transaction cùng lúc. Lý do là memory của ứng dụng sẽ tăng lên mỗi khi new transaction được mở.

// SHOULDN'T
for
    realm.executeTransaction 

// SHOULD DO THIS
realm.executeTransaction 
    for

Bạn cũng nên nhớ nếu thao tác với realm, lỗi RejectedExecutionException sẽ văng ra nếu tồn tại tại 1 thời điểm 100 queued transactions.

Attempting to paginate a RealmResults or “limit the number of results in it” is not needed

Việc hạn chế hay phân trang RealmResults là không có ý nghĩa. RealmResults KHÔNG chứa bất kỳ phần tử nào. Nó chứa các phương tiện để đánh giá các kết quả của query. Phần tử này hiện hành từ Realm chỉ khi bạn gọi realmResults.get (i), và chỉ một phần tử duy nhất được trả về lúc đó. Nó giống như một Cursor, ngoại trừ việc nó là một Danh sách. .

In memory realm

Với inMemory configuration, bạn có thể tạo 1 Realm chạy hoàn toàn trên bộ nhớ mà không cầnpersisted trên ổ đĩa (realm file).

RealmConfiguration myConfig = new RealmConfiguration.Builder()
    .name("myrealm.realm")
    .inMemory()
    .build();

In-memory Realms có thể vẫn dùng disk space nếu bộ nhớ trở nên chậm chạp, nhưng tất cả các files được tạo bởi in-memory Realm sẽ bị delete khi Realm được đóng.

Việc tạo in-memory Realm trùng tên với persisted Realm là không được phép. Khi tất cả in-memory Realm instances với tên riêng biệt không còn references nào nữa, lúc đó toàn bộ Realm data sẽ bị hủy. Để giữ một in-memory Realm “alive” trong quá trình thực hiện của ứng dụng, hãy giữ một reference đến nó.

RealmResults are live, auto-updating

RealmResults are live, auto-updating views into the underlying data.

Nếu một thread, process hoặc một device khác thay đổi một đối tượng trong RealmResults, sự thay đổi sẽ ngay lập tức được phản ánh, bạn không cần chạy lại query hay refresh lại dữ liệu.

final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => 0

realm.executeTransaction(new Realm.Transaction() {
    @Override
    void public execute(Realm realm) {
        Dog dog = realm.createObject(Dog.class);
        dog.setName("Fido");
        dog.setAge(1);
    }
});

puppies.addChangeListener(new RealmChangeListener() {
    @Override
    public void onChange(RealmResults<Dog> results) {
      // results and puppies point are both up to date
      results.size(); // => 1
      puppies.size(); // => 1
    }
});

Trên UI thread và tất cả các Looper thread khác, tất cả RealmObjects và RealmResults được tự động làm mới khi có thay đổi đối với Realm. Điều này có nghĩa là không cần phải lấy các đối tượng này một lần nữa khi phản ứng với một RealmChangedListener. Các đối tượng đã được cập nhật và sẵn sàng để được vẽ lại trên màn hình. chúng ta có thể tái sử dụng RealmResults and RealmObjects

private RealmResults<Person> allPersons;
    private RealmChangeListener realmListener = new RealmChangeListener() {
        @Override
        public void onChange(Realm realm) {
            // Just redraw the views. `allPersons` already contain the
            // latest data.
            invalidateView();
        }
    };
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realm = Realm.getDefaultInstance();
        realm.addRealmChangeListener(listener);
        allPerson = realm.where(Person.class).findAll(); // Create the "live" query result
        setupViews(); // Initial setup of views
        invalidateView(); // Redraw views with data
    }

Vì lý do luôn được cập nhật nên sẽ có thể bạn sẽ gặp lỗi trong trường hợp sau:

RealmResults<Person> guests = realm.where(Person.class).equalTo("invited", false).findAll();
realm.beginTransaction();
for (int i = 0; guests.size(); i++) {
    guests.get(i).setInvited(true);
}
realm.commitTransaction();

Trong ví dụ trên, bạn mong đợi vòng lặp đơn giản này để mời tất cả khách. Bởi vì RealmResults được cập nhật ngay lập tức, vậy nên số guests với invited = false sẽ giảm đi, vì thế vòng lặp for bị thay đổi chỉ số i vì size thay đổi, vậy nên thực tế chỉ có một nửa khách cuối cùng được mời!

Để ngăn chặn điều này, bạn có thể dùng snapshot dữ liệu. Snapshot đảm bảo thứ tự của các phần tử sẽ không thay đổi, ngay cả khi một phần tử bị xóa hoặc sửa đổi.

realm.beginTransaction();
OrderedRealmCollectionSnapshot<Person> guestsSnapshot = guests.createSnapshot();

for (int i = 0; guestsSnapshot.size(); i++) {
    guestsSnapshot.get(i).setInvited(true);
}
realm.commitTransaction();

hoặc cũng có thể dùng iterator:

RealmResults<Person> guests = realm.where(Person.class).equalTo("invited", false).findAll();

// Use an iterator to invite all guests
realm.beginTransaction();
for (Person guest : guests) {
    guest.setInvited(true);
}
realm.commitTransaction();

The only rule to using Realm across threads is to remember that Realm, RealmObject, and RealmResults instances cannot be passed across threads

Khi bạn muốn truy cập vào cùng một dữ liệu từ một thread khác, bạn có thể tạo một Realm instance mới (Realm.getInstance (RealmConfiguration config)) và nhận các đối tượng của bạn thông qua truy vấn. Các đối tượng sẽ ánh xạ tới cùng một dữ liệu trên đĩa (luôn mới nhất), và sẽ có thể đọc được và ghi được từ bất kỳ luồng nào.

Conclusion

Bài viết dựa trên Realm document và kinh nghiệm dự án hiện tại. Có thế có nhiều thiếu sót. Hy vọng nhận được góp ý từ mọi người để bài viết được hoàn thiện.

(Maybe continued...)

Happy Coding !

0