การป้องกันระเบียนซ้ำใน CRUD app ต้องมีหลายชั้น: unique constraint ที่ฐานข้อมูล, idempotency keys, และสถานะ UI ที่ป้องกันการส่งซ้ำ

Idempotency-Key) แต่ส่งใน JSON body ก็ได้\n3. ฝั่งเซิร์ฟเวอร์ เก็บคีย์พร้อมสโคปที่ชัดเจน: ใครเรียก (user หรือ account), เอ็นด์พอยต์ไหน และการสร้างอะไร บันทึก payload การตอบสุดท้ายด้วย\n4. ถ้าคีย์เดียวกันมาสำหรับสโคปเดียวกัน อย่าสร้างอะไรเพิ่ม คืน response ที่เก็บไว้ (รวมทั้ง ID ของเรคคอร์ดที่สร้างครั้งแรก)\n5. ทดสอบภายใต้ความเครียด: การคลิกรวดเร็ว การ retry ของไคลเอนต์ และการจำลอง timeout ในเครือข่าย ยืนยันว่ายังคงได้แค่หนึ่งแถวใหม่เท่านั้น\n\n### ตรรกะฝั่งเซิร์ฟเวอร์หน้าตาเป็นอย่างไร\n\nรายละเอียดสำคัญคือ "ตรวจสอบ + เก็บ" ต้องปลอดภัยต่อ concurrency ในทางปฏิบัติ ให้เก็บระเบียน idempotency โดยมี unique constraint บน (scope, key) และถือว่าความขัดแย้งเป็นสัญญาณให้ใช้ซ้ำ\n\ntext\nif idempotency_record exists for (user_id, key):\n return saved_response\nelse:\n begin transaction\n create the row\n save response under (user_id, key)\n commit\n return response\n\n\nตัวอย่าง: ลูกค้ากด "Create invoice" แอปส่งคีย์ abc123 และเซิร์ฟเวอร์สร้าง invoice inv_1007 ถ้าโทรศัพท์หลุดสัญญาณแล้ว retry เซิร์ฟเวอร์จะตอบด้วย inv_1007 เดิม ไม่ใช่ inv_1008\n\nตอนทดสอบ อย่าหยุดแค่ "กดสองครั้ง" จำลองคำขอที่ timeout บนไคลเอนต์แต่ยังเสร็จบนเซิร์ฟเวอร์ แล้ว retry ด้วยคีย์เดิม\n\n## สิ่งใน UI ที่ช่วยหยุดการกดซ้ำโดยไม่ได้ตั้งใจ\n\nการป้องกันฝั่งเซิร์ฟเวอร์สำคัญ แต่ระเบียนซ้ำหลายกรณียังเริ่มจากคนที่ทำสิ่งปกติซ้ำ การออกแบบ UI ที่ดีทำให้เส้นทางที่ปลอดภัยเด่นชัด\n\nปิดปุ่มส่งทันทีที่ผู้ใช้ส่ง ทำตอนกดครั้งแรก ไม่ใช่หลังการตรวจความถูกต้องหรือหลังคำขอเริ่ม ถ้าฟอร์มส่งได้จากหลายคอนโทรล (ปุ่มและ Enter) ล็อกสถานะทั้งฟอร์ม ไม่ใช่แค่ปุ่มเดียว\n\nแสดงสถานะการทำงานที่ชัดเจน ตอบคำถามเดียว: มันกำลังทำงานไหม? ป้าย "กำลังบันทึก..." หรือ spinner เล็ก ๆ ก็เพียงพอ รักษาเลย์เอาต์ให้คงที่เพื่อไม่ให้ปุ่มกระโดดแล้วล่อลวงให้กดอีกครั้ง\n\nชุดกฎเล็ก ๆ ป้องกันการกดซ้ำได้ส่วนใหญ่: ตั้งธง isSubmitting เมื่อเริ่ม handler ของ submit, ปฏิเสธการ submit ใหม่ในขณะที่ค่านั้นเป็นจริง (สำหรับทั้งการคลิกและ Enter), และไม่ล้างมันจนกว่าจะได้รับการตอบกลับจริง\n\nการตอบสนองช้าที่สุดคือที่หลายแอปพลาด ถ้าคุณเปิดใช้งานปุ่มใหม่ด้วยตัวจับเวลา (เช่น หลัง 2 วินาที) ผู้ใช้สามารถส่งอีกครั้งขณะที่คำขอแรกยังอยู่ในสถานะรับส่ง เปิดใช้งานใหม่เฉพาะเมื่อความพยายามนั้นเสร็จสิ้นจริง\n\nหลังสำเร็จ ให้ลดโอกาสการส่งซ้ำโดยการนำทางไปหน้าอื่น (ไปยังหน้ารายการหรือหน้ารายการที่สร้างแล้ว) หรือแสดงสถานะสำเร็จที่ชัดเจนพร้อมเรคคอร์ดที่สร้างแล้ว หลีกเลี่ยงการปล่อยฟอร์มที่กรอกแล้วค้างอยู่บนหน้าจอพร้อมปุ่มที่ยังใช้งานได้\n\n## กรณีพิเศษที่ต้องวางแผน (หลายแท็บ, รีเฟรช, มือถือ)\n\nบั๊กระเบียนซ้ำที่ดื้อรั้นมาจากพฤติกรรมประจำวันที่ "แปลกแต่เกิดขึ้นบ่อย": สองแท็บ รีเฟรช หรือโทรศัพท์ที่หลุดสัญญาณ\n\nก่อนอื่น ให้กำหนดขอบเขตความเป็นเอกลักษณ์ให้ถูกต้อง "เอกลักษณ์" แทบจะไม่หมายถึง "ทั่วทั้งฐานข้อมูล" เสมอไป อาจหมายถึงหนึ่งต่อผู้ใช้ หนึ่งต่อ workspace หรือหนึ่งต่อ tenant ถ้าคุณซิงก์กับระบบภายนอก คุณอาจต้องความเป็นเอกลักษณ์ต่อแหล่งภายนอกบวกกับ external ID วิธีปลอดภัยคือเขียนประโยคที่คุณหมายถึงไว้ชัดเจน (เช่น "หมายเลขใบแจ้งหนี้หนึ่งหมายเลขต่อ tenant ต่อปี") แล้วบังคับใช้ตามนั้น\n\nพฤติกรรมหลายแท็บเป็นกับดักคลาสสิก สถานะการโหลดใน UI ช่วยในแท็บเดียวแต่ไม่มีผลข้ามแท็บ นี่คือที่การป้องกันฝั่งเซิร์ฟเวอร์ต้องยังทำงานได้\n\nปุ่ม Back และรีเฟรชอาจกระตุ้นการส่งซ้ำ หลังสร้างสำเร็จ ผู้ใช้มักรีเฟรชเพื่อตรวจสอบ หรือตีกลับแล้วส่งฟอร์มที่ยังดูแก้ไขได้อีกครั้ง ควรแสดงมุมมองของเรคคอร์ดที่ถูกสร้างแล้วแทนฟอร์มเดิม และให้เซิร์ฟเวอร์รองรับการเล่นซ้ำอย่างปลอดภัย\n\nมือถือเพิ่มความซับซ้อน: backgrounding เครือข่ายแกว่ง และ retry อัตโนมัติ คำขออาจสำเร็จแต่แอปไม่เคยได้รับการตอบกลับ จึงลองอีกครั้งเมื่อกลับมา\n\n## ความผิดพลาดและกับดักที่พบบ่อย\n\nโหมดล้มเหลวที่พบบ่อยที่สุดคือการถือว่า UI เป็นเกราะป้องกันเดียว ปิดปุ่มและ spinner ช่วยได้ แต่ไม่ครอบคลุมการรีเฟรช เครือข่ายมือถือที่ไม่เสถียร แท็บที่สอง หรือบั๊กของไคลเอนต์ เซิร์ฟเวอร์และฐานข้อมูลยังต้องสามารถบอกได้ว่า "การสร้างนี้เกิดขึ้นแล้ว"\n\nกับดักอีกอย่างคือการเลือกฟิลด์สำหรับความเป็นเอกลักษณ์ผิด ถ้าคุณตั้ง unique constraint บนสิ่งที่ไม่เป็นเอกลักษณ์จริง ๆ (เช่น นามสกุล เวลาแบบปัดเศษ หรือหัวข้ออิสระ) คุณจะบล็อกเรคคอร์ดที่ถูกต้อง แทนที่จะทำเช่นนั้นให้ใช้ตัวระบุจริง (เช่น external provider ID) หรือกฎแบบมีสโคป (unique ต่อผู้ใช้ ต่อวัน หรือต่อ parent record)\n\nการทำ idempotency ผิดพลาดก็ง่ายเช่นกัน ถ้าไคลเอนต์สร้างคีย์ใหม่ทุกครั้งที่ retry คุณจะได้การสร้างใหม่ทุกครั้ง เก็บคีย์เดียวกันสำหรับความตั้งใจผู้ใช้ตั้งแต่การกดครั้งแรกจนถึงการ retry ทั้งหมด\n\nยังต้องระวังสิ่งที่คุณคืนเมื่อ retry ถ้าคำขอแรกสร้างเรคคอร์ด การ retry ควรคืนผลลัพธ์เดิม (หรืออย่างน้อย ID เดิม) ไม่ใช่ข้อผิดพลาดคลุมเครือที่ทำให้ผู้ใช้พยายามอีกครั้ง\n\nถ้าข้อจำกัดความเป็นเอกลักษณ์บล็อกการสร้างซ้ำ อย่าซ่อนมันด้วยข้อความ "เกิดข้อผิดพลาดบางอย่าง" ให้บอกเหตุผลเป็นภาษาธรรมดาว่าเกิดอะไรขึ้น: "หมายเลขใบแจ้งหนี้นี้มีอยู่แล้ว เราเก็บของเดิมไว้และไม่ได้สร้างรายการที่สอง"\n\n## เช็คลิสต์ด่วนก่อนปล่อย\n\nก่อนปล่อย ให้ตรวจสอบเส้นทางการสร้างที่อาจเกิดการซ้ำ ผลลัพธ์ที่ดีที่สุดมาจากการวางการป้องกันซ้อนกันเพื่อให้การคลิกพลาด retry หรือเครือข่ายช้าไม่ทำให้เกิดสองแถว\n\nยืนยันสามสิ่ง:\n\n- กฎในฐานข้อมูลสอดคล้องกับความจริง (unique constraints อยู่ใน production และมีสโคปที่ถูกต้อง)\n- เอ็นด์พอยต์การสร้างที่สำคัญปลอดภัยต่อการทำซ้ำ (idempotency keys คืนผลลัพธ์เดิม)\n- UI ทำให้สถานะชัดเจน (ปิดปุ่มส่ง ชัดเจนเรื่อง progress และมุมมองหลังสำเร็จที่ชัดเจน)\n\nเช็คแบบฝึกหัดง่าย ๆ: เปิดฟอร์ม กด submit สองครั้งอย่างรวดเร็ว แล้วรีเฟรชกลางการส่งและพยายามอีกครั้ง ถ้าคุณสร้างได้สองเรคคอร์ด ผู้ใช้จริงก็จะทำได้เช่นกัน\n\n## ตัวอย่าง: สร้างใบแจ้งหนี้โดยไม่มีระเบียนซ้ำ\n\nลองจินตนาการแอปเล็ก ๆ สำหรับออกใบแจ้งหนี้ ผู้ใช้กรอกใบแจ้งหนี้ใหม่แล้วกด Create เครือข่ายช้า หน้าจอไม่เปลี่ยนทันที และเขากด Create อีกครั้ง\n\nถ้ามีแค่การป้องกันจาก UI คุณอาจปิดปุ่มแล้วแสดง spinner นั่นช่วยได้ แต่ยังไม่พอ การแตะสองครั้งอาจเล็ดลอดบนอุปกรณ์บางรุ่น การ retry อาจเกิดหลัง timeout หรือผู้ใช้ส่งจากสองแท็บ\n\nถ้ามีแค่ unique constraint ฐานข้อมูล คุณอาจหยุดซ้ำที่ตรงตัวได้ แต่ประสบการณ์อาจไม่ดี คำขอแรกสำเร็จ คำขอที่สองชน constraint และผู้ใช้เห็นข้อผิดพลาดทั้งที่ใบแจ้งหนี้ถูกสร้างแล้ว\n\nผลลัพธ์ที่สะอาดคือใช้ idempotency บวก unique constraint:\n\n- ไคลเอนต์สร้าง idempotency key สำหรับการสร้างและนำมันมาใช้ซ้ำเมื่อ retry\n- เซิร์ฟเวอร์เก็บคีย์พร้อมผลลัพธ์การสร้างและคืน ID ของใบแจ้งหนี้เดียวกันสำหรับการเรียกซ้ำ\n- unique constraint ยังปกป้องคุณหากคีย์ต่างกันพยายามสร้างหมายเลขใบแจ้งหนี้เดียวกัน\n\nข้อความ UI ง่าย ๆ หลังการกดครั้งที่สอง: "สร้างใบแจ้งหนี้แล้ว — เราข้ามการส่งซ้ำและเก็บคำขอแรกของคุณไว้"\n\n## ก้าวต่อไป: ทำให้เชื่อถือได้ขณะแอปเติบโต\n\nเมื่อมีพื้นฐานแล้ว งานต่อไปคือการมองเห็น การทำความสะอาด และความสอดคล้อง\n\nเพิ่มการล็อกเบา ๆ รอบเส้นทางการสร้างเพื่อให้คุณแยกความแตกต่างระหว่างการกระทำของผู้ใช้จริงกับการ retry ได้ บันทึก idempotency key ฟิลด์ที่เป็นเอกลักษณ์ที่เกี่ยวข้อง และผลลัพธ์ (created vs returned existing vs rejected) คุณไม่ต้องการเครื่องมือหนักเพื่อเริ่มต้น\n\nถ้ามีระเบียนซ้ำอยู่แล้ว ทำความสะอาดด้วยกฎชัดเจนและเก็บประวัติการตรวจสอบ เช่น เก็บเรคคอร์ดเก่าเป็น "ผู้ชนะ" แนบแถวที่เกี่ยวข้องใหม่ (การชำระเงิน รายการบรรทัด) และมาร์กแถวอื่น ๆ ว่า merged แทนลบทิ้ง การทำเช่นนี้ช่วยงานสนับสนุนและรายงานได้มาก\n\nเขียนกฎความเป็นเอกลักษณ์และ idempotency ลงในที่เดียว: อะไรเป็นเอกลักษณ์และในสโคปไหน คีย์ idempotency เก็บนานเท่าไร ข้อผิดพลาดควรเป็นอย่างไร และ UI ควรทำอย่างไรเมื่อ retry นั่นจะป้องกันเอ็นด์พอยต์ใหม่ ๆ หลุดผ่านรั้วความปลอดภัยโดยเงียบ ๆ\n\nถ้าคุณสร้างหน้าจอ CRUD อย่างรวดเร็วใน Koder.ai (koder.ai) ควรทำพฤติกรรมเหล่านี้เป็นส่วนหนึ่งของเทมเพลตเริ่มต้น: unique constraints ในสคีมา, create endpoints ที่ idempotent ใน API, และสถานะการโหลดที่ชัดเจนใน UI เพื่อให้ความเร็วไม่ได้มาพร้อมกับข้อมูลที่ยุ่งเหยิง\nระเบียนซ้ำคือเมื่อสิ่งเดียวกันถูกเก็บไว้สองครั้ง เช่น คำสั่งซื้อสองรายการจากการเช็คเอาต์เดียวกันหรือบัตรแจ้งปัญหาสองใบสำหรับปัญหาเดียว โดยทั่วไปเกิดจากการที่คำสั่ง "สร้าง" ถูกเรียกใช้มากกว่าหนึ่งครั้ง เช่น การกดซ้ำของผู้ใช้ การ retry อัตโนมัติ หรือคำขอที่เกิดพร้อมกันหลายรายการ
เพราะเหตุการณ์สร้างที่สองอาจเกิดขึ้นโดยที่ผู้ใช้ไม่รู้ตัว เช่น การแตะสองครั้งบนมือถือ หรือกด Enter แล้วคลิกปุ่มต่ออีกครั้ง แม้ผู้ใช้จะส่งครั้งเดียว ไคลเอนต์ เครือข่าย หรือเซิร์ฟเวอร์อาจ retry คำขอนั้นหลัง timeout ได้ ดังนั้นอย่าถือว่า POST = once
ไม่เพียงพอเสมอไป การปิดปุ่มและแสดง "กำลังบันทึก..." ช่วยลดการกดซ้ำโดยไม่ตั้งใจ แต่จะไม่หยุดการ retry จากเครือข่ายที่ไม่เสถียร การรีเฟรช แท็บหลายหน้า หรืองาน background และการส่งซ้ำของ webhook จึงยังต้องมีการป้องกันที่เซิร์ฟเวอร์และฐานข้อมูลด้วย
ข้อจำกัดความเป็นเอกลักษณ์ที่ฐานข้อมูลคือเส้นสุดท้ายที่หยุดไม่ให้แถวสองแถวถูกใส่เข้ามาพร้อมกัน แม้คำขอจะมาถึงพร้อมกัน ควรตั้งบนกฎที่สะท้อนความเป็นเอกลักษณ์ในโลกจริงและมักจะมีสโคป เช่น ต่อ tenant หรือ workspace
ทั้งสองอย่างแก้ปัญหาคนละมุม Unique constraints หยุดการเสียบแถวที่ซ้ำกันตามกฎของฟิลด์ ส่วน idempotency keys ทำให้การพยายามสร้างเดิม ๆ ปลอดภัย (คำขอเดียวกันคืนผลลัพธ์เดิม) การใช้ทั้งคู่จะให้ความปลอดภัยและประสบการณ์ผู้ใช้ที่ดีกว่า
สร้างคีย์หนึ่งลูกต่อความตั้งใจของผู้ใช้ (เช่น เมื่อกด "Create") เก็บคีย์นั้นไว้ตลอดการพยายามเดียวกัน และส่งมันกับคำขอทุกครั้ง คีย์ควรคงที่ขณะเกิด timeout หรือ resume แต่ไม่ควรถูกนำไปใช้ซ้ำกับการสร้างอื่นในภายหลัง
เก็บระเบียน idempotency โดยมีสโคปชัดเจน (เช่น user หรือ account) + endpoint + idempotency key และบันทึกผลลัพธ์ที่คืนให้เมื่อคำขอแรกสำเร็จ หากคีย์เดิมมาซ้ำ ให้คืนผลลัพธ์เดิมพร้อม ID ของเรคคอร์ดที่สร้างแทนการเพิ่มแถวใหม่
ใช้แนวทาง "ตรวจสอบ + เก็บ" ที่ปลอดภัยต่อ concurrency โดยมักจะบังคับ unique constraint บนระเบียน idempotency เอง (สำหรับ scope และ key) เพื่อให้คำขอที่มาพร้อมกันสองรายการไม่สามารถต่างคนต่างคิดว่าเป็นคนแรกได้ ฝ่ายหนึ่งจะล้มเหลวในการแทรกและต้องนำผลลัพธ์ที่มีอยู่กลับมาใช้
เก็บให้พอครอบคลุมการ retry ที่สมเหตุสมผล ค่าเริ่มต้นที่พบบ่อยคือประมาณ 24 ชั่วโมง สำหรับการชำระเงินอาจเก็บ 48–72 ชั่วโมง ใช้ TTL เพื่อจำกัดขนาดที่เก็บและให้สอดคล้องกับช่วงเวลาที่ไคลเอนต์จะ retry ได้
เมื่อชัดเจนว่าเป็นการ retry เดิม ให้จัดการเหมือนสำเร็จและคืนเรคคอร์ดเดิม (ID เดิม) แทนการตอบผิดพลาดที่คลุมเครือ หากฟิลด์นั้นต้องเป็นเอกลักษณ์จริง ๆ (เช่น อีเมล) ให้คืนข้อความขัดแย้งที่ชัดเจนบอกว่าอะไรมีอยู่แล้วและเกิดอะไรขึ้นต่อไป