[ANDROID] มาทำความรู้จักกับ Database ชื่อที่ว่า Realm กันซักหน่อย

          พอดีวันก่อนในกลุ่ม Thailand Android Developer มีโพสถามว่าใช้ database บน Android ตัวไหนกันบ้าง ซึ่งก็ได้มีการพูดถึง GreenDAO กับ Realm และก็มีท่านพูดถึง Realm ว่าในทีมใช้อยู่ ด้วยจุดเด่นเรื่องความ "เร็ว" หลังจากได้ค้นหาข้อมูลเพิ่มเติม พบว่าน่าสนใจดี คิดว่าเอามาเขียนแนะนำคร่าวๆ ซักหน่อยน่าจะดี
สำหรับเรื่อง GreenDAO สามารถไปอ่านเพิ่มเติมได้ที่ Martroutine.com และ Devahoy.com เขียนไว้ได้ละเอียดมากครับ
http://www.slideshare.net/ChristianMelchior/realm-building-a-mobile-database

Realm คืออะไร ? 

          Realm เป็น mobile database มีจุดเด่นคือการใช้ engine ในการจัดเก็บข้อมูลของตัวเอง ที่ออกแบบให้มีความเรียบง่าย(Simplicity) และเร็ว(Speed) หรือก็คือไม่ได้ใช้ SQLite เหมือนกับ database ตัวอื่นๆ นั่นเอง
          นอกจากนี้ Realm ยังเคลมว่าตัวเองเร็วกว่า database เจ้าอื่นๆ ด้วยนะ ไม่เชื่อลองไปดู benchmarks สิ ไม่เพียงเท่านั้น มันยัง cross platform อีกต่างหาก support ทั้ง Objective-C, SwiftReact Native และ Java(Android) เจ๋งสุดๆ ความเจ๋งของ Realm ยังมีอีกหลายข้อ ลองไปอ่านเพิ่มเติมได้ที่เว็บ Realm ได้เลย

Prerequisites

  • Android Studio เวอร์ชัน 1.5.1 ขึ้นไป
  • Android SDK เวอร์ชันล่าสุด
  • JDK เวอร์ชัน 7 ขึ้นไป
  • รองรับ Android API Level 9 (Android 2.3 Gingerbread) ขึ้นไป

การติดตั้ง

          การติดตั้ง Realm ตั้งแต่ version 0.88.0 ขึ้นไป จะต้องติดตั้งด้วย Gradle plugin โดยจะมีขั้นตอนการติดตั้ง 2 ขั้นตอน (Version ณ ที่เขียนอยู่ตอนนี้ คือ 0.86.0 0.89.0 นะครับ)

Step 1:
เพิ่ม class path ใน build.gradle ของ project
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:0.89.0"
    }
}

Step 2: 

เพิ่ม realm-android plugin ไว้บนสุดใน build.gradle ของ application
apply plugin: 'realm-android'

การใช้งาน

          ก่อนอื่นต้องออกตัวก่อนว่า ตัวผมเองก็ไม่เคยใช้งาน Realm มาก่อน เพราะฉะนั้นในบทความนี้เนื้อหาก็คงเป็นในลักษณะของการแนะนำคร่าวๆ ก่อนนะครับ ซึ่งในเบื้องต้นเรามาทำความรู้จักกับส่วนหลักของ Realm กันก่อน :)

Models

          จะอยู่ในรูปแบบเหมือนกับ JavaBeans โดยต้อง extend RealmObject และใช้ annotation ใช้การจัดการข้อมูล ดังตัวอย่าง
public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // getters & setters
}
** ข้อควรระวัง!! **
getters และ setters จะถูก override ซึ่งหากมีการเขียน logic ต่างๆ ไว้ภายใน ก็จะไม่มีการถูกเรียกใช้งานอยู่ดีนะครับ
          จากโค้ดข้างบนจะเห็นว่ามีการใช้ annotation เพื่อจัดการข้อมูล ลองมาดูกันดีกว่าว่าแต่ละ annotation ใช้ทำอะไรบ้าง
@Required
          ใช้กับ field ที่ห้ามเป็น null
