ทำ API กับ Cloud Firestore ด้วย Cloud Functions for Firebase

Minseo Chayabanjonglerd
MikkiPastel
Published in
9 min readNov 29, 2019

ด้วยความที่เราอยากย้ายบล็อกใหม่ๆมาลง Firebase Hosting ของเรา เลยเอา data จาก blogspot มาลง Cloud Firestore แล้วก็ต้องสร้าง API เพื่อเอาไปใช้ต่อที่หน้าเว็บและในแอป

ภาพประกอบหลังจากการใช้ API ที่เขียนขึ้นมาเองเพื่อแสดงบทความในบล็อก ซึ่งยังไม่เสร็จในเร็ววัน

เกริ่นนำ

บรีฟคร่าวๆ : เราเองมีบล็อกที่ blogspot และใน medium บวกกับมีหน้าเว็บของตัวเองบน Firebase Hosting ทางนี้มองว่ามันก็มีหลายที่ไปนิดนึง เลยคิดว่าจะย้ายบล็อกที่ blogspot ไปยังตัวเว็บ Hosting ด้วยเลย จะได้มารวมในที่เดียว เราลองสำรวจแล้วคนชอบอ่านจาก medium มากกว่าด้วยนะ… เอาเป็นแนวทางปรับหน้าเว็บได้

และทางเรานั้นก็ติดปัญหาบางอย่างในการใช้ API ของ blogspot ด้วย มันดันทำ lazyload ไม่ได้ซะนี่ เจ้า paging อะไรก็ไม่มี =_= เลยก็ต้อง move เพื่อเอาไปใช้เองนี่แหละจ้า

บวกกับเรื่อง Inbound Marketing ที่ website ของเราเป็น asset อะเนอะ

คิดว่าอย่างน้อยๆคิดว่าน่าจะได้เอาความรู้ที่ได้จากตอนไป vue.js workshop มาใช้ด้วย บวกกับ Cloud Firestore ด้วย ดังนั้นจึงทำ API หลังบ้านก่อน เพื่อสามารถเอาไปใช้งานต่อได้เลย

คำเตือน ตัวโค้ดอาจจะมีทั้ง Kotlin และ Node.js ควรใช้จักรยาน เอ้ยย วิจารณญาณในการรับชมจ้า เท่าที่อ่าน document มันก็ต่างที่ syntax ของภาษาจริงๆง่ะ

มาเริ่มทำ API เองกันเถอะ~

น่ารักดีเลยเอามาใส่ ถือซะว่าตะลุยอวกาศแล้วกัน ref: https://giphy.com/gifs/molangofficialpage-buzz-molang-lightyear-MVUdrlCyCSgeorwMYK

แน่นอนว่าเราต้องวางแผนในการทำเว็บ version ใหม่ของเราบน Firebase Hosting ก่อน ซึ่งเรื่องการทำ API ก็เป็นเรื่องสำคัญมากๆเลยนะ ระหว่างที่ทำไป เขียนบล็อกไป ก็ทำผิดๆถูกๆไปบ้าง แหะๆ เลยนำสิ่งที่ลองทำมาแบ่งปันกันเนอะ

Database ของ Firebase ใช้อะไรดีหล่ะ?

ใน Firebase จะมี Database 2 ตัวด้วยกัน คือ Realtime Database และ Cloud Firestore ซึ่งแน่นอนว่ามันเป็น NoSQL ทั้งคู่เลย เลยเปรียบเทียบความแตกต่างของแต่ละตัวดูจ้า

Realtime Database

  • เป็น json tree ใหญ่ๆ ที่มี node ลึกสุดได้ 32 ชั้น

Cloud Firestore

  • เก็บเป็น collection และ document
  • global scale
  • support offline บน web ด้วยนะเออ

จากการพิจารณาแล้ว เราเลือก Cloud Firestore เพราะว่า

  • จัดการ data ง่ายกว่า Realtime Database ตรงที่เราสามารถดูเป็น item นั้นๆได้เลย
  • เหมาะกับการ query หรือ search ที่ตรงการใช้งานของเรา
  • support data type ได้หลากหลายกว่า
  • order item ตามเวลาที่เรา publish ได้ด้วย

