รู้จัก Firebase Realtime Database ตั้งแต่ Zero จนเป็น Hero

Firebase Realtime Database เป็น NoSQL cloud database ที่เก็บข้อมูลในรูปแบบของ JSON และมีการ sync ข้อมูลแบบ realtime กับทุก devices ที่เชื่อมต่อแบบอัตโนมัติในเสี้ยววินาที รองรับการทำงานเมื่อ offline(ข้อมูลจะถูกเก็บไว้ใน local จนกระทั่งกลับมา online ก็จะทำการ sync ข้อมูลให้อัตโนมัติ) รวมถึงมี Security Rules ให้เราสามารถออกแบบเงื่อนไขการเข้าถึงข้อมูลทั้งการ read และ write ได้ดังใจ ทั้ง Android, iOS และ Web

วิดีโอแนะนำการทำงานของ Firebase Realtime Database

ในการพัฒนา Firebase Realtime Database ขอแยกออกเป็น 5 parts ดังนี้

  1. การ Set up Firebase และ Realtime Database SDK
  2. การเขียนข้อมูล
  3. การอ่านข้อมูล
  4. การเปิดใช้งานโหมด Offline
  5. Security & Rules

เมื่อพร้อมแล้ว…ก็เปิด Android Studio ขึ้นมา โดยจะสร้างโปรเจคใหม่ หรือจะใช้โปรเจคเดิมก็ได้

Part 1 การ Set up Firebase และ Realtime Database SDK

ถ้าสร้างโปรเจคใหม่ ให้ไปดูการ Set up Firebase ที่บทความนี้ก่อน

เมื่อ Set up Firebase เรียบร้อยแล้ว ก็ให้เพิ่ม Realtime Database SDK ใน build.gradle ของ app-level แล้วกด Sync ก็เป็นอันจบส่วนที่ 1 ละ

dependencies {
     compile 'com.google.firebase:firebase-database:10.0.1'
}

การเข้าถึงข้อมูลสำหรับ Firebase Realtime Database ทั้ง read และ write โดยปกติ เราจะต้องทำการ Authentication ผ่าน Firebase Authentication ซะก่อน แต่เพื่อให้เราสามารถเข้าใจบทความนี้ได้โดยไม่ต้องอ่าน Firebase Authentication เราจะมาทำให้มันเข้าถึงได้แบบ public กัน โดยให้เข้าไปที่ Firebase Console เข้าไปที่โปรเจค จากนั้นเลือกเมนู Database แล้วเลือก tab ที่ชื่อว่า RULES จะพบหน้าตาของประมาณนี้

Default rules in Firebase Realtime Database

ด้านขวามือจะมี simulator ให้ลองทดสอบ rules ที่เราสร้างขึ้น ทั้งแบบ public หรือแบบ authentication แล้วก็ดี ดังนั้น ลองกด RUN แบบ default rules ก่อนเลย ผลปรากฎว่าถ้าไม่ได้ authentication ก็จะไม่สามารถเข้าถึงข้อมูลได้นั่นเอง ดังรูป

Run default rules ด้วย simulator

เอาหละ ต่อไปเรามาปรับให้มันเป็น public กันดีกว่า โดยให้เปลี่ยน rules ตามนี้

Run public rules ด้วย simulator

เมื่อกด RUN ดูก็พบว่า ไม่ต้อง Authentication ก็สามารถเข้าถึงข้อมูลได้ละ เมื่อเสร็จแล้วก็กดปุ่ม PUBLISH ด้วย เป็นอันจบ Part 1


Part 2 การเขียนข้อมูล (Write)

เริ่มด้วยการประกาศตัวแปร DatabaseReference รับค่า Instance และอ้างถึง path ที่เราต้องการใน database

DatabaseReference mRootRef = FirebaseDatabase.getInstance().getReference();

จากนั้นก็อ้างอิงไปที่ path ที่เราต้องการจะจัดการข้อมูล ตัวอย่างคือ users และ messages

DatabaseReference mUsersRef = mRootRef.child("users");
DatabaseReference mMessagesRef = mRootRef.child("messages");

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