@PrimaryKey
          กำหนดให้ field นั้นเป็น primary key ซึ่งจะสามารถมีได้เพียง 1 field เท่านั้น (รองรับ field types ดังต่อไปนี้ String, short, int และ long) และหากกำหนดให้เป็น primary key แล้วจะถือเป็นการกำหนดให้ field นั้น @Required ไปในตัว
@Index
          เป็นการบอกว่าให้เพิ่ม search index ให้ field นั้นๆ ซึ่งจะทำให้การ insert ช้าลงและข้อมูลมีขนาดใหญ่ขึ้น แต่ query ได้เร็วขึ้น (รองรับ field types ดังต่อไปนี้ String, byte, short, int, long, boolean และ Date)
@Ignore
          ใช้กับ field ที่ไม่ต้องการให้เก็บข้อมูลลง database
หากมองดูดีๆ จริงๆ แล้วก็เหมือนกับวิชา Database ที่เรียนมาเลยเนาะ และเรื่องสำคัญอีกอย่างที่จะต้องพูดถึง คือ

Field types

          โดย field type ที่รองรับได้แก่ boolean, byte, short, ìnt, long, float, double, String, Date และ byte[] แต่สำหรับ integer type ทั้งหลาย (byte, short, int, long) ไม่ว่าจะใช้ type ไหน Realm จะ map ไว้เป็น long ทั้งหมด
          นอกจากนี้ยังสามารถทำ model relations ได้ด้วย RealmObject และ RealmList<RealmObject>

การเขียนข้อมูล (Writes)

          การเขียนข้อมูลทำได้ทั้งแบบ synchronous และ asynchronous แต่ว่าหากเขียน synchronous จะเกิดการ blocking UI thread เพื่อรอให้เขียนเสร็จ ซึ่งในแง่ของ UX แล้วคงจะไม่ดีซักเท่าไรที่แอพจะนิ่งไปดื้อๆ เพราะฉะนั้นในส่วนการเขียนจะขอพูดถึงแต่การเขียนแบบ asynchronous นะครับ (อู้นั่นเอง :P)

ก่อนจะเขียนข้อมูล มาดูวิธีการสร้าง Object กันก่อน
การสร้าง object ทำได้ 2 วิธี คือ
1. ให้ Realm สร้างขึ้นมาโดยตรงจาก class ที่ extends RealmObject
User user = realm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
2. ใช้คำสั่ง copyToRealm() หลังจากที่ new instance object ขึ้นมา (class นั้นต้อง extends RealmObject)
User user = new User("John");
user.setEmail("john@corporation.com");

User realmUser = realm.copyToRealm(user);

การเขียนข้อมูลแบบ asynchronous
// สร้าง Realm object ขึ้นมาก่อน
Realm realm = Realm.getInstance(this);

// เรียกการใช้งานแบบ asynchronous
realm.executeTransaction(new Realm.Transaction() {
 @Override
 public void execute(Realm realm) {
  User user = realm.createObject(User.class);
  user.setName("John");
  user.setEmail("john@corporation.com");
 }
});

          การเขียนข้อมูลแบบ asynchronous จะทำงานอยู่บน background thread ซึ่งจะไม่เกิดการ blocking UI thread โดยเราจะสามารถรู้ได้ว่ามันทำงานเสร็จแล้วหรือยังได้ด้วย Realm.Transaction.Callback 
realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm bgRealm) {
                ...
            }
        }, new Realm.Transaction.Callback() {
            @Override
            public void onSuccess() {
                // เขียนข้อมูลสำเร็จ
            }

            @Override
            public void onError(Exception e) {
                // หากเกิดความผิดพลาด transaction จะถูก roll back อัตโนมัติ
            }
        });

          นอกจากนี้ asynchronous transaction สามารถอยู่ในรูปแบบ RealmAsyncTask object เพื่อใช้สำหรับ ยกเลิก transaction เมื่อออกจาก Activity/Fragment ก่อนที่ transaction จะทำงานสำเร็จ ซึ่งจะช่วยแก้ปัญหา app crash เมื่อ callback มีการอับเดต UI นั่นเอง