และเจ้า Cloud Firestore นั้น ได้ออกจาก beta ในวันที่ 31 มกราคม 2019 พร้อมเพิ่ม location ด้วยนะ

แล้ว collection of document อะไรเนี่ย มันคืออะไรอ่ะ?

ตอนแรกเราอ่านใน document เราก็งงๆนะ งั้นมองลองเป็นเอกสารใส่เข้าแฟ้มแล้วกันเนอะ แบบนี้ มองตัวแฟ้มเป็น Collection และ ตัวเอกสารกระดาษเป็น Document ซึ่งใน Document ก็จะมีรายละเอียดต่างๆ เรียกว่า Data ที่เก็บเป็น Field

ออกแบบ database โดยยกจากใน blogspot มาใช้

ก่อนอื่น เรามาดูกันก่อนว่า เมื่อเรา get blog จาก blogspot API แล้วได้อะไรบ้าง

อยากรู้ว่า blogspot API คืออะไร อ่านต่อได้ที่นี่จ้า

ขอยาดเซ็นเซอร์บางส่วนจ้า

จากภาพพบว่าเราไม่ได้ใช้ทั้งหมดแน่นอน ตั้งแต่ตอนทำในแอปแล้วหล่ะ ดังนั้นเราดึงเฉพาะที่ใช้ ดังนี้

  • หมวดไม่ต้อง modified ใดๆ จะมี id, published, url, title, content และ labels
  • images เนื่องจากมันมีอันเดียวเลยเปลี่ยนเป็น coverUrl ที่เป็น String?
  • เราเพิ่ม shortDescription โดยตัด content ให้เหลือ 140 คำแรก ให้เหมือนใน medium เพื่อนำไปแสดงในแต่ละ item ซึ่งก็ตามมีตามกรรม 555 ตัดคำไม่สวยหรอก เดี๋ยวหน้าบ้านเอาไปปรับต่อได้

เมื่อเราเอามา map กันจะได้แบบนี้

ซึ่งการออกแบบ database Cloud Firestore เราใส่ collection และ document แบบนี้

เรามีชื่อ Collection ว่า “blog” โดย Document เป็น id ของบล็อก และใน Document จะมี field ต่างๆ คือ id, published, url, title, content, coverUrl, labels และ shortDescription

ดังนั้น เราจึงเรียก API ของ blogspot ในแอพบล็อกที่เราเขียน ไปทำการ write data ลง Cloud Firestore

ผลสุดท้ายก็จะเป็นแบบนี้

เราเริ่มทำส่วนอื่นๆต่อเลยจะติดเจ้า activity มาด้วย ><

เรียนรู้กระบวนท่าต่างๆ

เรามาเรียนนรู้การใช้ Cloud Firestore แบบคร่าวๆเนอะ ไม่ได้เขียนทุกกระบวนท่าเน้อ เดี๋ยวบล็อกยาวไป

แน่นอนว่าจากเมื่อสักครู่นั้น เราได้เริ่มใช้ Cloud Firestore ในการ write data ลงไปแล้วเนอะ

ต่อจากนี้เราจะแสดงตัวอย่างโค้ดที่เป็น node.js นะ

Initialize

ก่อนอื่นประกาศตัวแปรที่เป็นเจ้า Firestore ขึ้นมาก่อนนะ เพราะตัวแปรนี้เราจะเอาไปใช้ต่อ

const db = admin.firestore();

Data Model & Read Data

ก่อนอื่นเราต้องอ้างอิงถึง reference กันก่อน เรามี Collection ที่ชื่อว่า blog และมี Document เป็น id ของบล็อกนั้นๆเนอะ เราจะเรียกกันแบบนี้

let blogRef = db.collection(’blog’).doc(’1015419615744730650’);

แน่นอนถ้าเราอยากเรียกมันทั้งก้อนของ Collection ก็จะเรียกแบบนี้

db.collection('blog');

จริงๆเราสามารถเรียก document ที่เราต้องการแบบนี้ก็ได้นะ

db.document('blog/1015419615744730652');

ดังนั้นการ Read Data นั้นเราต้องอ้างอิงจาก reference และใส่ get() ต่อท้าย และตามด้วย listener เพื่อนำ data ที่เรา get มาได้ไปใช้ต่อ

