Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ปกป้องข้อมูล: Optimistic Locking ใน Rails REST APIs

ลองจินตนาการ: ระบบจัดการเช่าทรัพย์สิน Employee A แก้ไข contact info ของ Rental X เพิ่มเบอร์โทร พร้อมกัน Employee B เห็น typo ใน contact info เดียวกัน ทำ update ไป สักครู่ Employee A update contact info ใหม่... และ update แก้ typo หายไป!

แย่มาก! และนี่แค่สถานการณ์เล็กๆ น้อยๆ bayangkan conflict คล้ายกันในระบบการเงิน!

หลีกเลี่ยงได้ไหม? ได้! ต้องการ concurrency protection และ locking — โดยเฉพาะ optimistic locking

มาสำรวจ optimistic locking ใน Rails REST APIs

'Lost Updates' และ Optimistic vs Pessimistic Locking

สถานการณ์ข้างต้นคือ 'Lost Update' 2 concurrent transactions update คอลัมน์เดียวของ row เดียวกัน ตัวที่สอง override changes ตัวแรก ถือว่าตัวแรกไม่เคยเกิด

โดยทั่วไปแก้ได้โดย:

  • Transaction Isolation level ให้ database จัดการ
  • Pessimistic locking — ป้องกัน concurrent transactions update row เดียวกัน ตัวที่สองรอตัวแรกจบก่อนถึงอ่านได้ ข้อดี: ไม่มี stale data ข้อเสีย: block การอ่านข้อมูลจาก row นั้น
  • Optimistic locking — หยุด modification ถ้า state ตอน modify ต่างจากตอน read

ปัญหาเราไม่ใช่ concurrent DB transactions (คือ business transactions) → solution แรกไม่ค่อยใช้ เหลือ pessimistic และ optimistic

Pessimistic locking ป้องกัน lost update ในสถานการณ์สมมติ แต่ทำให้ user ยาก ถ้าบล็อกข้อมูลนาน (อ่าน/แก้ 30+ นาที)

Optimistic locking น้อยกว่าการจำกัด อนุญาตหลาย users เข้าถึงข้อมูล แต่ถ้าหลายคน edit พร้อมกัน แค่คนเดียวสำเร็จ คนอื่นได้ error ว่า stale data ต้อง retry ไม่สมบูรณ์แต่ UX ดีอาจไม่เจ็บมาก

มาดู implement optimistic locking ใน hypothetical Rails REST API

Optimistic Locking ใน REST APIs

ก่อน implement ใน Rails คิดก่อน: optimistic locking ใน REST APIs ควรเป็นยังไง

ต้อง track object's original state ตอน read เพื่อ compare ตอน update หาก state ไม่เปลี่ยน → allow หากเปลี่ยน → fail

ใน REST APIs ต้องตอบ 3 คำถาม:

  • ตอน read resource ยังไง express current state และ return ใน response ให้ consumer?
  • Consumer ควรส่ง original state กลับมายัง API ตอน update ยังไง?
  • API ควร return อะไรถ้า state เปลี่ยนและ update ไม่ได้?

ข่าวดี: ทุกคำถามตอบได้และจัดการด้วย HTTP semantics

Tracking resource state ใช้ Entity Tags (ETags) คืน fingerprint/checksum/version ใน header ETag ให้ consumer ส่งกลับมาด้วย If-Match header API server compare checksums/version ง่ายมาก

Request สำเร็จถ้า ETag และ If-Match ตรงกัน ไม่งั้น respond 412 Precondition Failed — status ที่เหมาะสมที่สุด

อีก scenario: compare ETags ได้ก็ต่อเมื่อ consumer ส่ง If-Match ถ้าไม่ส่ง? อาจ ignore concurrency protection แต่ไม่ค่อยดี ทำให้ If-Match required และ respond 428 Precondition Required ถ้าไม่มี

เข้าใจภาพรวมแล้ว มา implement ใน Rails

Optimistic Locking ใน Rails

ข่าวดี: Rails มี optimistic locking out-of-the-box ผ่าน ActiveRecord::Locking::Optimistic เพิ่ม column lock_version (หรือชื่ออื่น ต้อง declare ใน model) ActiveRecord จะ increment หลัง change และ check version ปัจจุบัน หาก stale → raise ActiveRecord::StaleObjectError ตอน update/destroy

วิธีง่าย: ใช้ค่า lock_version เป็น ETag ใน RentalsController:

class RentalsController after_action :assign_etag, only: [:show] def show @rental = Rental.find(params[:id]) respond_with @rental end private def assign_etag response.headers["ETag"] = @rental.lock_version end end

Controller simplified สนใจแค่ optimistic locking ไม่รวม auth/authorization ตัวนี้ expose ETag ให้ consumer ต่อไปจัดการ If-Match header:

class RentalsController after_action :assign_etag, only: [:show, :update] def show @rental = Rental.find(params[:id]) respond_with @rental end def update @rental = Rental.find(params[:id]) @rental.update(rental_params) respond_with @rental end private def assign_etag response.headers["ETag"] = @rental.lock_version end def rental_params params .require(:rental) .permit(:some, :permitted, :attributes).merge(lock_version: lock_version_from_if_match_header) end def lock_version_from_if_match_header request.headers["If-Match"].to_i end end

พอมี minimal optimistic locking แล้ว! แต่ไม่อยาก return 500 ถ้า conflict ทำให้ If-Match required สำหรับ update:

class RentalsController before_action :ensure_if_match_header_provided, only: [:update] after_action :assign_etag, only: [:show, :update] rescue_from ActiveRecord::StaleObjectError do head 412 end def show @rental = Rental.find(params[:id]) respond_with @rental end def update @rental = Rental.find(params[:id]) @rental.update(rental_params) respond_with @rental end private def ensure_if_match_header_provided request.headers["If-Match"].present? or head 428 and return end def assign_etag response.headers["ETag"] = @rental.lock_version end def rental_params params .require(:rental) .permit(:some, :permitted, :attributes) .merge(lock_version: lock_version_from_if_match_header) end def lock_version_from_if_match_header request.headers["If-Match"].to_i end end

นี่คือทุกอย่างที่ต้องการ implement functionality ที่พูดไว้ อาจ improve เพิ่ม เช่น error messages เพิ่มเติม แต่ข้าม scope บทความ

สรุป: ความสำคัญของ Optimistic Locking ใน Rails APIs

Concurrency protection มักถูกมองข้ามเวลา design REST APIs → severe consequences

แต่ implement optimistic locking ใน Rails APIs ง่าย — ตามที่สาธิต — และช่วยหลีกเลี่ยง critical issues

Happy coding!

P.S. อยากอ่าน Ruby Magic posts ทันทีเผยแพร่? สมัครรับ Ruby Magic newsletter!