Deep Dive into Firebase Realtime Database

Jirawatee
Firebase Thailand
Published in
6 min readMar 9, 2017

--

สืบเนื่องจากการที่ผมมีโอกาสได้ไปบรรยายในหัวข้อ Deep Dive into Firebase Realtime Database ในงาน Firebase Dev Day ที่ผ่านมา จึงอยากเอาเนื้อหามาแบ่งปันกันผ่าน blog นี้(เผื่อคนที่ไม่ได้ไปร่วมงาน) เพราะเชื่อว่าเนื้อหาในวันนั้นจะทำให้หลายคนเข้าใจการใช้งาน Firebase Realtime Database มากขึ้น และน่าจะคลายข้อสงสัยได้หลายเรื่อง…เอ้ามาเริ่มกันเลย (วิดีโออยู่ท้ายบทความ)

เริ่มต้นรู้จักกับ DatabaseReference

การจะอ้างอิงถึง Database นั้นแสนง่ายดาย ตามตัวอย่างด้านล่างนี้

private DatabaseReference mDatabase;
// ...
mDatabase
= FirebaseDatabase.getInstance().getReference();

โครงสร้างของ Firebase Realtime Database นั้นเป็น JSON Object ที่มีรูปแบบเป็น JSON tree ตามรูปแบบด้านล่างนี้

คราวนี้เวลาที่เราจะอ้างอิงถึง object ใดตาม ก็สามารถอ้างอิงได้ง่ายๆแบบนี้

// อ้างอิงไปที่ chat
mDatabase
.child("chat");
// อ้างอิงไปที่ message_1
mDatabase
.child("chat").child("message_1");
// อ้างอิงไปที่ name
mDatabase
.child("chat").child("message_1").child("name");

เห็นมะ ง่ายนิดเดียว ไปต่อกันเลย

เรื่องของการ Read

จากข้อมูลตัวอย่างด้านบน เรามาหัด Read ข้อมูลง่ายกันด้วยการ addValueEventListener กันก่อน โดยหากสำเร็จจะมี callback เข้ามาที่ method ชื่อ onDatachange พร้อมกับข้อมูลที่เป็น DataSnapshot แต่หากไม่สำเร็จ ก็จะเข้า callback ที่ชื่อ onCancelled พร้อมกับข้อมูลที่เป็น DatabaseError ตามตัวอย่างนี้

valueEventListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// ดึงค่าที่เป็น string จาก dataSanpshot
String name = dataSnapshot.getValue(String.class);
// ดึงค่าจาก dataSanpshot แบบ data model
User user = dataSnapshot.getValue(User.class);
}

@Override
public void onCancelled(DatabaseError databaseError) {
Log.e("", databaseError.getMessage())
}
};
mUsersRef.addValueEventListener(valueEventListener);

เราสามารถดึงค่าจากตัว DataSnapshot ได้ ทั้งแบบที่เป็น String, Integer, Boolean หรือจะเป็น data model ที่ map ข้อมูลไว้แล้วก็ได้เช่นกัน

เมื่อแอพเราหยุดการทำงาน ก็ควรที่จะ remove ตัว listener ออกด้วย เพื่อคืนทรัพยากรให้เครื่องแบบนี้

if (valueEventListener != null) {
mUsersRef.removeEventListener(valueEventListener);
}

โดยปกติเราจะทำการ add ตัว listener ไว้ใน method onStart() และ remove ตัว listener ใน onStop() แต่จริงๆก็ไม่เสมอไป ทั้งนี้ขึ้นอยู่กับ logic การทำงานของแต่ละแอพนะครับ

เรื่องของการ Write

ขอแบ่งการ write แบบง่ายๆออกเป็น 3 แบบด้วยกัน

แบบที่ 1 การ set ค่าแบบ single ไปที่ object ที่เราต้องการ

mUsersRef.child("email").setValue("firebasethailand@gmail.com");

ผลที่ได้ก็คือ ค่า email ที่เราอ้างถึงก็จะเปลี่ยนจาก jirawatee@gmail.com เป็น firebasethailand@gmail.com ทันที