db.collection('blog').doc('1015419615744730652')
.get()
.then(snapshot => {
if (!snapshot.exists) {
console.log('No such document!');
} else {
console.log('Document data:', snapshot.data());
}
}.catch(err => {
console.log('Error getting document', err);
});

ซึ่งเราขอแยกส่วนการ read data ออกเป็นสองส่วน คือ เรามองว่าการอ้างอิง document หรือ collection ตามเงื่อนไขต่างๆ นับเป็น query แบบหนึ่งแล้วกัน

let query = db.collection('blog').doc('1015419615744730652');

จากนั้นเราจึง get data จาก query ที่เราต้องการ ซึ่งมีท่าประจำแบบนี้

query.get()
.then(snapshot => {
if (!snapshot.exists) {
console.log('No such document!');
} else {
console.log('Document data:', snapshot.data());
}
}.catch(err => {
console.log('Error getting document', err);
});

Write Data

ในตอนแรกเรา write data ผ่านแอพแอนดรอยด์ที่เราทำขึ้นมาแล้วเนอะ โดยเราจะเอา blog item แต่ละตัวไปอยู่ใน Collection ที่ชื่อว่า blog และ Document แต่ละ item เราจะอ้างอิงแต่ละ blog id และเราเอาแต่ละ item เข้าไปใน Document นั้นๆ แบบนี้

db.collection(“blog”).document(slug).set(blog)

การที่ set data เข้าไปใน Document นั้น เราจะต้องใส่เป็น hashmap ซึ่งจะมี key และ value

จากนั้นเราก็ใส่ listener เข้าไป เพื่อตรวจสอบว่าเขาเขียน data ได้เสร็จสมบูรณ์หรือไม่

Update Data

สมมุติเราจะ update ค่าต่างๆใน field เช่น เปลี่ยนหัวข้อในบล็อกใหม่ เราจะต้องเปลี่ยนค่าใน key ที่มีชื่อว่า title ใช่ม่ะ

db.collection(’blog’).doc(’1015419615744730652’)
 .update({title : "ทำ API กับ Cloud Firestore ด้วย Cloud Function for Firebase"});

Query Data

แน่นอนว่าเราต้องต้องดึงข้อมูลต่างๆตามเงื่อนไข API ที่เขากำหนดไว้ ซึ่ง API blog ของเราจะมี 4 ตัวด้วยกัน คือ

  1. get all blogs ดึงบล็อกทั้งหมดออกมา
  2. get tag blog ดึงบล็อกเฉพาะที่ติดแท็กเรื่องนั้นๆมา
  3. get blog by id ดึงเฉพาะบล็อก id นั้นๆมาแสดง
  4. search content ให้คนอ่านสามารถค้นหา blog ด้วย keyword ได้
  5. สามารถทำ lazyload หรือ paging ได้ใน API ที่ 1–2, 4

get all blogs : เราจะเรียงลำดับบล็อกของเราจากใหม่ไปเก่า และให้คืน result มา 20 อัน แบบนี้

let query = db.collection('blog')
.orderBy("published", "desc").limit(20)
  • orderBy เราต้องการให้ field ไหน เรียงข้อมูลแบบไหน สามารถเรียงได้สองแบบ คือ desc จากมากไปน้อย และ asc จากน้อยไปมา
  • limit ให้แสดงผลลัพธ์เป็นจำนวนเท่าไหร่

get blog by id : เรานำ id ที่ต้องการ ที่เราได้มาจากการกด item ของ blog นั้นๆ ไป search หาบล็อกที่มี id ตรงกัน เพื่อแสดงบล็อกนั้นๆ

let query = db.collection('blog')
.where("id", "==", request.query.id)

where เป็นการ filter ข้อมูลตามที่เราต้องการ ในที่นี้คือต้องการ id ซึ่งเราใส่ parameter id เข้าไปใน API ของเรา โดย query operation จะมี >, >=, == ,<=, < และมีอีกอันคือ array-contains เอาไว้ search data ที่อยู่ใน array

get tag blog เราค้นหา tag ต่างๆใน labels ซึ่งเป็นตัวแปรแบบ array ดังนั้นเราจะ where แบบ array-contains

let query = db.collection('blog')
.where("label", "array-contains", request.query.tag)

แต่ผลที่ได้ยังไม่ถูกต้องตรงใจนัก เราต้องการเรียงข้อมูลด้วยหน่ะสิ ดังนั้นเราจึงต้องเพิ่ม Composite Indexes ก่อน เพราะว่า ก่อนหน้านี้เราทำ Single-field indexes ใช่ม่ะ ซึ่งเป็น default ของเจ้า Firestore ดังนั้นการที่เรา where label ที่เป็น array พร้อมกับการ orderBy ด้วย จึงเป็นการ query แบบ multiple field

การเปิด Composite Indexes ไปที่หน้า Firebase console ของ Cloud Firestore ไปที่แท็ป Indexes เลือก Composite และ Add Index จากนั้นเราใส่ Collection และ Field to index ตามที่เราต้องการ หลังจากนั้นก็ใช้ได้เลยจ้า เย้ๆ

Simple Cursor

เราสามารถใช้เจ้า Simple Cursor ในการ query ค่าต่างๆได้ โดยมี 4 methods คือ

  • startAt(A) คืนค่าตั้งแต่ A ลงไป
  • startAfter(A) คืนค่าหลัง A คือ B-Z
  • endAt(Z) คืนค่าท้ายสุดที่ Z
  • endBefore(Z) คืนค่าท้ายสุดก่อน Z

จริงๆเราค่อนข้างติดเรื่องนี้นานมาก จนมาลองนอนกลิ้งคิดดู เรานำ param มาค่านึงเพื่อเอามาใส่ แล้วให้มันแสดงบล็อกเพิ่มก็ได้นี่เนอะ แล้วจะทำยังไงดีนะ

ในที่นี้เราให้บล็อกแสดงทีละ 20 บล็อก (ไม่รู้ว่ามันเยอะไปไหมนะ 555) พอเรา scroll ลงมา ก็จะให้โหลดมาเพิ่มอีก 20 บล็อกเนอะ

ดังนั้น เราใช้เจ้า Simple Cursor ให้เป็นประโยชน์ โดยเราให้แสดงบล็อกหลังจาก set ก่อนหน้านี้นั่นเอง เนื่องจากเราให้แสดงบล็อกเรียงลำดับใหม่ไปเก่า เราจึงต้องใช้กับเจ้า published นั่นเอง

firestore.collection(‘blog’)
.orderBy(“published”, “desc”)
.startAfter(request.query.published)
.limit(limit)

ผลที่ได้ คือเราจะได้ list ของบล็อกที่ต่อจากเดิมจ้า คือหลังจากที่เรียงใหม่ไปเก่าตอนแรกเราจะได้ 0–19 พอโหลดต่อจะได้ 20–39 จ้า

ส่วนเรื่อง Paginate Data เราอ่านแล้วแอบงงๆ และนั่นคือสาเหตุที่ไปผิดทางจ้า ฮืออ

ปล. ในใจอยากทำ start, length แต่ทำไม่เป็น ฮือออออออ

Access Data Offline

เราสามารถทำให้เข้าถึงเมื่อตอน offline ได้ และ set ขนาดของ cache ที่เราจะเก็บได้ด้วย

ทดสอบคร่าวๆผ่าน

ใน Firebase console ของ Cloud Firestore จะเป็นแบบนี้เนอะ

กดไปที่ปุ่ม filter จะเจอแบบนี้

เราสามารถเลือก field ว่าจะให้เรียงกันแบบไหน เช่น published เรียงแบบ descending ผลคือจะเรียงบล็อกที่เขียนล่าสุดลงมา

ซึ่งการใช้ condition นั้น สามารถเลือกได้เฉพาะ query operation ที่มี >, >=, == ,<=, <

การ run Cloud Function ที่เราเขียนขึ้นมา

เราเขียน function ของ API ที่สามารถ get all blogs, get blog by tag และ get blog by id ที่ชื่อว่า “blog” ดังนั้นเราจะ run function blog เนอะ

แน่นอนว่ามีสองแบบ คือแบบ run กับ emulator

firebase emulators:start — only functions:blog

กับแบบ deploy จริง

firebase deploy — only functions:blog

ทริคเล็กๆน้อยๆ โปรเจก Firebase ของเราอันนี้สร้างมานานมากแล้วหลายปี ดังนั้น Google Cloud Platform (GCP) resource location จะอยู่ที่ nam5 (us-central) ดังนั้นเวลาเรา deploy จริงแล้วเรียกใช้ มันจะช้าๆ เนื่องจากว่าเราอยู่ไทย ตัว server location อยู่ที่เมกา ดังนั้นเราจึงต้องเปลี่ยน location Cloud Function ให้อยู่ใกล้เรามากที่สุด เลยเลือก asia-east2 (Hong Kong)

https://firebase.google.com/docs/functions/locations

ดังนั้นเราจึง handle เพิ่มไปดังนี้

const builderFunction = functions.region('asia-east2').https;

แน่นอนว่ามันเร็วขึ้นแหละ แต่แอบขัดใจที่ตัว Firestore มันดันอยู่ที่ nam5 นี่สิ

แต่แอบเห็นอันนี้

Important: If you are using HTTP functions to serve dynamic content for Firebase Hosting, you must use us-central1.

เนื่องจากเราทำเว็บใหม่ซึ่งน่าจะเป็น dynamic content ดังนั้นจึง work ต่อในส่วนที่เราทำหน้าบ้านเนอะ เดี๋ยวเล่าให้ฟังอีกที

การทำหลังบ้านที่ถูกต้อง(หรอ?)

ใน 3 API นั้น เหมือนจะดี แต่เราไม่ควรพ่นข้อมูลออกมามากเกินไป ก็คือไม่ควรพ่นออกมาทั้งหมด ดังนั้นเราจึงเลือกให้พ่นแค่บางอย่างที่เราต้องการใช้เท่านั้น

ลองนึกเป็น SQL ดูสิ เรามีตารางชื่อว่า blog มี primary key คือ id ของบล็อกใช่ม่ะ

get all blogs และ get tag blog เราต้องการแค่ id, url, title, labels, shortDescription, coverUrl ก็จะเป็น

SELECT id, url, title, labels, shortDescription, coverUrl FROM blog

ส่วน get blog by id เราต้องการแค่ id, url, title, labels, coverUrl, published และ content ก็จะเป็น

SELECT id, url, title, labels, coverUrl, published, content FROM blog

ทั้งสองกรณีเราสามารถระบุให้แสดงผลลัพธ์เฉพาะ field ที่เราต้องการได้ อย่างในกรณีของ get all blogs และ get tag blog เราจึงสร้าง object ของผลลัพธ์แต่ละก้อน และนำมาใส่ใน list ของเรา และพ่นออกมาเป็น json data และเราสามารถระบุ field ที่ต้องการนำไปใช้ต่อได้แบบนี้

var data = {};
const result = [];
snapshot.forEach(doc => {
var blogElement = {};
var blog = doc.data();
console.log(blog);
blogElement.id = blog.id;
blogElement.url = blog.url;
blogElement.title = blog.title;
blogElement.label = blog.label;
blogElement.coverUrl = blog.coverUrl;
blogElement.shortDescription = blog.shortDescription;
result.push(blogElement);
});
response.contentType('application/json');
data.items = result;
response.send(data);

ส่วนในกรณีของ get blog by id นั้น สร้าง object ของผลลัพธ์แต่ละก้อน พ่นออกมาเป็น json data

var blogElement = {};
snapshot.forEach(doc => {
var blog = doc.data();
console.log(blog);
blogElement.id = blog.id;
blogElement.url = blog.url;
blogElement.title = blog.title;
blogElement.label = blog.label;
blogElement.coverUrl = blog.coverUrl;
blogElement.published = blog.published;
blogElement.content = blog.content;
});
response.contentType('application/json');
data.data = blogElement;
response.send(data);

จากการสอบถามพี่ backend ในทีมแล้ว ในกรณีที่เราต้องการผลลัพธ์เป็น list อย่าง get all blogs และ get tag blog เราจะต้องนำ list ของผลลัพธ์ทั้งหมด มาใส่ในรูปแบบนี้

{
"items": [
//ผลลัพธ์ที่เป็น list ทั้งหมด
]
}

ผลลัพธ์ที่ได้

ส่วน get blog by id นั้น เราจะให้พ่นมาแค่ก้อนเดียว ดังนั้นเรานำ object ที่ได้มาใส่ดังนี้

{
"data": {
//ผลลัพธ์ object
}
}

และผลลัพธ์ที่ได้

พิเศษสุดๆในกรณีที่ผลลัพธ์ออกมาเป็น list นั้น เราสร้าง object String เพิ่มมาตัวนึง เพื่อนำไปใช้ตอน lazy load ต่อไปจ้า

ทั้งนี้ ทั้งนั้น ทั้งโน้น จากการสอบถามพี่ backend เขาบอกว่า จริงๆมันไม่มี guideline อะไรที่ชัดเจนที่สามารถตอบคำถามของเราว่า “พี่ค่ะ หลังบ้านมีวิธีเขียนอย่างไรให้ถูกต้องบ้างค่ะ” และพี่เขาได้ฝากบล็อกนี้มาอ่านกันจ้า

หลังบ้านอ่ะ จริงๆเขาจะแนะนำให้เราใช้ express มากกว่า งั้นขอยกเรื่องเกี่ยวกับการเขียน API แบบจริงจัง ไปในบล็อกถัดๆไปตามความสะดวกของคนเขียนจ้า

Security Rule สำคัญมากๆนะ

ที่เราใช้หลักๆ อันแรกคือตอนอัพ data จาก Blogspot ไป Cloud Firestore ขอบอกว่า ไม่ควรใช้เน้อ เพราะใครก็ได้มาอ่านมาเขียนของเราอ่ะ

service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow write, read: if true;
}
}
}