การ write, update หรือ delete ข้อมูลใน Firebase Realtime Database จะรองรับค่าหลายประเภททั้ง String, Long, Double, Boolean, Map<String, Object> และ List<Object> โดยการ write จะมีด้วยกัน 4 รูปแบบดังนี้

1. setValue() เป็นการ write หรือ update ข้อมูล ไปยัง path ที่เราอ้างถึงได้ เช่น users/<user-id>/<username>

mUsersRef.child("id-12345").setValue("Jirawatee");

เราสามารถดูผลลัพธ์แบบ realtime ได้ที่ Firebase Console โดยไปที่เมนู Database แล้วเลือก tab แรก คือ Data เราก็จะเห็นข้อมูลทั้งหมดแบบทุกลมหายใจละ

ตัวอย่างการ setValue()

2. push() เป็นการเพิ่มชุดของข้อมูล ในที่นี้ผมจะสร้าง model object ชื่อ FriendlyMessage ซึ่งจะบรรจุ text และ username ไว้ โดยการ push นั้น Firebase จะสร้าง unique key ของชุดข้อมูลนั้นๆ เพื่อใช้อ้างอิงต่อไปได้ เช่น messages/<message-id>/<data-model>

FriendlyMessage friendlyMessage = new FriendlyMessage("Hello World!", "Jirawatee");
mMessageRef.push().setValue(friendlyMessage);
ตัวอย่างการ push()

3. updateChildren() เป็นการ write หรือ update ข้อมูลบางส่วน(บาง key) ตาม path ที่เราอ้างถึง โดยไม่ต้องทำการ replace ข้อมูลทั้งชุด และสามารถทำพร้อมๆกันได้หลาย object

ตัวอย่างจะเป็นการสร้าง post ใหม่ขึ้นมา โดยจะ write ข้อมูลไป 2 ที่คือ
/user-messages/Jirawatee/$postid และ /messages/$postid

// push เป็นการ generate $postid ของ object ชื่อ posts ออกมาก่อนเพื่อใช้ใน // /user-posts/$userid/$postid 
String key = mMessagesRef.push().getKey();
HashMap<String, Object> postValues = new HashMap<>();
postValues.put("username", "Jirawatee");
postValues.put("text", "Hello World!");

Map<String, Object> childUpdates = new HashMap<>();
childUpdates.put("/messages/" + key, postValues);
childUpdates.put("/user-messages/Jirawatee/" + key, postValues);

rootRef.updateChildren(childUpdates);

สีที่แสดงใน console จะบอกสถานะแตกต่างกันดังนี้
สีเหลือง: แสดงการอัพเดท
สีเขียว: แสดงการเพิ่ม
สีแดง: แสดงการลบ
สีน้ำเงิน: แสดงการเคลื่อนย้าย

write ข้อมูลเข้าไปทั้ง 2 objects

ในกรณีที่ต้องการอัพเดทข้อมูลบางส่วน ก็สามารถทำได้พร้อมๆกันได้ โดยจะต้องรู้ username และ message-id เป็นตัวระบุในแต่ละ object

update ข้อมูลบางส่วนทั้ง 2 objects

4. runTransaction() เป็นการอัพเดทข้อมูล ที่มี concurrent เยอะๆ ที่อาจเกิดชนกัน เกิดข้อผิดพลาดได้ ตัวอย่างเช่น การกด like และกด unlike ที่โพสเดียวกัน เวลาเดียวกัน จะต้องมีการนับยอด like ตลอดเวลา ว่าช่วงเวลานั้นเป็นเท่าไร

postRef.runTransaction(new Transaction.Handler() {
     @Override
     public Transaction.Result doTransaction(MutableData mutable) {
        Post p = mutable.getValue(Post.class);
        if (p == null) {
           return Transaction.success(mutable);
        }

        if (p.stars.containsKey(getUid())) {
           // Unlike the post and remove self from likes
           p.starCount = p.starCount - 1;
           p.stars.remove(getUid());
        } else {
           // Like the post and add self to likes
           p.starCount = p.starCount + 1;
           p.stars.put(getUid(), true);
        }

        // Set value and report transaction success
        mutable.setValue(p);
        return Transaction.success(mutable);
      }

      @Override
      public void onComplete(DatabaseError databaseError, boolean b, DataSnapshot dataSnapshot) {
          if (databaseError != null) {
             databaseError.getMessage()
             Log.w(TAG, databaseError.getMessage());
          } else {
             Log.d(TAG, "Transaction successful");
          }
      }
});

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

