ORM ยังเป็น anti-pattern อยู่หรือไม่?
(github.com/getlago)- ORM มักถูกวิจารณ์ว่าเป็น “ของเล่นสำหรับสตาร์ตอัป” แต่ประเด็นจริงมักใกล้เคียงกับ วิธีใช้งานที่ผิด มากกว่าตัว ORM เอง
- object graph กับ relational table เป็นคนละโมเดลกัน จึงเกิด paradigm mismatch แต่เพียงเท่านี้ก็ยังยากจะสรุปว่า ORM เป็น anti-pattern
- ข้อถกเถียงเรื่องการละเมิด SRP และ SOC ยังมีน้ำหนักอยู่ แต่ ORM เดิมทีก็เป็น abstraction ที่เชื่อมข้อมูลสองโมเดลเข้าด้วยกัน จึงตัดสินได้ยากจากแค่การละเมิดหลักการ
- ปัญหาด้านประสิทธิภาพมักไม่ได้เกิดจาก ORM โดยตรง แต่เกิดจากแพตเทิร์นที่นำข้อมูลมาประกอบกันใน ตรรกะของภาษา host มากกว่า และลดได้ด้วย ความสามารถคล้าย SQL ของ ORM
- เหตุผลหลักที่ Lago เปลี่ยน Active Record query ไปเป็น raw SQL ไม่ใช่เรื่องความเร็ว แต่เป็นเรื่อง การมองเห็นได้ชัดเจนและการดีบัก และสำหรับ query ขนาดใหญ่ การย้ายไปใช้ raw SQL อาจคุ้มค่ากว่าในทางปฏิบัติ
มุมมองพื้นฐานต่อคำวิจารณ์ ORM
- ORM มักถูกโจมตีว่าเป็น “anti-pattern” แต่ข้อสรุปว่า ORM เองไม่ดีนั้นค่อนข้างเกินจริง
- เช่นเดียวกับ abstraction อื่น ๆ ORM ก็ไม่ได้สมบูรณ์แบบ และอาจทำให้เกิด การสูญเสียความชัดเจนในการมองเห็น และ ต้นทุนด้านประสิทธิภาพ บางส่วน
- Lago เคยพึ่งพา Active Record ซึ่งเป็น ORM ของ Ruby on Rails แล้วพบปัญหา จึงใช้โอกาสนี้กลับมาทบทวนแนวทางการพึ่งพา ORM ใหม่
- บทสรุปไม่ได้ไปในทางว่าควรทิ้ง ORM แต่ใกล้เคียงกับการมองว่า ORM เป็น abstraction ที่มีทั้งข้อดีและข้อเสียชัดเจน
ความแตกต่างระหว่าง object model กับ relational database
- object ที่ ORM จัดการมีลักษณะใกล้เคียงกับ directed graph ที่ชี้ไปยัง node อื่น
- ตารางใน relational database มีลักษณะใกล้เคียงกับ undirected graph ที่ข้อมูลเชื่อมถึงกันแบบสองทางผ่าน shared key
- ORM สามารถเลียนแบบ undirected graph ได้ แต่การตั้งค่าและพฤติกรรมไม่ได้เรียบง่ายเสมอไป
- object
Userอาจไม่มีอาร์เรย์Posts - entity ในอาร์เรย์
Postsอาจไม่มี reference ย้อนกลับไปยัง objectUserเดิม - ต่อให้มี reference ย้อนกลับ ก็อาจไม่ใช่ object เดิม แต่อาจเป็น object ที่ถูกคัดลอกขึ้นมา
- object
- ตัวอย่างเช่น ORM อาจคืนโพสต์ของผู้ใช้มาเป็นอาร์เรย์ แต่ไม่ได้ใส่ user back-reference ในบทบาทผู้เขียนไว้ในแต่ละโพสต์
- ความไม่สอดคล้องนี้เป็น คำวิจารณ์เชิงวิชาการ ที่ดีต่อ ORM แต่ในระบบจริง ปัญหาที่ใหญ่กว่ามักปรากฏจากรูปแบบการใช้งานที่เฉพาะเจาะจง
ข้อถกเถียงเรื่อง SRP และ SOC
-
Single Responsibility Principle (SRP)
- ORM ถูกวิจารณ์ว่าละเมิด Single Responsibility Principle (SRP)
- เพราะมันอาจทำให้ class หรือ layer เดียวต้องรับหลายบทบาทพร้อมกัน
- ทำงานกับฐานข้อมูลและ transaction
- แสดงแทน record
- นิยามความสัมพันธ์
- สร้างและรัน migration
- แต่เนื่องจากจุดประสงค์ของ ORM คือการเชื่อม data paradigm ที่ต่างกันสองแบบ จึงยากจะปฏิเสธ ORM เพียงเพราะมีการละเมิดหลักการบางส่วน
-
Separation of Concerns (SOC)
- ORM อาจทำให้หลัก Separation of Concerns (SOC) เลือนรางลงด้วย
- SOC คือหลักที่ว่าองค์ประกอบของโครงสร้างพื้นฐานควรมี concern เดียว
- ORM ย้ายการจัดการฐานข้อมูลเข้ามาอยู่ฝั่ง backend มากขึ้น จึงอาจมองได้ว่าเป็นการละเมิด SOC
- อย่างไรก็ตาม ปัจจุบันองค์ประกอบโครงสร้างพื้นฐานและ coding pattern มักผสานหลายบทบาทเข้าด้วยกันเพื่อประสิทธิภาพ latency และการจัดระเบียบโค้ด
- CPU aggregator ภายในฐานข้อมูล OLAP
- edge backend-frontend
- monorepo
ปัญหาด้านประสิทธิภาพมักเกิดจากวิธีใช้มากกว่าตัว ORM
- คำวิจารณ์ที่ว่า ORM ไม่มีประสิทธิภาพเสมอนั้น โดยมากไม่ถูกต้อง
- ORM หลายตัวทำงานได้มีประสิทธิภาพกว่าที่นักพัฒนาคิดไว้มาก
- ปัญหาคือ ORM ทำให้การนำข้อมูลมาประกอบกันด้วย ตรรกะของภาษา host อย่าง JavaScript หรือ Ruby เป็นเรื่องง่าย
- ในตัวอย่างของ TypeORM รูปแบบที่ไม่ดีคือการดึงผู้เขียนของบริษัทหนึ่งมาก่อน จากนั้นค่อยดึงโพสต์ของผู้เขียนแต่ละคนอีกครั้ง และบันทึกแต่ละโพสต์แยกกัน
- วิธีที่ดีกว่าคือใช้
createQueryBuilder()ของ TypeORM เพื่อประกอบ update query เดียว - ในการรีแฟกเตอร์ SQL สำหรับระบบ billing ของ Lago ก็ ไม่มีความต่างด้านประสิทธิภาพ ระหว่างเวอร์ชัน Active Record กับ raw SQL
- เพราะ query เดิมก็ถูก optimize ไว้แล้วจากการใช้ความสามารถในการรวมข้อมูลของ Active Record อย่างมาก
- การเขียนใหม่เป็น raw SQL จึงใกล้เคียงกับการเพิ่มความชัดเจนในการมองเห็น มากกว่าการเพิ่มประสิทธิภาพ
กรณีที่ ORM อาจช้าลงได้จริง
- ORM ไม่ได้มีประสิทธิภาพเทียบ raw SQL เสมอไป และในบางสถานการณ์อาจไม่มีประสิทธิภาพอย่างมาก
- ปัญหาแรกคือ ORM อาจสร้าง overhead ในการคำนวณ สูงมากตอนแปลงผลลัพธ์ของ query ให้เป็น object
- TypeORM ถูกยกเป็นตัวอย่างของปัญหานี้
- ปัญหาที่สองคือกรณีวนผ่านความสัมพันธ์แบบ one-to-many หรือ many-to-many แล้วส่งคำขอไปกลับฐานข้อมูลหลายครั้ง
- แพตเทิร์นนี้เรียกว่า ปัญหา N+1
- จาก query หลัก 1 ครั้ง มี subquery เพิ่มอีก N ครั้ง
- ในตัวอย่างของ Prisma หากดึง user, post และ comment แบบซ้อนกัน อาจเกิดคำขอฐานข้อมูลใหม่สำหรับแต่ละ comment
- N+1 พบได้บ่อยใน ORM แต่หากใช้ data loader ก็สามารถยุบ query จาก N+1 ครั้งให้เหลือ 2 ครั้งได้
- ปัญหาทั่วไปของ ORM จำนวนมากสามารถหลีกเลี่ยงได้ หากใช้ความสามารถของ ORM ได้เต็มที่เพียงพอ
ปัญหาที่ใหญ่กว่าคือความชัดเจนในการมองเห็นและการดีบัก
- จุดอ่อนที่สุดของ ORM คือ ความชัดเจนในการมองเห็น
- เพราะ ORM แท้จริงแล้วเป็น query builder ดังนั้นยกเว้นกรณีอย่าง type error ที่ชัดเจนของ raw type มันจึงไม่ใช่ตัวส่งต่อข้อผิดพลาดปลายทางโดยตรง
- เมื่อ SQL error ถูกส่งกลับมา ORM ต้องตีความแล้วส่งต่อให้ผู้ใช้อีกทอดหนึ่ง
- Active Record มีปัญหาในจุดนี้ และนี่ก็เป็นเหตุผลที่ Lago รีแฟกเตอร์ query สำหรับ billing subscription
- เมื่อได้ผลลัพธ์ไม่ตรงคาด ต้องวนทำกระบวนการต่อไปนี้ซ้ำ
- ตรวจดู SQL query ที่ render ออกมา
- รัน SQL นั้นใหม่อีกครั้ง
- ย้อน SQL error กลับไปเป็นการแก้ไขฝั่ง Active Record
- การไปกลับแบบนี้ทำให้เป้าหมายเดิมของการใช้ Active Record เพื่อไม่ต้องโต้ตอบกับฐานข้อมูล SQL โดยตรงอ่อนลง
การตัดสินใจเชิงปฏิบัติ
- หากใช้อย่างถูกต้อง ORM สามารถให้ประสิทธิภาพใกล้เคียง raw SQL ได้
- ปัญหาจำนวนมากเกิดขึ้นเมื่อไม่ใช้ความสามารถคล้าย SQL ของ ORM และพึ่งโครงสร้างตรรกะของภาษา host มากเกินไป
- หาก query ขนาดใหญ่ทำให้นักพัฒนาทำงานลำบากและขัดขวางการดีบัก การย้ายไปใช้ raw SQL query อาจเป็นการลงทุนที่ดี
- ORM ส่วนใหญ่มีความสามารถให้รัน SQL query ภายใน ORM ได้
1 ความคิดเห็น
ความเห็นจาก Hacker News
ฉันใช้ ORM มาตั้งแต่สมัยที่ Hibernate เพิ่งออกมาใหม่ ๆ ใน Java แต่ก็ไม่ค่อยชอบมันมาตลอด
ข้อดีที่ว่า “สามารถเปลี่ยนไปใช้ฐานข้อมูลตัวอื่นได้” พอมองตอนนี้ก็แทบเป็นคำพูดลอย ๆ เพราะในความเป็นจริงไม่มีใครใช้แบบนั้น
คำพูดที่ว่า “ไม่จำเป็นต้องรู้ SQL” ก็ไม่จริง และแอปที่ไม่ trivial และอยู่ยาว สุดท้ายก็ต้องมาปรับแต่ง query รายตัวในระดับสตริงอยู่ดี
สำหรับ data layer วิธีที่ถูกต้องคือสร้างและประกอบ query ทีละตัวด้วย SQL แบบสตริง และยิ่งใกล้กับ raw JDBC เท่าไรก็ยิ่งดีกว่าในมุมมองของฉัน
คำอ้างว่า ORM สนับสนุน “domain model” ก็เป็นเหตุผลแย่ ๆ เช่นกัน เพราะ domain model แบบนั้นสุดท้ายจะกลายเป็น anemic domain model ที่ไม่มี logic อยู่เสมอ
พอคิดถึงเวลาที่เสียไปกับ ORM, XML, annotation และการ debug SQL ที่ถูก generate ออกมา ก็อยากจะร้องไห้
ส่วนที่ง่ายมันก็ง่ายอยู่แล้วเลยไม่ได้สนใจมาก แต่สิ่งที่เจอบ่อยคือในช่วงเลวร้ายที่สุด ตอนที่ query พังเมื่อระบบเริ่มมีขนาดใหญ่ งานกลับยากขึ้นเป็นล้านเท่าเพราะต้องมานั่งสู้กับ ORM เอง
query builder อาจไม่เหมือน ORM เสียทีเดียว แต่ฉันชอบบทความ https://gajus.medium.com/stop-using-knex-js-and-earn-30-bf41... และกลายเป็นแฟนของเทคโนโลยีสาย Slonik
มันให้ความสามารถที่ต้องการราว 90% เช่น bind ผลลัพธ์เข้ากับ object อัตโนมัติและตรวจสอบ type อย่างเข้มงวด ขณะเดียวกันก็ยังเปิดให้ใช้ SQL แบบธรรมดาได้
ฉันก็ไม่เข้าใจเหมือนกันว่าทำไมคนถึงคิดว่า SQL เป็นภาษาที่ลึกลับและยากเย็น มันอาจเก่าและมีข้อบกพร่อง แต่ก็ยังดีกว่าไปเรียนรู้ภาษา query เฉพาะทางที่เครื่องมือ ORM สร้างขึ้นมา
ตอนนั้นถ้าพูดคำวิจารณ์แบบนี้ นักพัฒนาคนอื่นมักจะมองลงมาใส่
แม้แต่ตอนนี้ก็ยังมีเครื่องมือและเทคนิคจำนวนมากที่ในฐานะนักพัฒนาต้องฝืนใช้ทั้งที่เป็น anti-pattern และความจริงอันน่าเศร้าของวงการก็คือ เครื่องมือที่ถูกใช้อย่างแพร่หลายจำนวนไม่น้อยนั้นไม่ค่อยดีและยิ่งส่งเสริม anti-pattern
ในอุตสาหกรรมเทคโนโลยีมีนักพัฒนาและอดีตนักพัฒนาจำนวนน้อยที่มีเส้นสายและถืออำนาจในการตัดสินใจ ซึ่งพวกเขาก็ให้ความรู้สึกว่าไม่ค่อยดีนัก
ก็มี ORM ที่สัดส่วนข้อดีข้อเสียค่อนข้างโอเคอยู่เหมือนกัน โดยเฉพาะฝั่ง Python
Django ORM อาจจะหยาบ ๆ และประสิทธิภาพไม่เด่น แต่ผสานรวมได้ดี ใช้งานได้จริง productive และประสบการณ์ใช้งานก็ดี
Peewee เป็นแพ็กเกจเล็ก ๆ ที่ให้ ORM มาสำหรับเวลาที่ไม่ได้ต้องการฟีเจอร์ยิ่งใหญ่อะไรมาก และแค่อยากเขียนโปรแกรมเล็ก ๆ แบบสบาย ๆ ไม่ให้หงุดหงิด
SQLAlchemy ต้องลงทุนเรียนรู้มากกว่า แต่ยืดหยุ่นมาก สร้าง SQL ที่สะอาด และทำงานได้ถูกต้อง
มันยังเปิดให้ใช้ query builder ระดับล่างกว่าด้วย สำหรับเวลาที่ไม่ต้องการแนวคิดเชิงวัตถุ แต่ต้องการ abstract พฤติกรรม SQL แบบ idiomatic มาแสดงใน Python
สุดท้ายมันก็เป็นการตัดสินใจทางวิศวกรรมทั่วไปที่ต้องชั่ง ผลตอบแทนต่อการลงทุน
ฉันทำมาเกิน 10 ปีแล้ว และไม่เคยมี query ไหนที่อ่านเอกสาร Hibernate แค่ 5 นาทีแล้วทำตามแนวทางสำหรับกรณีนั้นจะจัดการไม่ได้ดีกว่า
ดูเหมือนว่าเหล่า แฟนบอย SQL ที่เชื่อว่านักพัฒนาทุกคนต้องท่องตารางตรรกะสามค่าและชื่อฟังก์ชันสไตล์ COBOL กลับไม่ยอมเสียแรงอ่านเอกสาร Hibernate เสียมากกว่า
ORM มีไว้สำหรับ OLTP workload ไม่ใช่ OLAP และหนึ่งในผู้ก่อตั้ง Hibernate ก็เคยพูดแบบนั้น
ประโยชน์หลักมีแค่ช่วยไม่ต้องเขียน query INSERT/UPDATE ยาว ๆ ที่ผิดพลาดได้ง่ายด้วยมือ และช่วย map row ของ SQL ไปเป็น object แบบอัตโนมัติ
ถ้าเลือกไปทาง raw คุณก็ต้องกลับไปเขียนหลายส่วนพวกนั้นขึ้นมาใหม่เอง ดังนั้นมันก็ไม่ใช่เกมที่ผลรวมเป็นศูนย์อยู่ดี
ในแอปที่ซับซ้อนมากพอ สุดท้ายก็มักจะมีบางอย่างอย่าง query builder เกิดขึ้น เพื่อให้หลายส่วนของแอปร่วมกันประกอบ query แบบหน่วงเวลาได้
โครงสร้างจะกลายเป็นการประกอบเป็น query แทนที่จะเอาข้อมูลมารวม กรอง และเรียงลำดับกันในหน่วยความจำ
ORM เป็นเครื่องมือที่ช่วยให้ทำแบบนี้ได้ง่าย แต่ก็อาจทำให้เผลอสร้าง N+1 query โดยไม่รู้ตัวจนเจ็บตัวได้เช่นกัน
เหมือนเครื่องมือทุกชนิด ต้องชั่งน้ำหนัก trade-off กับกรณีใช้งานจริง และไม่แนะนำวิธีจัดการสตริงคำสั่ง SQL โดยตรงไม่ว่าในกรณีใดก็ตาม
ถึงอย่างนั้นก็ยังมีคนที่มองข้ามความต่างเหล่านั้นแล้วพูดแบบเหมารวมว่า ORM ดีหรือแย่เสมอ
แบบนั้นมองว่าเป็นท่าทีชวนถกเพื่อเรียกความสนใจแล้วข้ามไปดีกว่า และอยากโฟกัสกับการคุยที่เริ่มจากว่า “ORM มีข้อดี แต่ต้องรู้ว่ามันใช้ได้ผลตรงไหน และต้องไม่ให้ปัญหาในการใช้งานมีมากกว่าข้อดี”
ถ้าต้องการข้อมูลก็ไปที่ data access layer และถ้ามันยังไม่ให้สิ่งที่ต้องการ ก็สร้าง repository หรือ provider ใหม่ตามแพตเทิร์นที่ใช้อยู่
ไม่ว่าจะเป็น ORM หรือเครื่องมือประกอบ query ที่ทำเอง ถ้าแอปเริ่มทำให้เราหลงคิดว่า “มันซับซ้อนพอจนต้องมีสิ่งนี้” แปลว่ามันซับซ้อนเกินกว่าจะใช้งานอย่างมั่นคงแล้ว
สุดท้ายก็มักไปประเมิน builder ผิดชั้น หรือพลาดไม่ทันสังเกตว่ากำลังสร้าง N+1 query อยู่ตอนวนลูปผลลัพธ์
ORM ไม่จำเป็น
LINQ ไม่ใช่ ORM ตรง ๆ และเมื่อสร้างคำสั่ง LINQ แบบ strongly typed มันจะถูกแปลงเป็น expression tree แล้วค่อยให้ query provider แปลงต่อเป็น query
ระหว่างพัฒนาจะชอบความอ่านง่ายของ ORM ในตัว model แต่เมื่อจำเป็นก็จะคอยจับตา N+1 และความไม่มีประสิทธิภาพอื่น ๆ อย่างใกล้ชิดแล้วค่อยปรับให้เหมาะสม
เส้นแบ่งของคำว่า “เมื่อจำเป็น” ไม่ชัดนัก แต่ก็พยายามไม่ optimize เร็วเกินไป
เวลาขึ้นโปรเจกต์ใหม่มักเริ่มจากฐานข้อมูล และก่อนเขียนโค้ดจะใช้ factory, seeder, raw SQL query เพื่อทำความเข้าใจกับ data model ก่อน
ORM อย่าง Eloquent ของ Laravel มีเมธอดดี ๆ สำหรับแก้ปัญหา N+1 และทำ lazy loading แต่ก็มี trade-off เสมอ
แทนที่จะเป็นไลบรารีที่ยอมให้ประกอบ query แบบหน่วงเวลา ถ้าเขียน query ลงไปตรง ๆ ไลบรารีจะสร้างโค้ดสำหรับ query นั้นตอนคอมไพล์
เช่น ถ้าเขียน query อย่าง
getUser: SELECT * FROM users WHERE id = ?มันก็จะสร้างคลาสGetUserQueryและเมธอดอย่างgetUser(id: String): GetUserQueryResultให้ และมองว่าโมเดลแบบนี้ดีกว่ามากเหตุผลหลักที่ไม่ชอบ ORM คือมันมีแนวโน้มจะปฏิบัติต่อ SQL engine สมัยใหม่เหมือนเป็น ที่เก็บบิตโง่ ๆ ที่ถูกทำให้เกินจริง
ทั้งที่ CTE, LATERAL join, และ RETURNING clause สามารถทำให้การประมวลผลง่ายขึ้นมากหรือกำจัดความเป็นไปได้ของข้อมูลไม่สอดคล้องกันได้ แต่ ORM ส่วนใหญ่ก็ยังหยุดอยู่แค่การแมปตารางพื้นฐานและวิวเข้ากับอ็อบเจ็กต์ที่กำหนดไว้ล่วงหน้าแบบง่าย ๆ
ถ้า ORM สร้างตารางให้ด้วยก็ยิ่งแย่
SQL โดยแก่นแท้เป็นทั้งเครื่องมือดึงข้อมูลและ ภาษาเพื่อการแปลงข้อมูล แต่ ORM แทบไม่สนใจด้านนี้เลย จนทำให้นักพัฒนาจำนวนมากไม่รู้ว่าใน SQL ยังมีอะไรนอกจาก INSERT/SELECT/UPDATE/DELETE
ยังมี pivot table, temporal query, CUBE/ROLLUP, window function, set-returning function, materialized view, external table, การจัดการ JSON, การจัดการวันที่, exclusion constraint, ชนิดข้อมูลอย่าง range·interval·domain, row-level security, MERGE และอื่น ๆ
เหมือนมีโรงเก็บเครื่องมือครบชุด แต่มีคนยื่นให้แค่ค้อน ไขควง และเลื่อยเหล็กอย่างละอัน พร้อมกล่อมว่าพอแล้ว ทั้งที่โต๊ะเลื่อย เราเตอร์ เครื่องขัด และชุดลิ่มก็อยู่ห่างไปไม่กี่เมตร และคุณอาจใช้เวลาทั้งอาชีพโดยไม่รู้เลยว่ามันมีอยู่
ORM มักทำให้คนคิดอยู่กับคำสั่ง CRUD ง่าย ๆ เป็นหลัก แต่ก็มองว่านั่นอาจจะดีกว่า และควรใช้ความสามารถขั้นสูงอย่างประหยัด
ถึงอย่างนั้นถ้าต้องใช้ DB ก็จะตั้งใจใช้แค่ ส่วนย่อยเล็กมากของความสามารถ DB เท่านั้น
เพราะมองว่า “โค้ด” เป็นพื้นที่ที่ควบคุมได้ ขณะที่ “ฐานข้อมูล” เป็นพื้นที่เสี่ยง เข้าถึงยาก และข้อผิดพลาดมักไปโผล่ทีหลังในที่อย่าง integration test ที่ช้า
logic ที่เกิดขึ้นใน DB อยู่นอกขอบเขตของ low-level test เลยทำให้น่ากลัว และคงไม่ใช้แม้แต่ query แบบ
select-from-selectเครื่องมือส่วนใหญ่ในโรงเก็บนั้นยินดีจะปล่อยทิ้งไว้
เวลาส่วนใหญ่หมดไปกับการสร้างทางอ้อมที่ซับซ้อนเพื่อประดิษฐ์กลไกปกป้องความถูกต้องของข้อมูลพื้นฐานขึ้นมาใหม่
การสร้างฐานข้อมูลของตัวเองอาจสนุกในเชิงการเรียนรู้ แต่ไม่ใช่สำหรับแอป production
ActiveRecord ให้ความรู้สึกดีมาโดยตลอด
แม้จะมีข้อยกเว้นอยู่บ้าง แต่ก็จับจุด 80/20 ได้ดี ถ้าจำเป็นก็หนีไปใช้ SQL ได้ และ lifecycle hook ที่ช่วยจัดระเบียบ domain logic ก็ยอดเยี่ยมและเข้าใจง่าย
ORM เชิงปฏิบัติที่ไม่ซ่อนทุกอย่างไว้หลัง “bundle” หรือไม่เอาภาษา query ของตัวเองเข้ามานั้นเป็นเรื่องที่ดี
ORM ที่พยายาม “แก้” ปัญหา ORM ให้หมดจดต่างหากที่ใช้งานยาก
ORM ควรถูกสร้างแบบนี้
แต่ถ้าจะใช้ให้ถูกต้องก็ต้องมีพื้นฐาน SQL ที่ดีล่วงหน้า
ปัญหาเกิดขึ้นตอนที่คนไม่เรียน SQL และไม่เข้าใจ query ที่ AR สร้างขึ้นรวมถึงข้อจำกัดของมัน
สมมติฐานตั้งต้นผิด ORM ไม่เคยเป็น “แนวคิดที่เป็นอันตราย” มาก่อน
มี ORM ที่ทำงานด้วยแล้วน่าปวดหัวอยู่จริง แต่ไม่ได้แปลว่าทั้งแนวทางนี้เป็นความคิดที่แย่
ใช้ SQLAlchemy อย่างมีความสุขมาหลายปี เพราะมันไม่ได้พยายามสร้างแนวคิดของ SQL ขึ้นมาใหม่
แต่มันให้ชั้นที่สะดวกสำหรับสร้างและส่งต่อคิวรี พร้อมจัดการรายละเอียดจุกจิกต่างๆ ได้ดี ทำให้ไม่ต้องไปง่วนกับมันเอง
ช่วงหลังระหว่างเรียน Rust รู้สึกว่าการต้องเขียนโค้ด boilerplate สำหรับจัดการ CRUD ง่ายๆ เองนั้นทั้งทรมานและเสียเวลา
ORM ของ SQLAlchemy อาจเกะกะบ้างกับคิวรีที่ซับซ้อนกว่าหรือต้องการ optimize แต่ในกรณีนั้นก็ลงไปเขียน SQL ตรงๆ ได้เสมอ
เหมือนกับทุกอย่าง มันมีทั้งกรณีที่เหมาะและไม่เหมาะ และเทคโนโลยีก็ถูกใช้เกินเลยหรือใช้ผิดทางได้ง่าย
ตัวอย่างเช่น ORM อย่าง Entity Framework ของ .NET อาจเป็นทรัพย์สินชิ้นใหญ่ได้ โดยเฉพาะเวลาทำงานแบบ database-first
ถ้าเป็น DB ที่มีหลายร้อยตารางและมีความสัมพันธ์ foreign key ที่ถูกต้อง ORM จะช่วยในการสำรวจสคีมาที่ซับซ้อนและให้การตรวจสอบตอนคอมไพล์เมื่อมีการเปลี่ยนแปลง
SQL แบบฝังในโค้ดเป็นดาบสองคมที่ดูเหมือนมีข้อได้เปรียบด้านประสิทธิภาพ แต่โดยเฉพาะกับ join แล้ว SQL ยาวๆ ที่ hardcode ไว้นั้นดูแลรักษายากมาก
ไม่ได้พูดถึงตอนมี 5~10 ตาราง แต่พูดถึงตอนมีเป็นร้อยตาราง
ไม่ได้ตั้งใจจะโน้มน้าวใคร แต่ก็ไม่ควรเชื่อทุกอย่างที่อ่านเจอในอินเทอร์เน็ตเรื่อง best practice แบบตรงๆ
คนอาจกำลังพูดถึงคนละสิ่งกันโดยสิ้นเชิง และไม่ว่าจะใช้เทคโนโลยีอะไร ก็ย่อมมีนักพัฒนาที่ทำงานห่วยอยู่เสมอ
ถ้าไม่ใช้ ORM หรือ DB interface อื่นๆ สุดท้ายคุณก็จะต้องเขียนและดูแลของตัวเองขึ้นมาสำหรับเรื่องอย่าง CRUD
และมันมีโอกาสสูงที่จะมีบั๊กแปลกๆ edge case และช่องโหว่ด้านความปลอดภัยแฝงอยู่ข้างใน
การสอนหลักการอย่าง SOLID ในชั้นเรียนมหาวิทยาลัยในฐานะหลักการออกแบบซอฟต์แวร์ก็มีปัญหาเหมือนกัน
ในแวดวงวิชาการมันฟังดูสมเหตุสมผล เพราะข้อจำกัดอย่างเวลา ความง่ายในการดูแลรักษา เงิน และแรง ไม่ใช่เรื่องสำคัญเท่าไร แต่พอบัณฑิตจบใหม่เข้าสู่การทำงานจริงก็มักเกิดการปะทะอย่างหนัก
เพราะคุ้นชินกับการเสียเวลาทั้งวันไปกับการเล็มขนจามรีและประดิษฐ์ล้อขึ้นใหม่เพื่อตามหาโซลูชันที่ “สมบูรณ์แบบ”
ถ้าไม่มีซีเนียร์หรือลีดที่เก่งพอและมีจำนวนพอจะคอยห้ามไว้ คุณก็จะได้ผลิตภัณฑ์ที่น่าสนใจมาก งบบานปลาย ไฟไหม้ครั้งใหญ่ หรือไม่ก็ทั้งหมดรวมกัน
แย่ยิ่งกว่านั้นคือเวลาของที่สร้างในโลกวิชาการและออกแบบเพื่อโลกวิชาการถูกปล่อยออกมาสู่คนทั่วไป
ตัวอย่างที่ทั้งดีและแย่ในคราวเดียวคือ OpenStack ซึ่งมีปุ่มจับและจุดปรับแต่งมากมาย รวมถึงแยกองค์ประกอบเป็นโมดูลแบบสุดขั้วเพื่อรองรับสภาพแวดล้อมเฉพาะของ CERN และมหาวิทยาลัยอีกนับร้อย แต่ก็ซับซ้อนแบบไบแซนไทน์จนแทบเริ่มต้นใช้งานไม่ได้
ORM ไม่ได้ทำให้คุณสลับฐานข้อมูลได้โดยไม่ต้องออกแรง ไม่ได้ทำให้ไม่จำเป็นต้องเรียนรู้และเข้าใจ SQL ที่มันสร้างขึ้น และไม่ได้ทำให้สคีมาลื่นไหลไร้ปัญหา
ถึงอย่างนั้นเหตุผลที่ยังใช้มันก็เพราะมันคือ standard abstraction ที่ยังไงก็ต้องทำซ้ำอยู่ดี
มันทำให้การแคชการเข้าถึง DB เป็นไปได้ ซึ่งถ้าใช้ SQL เขียนมือจะยากอย่างน่าประหลาดเมื่อระบบใหญ่ขึ้น
มันมาพร้อมเครื่องมือที่เป็นมาตรฐานและ best practice ที่ช่วยให้ปัญหาอย่าง N+1 กลายเป็นเรื่องไม่สำคัญ
ปัญหาหลักของ ORM คือ ความคาดหวังที่ผิด และ ORM ที่ห่วย
ถ้าอยากรู้ว่า ORM มีคุณค่าแค่ไหน ก็ลองเปิดโปรเจกต์ที่เขียนโดยไม่มี ORM แล้วลองแก้อะไรก็ได้ดู
ดู https://news.ycombinator.com/item?id=36498583
งานที่มีประโยชน์ไม่เดินหน้าเลย หรือใช้เวลานานกว่าแนวทางเร็วๆ แบบเละๆ ถึง 20~100 เท่า
ที่แย่ที่สุดคือกรณีที่ทีมดีไซเนอร์ HTML มากประสบการณ์ถูกบังคับให้เรียน functional programming
พ่อมดตั้งค่าแบ็กเอนด์คนนั้นเป็นสิ่งมหัศจรรย์ของวิศวกรรมสมัยใหม่และการออกแบบอันงดงาม แต่ต้องใช้คน 10 คนทำอยู่ 1 ปี
เงิน VC ทำให้คนแปลกไป
ภาษา SQL นั้นซับซ้อนมาก และการสร้าง query builder ที่สมบูรณ์และมีฟีเจอร์ครบถ้วนจริงๆ ไม่ใช่เรื่องเล็กน้อยเลย
ต่อให้ไม่นับส่วนของ relational mapping ที่แท้จริงก็ยังเป็นแบบนั้น
มองว่า SQL เป็นกรณีที่เข้ากับคำว่า “ก็เขียนไอ้บ้าบอนั่นตรง ๆ ไปเลยสิ”
การเรียน SQL หรือไลบรารี ORM เองไม่ได้มีความหมายมากนัก และ การเรียนและใช้ SQL โดยตรง ดีกว่า
การเข้าถึงฐานข้อมูลน่าจะเป็นงานพื้นฐานที่ต้องทำไปตลอดทั้งอาชีพ ดังนั้นการเรียนทักษะจริงโดยตรงจึงดีที่สุด
CSS ก็คล้ายกัน แทนที่จะเรียนไลบรารี CSS ก็ควรเรียนและใช้ CSS เอง
จริง ๆ แล้วแทบทุกด้านของการพัฒนา frontend ก็เป็นแบบนั้น และแทนที่จะใช้ “ไลบรารีฟอร์ม” ก็จัดการ Form API ของเบราว์เซอร์โดยตรงได้เลย
ส่วนตัวแล้วรู้สึกว่า SQL เรียนง่ายกว่า ORM ไหน ๆ มาก
อย่าเสียต้นทุนไปกับการเรียนสิ่งที่ผิด
CSS ก็เหมือนกัน ถ้ารู้ CSS เวลาจะเลือกไลบรารีมาใช้เพื่อความสะดวกก็จะยืดหยุ่นและเข้าใจได้ดีกว่ามาก
ผ่านมาทั้งเมนเฟรม เดสก์ท็อป ไคลเอนต์/เซิร์ฟเวอร์ เว็บ และภาษาอีกหลากหลายบนแพลตฟอร์มเหล่านั้น SQL ก็ยังเป็นเส้นด้ายร่วมที่มีอยู่เสมอ
เห็นด้วยเต็มที่ว่านักพัฒนาที่เข้าใจ data layer และรู้ดีว่าเกิดอะไรขึ้นข้างล่างนั้น จะเป็นนักพัฒนาที่แข็งแกร่งกว่า
ถ้าจะใช้เวลาไปกับการเรียนความพิสดารของ ORM ก็มีคำถามคลาสสิกว่า ทำไมไม่นำเวลานั้นไปลงทุนเรียน SQL แทน
ผลทบต้นของประสบการณ์ SQL มีแนวโน้มว่าจะมีค่ามากกว่า ORM ของแพลตฟอร์มหรือภาษาหนึ่ง ๆ ในอีก 10~15 ปีข้างหน้า
มีเหตุผลที่จะเรียนและใช้ ORM อยู่ แต่ไม่ควรเอนเอียงไปทาง ORM เพียงเพราะไม่อยากเรียน SQL
แบบนั้นจึงจะรู้ว่าต้องจ่ายอะไรเป็นต้นทุนให้ abstraction นั้น แน่นอน ถ้าเป็น “zero-cost abstraction” ก็อีกเรื่องหนึ่ง
แต่ตอนนี้ใช้
crudของazerซึ่งจัดว่าเป็น ORM เพราะเรื่อง marshalling/unmarshalling กับ Go typessqlในไลบรารีมาตรฐานของ Go พื้นฐานเกินไป จนแทบต้องเขียน object marshalling/unmarshalling เองทั้งหมด ซึ่งน่าเบื่อมากถ้าทุกวันนี้จะใช้ ORM ก็คงใช้เพื่อจุดประสงค์นั้นเท่านั้น
ชอบ micro ORM แบบ “จุดกึ่งกลาง” อย่าง Dapper หรือ Diesel
เขียนคิวรีเป็น SQL แต่ได้ strong typing และการแมปผลลัพธ์เป็นอ็อบเจ็กต์แบบอัตโนมัติ
เท่ากับได้ทั้ง productivity และ “ภาษาคิวรีจริง” ที่มีความสามารถครบถ้วน
อีกแบบที่เริ่มชอบคือใช้ object mapper กับคิวรีแบบอ่านอย่างเดียวเท่านั้น
ส่วนการอัปเดตใช้ command pattern หรือก็คือ stored procedure แล้วแมปให้ดูเหมือน native methods ได้
นักพัฒนาไม่ควรแกล้งทำเหมือน SQL ไม่มีอยู่จริง แต่ควรยอมรับมันไปเลย
Eloquent abstract งานจุกจิกพวกนั้นออกไปได้ดีพอ จึงโฟกัสกับ logic ได้
กำลังเขียนแอป PHP “ยุคเก่า” ที่มีแค่อาร์เรย์กับคิวรีใหม่ให้เป็นโครงสร้างที่ใช้โมเดลและ Eloquent ซึ่งให้ความรู้สึกเหมือนเปลี่ยนเกมไปเลย
สมัยก่อนตอนใช้ SQLite บน .NET ก็เคยทำไลบรารี wrapper แนวนี้เหมือนกัน: https://github.com/zmj/sqlite-fast
เธรดนี้แสดงให้เห็นชัดถึง การครอง mindshare ของ ORM
ในคอมเมนต์แทบทั้งหมด ทางเลือกที่ถูกนึกถึงโดยปริยายแทน ORM คือ raw SQL ที่เป็น string literal อยู่ในโค้ดแอป
ทั้งที่ตั้งแต่ 6 ปีก่อนก็มีเครื่องมือที่ใช้ SQL เป็นภาษาหนึ่งอย่างจริงจัง และสร้าง wrapper เพื่อ expose คิวรีเป็นเมธอดในแอปแล้ว
เช่น queryfirst, pgtyped, pugsql, sqlc
นี่เป็นการเปลี่ยนกระบวนทัศน์ หรืออย่างน้อยก็อาจเป็นได้ และเห็นได้ชัดว่าเป็นวิธีทำงานที่เหนือกว่า แต่ ORM กับการถกเถียงแบบเดิม ๆ ที่ไม่เปลี่ยนแปลงก็ดูดออกซิเจนไปหมด จนสิ่งเหล่านี้ยังอยู่ชายขอบ
SQL คือภาษา และยังเป็นภาษาระดับสูงมากด้วย
การพยายามเอาภาษาระดับต่ำกว่ามาต่อแปะ SQL เป็นตัวอย่างชัดของ “ถ้ามีแต่ค้อน ทุกปัญหาก็ดูเหมือนตะปู”
ยกตัวอย่าง ถ้าคุณเขียนโค้ดด้วย assembly ซึ่งเป็นภาษาระดับต่ำ และต้องโต้ตอบกับระบบขนาดใหญ่ โดยเอกสารของระบบนั้นบอกว่าอินพุตจะแสดงได้ด้วย JavaScript เท่านั้น คุณก็มีทางเลือก
จะไปเอาไลบรารีพิสดารที่ช่วยให้ต่ออะไรบางอย่างขึ้นมาด้วย assembly ล้วน ๆ โดยไม่ต้อง commit ไฟล์
xxx.jsเข้า repo ก็ได้ หรือจะใช้พลังของภาษาระดับสูงแล้วเขียนเป็นไฟล์.jsไปตรง ๆ ก็ได้มันทำให้มี single source of truth สำหรับ SQL query ที่ใช้งานอยู่ และเพราะสิ่งที่ต้องเขียนและดูแลจริง ๆ คือตัวคิวรีเอง การปรับแต่งอย่างการสร้างดัชนีใหม่ก็เลยง่ายขึ้น
การมี abstraction อยู่บน raw SQL string นั้นมีประโยชน์มาก แต่การแกล้งทำให้ความสัมพันธ์ดูเหมือนเป็นอ็อบเจ็กต์นั้นเป็นแนวทางที่ซับซ้อนพันกันเกินไป
TopLink ก็น่าจะเป็นแบบนั้นเหมือนกัน
วิธีที่มันจัดการการแมประหว่าง stored procedure กับ local entity ก็เป็นแบบนี้