ส่วนปัจจุบัน security rule ของเราเป็นแบบนี้ ซึ่งมันก็น่าจะปลอดภัยได้กว่านี้อีกมั้งนะ

service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow write, read: if request.auth.uid != null;
}
}
}

ทาง Firebase มี video series ของเรื่อง Security Rule จ้า ซึ่งมีของ Firestore ด้วยแหละ นอกจาก read write ยังสามารถใส่ get, update, delete และอื่นๆได้ด้วยน้า

ทดสอบการใช้งาน API

ขอรวบรัดแบบย่อๆ

จริงๆเราเองก็สามารถทดสอบได้ระหว่างเขียน API นะเออ โดยการทดลองเอาไปเปิดใน postman ก่อนเนอะ จะเป็นอย่างนี้ถ้าสำเร็จ

ก่อนหน้านี้เป็นหน้าตาแบบนี้แหละจ้า แต่ทางนี้แก้ไปหลายรอบมาก และอาจจะมีแก้อีก

เราสามารถตรวจสอบ log ต่างๆได้ที่ Firebase Console ที่หน้า Functions และไปที่ Logs จ้า ถ้ามัน error หรือ crash ก็สามารถนำไปแก้ได้ทันทีจ้า

ซึ่งอันนี้ก็คือ success case เนอะ ถ้า fail มันจะเป็นสีแดง

