ตรวจสอบข้อมูลก่อนบันทึก (Validations)
โดยทั่วไปเวลาที่เราสร้าง Model สำหรับการเก็บข้อมูลลงฐานข้อมูล จะมีเงื่อนไขบังคับในแต่ละฟิลด์อยู่ (Constraint) ไม่ว่าจะเป็นฟิลด์ต้องไม่เป็นค่าว่างหรือ null บ้างก็ฟิลด์จะต้องเป็นตัวอักษรขนาดไม่เกิน 50 ตัวอักษร หรือฟิลด์ต้องจำนวนนับทศนิยมได้ไม่เกิน 2 ตำแหน่ง เป็นต้น ที่นี้เมื่อเราใช้งาน ActiveRecord จะมีการเขียน Validations ไว้อีกรอบเพื่อให้มั่นใจว่าข้อมูลที่จะเราจะทำการบันทึกลงฐานข้อมูลนั้นถูกต้องตามเงื่อนไขที่ได้กำหนด และแน่นอนว่าเมื่อโลจิกของโปรแกรมเริ่มมีความซับซ้อนขึ้น เงื่อนไขในการตรวจสอบก็ย่อมจะซับซ้อนขึ้นตาม ซึ่งการใช้ built-in Validations ไม่สามารถตอบโจทย์ได้ ทำให้เราจะต้องสร้าง Validator ของเราขึ้นมาเอง ซึ่งก็ทำได้ไม่ยากมาดูกันเลย
โจทย์
สำหรับโจทย์วันนี้ที่เราจะแก้ไขก็มาจากที่ว่า โปรแกรมที่พัฒนาจะมีการบันทึกข้อมูลคนไข้เยี่ยมบ้าน ซึ่งในแต่ละปีงบประมาณจะอนุญาตให้สร้างคนไข้เยี่ยมบ้านที่มีเลขบัตรประชาชนซ้ำกันเกิดขึ้นไม่ได้โดยอ้างอิงจากวันที่สำรวจ ซึ่งหน้าตาของตาราง Patient นั้นก็เป็นดังตารางด้านล่าง (เอามาเฉพาะบางฟิลด์เท่านั้นนะ)
ตาราง Patient
id | pid | first_name | surveyed_date | created_at | updated_at |
---|---|---|---|---|---|
1 | 183xxxxxxx515 | demo | 2020-08-19 | 2020-08-19 | 2020-08-19 11:05:57 |
2 | 183xxxxxxx515 | demo | 2021-05-02 | 2021-05-02 | 2021-05-02 14:27:12 |
จากตารางจะพบว่าเราสามารถบันทึกข้อมูลคนไข้เยี่ยมบ้านที่มีเลขบัตรประชาชนซ้ำกันได้ก็ต่อเมื่อวันที่เยี่ยมบ้าน (surveyed_date) จะต้องอยู่คนละปีงบประมาณกัน แถวแรกจะเป็นปีงบประมาณ 2563 (1 ตุลาคม 2562 - 20 กันยายน 2563) และแถวที่สองเป็นปีงบประมาณ 2564 (1 ตุลาคม 2563 - 30 กันยายน 2564)
ก่อนจะไปถึงวิธีแก้ไขโจทย์ของเรา ถ้าย้อนกลับไปเป็นโจทย์ที่ว่าเราสามารถบันทึกข้อมูลคนไข้ซ้ำกันไม่ได้เลย การตรวจสอบข้อมูลก็สามารถเขียนได้ดังนี้
class Patient
validates :pid, uniqueness: true
end
ที่นี้ก็มาสร้าง Validator ของเรากันเลย ก็สามารถทำได้ง่ายๆ โดยการสร้าง class ที่สืบทอดความสามารถต่อจาก class ActiveModel::Validator
และก็ต้อง implement เมธอดที่ชื่อ validate
app/validators/patient_validator.rb
class PatientValidator < ActiveModel::Validator
def validate(record)
budget_year = current_budget_year
results = record.class.name.constantize.where("pid = :pid and extract(year from surveyed_date) = :budget_year", pid: record.pid, budget_year: budget_year)
record.errors.add :pid, :duplicated_pid_per_budget_year unless results.size.zero?
end
end
ในกรณีที่เราเขียนเงื่อนไขตรวจสอบแล้วไม่ถูกต้อง เราจะทำการเพิ่ม error เข้าไปใน record.errors
ซึ่งก็จะระบุฟิลด์ กับข้อความที่จะแสดง แต่ถ้าไม่มีอะไรผิดผ่านเราก็ปล่อยผ่านไปเลย ทันทีที่โมเดลเราทำการตรวจสอบด้วย valid?
และพบว่ามี errors ปรากฏอยู่ข้อมูลก็จะไม่บันทึกลงในฐานข้อมูล
สำหรับวิธีการเรียกใช้ PatientValidator ก็สามารถทำได้ดังนี้
class Patient
# แบบแรก
validates_with PatientValidator
# แบบที่สอง
validates :once_in_budget_year
def once_in_budget_year
validates_with PatientValidator
end
end
เพียงเท่านี้เราก็สามารถตอบโจทย์เราได้แล้ว แต่ถ้าเราลองพิจารณาเพิ่มไปอีกนิดจะพบว่าถ้าเราเพิ่มฟิลด์ budget_year เข้าไปในตารางก็น่าจะทำให้การตรวจสอบทำได้โดยไม่ต้องสร้าง Validator ใหม่ขึ้นมาได้ และโค้ดของเราก็จะเป็นประมาณนี้
class Patient
validates :pid, uniqueness: { scope: :budget_year,
message: "Should happen once per budget year" }
end