private RealmAsyncTask transaction;
public void saveUser() {
    transaction = realm.executeTransaction(new Realm.Transaction() { ... });
}

public void onStop() {
    if (transaction != null && !transaction.isCancelled()) {
        transaction.cancel();
    }
}

การอ่านข้อมูล (Queries)

          Realm’s query อยู่ในรูปแบบของ Fluent interface ซึ่งจะทำให้ง่ายต่อการ query หลายๆ เงื่อนไข ถ้านึกภาพไม่ออกลองมาดูโค้ดกันเลยดีกว่า
RealmResults<User> result = realm.where(User.class)
                          .equalTo("name", "John")
                          .or()
                          .equalTo("name", "Peter")
                          .findAll();
** ข้อควรระวัง!! **
เมื่อไม่พบข้อมูลใดๆ RealmResults จะไม่เป็น null แต่จะมี size() เป็น 0 แทน

เงื่อนไข

สำหรับเงื่อนไขที่รองรับในการ query ก็จะมีดังต่อไปนี้
  • between, greaterThan(), lessThan(), greaterThanOrEqualTo() และ lessThanOrEqualTo()
  • equalTo() และ notEqualTo()
  • contains(), beginsWith() และ endsWith()
โดยมีข้อจำกัด คือ เงื่อนไขทั้งหมดไม่ได้รองรับทุก type สามารถดูรายละเอียดเพิ่มเติมที่ RealmQuery

Iterations

สามารถ loop เพื่อเข้าถึงแต่ละ object ใน RealmResults ได้ด้วยความสามารถของ Iterations
for (User u : result) {
    ...
}

หรือจะใช้ for loop ปกติก็ยังได้
for (int i = 0; i < result.size(); i++) {
    User u = result.get(i);
    ...
}

การเรียงลำดับ (Sorting)

          โดย default จะเป็นการเรียงแบบ Ascending และสามารถสั่งเรียงแบบ Descending ได้โดยส่ง Sort.DESCENDING เข้าไป
RealmResults<User> result = realm.where(User.class).findAll();
result.sort("age"); // Sort ascending
result.sort("age", Sort.DESCENDING);

การลบข้อมูล (Deletion)

// remove single match
result.remove(0);
result.removeLast();

// remove a single object
Dog dog = result.get(5);
dog.removeFromRealm();

// Delete all matches
result.clear();

Asynchronous Queries

          โดยปกติการ Synchronous queries (แบบด้านบน) นั้นมีความเร็วมากพอที่จะไม่กระทบกับ UI thread แต่ในบางกรณี เช่น ข้อมูลที่ query ซับซ้อน หรือข้อมูลมีปริมาณเยอะๆ เราก็สามารถหลีกเลี่ยงการที่จะไป blocking UI thread ได้ ด้วยการใช้ Asynchronous queries แทน
RealmResults<User> result = realm.where(User.class)
                          .equalTo("name", "John")
                          .or()
                          .equalTo("name", "Peter")
                          .findAllAsync();

          ด้วยความที่เป็น Asynchronous หากอยากรู้ว่า query ทำงานสำเร็จแล้วหรือยัง ต้อง add RealmChangeListener เข้าไป ซึ่งจะเข้า method onChange() เมื่อ query สำเร็จ และทุกๆ ครั้งที่ RealmResults มีการ update

result.addChangeListener(new RealmChangeListener() {
    @Override
    public void onChange() {
        // จะเข้า method เมื่อ query สำเร็จ และทุกๆ ครั้งที่ RealmResults มีการ update
    }
});

และเพื่อหลีกเลี่ยงเรื่อง memory leaks ควรทำการ unregister listeners ทุกครั้งที่ออกจาก Activity/Fragment

public void onStop () {
    result.removeChangeListener(callback); // remove a particular listener
    // or 
    result.removeChangeListeners(); // remove all registered listeners
}

นอกจากนี้ ยังสามารถเช็คว่า query ทำงานสำเร็จแล้วหรือยัง ด้วยการใช้ method isLoaded() ได้อีกด้วย

if (result.isLoaded()) {
  // ข้อมูลพร้อมใช้งานแล้ว
}