แบบที่ 2 การ set ค่าแบบ object ไปยัง object ที่เราอ้างถึง

User user = new User(
"jirawatee@gmail.com",
"Jirawatee",
"https://pbs.twimg.com/profile_images/744165523978493952.jpg",
true
);
mUsersRef.setValue(user);

ผลที่ได้ เราก็สามารถที่จะเปลี่ยนค่าไปพร้อมๆกันหลายค่าใน object ได้

แบบที่ 3 เป็นการ push ค่าเข้าไป โดยความแตกต่างระหว่างการ set กับ การ push คือ การ push เปรียบเหมือนการ insert โดยจะมีการสร้าง key ขึ้นมา และ key ที่สร้างออกมานั้น จะมีอัลกอริทึมที่เอาเรื่องของเวลาเข้ามาเกี่ยวข้องด้วย ดังนั้น key ที่ได้ ก็จะสามารถเรียงลำดับได้เช่นกัน ตัวอย่างจะอ้างอิงไปที่ object ที่ชื่อ users และ push ข้อมูลเข้าไปตามนี้จ้า

User user = new User(
"firebasethailand@gmail.com",
"Firebaser",
"https://pbs.twimg.com/profile_images/744165523978493952.jpg",
true
);
mDatabase.child("users").push().setValue(user);

ผลที่ได้ ก็จะสังเกตว่ามี object ใหม่โผล่ขึ้นมาแล้ว

เรื่องของ Listener

จากตัวอย่างก่อนหน้านี้ เรารู้จักการ read จาก ValueEventListener แต่คราวนี้ เราจะมารู้จักกับ ChildEventListener โดยก่อนอื่นมาฝึกมองกันก่อน ว่าแบบไหนควรใช้ ValueEventListener หรือแบบไหนที่ควรใช้ ChildEventListener

ตัวอย่าง JSON tree ที่ควรใช้ ValueEventListener

จากรูปด้านบน ถ้าเราอ้างอิงไปหา key ที่ชื่อภายใต้ topics เราควรใช้ ValueEventListener เนื่องจาก เราต้องการเฝ้าดูข้อมูลใน key นั้นทั้งหมด

ตัวอย่าง JSON tree ที่ควรใช้ ChildEventListener

จากรูปด้านบน ถ้าเราอ้างอิงไปที่ topics จะเห็นว่าใน topics มีข้อมูลที่เป็น array อยู่ภายในเพียบ ซึ่งถ้าเป็นแบบนี้ เราควรใช้ ChildEventListener นะจ๊ะ

mChildEventListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
// เมื่อมีการเพิ่ม หรือการโหลดข้อมูลครั้งแรก(ทีละตัว)
}

@Override
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
// เมื่อมีการเปลี่ยนแปลงข้อมูลนั้นๆ
}

@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
// เมื่อมีการลบข้อมูลนั้นๆ
}

@Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
// เมื่อทีการย้ายข้อมูลนั้นๆ
}

@Override
public void onCancelled(DatabaseError databaseError) {
// เมื่อไม่ error เกิดขึ้น
}
};
query.addChildEventListener(mChildEventListener);

ซึ่งใน ChildEventListener จะมี method ที่ callback มากมาย ตามการเปลี่ยนแปลงที่เกิดขึ้นกับข้อมูล

การโหลดข้อมูลครั้งแรกที่ละตัว

อ้าว! แล้วแบบนี้ใช้ ValueEventListener ไม่ได้เหรอ…คำตอบคือได้ แต่ไม่แนะนำเนื่องจาก หากเราใช้ ValueEventLister กับข้อมูลที่เฝ้าดูแบบ array แล้ว เมื่อมีการเปลี่ยนแปลงใดๆก็ตาม ข้อมูลจะถูกส่งออกมาทั้งหมดทุกสมาชิกใน array ซึ่งทำให้เราเปลือง bandwidth เปลือง memory หรืออาจทำให้เรามีค่าใช้จ่ายเพิ่มขึ้นได้