และเอาไปแปะในแอพ และในเว็บที่ยังทำไม่เสร็จ

เก็บตกอื่นๆ

1) เราลองทำ search content เช่น คนอ่านอยากอ่านบล็อกที่เกี่ยวกับ Firebase ก็ให้แสดงบล็อกที่มีเนื้อหาที่เกี่ยวข้องกัน ซึ่งก็ต้องใช้ 3rd-party ตามที่ document บอกง่ะ

เอาจริงๆเขียนบล็อกนี้นานมากและเหนื่อยมากๆเลยอ่ะ แฮร่ๆ ได้แต่หวังว่าคนอ่านจะอ่านรู้เรื่องและนำไปใช้งานต่อได้จ้า

2) ลองถามอากู๋ถึงวิธีการทำ lazy load ใน Firestore ว่าทำยังไงดีนะ เจออันนี้ก็น่าจะสว่างแจ่มแจ้งเน้อ

ref: https://stackoverflow.com/questions/56411802/flutter-lazy-load-data-from-firestore

จริงๆก็แอบเจออันนี้อยู่นะ

สุดท้ายฝากร้านกันสักนิด ฝากเพจด้วยนะจ๊ะ

https://www.facebook.com/MikkiPastel/posts/1232705986862704

--

--

Minseo Chayabanjonglerd
MikkiPastel