บทเรียนการออกแบบฐานข้อมูลสำหรับ Google Calendar
(kb.databasedesignbook.com)- ใช้ฟีเจอร์กำหนดการของโคลน Google Calendar เป็นตัวอย่าง แสดงลำดับการออกแบบที่ไม่เริ่มจากการสร้างตารางทันที แต่ทำ โมเดลเชิงตรรกะ ให้สมบูรณ์ก่อน แล้วค่อยแปลงเป็น SQL schema
- วิธีหลักคือหา anchor เช่น User, DayEvent, TimeEvent, Timezone ก่อน จากนั้นตรวจสอบ requirements และ cardinality ด้วย attribute และลิงก์แบบ 1:N·M:N
- กำหนดการแบ่งเป็นอีเวนต์ทั้งวันและอีเวนต์แบบอิงเวลา โดยอีเวนต์แบบอิงเวลาจะบันทึก เวลา local ของเวลาเริ่มต้น·สิ้นสุด รวมถึง Timezone ของแต่ละรายการ เพื่อรองรับความเป็นไปได้ที่เขตเวลาจะเปลี่ยนในอนาคต
- กำหนดการที่เกิดซ้ำแสดงด้วยความถี่ ช่วงห่าง รูปแบบการเกิดซ้ำรายเดือน เงื่อนไขสิ้นสุด และการเชื่อมโยงกับวันในสัปดาห์ พร้อมแยก DaySlot และ TimeSlot เป็น anchor ต่างหากเพื่อใช้ในการ render หน้าจอและแก้ไข instance รายตัว
- การออกแบบเชิงกายภาพขั้นสุดท้ายยึดหลัก “หนึ่งตารางต่อหนึ่ง anchor” แต่ไม่สร้าง DayOfTheWeek เป็นตาราง จึงสรุปเป็น ตาราง SQL ทั้งหมด 8 ตาราง ได้แก่ตาราง anchor 6 ตารางและตารางลิงก์ M:N 2 ตาราง
แนวทางการออกแบบ: สร้างโมเดลเชิงตรรกะก่อน
- เป้าหมายคือแสดงกระบวนการ ออกแบบตารางฐานข้อมูล ที่ใกล้เคียงโปรเจกต์จริง สำหรับฟีเจอร์ปฏิทินของโคลน Google Calendar
- ลำดับโดยรวมคือสร้าง โมเดลเชิงตรรกะที่สมบูรณ์ ซึ่งอธิบายข้อมูลปฏิทินที่ต้องจัดเก็บก่อน แล้วจึงย้ายโมเดลนั้นไปเป็นโครงสร้างตารางเชิงกายภาพ
- ส่วนแรกกล่าวถึงโมเดลเชิงตรรกะที่ไม่ผูกกับฐานข้อมูลใดฐานข้อมูลหนึ่ง
- โมเดล business requirements อย่างเป็นอิสระจาก MySQL, Postgres, เซิร์ฟเวอร์ relational แบบดั้งเดิม, NoSQL และฐานข้อมูลบนคลาวด์
- ส่วนหลังแสดงแนวทางหนึ่งในการแปลงโมเดลเชิงตรรกะเป็นตารางเชิงกายภาพ
- สำหรับระบบที่ requirements ไม่ได้ซับซ้อนมากเป็นพิเศษ สามารถมองว่าเป็นวิธีออกแบบที่ใช้ได้
ขอบเขตปัญหา: ฟีเจอร์กำหนดการหลักของ Google Calendar
- ระบบเป้าหมายเป็นระบบ ผู้ใช้หลายคน และโมเดลข้อมูลเกี่ยวกับผู้ใช้ไว้เพียงขั้นต่ำ
- อีเวนต์อาจมี attribute เช่น ชื่อ คำอธิบาย และสถานที่ แต่ส่วนที่ซับซ้อนที่สุดคือ เวลาและวันที่
- อีเวนต์ที่นำมาโมเดลครอบคลุมความแตกต่างต่อไปนี้
- อีเวนต์ทั้งวันและอีเวนต์แบบอิงเวลา
- อีเวนต์เกิดซ้ำและอีเวนต์ไม่เกิดซ้ำ
- อีเวนต์ทั้งวันที่คร่อมหลายวัน
- อีเวนต์แบบอิงเวลาที่เชื่อมกับเขตเวลา และเวลาเริ่มต้น·สิ้นสุดอาจอยู่คนละวันที่หรือคนละเขตเวลา
- อีเวนต์เกิดซ้ำครอบคลุมรูปแบบต่อไปนี้
- เกิดซ้ำทุกวันหรือทุก N วัน
- เกิดซ้ำทุกสัปดาห์ในวันใดวันหนึ่ง โดยรองรับช่วงห่างตั้งแต่ 2 สัปดาห์ขึ้นไป
- เกิดซ้ำทุกเดือนตามวันที่ระบุ หรือตามวันในสัปดาห์ของสัปดาห์ลำดับที่ระบุ
- เกิดซ้ำทุกปี
- เกิดซ้ำตลอดไป จนถึงวันที่กำหนด หรือซ้ำตามจำนวนครั้งที่กำหนด
- instance เฉพาะของอีเวนต์เกิดซ้ำสามารถย้ายไปเป็นวัน·เวลาอื่น หรือลบได้
- ต้องสามารถแก้ไขตัวกำหนดการเกิดซ้ำเองได้ แม้บางอีเวนต์จะผ่านไปแล้ว
แสดง requirements ด้วย anchor, attribute และลิงก์
- การโมเดลเริ่มจากการหา anchor ก่อน
- anchor คือสิ่งที่นับได้และเพิ่มเข้าไปในฐานข้อมูลได้ เช่น User, Event
- หากสามารถพูดได้อย่างเป็นธรรมชาติ เช่น “There are 200 Users in our database”, “When this button is clicked, a new DayEvent is created” ก็ถือเป็น anchor ได้
- attribute ใช้จัดเก็บข้อมูลจริงเกี่ยวกับ anchor
- User มี attribute อีเมล
- DayEvent มีชื่อ วันที่เริ่มต้น และวันที่สิ้นสุด
- แม้เป็นอีเวนต์วันเดียว ก็จัดเก็บวันที่เริ่มต้นและวันที่สิ้นสุดเป็นค่าเดียวกัน เพื่อให้จัดการด้วยโครงสร้างเดียวกับอีเวนต์หลายวัน
- เมื่อ anchor สองตัวมีความสัมพันธ์กัน ให้ใช้ ลิงก์ ไม่ใช่ attribute
- “User คนใดสร้าง DayEvent” ไม่ใช่ attribute เชิงตรรกะที่ใส่ User ID ลงใน attribute ของ DayEvent โดยตรง แต่เป็นลิงก์ระหว่าง User กับ DayEvent
- ลิงก์ใช้ cardinality แบบ 1:N, M:N, 1:1 และตรวจสอบความหมายสองทิศทางด้วยประโยคสองประโยค
- ระหว่าง User กับ DayEvent มีลิงก์แบบ 1:N
- User สามารถสร้าง DayEvent ได้หลายรายการ
- DayEvent ถูกสร้างโดย User หนึ่งคน
อีเวนต์แบบอิงเวลาและการจัดการ Timezone
- เพิ่ม anchor Timezone และ TimeEvent สำหรับอีเวนต์แบบอิงเวลา
- โมเดล Timezone ถูกทำให้น้อยที่สุดเพื่อวัตถุประสงค์ของบทเรียน โดยมีเพียงชื่อที่มนุษย์อ่านได้เป็น attribute
- ค่าตัวอย่างคือ
Europe/Kyiv - นิยามเขตเวลาจริงต้องมี UTC offset, การมีหรือไม่มี daylight saving time, จุดเริ่มต้น·สิ้นสุด DST, UTC offset เมื่อใช้ DST, นิยามในอดีต, สถานะใช้งาน·เลิกใช้ เป็นต้น แต่ยกไว้เป็นงานต่างหาก
- ค่าตัวอย่างคือ
- TimeEvent มีชื่อ วันที่·เวลา local เริ่มต้น และวันที่·เวลา local สิ้นสุด
- ค่าตัวอย่างคือ
2024-01-14 12:30,2024-01-14 13:15
- ค่าตัวอย่างคือ
- เวลาไม่ได้จัดเก็บเฉพาะ UTC แต่จัดเก็บเป็น เวลา local ที่ผู้ใช้ป้อน
- ตัวอย่างเช่น เกมบิลเลียดวันที่ 6 กันยายน 2058 เวลา 09:30~11:00 ตามเวลา Cologne ยังไม่สามารถรู้ UTC offset ณ เวลานั้นได้ในปัจจุบัน จึงต้องจัดเก็บตามที่ผู้ใช้ป้อน
- หากภายหลังกฎหมายท้องถิ่นเปลี่ยนไป ต้องสามารถปรับให้สอดคล้องได้
- TimeEvent มีลิงก์ 1:N กับ Timezone แยกกันสำหรับเขตเวลาเริ่มต้นและเขตเวลาสิ้นสุด
- เช่นตัวอย่างตั๋วเครื่องบิน เที่ยวบินจาก Amsterdam ไป London อาจมีเวลาออกเดินทางและเวลาถึงปลายทางอยู่คนละเขตเวลา
- อีเวนต์แบบอิงเวลาส่วนใหญ่มีเขตเวลาเริ่มต้น·สิ้นสุดเหมือนกัน แต่กำหนดทั้งสองฝั่งเสมอเพื่อรองรับกรณีทั่วไป
การโมเดลอีเวนต์ทั้งวันที่เกิดซ้ำ
- อีเวนต์ทั้งวันที่เกิดซ้ำแสดงด้วยการเพิ่ม attribute และลิงก์ที่เกี่ยวกับการเกิดซ้ำให้กับ DayEvent
- ความถี่การเกิดซ้ำเป็น attribute แบบ either/or/or
- ค่าที่เป็นไปได้คือ daily, weekly, monthly, annually
- หากไม่ได้ตั้งค่า จะถือเป็นอีเวนต์ไม่เกิดซ้ำ
- ช่วงห่างของการเกิดซ้ำเป็น attribute จำนวนเต็มที่มีความหมายเฉพาะ “กรณีเป็นอีเวนต์เกิดซ้ำ” เท่านั้น
- ค่าตัวอย่างคือ
2 - ใช้ในความหมาย เช่น ทุก 2 วัน ทุก 2 สัปดาห์ ทุก 2 เดือน
- ค่าตัวอย่างคือ
- การเกิดซ้ำรายเดือนมี attribute สำหรับเลือกแยกต่างหาก
same_dayคือเกิดซ้ำในวันที่เดียวกันsame_weekdayคือเกิดซ้ำในวันของสัปดาห์ที่มีลำดับเดียวกับวันที่เดิม
- วันใดวันหนึ่งของสัปดาห์สำหรับการเกิดซ้ำรายสัปดาห์ไม่ได้จัดการเป็น attribute แบบอาร์เรย์สตริง แต่โมเดลด้วย anchor DayOfTheWeek และลิงก์ M:N
- ตัวอย่าง ID ของ DayOfTheWeek คือ
Mon,Tue,Wed,Thu,Fri,Sat,Sun - DayEvent สามารถเกิดได้ใน DayOfTheWeek หลายรายการ และ DayOfTheWeek สามารถมี DayEvent ได้หลายรายการ
- ตัวอย่าง ID ของ DayOfTheWeek คือ
- เงื่อนไขสิ้นสุดการเกิดซ้ำก็จัดเป็น attribute
- forever
- until_date
- N_repetitions
- หากเป็น until_date ให้เพิ่ม attribute วันที่สิ้นสุด
- หากเป็น N_repetitions ให้เพิ่ม attribute จำนวนครั้งที่เกิดซ้ำ
Slot สำหรับการ render และการแก้ไข instance รายตัว
- หากจัดเก็บอีเวนต์เกิดซ้ำด้วยอีเวนต์ต้นฉบับเพียงรายการเดียว query สำหรับหาอีเวนต์ที่จะแสดงในหน้าสัปดาห์ใดสัปดาห์หนึ่งอาจซับซ้อน
- เพื่อการ render หน้าจอและการแก้ไขราย instance ของอีเวนต์เกิดซ้ำ จึงนำแนวคิด Slot มาใช้
- DaySlot แทน instance ของอีเวนต์ทั้งวันในวันที่ระบุ
- TimeSlot แทน instance ของอีเวนต์แบบอิงเวลาในวันที่และเวลาที่ระบุ
- DaySlot มีวันที่และสถานะว่าข้ามหรือไม่
- วันที่ของ slot เฉพาะสามารถเปลี่ยนได้
- หากยกเลิกการประชุมในสัปดาห์ใดสัปดาห์หนึ่ง สามารถทำเครื่องหมาย DaySlot นั้นเป็น skipped ได้
- DayEvent กับ DaySlot มีลิงก์ 1:N
- DayEvent หนึ่งรายการสามารถสร้าง DaySlot ได้หลายรายการ
- DaySlot ตรงกับ DayEvent หนึ่งรายการ
- แม้เป็น DayEvent ที่ไม่เกิดซ้ำ ก็สร้าง DaySlot เสมอเพื่อทำให้โค้ด render ง่ายขึ้น
- อีเวนต์เกิดซ้ำไม่สิ้นสุดมีปัญหาเรื่องขอบเขตการสร้าง slot
- อีเวนต์เช่นวันเกิดที่เกิดซ้ำทุกปีตลอดไป อาจสร้าง slot ไปจนถึงขอบเขตสมมติ เช่น 100 ปี
- หรือสร้างแบบ on-demand เมื่อผู้ใช้ร้องขอหน้าปฏิทินในอนาคตไกลก็ได้
- การคำนวณวันที่ต้องระวัง
- วันเกิดวันที่ 29 กุมภาพันธ์
- อีเวนต์รายเดือนที่เกิดทุกวันที่ 31
- ในกรณีเหล่านี้ต้องตัดสินใจว่าจะห้ามใช้งาน หรือเลื่อนไปข้างหน้าหรือข้างหลังหนึ่งวัน
TimeSlot และเขตเวลา
- สำหรับอีเวนต์แบบอิงเวลาที่เกิดซ้ำ ก็ใช้แนวทางเดียวกับอีเวนต์ทั้งวันโดยมี anchor TimeSlot
- TimeSlot คือ instance ของอีเวนต์หนึ่งในวันที่·เวลาหนึ่ง และ TimeEvent ที่เกิดซ้ำจะสัมพันธ์กับ TimeSlot หลายรายการ
- attribute ของ TimeSlot คือวันที่·เวลา local เริ่มต้น วันที่·เวลา local สิ้นสุด และสถานะว่าข้ามหรือไม่
- TimeSlot เฉพาะสามารถย้ายไปวันอื่นได้ จึงต้องเก็บข้อมูลเวลาเริ่มต้น·สิ้นสุดแยกไว้
- TimeSlot ก็มีลิงก์ 1:N กับ Timezone แยกกันสำหรับเขตเวลาเริ่มต้นและเขตเวลาสิ้นสุด
- ระหว่าง TimeEvent กับ TimeSlot มีลิงก์ 1:N และสร้าง TimeSlot แม้สำหรับ TimeEvent ที่ไม่เกิดซ้ำ
โมเดลเชิงตรรกะที่สมบูรณ์
- anchor ที่นิยามมาถึงตอนนี้มีทั้งหมด 7 รายการ
- User
- Timezone
- DayEvent
- TimeEvent
- DayOfTheWeek
- DaySlot
- TimeSlot
- attribute ประกอบด้วยอีเมลของ User, ชื่อของ Timezone, ชื่อ·วันที่เริ่มต้น·วันที่สิ้นสุด·ข้อมูลการเกิดซ้ำของ DayEvent, ชื่อ·เวลา local เริ่มต้น·สิ้นสุดของ TimeEvent, และเวลาที่เกิดขึ้นกับสถานะ skipped ของ DaySlot และ TimeSlot
- ลิงก์สรุปได้ทั้งหมด 10 รายการ
- User กับ DayEvent, User กับ TimeEvent
- Timezone กับเขตเวลาเริ่มต้น·สิ้นสุดของ TimeEvent
- DayEvent กับ DayOfTheWeek, TimeEvent กับ DayOfTheWeek
- DayEvent กับ DaySlot, TimeEvent กับ TimeSlot
- Timezone กับเขตเวลาเริ่มต้น·สิ้นสุดของ TimeSlot
ย้ายไปเป็นตาราง SQL
- การออกแบบเชิงกายภาพใช้กลยุทธ์ หนึ่งตารางต่อหนึ่ง anchor เพื่อวัตถุประสงค์ด้านการศึกษา
- โมเดลเชิงตรรกะมี anchor 7 รายการ, attribute 21 รายการ และลิงก์ 10 รายการ
- โดยทั่วไปจะได้ 9 ตารางจากการนำจำนวน anchor มาบวกกับจำนวนลิงก์ M:N แต่ในการออกแบบขั้นสุดท้าย DayOfTheWeek ถูกจัดการเป็นกรณีพิเศษ จึงไม่สร้างตารางเชิงกายภาพ
- ตาราง SQL ขั้นสุดท้ายมีทั้งหมด 8 ตาราง
userstimezonesday_eventstime_eventsday_slotstime_slotsday_event_dowstime_event_dows
- ลิงก์ 1:N ถูกใส่เป็นคอลัมน์ลักษณะ foreign key ในตารางฝั่ง N
day_events.user_idtime_events.user_idtime_events.start_timezone_idtime_events.end_timezone_idday_slots.day_event_idtime_slots.time_event_idtime_slots.start_timezone_idtime_slots.end_timezone_id
- ลิงก์ M:N สร้างเป็นตารางแยก
day_event_dowsเชื่อม DayEvent กับวันในสัปดาห์time_event_dowsเชื่อม TimeEvent กับวันในสัปดาห์
- เลือก physical type ให้สอดคล้องกับ logical type
- สตริงใช้
VARCHAR - วันที่ใช้
DATE - วันที่·เวลา local ใช้
DATETIME - yes/no ใช้
TINYINT UNSIGNED - attribute ที่เกี่ยวพันกันและมีความหมายเฉพาะตามเงื่อนไข ใช้ type ที่อนุญาต
NULL
- สตริงใช้
NULLและค่า sentinel ถือเป็นแนวคิดของ physical schema ไม่ใช่โมเดลเชิงตรรกะ
อินเด็กซ์และ attribute ที่ละไว้
- ตัวอย่าง SQL schema ไม่มีอินเด็กซ์บางรายการที่นักพัฒนาฐานข้อมูลผู้มีประสบการณ์มองว่าจำเป็น
- เช่น
day_events.user_idอาจต้องมีอินเด็กซ์
- เช่น
- การจะใส่อินเด็กซ์ที่คอลัมน์ใดหรือชุดคอลัมน์ใด ขึ้นอยู่กับว่าแอปพลิเคชัน query ตารางอย่างไร
- สำหรับหัวข้ออินเด็กซ์ ดูได้ที่ Use The Index, Luke
- ระหว่างอธิบาย logical schema มี attribute บางรายการที่ละไว้เพราะใช้แพตเทิร์นคล้ายกัน
- ชื่อผู้ใช้
- hash รหัสผ่านผู้ใช้
- สถานที่ของอีเวนต์
- รายชื่อผู้ได้รับเชิญ
- องค์ประกอบข้อมูลที่ละไว้สามารถเพิ่มเข้าไปในตารางโมเดลเชิงตรรกะด้วยวิธีเดียวกัน แล้วสะท้อนใน SQL schema ได้
ขั้นตอนทั้งหมด
- เริ่มจากข้อความรูปแบบอิสระที่อธิบายปัญหาทางธุรกิจ
- จัดทำรายการ anchor
- จัดทำรายการ attribute โดยเขียนความหมายของ attribute ให้ชัดเจนในรูปคำถาม
- จัดทำรายการลิงก์ และตรวจสอบด้วยประโยคว่า cardinality ถูกต้องหรือไม่
- หากจำเป็น ให้สร้าง schema แบบกราฟิกจากโมเดลเชิงตรรกะ
- สร้างโมเดลเชิงกายภาพโดยเติมชื่อตาราง ชื่อคอลัมน์ และชนิดข้อมูลเชิงกายภาพ
- ใช้ข้อมูลจากขั้นก่อนหน้าเขียน SQL schema เป็นคำสั่ง
CREATE TABLE - ส่ง schema ไปยังเซิร์ฟเวอร์ฐานข้อมูล แก้ typo แล้วส่งอีกครั้ง
- แชร์โมเดลเชิงตรรกะกับทีม
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าซีเรียลไลซ์เหตุการณ์แบบสุ่มหนึ่งรายการในปฏิทินของผมเป็น iCalendar จะมีขนาดราว 740 ไบต์ และแม้แต่ปฏิทินที่ยุ่งสุดขั้วซึ่งมีเหตุการณ์ทุก ๆ 15 นาทีระหว่าง 9 โมงเช้าถึง 5 โมงเย็น ตลอดทั้งปีก็มีแค่ 11,680 รายการ หรือราว 8MB เท่านั้น
ดังนั้นแทนที่จะออกแบบสคีมา ปัญหานี้ดูเหมือนปฏิทินอาจเหมาะกับการทำเป็น การสแกนตามลำดับ มากกว่ามาก ถ้าปรับ iCalendar parser ให้กวาดช่วงเหตุการณ์ที่ dump ออกมาได้ด้วย throughput ระดับ GB/s กรณีเลวร้ายข้างต้นก็สแกนได้ภายในไม่กี่มิลลิวินาที
การปรับ parser ให้เหมาะสมเป็นปัญหาที่ง่ายกว่ามากและแก้ครั้งเดียวจบ เมื่อเทียบกับการเปลี่ยนหรือขยายโมเดลข้อมูลที่ไม่ดีหลังจากแพร่ไปยังผู้ใช้จำนวนมากแล้ว และยังไงงานแรกของโมเดลข้อมูลใหม่ก็น่าจะเป็นการ import/export iCalendar อยู่ดี จึงดูเป็นการประนีประนอมที่ค่อนข้างดี
BETWEEN ... AND ...ของ SQL ขึ้นมาใหม่ไม่ใช่หรือ? การหาเหตุการณ์ของผู้ใช้ 1, 2, 3 ก็เหมือนกันถ้าเป็นแอปพลิเคชันจริง ก็น่าจะมีสิ่งอย่างบัญชีผู้ใช้อยู่ในฐานข้อมูลเชิงสัมพันธ์อยู่แล้ว แต่จู่ ๆ ก็ต้องสแกนเหตุการณ์ในไดเรกทอรีแล้วเอาผลลัพธ์นั้นไปเชื่อมกับเรคคอร์ดในฐานข้อมูล
สำหรับบางแอปพลิเคชันอาจถูกต้อง แต่มีงานบางอย่างที่ฐานข้อมูลทำได้ดีจริง ๆ และนั่นทำให้มันเป็นตัวเลือกที่ดี หากมีดัชนีที่เหมาะสม ก็น่าจะได้ throughput ระดับเดียวกันหรือดีกว่าด้วยซ้ำ เว้นแต่จะสร้างโครงสร้างไดเรกทอรีอัจฉริยะสำหรับเหตุการณ์ ซึ่งจริง ๆ แล้วก็เหมือนดัชนีมิติเดียว และในฐานข้อมูลสามารถสร้างดัชนีได้หลายมิติรวมถึงการผสมมิติต่าง ๆ
สุดท้ายก็พูดถูก และเป็นเรื่องของ การแลกเปลี่ยนข้อดีข้อเสีย
ตัวรูปแบบเผยให้เห็นอายุของมัน คือดูออกว่าออกแบบมาก่อนที่ XML/JSON จะ “ฮิต”
อ้างอิง: https://en.wikipedia.org/wiki/ICalendar
เป็นบทความเกี่ยวกับการจัดการ กำหนดการซ้ำ ในแอปพลิเคชัน ซึ่งส่วนตัวแล้วเปิดหูเปิดตามาก แนะนำให้อ่านจริง ๆ: https://github.com/bmoeskau/Extensible/blob/master/recurrenc...
บทความต้นฉบับบอกให้เก็บเขตเวลาไว้กับทุกวันที่เสมอ ส่วนบทความที่ลิงก์บอกให้แปลงทั้งหมดเป็น UTC ตรงส่วนนี้ผมเห็นด้วยกับบทความต้นฉบับ
บทความต้นฉบับบอกให้สร้างแถวในฐานข้อมูลสำหรับแต่ละเหตุการณ์ ส่วนบทความที่ลิงก์บอกว่าอย่าทำ ตรงส่วนนี้ผมเห็นด้วยกับบทความที่ลิงก์
ครั้งที่ผมพลาดหนักที่สุดในการสัมภาษณ์คือตอนต้องทำระบบจองแบบง่าย ๆ แต่ต้องรองรับ การจองซ้ำ ด้วย
ตั้งแต่ช่วงปีแรก ๆ ในมหาวิทยาลัย ผมไม่เคยรู้สึกหลงทางและสับสนขนาดนั้นเวลาพยายามทำอะไรให้สำเร็จด้วยโค้ดเลย ทุกวันนี้ถ้าต้องทำงานเกี่ยวกับวัน/เวลา ผมก็กลัวขึ้นมาก่อน และไม่รู้ทำไมมันถึงไม่เข้าที่เข้าทางในหัว
ดูเผิน ๆ เหมือนง่าย แต่ถ้าพยายามให้ทำงานได้ทั่วไปในโลกจริง ปัญหาจะ ซับซ้อนจนแทบเป็นไปไม่ได้
นี่เป็น ปัญหาที่ยากแบบหลอกตา และต้องใช้ทักษะการทำ data modeling ที่ยอดเยี่ยม
ความสามารถในการสร้างแบบจำลองระบบถูกประเมินต่ำเกินไป ในโดเมนใหม่ ๆ ควรเริ่มจาก การวิเคราะห์ปัญหาทั้งหมด เพื่อจับให้ได้ทั้งโครงสร้างเชิงสถิตของระบบ คือโมเดลคลาส และพฤติกรรมเชิงไดนามิก คือกรณีการใช้งาน
ถ้ากระโดดเข้าไปที่โมเดลฐานข้อมูลเชิงสถิตทันที ก็มีโอกาสพลาดพฤติกรรมเชิงไดนามิกได้ง่าย ในแอป CRUD ง่าย ๆ แบบนี้อาจไม่เป็นไร แต่ในระบบที่ซับซ้อนกว่านั้นอาจเป็นความผิดพลาดครั้งใหญ่
คำถามล่าสุดที่จำได้คือจะสร้างตารางสินค้าที่มีคูปองอย่างไร ตอนแรกคิดว่าสองตารางก็น่าจะพอ แต่ต้องเก็บประวัติด้วยไม่ใช่หรือ? ถ้าอย่างนั้นก็ต้องใส่วัน/เวลาตามสินค้าและคูปอง แล้วคอยอัปเดต ทีนี้ก็ต้องคิดถึงดัชนีของตารางอีก และต้องพิจารณาด้วยว่าการ join เพื่อหาราคาหลังส่วนลดเป็นวิธีที่ดีไหม คูปองส่วนใหญ่ใช้ได้คนละครั้ง แล้วจะ implement ยังไงกันแน่?
เขาน่าจะต้องการแค่ ตารางสินค้า + คูปอง ง่าย ๆ แต่ผมกลับขุดลึกแบบบ้าคลั่งอยู่คนเดียวไปพักใหญ่
ผมคิดว่าเป็นโดเมนที่เลือกมาได้ดีสำหรับแสดงการ modeling และเป็นบทความปูพื้นฐานที่ดี
คำว่า “anchor” รู้สึกแปลกอยู่นิดหน่อย แต่คำอธิบายก็เป็นรูปธรรมและติดดินเหมือนสมอจริง ๆ จึงพอเข้าใจได้
แนวคิดในการนิยามคุณลักษณะด้วยคำถามนั้นแข็งแรงดี เรามักกระโดดไปตั้งชื่อคอลัมน์/คุณลักษณะขั้นต่ำทันทีโดยยังไม่ได้นิยามว่าเรากำลังพยายามตอบคำถามอะไร ซึ่งทำให้ไม่สามารถขจัดความคลุมเครือในหัวลูกค้าได้ การนิยามคุณลักษณะโดยอิงคำถาม เป็นวิธีที่ดีในการทำให้ชัดเจนได้อย่างรวดเร็ว
อีกอย่าง คำนี้ถูกใช้ทับซ้อนกันหนักมาก ในวิทยาการคอมพิวเตอร์ แทบทุกอย่างไม่เป็น object ก็เป็น entity
ปฏิกิริยาที่ว่าแนวคิดนิยามคุณลักษณะด้วยคำถามนั้นแข็งแรงดี เป็นการยืนยันสำคัญว่าแนวทางนี้สมเหตุสมผล
เขตเวลา ทำให้ปวดหัวเป็นพิเศษ โดยเฉพาะช่วงใกล้จุดเปลี่ยนเวลา
ลองสมมติว่าเขตเวลาขยับไปข้างหน้าหนึ่งชั่วโมงเมื่อเข้าสู่เวลาออมแสง และถอยกลับหนึ่งชั่วโมงเมื่อกลับสู่เวลามาตรฐาน
เมื่อเวลาข้ามไปข้างหน้าหนึ่งชั่วโมง อีเวนต์ความยาว 1 ชั่วโมงอาจดูบนหน้าจอเหมือนกินเวลา 2 ชั่วโมง และชั่วโมงที่สองนั้นอาจเข้าถึงไม่ได้หรือไม่มีอยู่จริง
เมื่อเวลาถอยกลับหนึ่งชั่วโมง อีเวนต์ความยาว 1 ชั่วโมงอาจดูเหมือน 2 ชั่วโมง หรือดูเหมือน 0 ชั่วโมงก็ได้
เขตเวลาเป็นสิ่งที่มนุษย์สร้างขึ้น จึงไม่ควรฮาร์ดโค้ดค่าไว้ สักวันมันก็เปลี่ยน
คุณอาจข้ามเขตเวลาไปมาได้ทั้งสองทิศทาง ข้ามเส้นแบ่งวันสากลได้ และอาจเกิดกรณีที่วันที่/เวลาเริ่มต้นตามเวลาท้องถิ่นอยู่หลังวันที่/เวลาสิ้นสุดตามเวลาท้องถิ่นด้วย
สู้ไปโฟกัสที่การเลิก แนวคิดเวลาออมแสง ในบางพื้นที่ที่ยังใช้อยู่จะดีกว่า โดยเฉพาะตอนวางแผนกำหนดการหลังการเปลี่ยนเวลาที่กำลังจะมาถึง มันเป็นตัวก่อความสับสนระหว่างเขตเวลามากที่สุด
ไม่อยากเก็บวันที่สองค่าไว้ในอีเวนต์ เก็บ เวลาเริ่มต้นและระยะเวลา ของอีเวนต์จะง่ายกว่า
วิธีนี้ทำให้ลอจิกการอัปเดตอีเวนต์เรียบง่ายขึ้น เวลาสิ้นสุดสามารถคำนวณจากเวลาเริ่มต้นและระยะเวลาได้ทุกเมื่อ
เคยทำแอปปฏิทินที่มีตารางเวลาซ้ำ ๆ มาก่อน หลังจากค้นคว้ามาเยอะ เราตัดสินใจใช้ RRules เพื่อแทนสิ่งนี้ และพอใจกับมันมาก งานช่วงแรกนั้นทำตอนที่ผมอยู่เอเจนซี
ต่อมาผมเข้าร่วมบริษัทนั้นแบบพนักงานประจำ แล้วก็ถึงกับตกใจเมื่อเห็นว่าผู้รับเหมาจากบริษัทอื่นลบ RRules ออก และเปลี่ยนไปเป็นวิธีสร้างและลบอินสแตนซ์ของอีเวนต์แบบทันทีแทน มันแทบไม่มีความทนทานต่อความล้มเหลวเลย สคริปต์ที่บางครั้งก็ล้มเหลวระหว่างทำงานอื่น ๆ ไปด้วย มักจะสร้างอีเวนต์ใหม่ไม่สำเร็จ ผลก็คืออีเวนต์รายเดือนที่ควรเกิดซ้ำมีบางเดือนหายไป
มันน่าหงุดหงิดมากที่มีคนลบสิ่งที่ผ่านการคิดและค้นคว้ามาอย่างมากออกไป โดยไม่ได้ลงแรงเท่า ๆ กันเลย อยู่บริษัทนั้นไม่กี่สัปดาห์ก็รู้ได้แล้วว่า CEO คาดหวังให้ทีมวิศวกรรมผลิตฟีเจอร์ตามใจเขาที่ไม่มีใครใช้ต่อไปเรื่อย ๆ และเพราะความไม่แน่นอนของตลาดงาน ผมจึงต้องอยู่ต่ออย่างเศร้า ๆ ถึง 2 ปี
แถมภายหลังพอค้นดู ก็รู้สึกขมขื่นมากเมื่อเห็นรีวิวปลอมบน Glassdoor ที่เห็นชัดว่า CEO เป็นคนเขียน ทั้งหมดใช้สำนวนเดียวกันและไม่มีคำวิจารณ์แย่ ๆ เลย ผมกับคนรู้จักหลายคนที่เคยทำงานที่นั่นเกลียดเขา แต่ก็ยังพอปลอบใจได้ว่าผมได้เขียนเอสเซย์ที่ดีที่สุดบางชิ้นตอนอยู่บริษัทนั้น ส่วน CTO ก็หมดคำจะพูดเหมือนกัน
ครั้งหนึ่งเคยทำแบ็กเอนด์สำหรับควบคุมปฏิทินและทรัพยากรให้แพลตฟอร์ม low-code
คอนโทรลนั้นปรับแต่งได้สูงมาก เลือกได้ทั้งมุมมองหลายแบบ เช่น รายวัน รายเดือน รายปี รวมถึงมุมมองทรัพยากรด้วย สามารถจองทรัพยากรด้วยกลุ่มแบบกำหนดเอง และจัดกลุ่มตามปลั๊กอิน, ตาม resource ID ฯลฯ ได้ มีการกำหนด “plugins” ในแหล่งข้อมูล รวมถึงระบุคอลัมน์เริ่มต้น/สิ้นสุด คอลัมน์ชื่อเรื่อง และคอลัมน์ทรัพยากร ทรัพยากรอาจมาจาก foreign key หรือความสัมพันธ์ 1:1 ก็ได้ หรือมาจากความสัมพันธ์ 1:N ของแหล่งข้อมูล “ลูก” หรือจากแหล่งข้อมูล/ตารางเดียวกันก็ได้
นอกจากนี้ยังทำ ชุดนัดหมาย หลากหลายแบบ เช่น รายเดือน รายสัปดาห์ เฉพาะวันในสัปดาห์ และรายวัน พร้อมให้เลือกได้ว่าจะคัดลอกค่าคอลัมน์ใดบ้าง เรายังจัดการการชนกันของนัดหมายด้วย โดยสามารถกำหนดให้ถือว่าชนกันเฉพาะเมื่อจองทรัพยากรเดียวกันได้ ก่อนหรือหลังนัดหมายยังตั้ง buffer ที่ห้ามใส่นัดหมายอื่นได้ด้วย
แม้จะท้าทายเป็นครั้งคราวเพราะเขตเวลาในยุโรปและเวลาแบบฤดูร้อน/ฤดูหนาว แต่ก็เป็นงานที่ค่อนข้างสนุก
สิ่งที่อยากได้จาก Google Calendar มีแค่ change log ของตัวปฏิทินเองเท่านั้น ขอเถอะ ช่วยเพิ่มสิ่งนี้ลงในฐานข้อมูลที