การเรียกกลับของ ActiveRecord เป็นวิธีที่ง่ายในการเรียกใช้โค้ดในช่วงต่างๆ ของชีวิตโมเดลของคุณ
ตัวอย่างเช่น สมมติว่าคุณมีไซต์ถาม &ตอบ และคุณต้องการค้นหาคำถามทั้งหมดได้ ทุกครั้งที่คุณเปลี่ยนแปลงคำถาม คุณจะต้องจัดทำดัชนีในบางอย่าง เช่น ElasticSearch การจัดทำดัชนีใช้เวลาสักครู่และไม่เร่งด่วน ดังนั้นคุณจะทำในเบื้องหลังด้วย Sidekiq
ดูเหมือนว่าจะเป็นเวลาที่เหมาะสมที่สุดในการใช้ after_save
โทรกลับ! ดังนั้นในแบบจำลองของคุณ คุณจะเขียนบางอย่างเช่น:
class Question < ActiveRecord::Base
after_save :index_for_search
# ...
private
def index_for_search
QuestionIndexerJob.perform_later(self)
end
end
class QuestionIndexerJob < ActiveJob::Base
queue_as :default
def perform(question)
# ... index the question ...
end
end
มันใช้งานได้ดี! หรืออย่างน้อยก็ดูเหมือนว่าจะ จนกว่าคุณจะรอคิวงานมากขึ้นและเห็นข้อผิดพลาดเหล่านี้ปรากฏขึ้น:
2015-03-10T05:29:02.881Z 52530 TID-oupf889w4 WARN: Error while trying to deserialize arguments: Couldn't find Question with 'id'=3
แน่นอนว่า Sidekiq จะลองงานใหม่อีกครั้ง และมันอาจจะใช้ได้ในครั้งต่อไป แต่ก็ยังแปลกอยู่นิดหน่อย เหตุใด Sidekiq จึงไม่พบคำถามที่คุณเพิ่งบันทึกไว้
สภาวะการแข่งขันระหว่างกระบวนการ
Rails โทร after_save
โทรกลับทันทีหลังจากบันทึก แต่การเชื่อมต่อฐานข้อมูลอื่นไม่สามารถเห็นบันทึกนั้นได้ เช่นเดียวกับที่ Sidekiq กำลังใช้ จนกว่าฐานข้อมูลจะ ธุรกรรม เป็นความมุ่งมั่นซึ่งจะเกิดขึ้นภายหลังเล็กน้อย ซึ่งหมายความว่ามีโอกาสที่ Sidekiq จะพยายามค้นหาคำถามของคุณหลังจากที่คุณบันทึกแล้ว แต่ก่อนที่คุณจะส่งคำถาม ไม่พบบันทึกของคุณ และมันก็ระเบิด
ปัญหานี้เป็นเรื่องธรรมดามากที่ Sidekiq มีรายการคำถามที่พบบ่อยเกี่ยวกับเรื่องนี้ และมีวิธีแก้ไขที่ง่าย
แทน after_save
:
class Question < ActiveRecord::Base
after_save :index_for_search
# ...
end
ใช้ after_commit
:
class Question < ActiveRecord::Base
after_commit :index_for_search
# ...
end
และงานของคุณจะไม่ถูกเข้าคิวจนกว่า Sidekiq จะมองเห็นโมเดลของคุณ
ดังนั้น เมื่อคุณจัดคิวงานพื้นหลังหรือบอกกระบวนการอื่นเกี่ยวกับการเปลี่ยนแปลงที่คุณเพิ่งทำ ให้ใช้ after_commit
. หากไม่เป็นเช่นนั้น พวกเขาอาจไม่พบบันทึกที่คุณเพิ่งสัมผัส
แต่ยังมีอีกหนึ่งปัญหา…
โอเค คุณเปลี่ยน after_save
. ของคุณไปหลายชุดแล้ว ขอใช้ after_commit
แทนที่. ดูเหมือนว่าทุกอย่างจะได้ผล ได้เวลาเช็คของแล้วกลับบ้านได้ใช่มั้ย
ก่อนอื่น คุณจะต้องทำการทดสอบ:
require 'test_helper'
class QuestionTest < ActiveSupport::TestCase
test "A saved question is queued for indexing" do
assert_enqueued_with(job: QuestionIndexerJob) do
Question.create(title: "Is it legal to kill a zombie?")
end
end
end
1) Failure:
QuestionTest#test_A_saved_question_is_queued_for_indexing [/Users/jweiss/Source/testapps/after_commit/test/models/question_test.rb:7]:
No enqueued job found with {:job=>QuestionIndexerJob}
อ๊ะ! การทดสอบไม่ควรเข้าคิวงาน? เกิดอะไรขึ้นที่นั่น
ตามค่าเริ่มต้น Rails จะล้อมแต่ละกรณีทดสอบในธุรกรรมฐานข้อมูลของตนเอง สิ่งนี้สามารถเร่งความเร็วได้จริงๆ ใช้คำสั่งฐานข้อมูลเพียงคำสั่งเดียวเพื่อยกเลิกการเปลี่ยนแปลงทั้งหมดที่คุณทำระหว่างการทดสอบ
แต่นี่ก็หมายถึง after_commit
. ของคุณ การโทรกลับจะไม่ทำงาน เพราะว่า after_commit
การโทรกลับจะทำงานเฉพาะเมื่อ อยู่นอกสุด ได้ตกลงทำธุรกรรมแล้ว
เมื่อคุณโทร save
ภายในกรณีทดสอบ มันยังคงทำธุรกรรม (มากหรือน้อย) แต่นั่นคือ รองจากนอกสุด การทำธุรกรรมในขณะนี้ ดังนั้น after_commit
. ของคุณ การโทรกลับจะไม่ทำงานเมื่อคุณคาดหวัง และคุณไม่สามารถทดสอบสิ่งที่เกิดขึ้นภายในพวกเขาได้
ปัญหานี้ยังมีการแก้ไขที่ง่าย รวม test_after_commit
อัญมณีใน Gemfile ของคุณ:
group :test do
gem "test_after_commit"
end
และ after_commit
. ของคุณ hooks จะทำงานต่อจาก ตัวสุดท้าย . ของคุณ การทำธุรกรรม ซึ่งเป็นสิ่งที่คุณคาดหวังให้เกิดขึ้น
คุณอาจจะคิดว่า “นั่นแปลก เหตุใดฉันจึงต้องใช้ gem แยกต่างหากทั้งหมดเพื่อทดสอบการโทรกลับที่มาพร้อมกับ Rails มันควรจะเกิดขึ้นโดยอัตโนมัติไม่ใช่หรือ”
คุณถูก. มันแปลก. แต่มันจะไม่แปลกอีกต่อไป
เมื่อ Rails 5 จัดส่งแล้ว คุณจะไม่ต้องกังวลกับ test_after_commit
. เนื่องจากปัญหานี้ได้รับการแก้ไขแล้วใน Rails เมื่อประมาณหนึ่งเดือนที่แล้ว
ในรหัสของฉันเอง ฉันใช้ after_commit
มาก. ฉันอาจจะใช้มันมากกว่าที่ฉันใช้ after_save
! แต่มันก็ไม่ได้มาโดยไม่มีปัญหาและเคสที่ล้ำสมัย
เวอร์ชันต่อเวอร์ชัน ดีขึ้นเรื่อยๆ และเมื่อคุณใช้ after_commit
ในสถานที่ที่เหมาะสม ข้อยกเว้นแบบสุ่มๆ แปลกๆ มากมายจะไม่เกิดขึ้นอีกต่อไป