การลบข้อมูลนั้น ให้เราระบุ path ที่เราต้องการจะลบ จากนั้นก็เรียกคำสั่ง removeValue() ตัวอย่างเช่น ต้องการลบข้อความทั้งหมดใน object ชื่อ messages

mMessageRef.removeValue();

นอกจากนั้นเรายังสามารถลบข้อมูล ได้ด้วยการส่งค่า null ไปที่ setValue(null) และสามารถใช้ค่า null กับเทคนิค updateChildren() เพื่อลบข้อมูลหลายๆ object ได้ด้วย

// ลบแบบ setValue()
mMessageRef.setValue(null);
// ลบแบบ updateChildren();
childUpdates.put("/messages/", null);

Part 3 การอ่านข้อมูล (Read)

เริ่มด้วยการประกาศตัวแปร DatabaseReference รับค่า Instance และอ้างถึง path ที่เราต้องการใน database

DatabaseReference mRootRef = FirebaseDatabase.getInstance().getReference();

การอ่านข้อมูลใน Firebase Realtime Database จะมี 2 ประเภทแยกตาม Listener ดังนี้

1. ValueEventListener

จะอ่านข้อมูลตั้งแต่เริ่ม และ จะอ่านข้อมูลทุกครั้งที่มีการเปลี่ยนแปลงของข้อมูลทั้งหมดภายใต้ path ที่เราอ้างถึง วิธีการคือใช้ object ที่เราอ้างถึงมา addValueEventListener โดยจะมี callback 2 แบบ

  • onDataChange จะถูกเรียกตอนเริ่ม และถูกเรียกทุกครั้งที่ข้อมูลภายใต้ path ที่เราอ้างถึงมีการเปลี่ยนแปลง
  • onCancelled จะถูกเรียกเมื่อไม่สามารถอ่านข้อมูลจาก database ได้
mRootRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        String value = dataSnapshot.getValue(String.class);
        mTextView.setText(value);
    }

    @Override
    public void onCancelled(DatabaseError error) {
        mTextView.setText("Failed: " + databaseError.getMessage());
    }
});

ข้อควรระวังของ ValueEventListener คือไม่ควรอ้างถึง root เนื่องจากมันจะคอยดูข้อมูลทั้งหมดของ database ซึ่งมันอาจมีขนาดใหญ่ และ เปลือง bandwidth โดยใช่เหตุ เราจึงควรใช้วิธีนี้แบบเจาะจงเฉพาะ หรืออ่านข้อมูลทั้งหมดครั้งแรก แล้ว removeEventListener() ออกไป

2. ChildEventListener

จะคอยรับข้อมูลจาก การเพิ่ม, การเปลี่ยนแปลง, การลบ และ การย้าย เฉพาะของ child ที่เราอ้างถึง วิธีการคือใช้ object ที่เราอ้างถึงมา addChildEventListener โดยจะมี callback 5 แบบ

  • onChildAdded() จะถูกเรียกเมื่อมีการเพิ่มชุดข้อมูลเข้ามาใน child
  • onChildChanged() จะถูกเรียกเมื่อข้อมูลใน child มีการเปลี่ยนแปลง
  • onChildRemoved() จะถูกเรียกเมื่อข้อมูลใน child ถูกลบ
  • onChildMoved() จะถูกเรียกเมื่อมีการเรียงลำดับของข้อมูลใน child เกิดขึ้น
  • onCancelled() จะถูกเรียกเมื่อโหลดข้อมูลจาก child ไม่สำเร็จ

ตัวอย่างการ addChildEventListener ไปกับการ comment ซึ่งเหมาะมากกับ RecyclerView

