Automatic Seeding Android with Realm
Dalam postingan kali ini, saya ingin sharing bagaimana cara melakukan seeding data pada aplikasi Android yang menggunakan Realm sebagai database.
Beberapa Pendekatan Seeding dengan Realm
Terdapat beberapa pendekatan seeding yang dapat dilakukan dengan Realm. Tentunya, masing-masing pendekatan terdapat kelebihan dan kekurangannya masing-masing.
Prepopulate Realm.db
Seperti halnya SQLite, Realm sebenarnya memiliki sebuah file di dalam aplikasi yang mengandung informasi sepenuhnya terkait data-data yang tersimpan. Terdapat tools yang dapat mengkonversikan data yang tersimpan dalam berbagai format (csv, json, SQLite) menjadi file database Realm yang secara default bernama Realm.db.
Advantages
Melalui pendekatan ini, jika kita sudah memiliki prepopulated database dalam berbagai format (csv, json, dll.). Kita dapat melakukan migrasi menjadi Realm dengan menggunakan salah satu tools yang dapat ditemui di internet.
Drawbacks
Untuk menggunakan file Realm.db yang sudah dipopulate terlebih dahulu, dibutuhkan code tambahan yang melakukan migrasi file Realm.db yang kita buat (biasanya diletakkan di raw resource folder).
Permasalahan lainnya adalah, tools yang dapat mengkonversikan database menjadi Realm tidak bersifat otomatis, sehingga setiap kali ingin mengganti Realm.db perlu melakukan langkah-langkah repetitif secara manual.
Melakukan Seeding saat Aplikasi dijalankan
Berbeda dengan menggunakan prepopulated database, seeding dilakukan setiap kali aplikasi dijalankan. Dengan cara ini, seeding sepenuhnya dilakukan oleh code sehingga berjalan secara otomatis.
Lalu, bagaimana menjamin seeding hanya dilakukan satu kali saja (saat aplikasi pertama di-instal)?
Untuk mencegah melakukan seeding berulang kali setiap kali aplikasi dijalankan, kita dapat menggunakan sebuah Object Realm yang menandakan apakah suatu proses seeding sudah pernah dijalankan. Jika sudah pernah, maka kita dapat mencegah seeding terjadi lagi.
Bagaimana membedakan seeding pada variant aplikasi yang berbeda (dev, staging, production)?
Untuk membedakan seeding yang perlu dilakukan, kita dapat memanfaatkan BuildConfig pada build.gradle dan mengkonfigurasikannya untuk setiap variant app. Nilai setting BuildConfig dapat kita akses melalui program kita untuk membedakan proses seeding yang dilakukan.
Advantages
Seluruh proses seeding didefinisikan melalui code sehingga dapat diotomasi dan mudah untuk dikembangkan (extensible). Selain itu, konfigurasi build variant yang berbeda dapat dilakukan dalam code juga.
Drawbacks
Proses seeding memakan waktu saat menggunakan aplikasi pertama kali (cenderung kecil). Berbeda dengan pendekatan pertama yang hanya memindahkan prepopulated Realm.db pada app sehingga tidak memakan waktu yang lama.
Implementasi Kami (Pendekatan kedua)
Kami menggunakan pendekatan kedua dalam melakukan automatic seeding. Untuk itu, kami membuat Base Class Seeder sebagai dasar implementasi pendekatan kedua, dan membuat berbagai macam subclass seeder lainnya sebagai implementasi berbagai variasi seeder yang spesifik.
Seeder Class
public class Seeder {
private String id;
private String description;
private List<Seeder> dependencies;
public Seeder(String id, String description) {
this.id = id;
this.description = description;
this.dependencies = new ArrayList<Seeder>();
}
public Seeder(String id, String description, List<Seeder> dependencies) {
this(id, description);
this.dependencies = dependencies;
}
public String getId() {
return this.id;
}
public String getDescription() {
return this.description;
}
public List<Seeder> getDependencies() {
return this.dependencies;
}
public void seed(RealmDataManager realmDataManager) {
if (this.isAlreadyExecuted(realmDataManager))
return;
this.seedDependencies(realmDataManager);
this.handleSeed(realmDataManager);
this.commitTransaction(realmDataManager);
}
public void handleSeed(RealmDataManager realmDataManager) {
return;
}
public boolean isAlreadyExecuted(RealmDataManager realmDataManager) {
SeederTransaction transaction = realmDataManager.getSeederTransactionById(this.id);
if (transaction != null)
return true;
else
return false;
}
public void commitTransaction(RealmDataManager realmDataManager) {
SeederTransaction transaction = new SeederTransaction(this.id, this.description);
realmDataManager.createSeederTransaction(transaction);
}
public void seedDependencies(RealmDataManager realmDataManager) {
for (Seeder dependency: this.getDependencies()) {
dependency.seed(realmDataManager);
}
}
}
Note: RealmDataManager merupakan wrapper class untuk melakukan CRUD pada model Realm. Implementasi dapat dibebaskan pada masing-masing aplikasi.
Class tersebut memodelkan sebuah seeder sebagai keseluruhan proses (method seed) yang memiliki dependensi terhadap seeder lainnya (diatasi pada method seedDependencies), kemudian melakukan proses seeding itu sendiri (pada method handleSeed yang dapat diimplementasikan pada subclass), dan melakukan commit untuk menyatakan bahwa proses seeding sudah berhasil dilakukan (method commitTransaction). Selain itu, jika seeding sudah pernah dilakukan sekali (sudah pernah di-commit), maka proses tidak akan dilakukan untuk kedepannya (dalam pengecekan method isAlreadyExecuted).
Sample Subclass
public class DummyProductSeeder extends Seeder {
private static final String ID = "DummyProductSeeder";
private static final String DESCRIPTION = "Seed 10 dummy product data";
private static final List<Seeder> DEPENDENCIES =
Collections.unmodifiableList(Arrays.asList(
new StockUnitSeeder() // Dependensi seeder lainnya
));
public DummyProductSeeder() {
super(ID, DESCRIPTION, DEPENDENCIES);
}
@Override // Melakukan implementasi seeding yang spesifik
public void handleSeed(RealmDataManager realmDataManager) {
List<StockUnit> stockUnitList = realmDataManager.getAllStockUnits();
for (int i = 0; i < 10; ++i) {
Product product = this.generateDummyProduct();
realmDataManager.syncCreateOrUpdateProduct(product);
}
}
}
Dengan inheritance, setiap subclass tidak perlu mendefinisikan lagi proses generic yang perlu dilakukan setiap seeder. Dalam contoh ini, DummyProductSeeder akan melakukan seeding data Product dengan terlebih dahulu mengecek apakah seluruh seeder dependensinya sudah terpenuhi (jika belum maka dieksekusi terlebih dahulu).
Sample Execution
private void seedData() {
Realm realm = Realm.getDefaultInstance();
RealmDataManager realmDataManager = new RealmDataManager(realm);
Seeder seeder = new MandatorySeeder();
seeder.seed(realmDataManager);
if (BuildConfig.DUMMY_SEEDER_ENABLED == true) {
seeder = new DummyProductSeeder();
seeder.seed(realmDataManager);
}
realmDataManager.closeRealm();
}
Method ini dapat dipanggil ketika pertama kali aplikasi dijalankan dan akan secara otomatis melakukan seeding sesuai dengan setting konfigurasi BuildConfig yang sesuai dengan variant aplikasinya.
Conclusion
Dengan pendekatan seeding saat aplikasi dijalankan, pengelolaan seeders yang kami miliki menjadi lebih mudah dan extensible. Selain itu, berbeda dengan pendekatan pertama yang telah disampaikan, seluruh proses seeding dapat berjalan secara otomatis.