Một vài kinh nghiệm khi dùng Realm trong android
Bài đăng này đã không được cập nhật trong 6 năm
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ỗiRejectedExecutionException
sẽ văng ra nếu tồn tại tại 1 thời điểm100 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 thatRealm
,RealmObject
, andRealmResults
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 !
All rights reserved