ChildEventListener childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
       // A new comment has been added, add it to the displayed list
       Comment comment = dataSnapshot.getValue(Comment.class);
       // ...
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
        // A comment has changed, use the key
        // to determine if we are displaying this
        // comment and if so displayed the changed comment.
        Comment newComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();
        // ...
    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
        // A comment has changed, use the key
        // to determine if we are displaying this
        // comment and if so remove it.
        String commentKey = dataSnapshot.getKey();
        // ...
    }

    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
        // A comment has changed position,
        // use the key to determine if we are
        // displaying this comment and if so move it.
        Comment movedComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();
        // ...
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Toast.makeText(mContext, "Failed to load comments.",
                Toast.LENGTH_SHORT).show();
    }
};
ref.addChildEventListener(childEventListener);

การเพิ่ม listener เข้าไปหลายตัว นั่นก็แปลว่าจะต้องมีการ call เกิดขึ้นมากมายตามแต่ event ซึ่ง หากเราไม่ได้ใช้ หรือออกจากหน้าดังกล่าว ก็ควรจะถอด listener เหล่านั้นออกไปด้วย เพื่อการใช้ bandwidth แบบคุ้มค่ามากที่สุดโดยสามารถถอดออกได้ด้วยคำสั่ง removeEventListener()

@Override
protected void onStop() {
   super.onStop();
   if (mValueEventListener != null) {
      mRootRef.removeEventListener(mValueEventListener);
   }
}

การอ่านข้อมูลเพียงครั้งเดียว

บางครั้งเราอาจต้องการอ่านข้อมูลแค่ครั้งเดียวและก็ไม่สนใจมันอีก Firebase ได้เตรียม addListenerForSingleValueEvent() ที่เมื่อได้ callback แล้วก็จะ remove listener ทิ้งอัตโนมัติ ตัวอย่างเช่น ดึงข้อมูลผู้ใช้ก่อนทำการโพส ว่าเขาผ่านการ sign-in มาหรือยัง หรือยังขาดข้อมูลอะไรที่ต้องการ หากครบถ้วนก็โพสได้ แต่หากไม่ครบอาจพาไปหน้า sign-in หรือพาไปหน้ากรอกข้อมูลให้ครบถ้วน

mDatabase.child("users").child(userId).addListenerForSingleValueEvent(new ValueEventListener() {
   @Override
   public void onDataChange(DataSnapshot dataSnapshot) {
      User user = dataSnapshot.getValue(User.class);
      if (user == null) {
         Toast.makeText(NewPostActivity.this, "Error: could not fetch user.", Toast.LENGTH_LONG).show();
      } else {
         writeNewPost(userId, user.username, title, body);
      }
      finish();
   }

   @Override
   public void onCancelled(DatabaseError databaseError) {
      Log.e(TAG, databaseError.getMessage());
   }
});

การเรียงลำดับและการกรองข้อมูล (Sorting and Filtering)

การ query ข้อมูล ของ Firebase Realtime Database นั้น รองรับการ sort และ filter ได้ อย่างก็ดีการ sort และ filter สามารถทำให้การ query นั้นช้าได้ ทางที่ดีควรศึกษาเรื่องการทำ index (.indexOn) ไปด้วยจะทำให้การ query นั้นมีประสิทธิภาพมากขึ้น

การ Sort ข้อมูล มี 3 รูปแบบ ดังนี้ (Ordering Function)

  • orderByChild() เป็นการเรียงลำดับ value ของ child key ที่ถูกเลือก (การใช้งานคล้าย WHERE ใน SQL)
  • orderByKey() เป็นการเรียงลำดับ child key (ใช้เรียง PK เหมาะกับแสดงข้อมูลแบบมี limit หรือแสดง pagination)
  • orderByValue() เป็นการเรียงลำดับ child value (เหมาะกับการเรียงค่าตัวเลข)
// ตัวอย่างการเรียงลำดับโพสของฉันที่ได้รับคะแนนโหวตมากที่สุด
dbReference.child("user-posts").child(getUid()).orderByChild("starCount");