Best Practice for Android

          เนื่องจาก RealmObjects และ RealmResults ถูกออกแบบให้สามารถเข้าถึงได้ตลอดเวลา และเพื่อหลีกหลีก overhead ที่จะเกิดขึ้นในตอน เปิด/ปิด connenction จึงแนะนำสร้าง Realm instance ผ่านการเรียก Realm.getDefaultInstance() ลองมาดูโค้ดกันซักเล็กน้อย

Application

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // setup RealmConfiguration
        RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this).build();
        Realm.setDefaultConfiguration(realmConfiguration);
    }
}

Activity

public class MyActivity extends Activity {
    private Realm realm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realm = Realm.getDefaultInstance();
    }    

    @Override
    protected void onDestroy() {
        super.onDestroy();
        realm.close(); 
    }
}

Fragment

ใช้ onStart()/onStop() สำหรับ Fragments เนื่องจาก onDestroy() อาจจะไม่ถูก call
public class MyFragment extends Fragment {
    private Realm realm;

    @Override
    public void onStart() {
        super.onStart();
        realm = Realm.getDefaultInstance();
    }
    
    @Override
    public void onStop() {
        super.onStop();
        realm.close();
    }
}

Reuse RealmResults and RealmObjects

          เมื่อมีการเปลี่ยนแปลงข้อมูล RealmObjects และ RealmResults จะถูก refresh อัตโนมัติ เพราะฉะนั้นหากต้องการให้ UI เปลี่ยนตามข้อมูลที่เปลี่ยนไป ก็สามารถเปลี่ยน UI ได้โดยผ่าน RealmChangedListener นั่นเอง
public class MyActivity extends Activity {
    
    private Realm realm;
    private RealmResults allPersons;
    private RealmChangeListener realmListener = new RealmChangeListener() {
        @Override
        public void onChange() {
            // 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
    }    

    // ...
}

          ถ้าหากเปรียบบทความนี้กับเรื่องความรักของหนุ่มสาวแล้ว บทความนี้ก็เป็นเพียงแค่แม่สื่อ ที่จะทำให้หนุ่มสาวได้รู้จักกัน ซึ่งหลังจากนี้จะสานสัมพันธ์กันต่อหรือไม่ ก็เป็นเรื่องของคน 2 คนแล้วหละครับ :3
          นอกจากเรื่องที่ได้เล่าไปในบทความ ยังมีเรื่องที่น่าสนใจอีก ไม่ว่าจะเป็น Relationships Migrations และอื่นๆ อีกมากมาย แต่ด้วยพลังวัฒรที่ยังไม่กล้าแกร่งพอ จึงขอจบบทความไว้ด้วยเนื้อหาเพียงเท่านี้ก่อน
          หากสนใจขอแนะนำให้ไปอ่าน document ในเว็บ https://realm.io ได้เลยนะครับ เขียนไว้ดีมากๆ (แนะนำควรอ่านอย่างยิ่ง ถ้าไปอ่านแล้วก็รบกวนเขียนต่อด้วยนะ ถ้าไม่เขียนต่อภายใน 3 วัน 7 วัน ท่านจะต้อง .... น่าจะผิดเรื่องแล้วหละ) อีกส่วนหนึ่งที่น่าประทับใจของ library ตัวนี้ คือมี example ให้ดูเพียบเลย
       
สุดท้ายนี้หากมีข้อผิดพลาดประการใดก็ขออภัยไว้ ณ ที่นี้ด้วยครับ
ปล. ท่านไหนเคยใช้งานจริงๆ ติดปัญหา หรือ มีคำแนะนำอะไร ฝากไว้ในช่อง comment ได้เลยนะคร้าบ

ลิงค์อ้างอิง

Official Realm website : https://realm.io/
Realm repository : https://github.com/realm/realm-java
Slideshare(Christian Melchior) : Realm: Building a mobile database

Teeranai P

Developer ตัวน้อยๆ ที่หลงใหลในโลกของการพัฒนา Software. รักการเขียนโปรแกรมเป็นอันดับ 2 รองลงมาจากการนอน