เช่นผมต้องการเปลี่ยน title จาก Firebase Dev Day เป็น Firebase Dev Day BKK ผลก็คือ…

เปลี่ยน title จาก Firebase Dev Day เป็น Firebase Dev Day BKK

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

สุดท้ายคือ ListenerForSingleValueEvent โดย listener แบบนี้ จะคล้ายกับ ValueEventListener แต่จะต่างกันที่จะดักฟังแค่ครั้งเดียว ได้ข้อมูลคือมาปุ๊บ จะหยุดฟังปั๊บ

mTopicReference.child(mTopicKey).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {

}
@Override
public void onCancelled(DatabaseError databaseError) {

}
});

ตัวอย่างที่ใช้ ListenerForSingleValueEvent ได้แก่ การ sign-in เพื่อเข้าไปเช็คว่ามีข้อมูลผู้ใช้คนนั้นในระบบหรือไม่ หรือหน้าการแก้ไขข้อมูล เพื่อไม่ให้ข้อมูลเกิดการเปลี่ยนแปลงในขณะที่เรากำลังแก้ไขได้

เรื่องของการ Ordering

เริ่มต้นที่ข้อมูลที่ผมจำลองอยู่ใน database ตามนี้

จากข้อมูลตัวอย่าง จะเห็นว่าข้อมูลผมเริ่มต้นด้วย Firebase Cloud Messaging และจบด้วย Firebase Dev Day

เริ่มต้นการเรียงข้อมูลจากเก่าไปใหม่ด้วยคำสั่งง่ายๆแบบนี้ โดยจะแสดง voteCount และ title ใน Log

mQuery = mDatabase.child("topics");
// หรือ mDatabase.child("topics").orderByKey();

ถัดไปจะเป็นการ orderByChild() ซึ่งการ order แบบนี้ก็เหมือนกับการ WHERE ใน SQL โดยตัวอย่างผมจะเรียงลำดับจาก voteCount ตามตัวอย่างนี้

mDatabase.child("topics").orderByChild("voteCount")

จะสังเกตว่า คะแนนจะเรียงจากน้อยไปหามากแล้ว

และสุดท้ายคือการ orderValue()

ข้อมูลตัวอย่างที่จะใช้ในการ orderByValue
mDatabase.child("Petrol").orderByValue();

ผลก็คือราคาน้ำมันก็เรียงลำดับจากน้อยไปหามากได้ละ

ในการทำ Query 1 ครั้ง เราสามารถใช้งาน orderBy ได้ 1 ครั้งเท่านั้น

เรื่องของการ Filtering

หลังจากการ ordering เราสามารถใช้งาน filtering ข้อมูลลงไปได้อีก

limitToFirst()

เป็นการ limit ข้อมูลจากตำแหน่งเริ่มต้น

mDatabase.child("topics").limitToFirst(3);

ผลที่ได้ ข้อมูล 3 ตำแหน่งเริ่มต้นก็จะออกมา

limitToLast()

เป็นการ limit ข้อมูลจากตำแหน่งสุดท้าย

mDatabase.child("topics").limitToLast(3);

ผลที่ได้ ข้อมูล 3 ตำแหน่งสุดท้ายก็จะออกมา

equalTo()

เป็นการ match ข้อมูล

mDatabase.child("topics").orderByChild("title").equalTo("Firebase Dev Day BKK");

startAt()

เป็นการบอกตำแหน่งเริ่มต้นของข้อมูลที่เราต้องการ โดยค่าที่อยู่ใน startAt จะต้อง match กับข้อมูลใน database จากตัวอย่างจะเรียง title จากน้อยไปหามากก่อน

mDatabase.child("topics").orderByChild("title");
mDatabase.child("topics").orderByChild("title").startAt("Firebase Hosting");

แล้วเมื่อ startAt(“Firebase Hosting”) ก็จะได้ข้อมูลดังนี้

endAt()

เป็นการบอกตำแหน่งสุดท้ายของข้อมูลที่เราต้องการ โดยข้อมูลตัวอย่างจะเรียงลำดับ oderByChild(“title”) เหมือนด้านบน จากนั้นก็ใช้ฟังก์ชัน endAt ดังนี้