การ Filter ข้อมูล มี 5 รูปแบบ ดังนี้ (Querying Function)

  • limitToFirst() การระบุจำนวน item ซึ่งจะเรียงลำดับจากแถวแรก
  • limitToLast() การระบุจำนวน item ซึ่งจะเรียงลำดับจากแถวสุดท้าย
  • startAt() จะดึงจำนวน item ที่มากกว่า หรือ เท่ากับ ที่ระบุ key หรือ value โดยขึ้นอยู่กับการ order-by
  • endAt() จะดึงจำนวน item ที่น้อยกว่า หรือ เท่ากับ ที่ระบุใน key หรือ value โดยขึ้นอยู่กับการ order-by
  • equalTo() จะดึงจำนวน item ที่เท่ากับที่ระบุใน key หรือ value โดยขึ้นอยู่กับการ order-by
// ตัวอย่างการดึงข้อมูลโพส 100 อันล่าสุด
databaseReference.child("posts").limitToFirst(100);

Part 4 การเปิดใช้งานโหมด offline

ลักษณะการทำงาน offline (Persistence Behavior)

Firebase Realtime Database มีการจัดการเรื่องการเชื่อมต่อให้แล้ว เมื่อไม่สามารถเชื่อมต่ออินเตอร์เน็ต หรืออยู่ในโหมด offline เรายังสามารถใช้งานแอพได้ โดยตัว Firebase จะทำการ cache ข้อมูลไว้ทุกการกระทำ และจะทำการ sync ข้อมูลให้อัตโนมัติเมื่อเรากลับเข้าสู่โหมด online แม้ว่าเราจะปิดแล้วเปิดแอพใหม่ก็ตาม มันยอดมากเลย แค่ประกาศใช้งานง่ายๆเพียงบรรทัดเดียว

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

*******************************************************************
ปัญหาที่หลายคนประกาศ 
.setPersistenceEnabled แล้วเมื่อออกและเข้ามาใหม่ปรากฏว่า crash โดยแสดง error ประมาณนี้

com.google.firebase.database.DatabaseException: Calls to setPersistenceEnabled() must be made before any other usage of FirebaseDatabase instance

ผู้เขียนแนะนำให้แก้ปัญหานี้โดย
1. สร้าง class ที่ extends Application แล้วไปประกาศ
setPersistenceEnabled(true)ที่นั่น

class MyApp extends android.app.Application 

@Override
public void onCreate() {
    super.onCreate();
    FirebaseDatabase.getInstance().setPersistenceEnabled(true);
}

2. ใน AndroidManifest ให้ไปประกาศ class ในข้อ 1 ใน <application> ซะ

android:name="com.example.MyApp"

ผู้เขียนทดสอบแล้ว ประกาศที่เดียว จบ
*******************************************************************

แอบ sync ข้อมูลแบบลับๆ (Keeping Data Fresh)

Firebase Realtime Database จะ sync และเก็บข้อมูลใน local ของ client เฉพาะข้อมูลที่ตัว listener ทำงานอยู่ แต่เราสามารถจะ sync ข้อมูลส่วนอื่นที่ยังไม่ได้ active ได้ โดยให้อ้างถึง path ที่ต้องการ sync ตัวอย่างเช่น

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

และโดยทั่วไปเราจะเก็บข้อมูลลง cache ได้ไม่เกิน 10MB แต่หากแอพมีข้อมูลที่ต้องการ cache มากกว่านั้น Firebase Realtime Database จะ purge cache ออก แต่ข้อมูลที่เก็บแบบ keepSynced จะไม่ถูก purge นะเออ

การ Query ข้อมูลในขณะที่ Offline

เมื่อแอพ sync ข้อมูลตอน online มาไว้ในเครื่องแล้ว และเมื่อแอพเข้าสู่โหมด offline เราก็ยังสามารถ query ข้อมูลที่ sync มาแล้วได้ ตัวอย่างเช่น

// ตอน online ได้ผลลัพธ์ 4 แถว จากการ query
scoresRef.orderByValue().limitToLast(4)
// เมื่อ offline หากต้องการ query ผลลัพธ์ 2 แถว ก็ทำได้
scoresRef.orderByValue().limitToLast(2)

การ run ตัว Transactions ขณะ Offline

จริงๆแล้วก็เหมือนกับกรณี offline ปกติ คือเมื่อ run ตัว transaction ในขณะ offline ข้อมูลต่างๆก็จะถูกเข้าคิวไว้ แต่หากมีการปิดแล้วเปิดแอพใหม่คิวจะไม่ถูกเก็บไว้ โดยเราควรแจ้งผู้ใช้ให้ทราบว่า transaction ไม่สำเร็จในกรณีที่ปิดแล้วเปิดแอพอีกครั้ง


Part 5 Security & Rules

Firebase Realtime Database Rules เป็นการกำหนดการเข้าถึง database ทั้งการ read, write และการทำ index โดย rules ทั้งหมดจะอยู่บน Firebase server แล้วเราสามารถปรับเปลี่ยนและมีผลทันทีได้ตลอดเวลา ซึ่งมี syntax เป็น Javascript-like โดยจะแบ่ง rules ออกเป็น 4 ประเภทดังนี้

  1. .read คือ การอนุญาตให้อ่าน
  2. .write คือ การอนุญาตให้เขียน
  3. .validate คือ การตรวจสอบข้อมูลที่เข้ามา
  4. .indexOn คือ การทำ index เพื่อความรวดเร็วในการเรียงลำดับ และการ query

โดยการกำหนด rules ต่างๆให้เข้าไปที่ Firebase Console เลือกโปรเจค จากนั้นเลือกเมนู Database แล้วเลือก tab ชื่อ RULES

สำหรับรายละเอียด Security & Rules นั้นมีมากมาย เช่น การกำหนด rules สำหรับการ read และ write ของแต่ละ path ที่ยึดตามโครงสร้างของ database, การกำหนด rules สำหรับการ validate การเพิ่มข้อมูล หรือ อัพเดทข้อมูล และการให้เข้าถึงข้อมูลตัวเองของผู้ใช้คนนั้นๆ เป็นต้น แนะนำให้ไปศึกษาเพิ่มเติมตามลิงค์นี้ครับ https://firebase.google.com/docs/database/security/ รับรองว่ามันไม่ยาก แค่มันไม่ง่ายแค่นั้นเอง


เคล็ดไม่ลับ

รับ Callback หลังจาก setValue() หรือ updateChildren()

เราสามารถ add completion callback ได้ทั้ง setValue() และ updateChildren() ได้ เผื่อใครอยากจะ handle กรณีที่ไม่สำเร็จ ดังตัวอย่างด้านล่าง

mUsersRef.child("id-12345").setValue("Jirawatee", new DatabaseReference.CompletionListener() {
   @Override
   public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
      if (databaseError != null) {
         Log.w(TAG, "setValue() failure");
      } else {
         Log.d(TAG, "setValue() successful");
      }
   }
});

การ Minify

หากทำการ minify กับโปรเจค ที่มีการใช้งาน model object กับฟังก์ชัน DataSnapshot.getValue(Class) หรือ DatabaseReference.setValue(Object) เราจำเป็นต้องเพิ่ม code ด้านล่างนี้ในไฟล์ proguard-rules.pro ด้วยนะ

# Add this global rule
-keepattributes Signature

# This rule will properly ProGuard all the model classes in
# the package com.yourcompany.models. Modify to fit the structure
# of your app.
-keepclassmembers class com.yourcompany.models.** {
  *;
}

Best practice ในการออกแบบโครงสร้างของข้อมูล

เราควรหลีกเลี่ยงการซ้อนกันของ data เพราะ Firebase Realtime Database อนุญาตให้เราออกแบบ data ซ้อนกันได้ไม่เกิน 32 ชั้น เพราะหากเรา fetch ข้อมูลจาก database ที่มีข้อมูลซ้อนกันเยอะ เราอาจต้อง fetch ข้อมูลจำนวนมาก ทำให้เปลือง bandwidth ได้ ควรออกแบบโครงสร้างแบบ flat เพื่อขนาด data ที่เล็กและเร็วในการ fetch ซึ่งประสิทธิภาพที่สูงกว่านั้น เราจะต้องแลกด้วยการไม่ทำ normalize ข้อมูล หรือเรียกว่า denormalize นั่นเอง ขอแค่อย่าลืมว่า เมื่อมีการอัพเดทหรือลบข้อมูลก็ต้องทำให้ครบทุกที่ที่เราเพิ่มข้อมูลเหล่านั้นเข้าไปด้วย