mDatabase.child("topics").orderByChild("title").endAt("Firebase Hosting");

จะเห็นว่าข้อมูลจะมาจบที่ Firebase Hosting ละ

และการ filtering นั้นเราจะใช้ได้มากกว่า 1 ครั้ง ตัวอย่าง

mDatabase.child("topics").orderByChild("title").endAt("Firebase Hosting").limitToFirst(2);

ก็จะเห็นว่าข้อมูลมา 2 ตัวจากตำแหน่งเริ่มต้นจริงด้วย

การ Remove ข้อมูล

การลบข้อมูลมีด้วยกัน 3 แบบ

แบบที่ 1 คืออ้างอิงไปที่ object แล้วใช้ฟังก์ชัน removeValue();

mDatabase.child("topics").child(topicKey).removeValue();

แบบที่ 2 คือการอ้างอิงไปยัง object แล้ว setValue(null);

mDatabase.child("topics").child(topicKey).setValue(null);

แบบที่ 3 คือการลบหลาย object พร้อมๆกันด้วย updateChildren

Map<String, Object> childUpdates = new HashMap<>();
childUpdates.put("/topics/" + topicKey, null);
childUpdates.put("/user-topics/" + topic.uid + "/" + topicKey, null);
FirebaseDatabase.getInstance().getReference().updateChildren(childUpdates);

การ updateChildren นั้นสามารถเอาไปประยุกต์ใช้ในการ write ได้ด้วย

Validation ข้อมูลด้วย Firebase Security Rules

เป็นการสร้าง Rules ในรูปแบบของ JSON ซึ่งเหมือนกับการ validation ฝั่ง server side และสามารถทำ index กับ object ที่เรา ordering บ่อยๆได้ ผู้เขียนแนะนำบทความตอนนี้ให้ไปอ่านกัน

Enable Offline Mode

จุดเด่นอีกเรื่องหนึ่งของ Firebase Realtime Database ก็คือการ enable offline mode จากภาพด้านบน จะสื่อว่า เมื่อไม่มีการเชื่อมต่อ internet เราก็ยังสามารถ Read ข้อมูลที่โหลดมาแล้ว และยังสามารถ Write ข้อมูลได้อีกด้วย โดยจะเขียนไว้ใน internal storage ก่อน เมื่อสามารถเชื่อมต่อ internet ได้ ตัว Firebase จะทำการ sync ข้อมูลให้อัตโนมัติ และเรียงตาม key ที่สร้างออกมาด้วย

โดยการ enable offline mode ก็มี best practice ที่ง่ายนิดเดียว เพียงแค่ไปประกาศ setPersistenceEnabled() ใน class ที่ extends Application

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

Firebase Dev Day (Deep Dive into Firebase Realtime Database)

Source code

Video

Firebase Dev Day ก็ได้รับเสียงตอบรับที่ดีมาก ลงทะเบียนก็เต็มเร็วมาก ขอบคุณทุกท่านที่มาร่วมงาน หวังว่าจะได้ความรู้ ของที่ระลึก และความสนุกสนานกลับบ้านกันทุกคนนะครับ โดยสามารถติดตามวิดีโอย้อนหลังทั้งหมดได้จาก Channel Firebase Thailand ใน Youtube นะครับ

สำหรับบทความนี้ ก็เป็นบทความแรกของปี 2017(ที่มาช้ามาก) ขออภัยจริงๆ มีภารกิจสำคัญมากมายที่เข้ามาช่วงต้นปี อย่างไรก็ขอฝากบทความตอนนี้ไว้ด้วยนะครับ และจะรีบสานต่อบทความต่อไปเร็วๆนี้ สำหรับวันนี้ขอลาไปก่อน ราตรีสวัสดิ์ พี่น้องชาว Firebase Thailand

--

--

Jirawatee
Firebase Thailand

Technology Evangelist at LINE Thailand / Google Developer Expert in Firebase