จากตัวอย่างเรามีการ add key มากกว่า 1 ที่ โดยสามารถแยกการ query ออกเป็น 3 มุมมองดังนี้

  1. Query ข้อมูลของ user ทั้งหมดจาก users
  2. Query ข้อมูลของ content ทั้งหมดจาก posts
  3. Query ข้อมูล content ของ user แต่ละคนจาก user-posts

ทั้ง 3 แบบเราสามารถ query ข้อมูลได้อย่างมีอย่างรวดเร็ว และยังสามารถทำ offset และ limit ของ data ได้ด้วย

ประเมินค่าใช้จ่าย

Firebase Realtime Database นั้นเป็นบริการที่ฟรีแบบจำกัด โดยจะมีเรื่องให้พิจารณาด้วยกัน 4 เรื่องคือ

  1. จำนวนการใช้งานพร้อมๆกัน (Connection)
  2. พื้นที่ที่เก็บข้อมูล (Storage)
  3. ขนาดของการส่งรับข้อมูล (Bandwidth)
  4. การสำรองข้อมูล (Backup)

โดยสถิติการใช้งานทั้งหมดเราสามารถเช็คได้ที่ Firebase Console โดยเข้าไปที่เมนู Database แล้วเลือก tab ชื่อ USAGE

และในหน้า Pricing ของ Firebase เราสามารถจะประมาณการใช้งานได้ว่าใช้เท่าไรคิดเงินเท่าไร ตัวอย่าง ผมจะจำลองการใช้งานของแพ็คเกจ SPARK ซึ่งเป็นแพ็คเกจฟรี โดย storage 1 GB เก็บข้อมูลได้ประมาณ 20 ล้านข้อความ และ bandwidth 10 GB จะ read และ write ได้ประมาณ 200 ล้านข้อความ เรียกได้ว่าแค่แพ็คเกจฟรี ก็ใช้งานได้เยอะเลยทีเดียว

หากแอพของเรามีข้อมูลเยอะ transaction เยอะ ก็เลือกใช้ตามแพ็คที่มีค่าใช้จ่ายที่เหมาะสมต่อไป ดูจากราคาแล้วเรียกว่าคุ้มค่ากับความเสถียร, ความปลอดภัย และเวลาในการพัฒนาแน่นอน

ตัวอย่างพร้อมแล้วใน GitHub

ตัวอย่างมีทั้งแบบการใช้งานแบบ authentication กับ public เลยครับ โดยตัว public จะเป็นตัว basic และตัว authentication จะ advance หน่อย มีตัวอย่างทั้งแอพ quickstart ของ Firebase ที่เอามา modify และตัวอย่าง chat app ด้วย โหลดเลย


ทิ้งไว้กลางทาง…เอ้ย ทิ้งท้าย

เป็นอย่างไรบ้างครับกับ Firebase Realtime Database ดูท่าทางน่าจะทำให้ชีวิตหลายคนดีขึ้นมาเลยทีเดียว ก่อนจะเอาขึ้น Production อย่าลืมไปตั้ง Security Rules ให้ต้อง Authentication ก่อนเข้าถึง database นะครับ ไม่งั้นเดี๋ยวคนอื่นแอบมาเขียน database เราไม่รู้ด้วย สำหรับบทความนี้ก็ทิ้งห่างจากบทความที่แล้วไปครึ่งเดือนเท่านั้นเอง ไม่นานเล้ยยย(เสียงสูง) เอาเป็นว่าบทความต่อไปก็จะเป็นเรื่องของ Firebase Storage ซึ่งจะไม่ทิ้งช่วงนานแน่นอน เราจะทำตามสัญญา ขอเวลาอีกไม่นาน…สำหรับวันนี้ต้องลาไปก่อน ราตรีสวัสดิ์พี่น้องชาวไทย

ref:https://developers.ascendcorp.com/%E0%B8%A3%E0%B8%B9%E0%B9%89%E0%B8%88%E0%B8%B1%E0%B8%81-firebase-realtime-database-%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87%E0%B9%81%E0%B8%95%E0%B9%88-zero-%E0%B8%88%E0%B8%99%E0%B9%80%E0%B8%9B%E0%B9%87%E0%B8%99-hero-5d09210e6fd6