บางครั้งสถานการณ์และสิ่งต่าง ๆ ที่อยู่นอกเหนือการควบคุมของเรานำไปสู่ข้อกำหนดที่ผิดธรรมดาอย่างดุเดือด เมื่อเร็ว ๆ นี้ ฉันมีประสบการณ์ที่ฉันต้องการใช้ ActiveRecord โดยไม่ต้องพึ่ง ID ฐานข้อมูลสำหรับระเบียนใดๆ หากใครกำลังคิดจะทำแบบเดียวกัน ผมขอแนะนำให้หาวิธีอื่น! แต่ขอไปยังส่วนที่เหลือของเรื่อง
มีการตัดสินใจ ฐานข้อมูลขนาดเล็กกว่า (โคลนในโครงสร้างแต่ไม่ใช่ในข้อมูล) จำเป็นต้องรวมเข้าด้วยกัน ฉันเข้าร่วมโปรเจ็กต์ในขณะที่ทีมกำลังปรับแต่งสคริปต์ที่คัดลอกและวางบันทึกฐานข้อมูลจากฐานข้อมูลหนึ่งไปยังอีกฐานข้อมูลหนึ่ง มันคัดลอกทุกอย่างตามที่เป็นอยู่ รวมถึงรหัสด้วย
ฐานข้อมูล A
id | ผลไม้ | user_id |
---|---|---|
... | ... | ... |
123 | สีส้ม | 456 |
... | ... | ... |
ฐานข้อมูล ข
id | ผลไม้ | user_id |
---|---|---|
... | ... | ... |
123 | กล้วย | 74 |
... | ... | ... |
ฐานข้อมูล A หลังจากผสาน
id | ผลไม้ | user_id |
---|---|---|
... | ... | ... |
123 | สีส้ม | 456 |
123 | กล้วย | 74 |
... | ... | ... |
สิ่งนี้จะแบ่งเหตุผลพื้นฐานสำหรับการมี ID:การระบุตัวตนที่ไม่ซ้ำ ฉันไม่ทราบข้อมูลเฉพาะ แต่ฉันรู้สึกว่าปัญหาทุกประเภทจะปรากฏขึ้นเมื่อมีการแนะนำ ID ที่ซ้ำกันเข้าสู่ระบบ ฉันพยายามจะพูดบางอย่าง แต่ฉันยังใหม่กับโครงการนี้ และคนอื่นๆ ดูเหมือนจะแน่ใจว่านี่เป็นเส้นทางที่ดีที่สุด ในอีกสองสามวัน เราจะปรับใช้โค้ดและเริ่มจัดการข้อมูลด้วย ID ที่ซ้ำกัน ไม่มีคำถามอีกต่อไปว่า "เราควรทำเช่นนี้หรือไม่"; กลับกลายเป็นคำถามว่า "เราจะทำสิ่งนี้ได้อย่างไร" และ "จะใช้เวลานานแค่ไหน"
การทำงานกับ ID ที่ซ้ำกัน
ดังนั้นคุณจะจัดการกับข้อมูลที่มี ID ซ้ำกันอย่างไร? วิธีแก้ไขคือสร้างรหัสผสมของหลายฟิลด์ การดึงฐานข้อมูลของเราส่วนใหญ่มีลักษณะดังนี้:
# This doesn't work, there may be 2 users with id: 123
FavoriteFruit.find(123)
# Multiple IDs scope the query to the correct record
FavoriteFruit.find_by(id: 123, user_id: 456)
การโทร ActiveRecord ทั้งหมดได้รับการอัปเดตในลักษณะนี้ และเมื่อฉันดูรหัส ดูเหมือนว่าจะสมเหตุสมผล จนกว่าเราจะนำไปใช้
นรกแตกกระจาย
หลังจากที่เราปรับใช้โค้ดได้ไม่นาน โทรศัพท์ก็เริ่มส่งเสียง ลูกค้าเห็นเลขไม่ขึ้น พวกเขาไม่สามารถอัปเดตบันทึกของตนเองได้ ฟีเจอร์ทุกประเภทกำลังพัง
เราควรทำอย่างไร? เราไม่ได้เพียงแค่ปรับใช้โค้ด เรายังย้ายข้อมูลจากฐานข้อมูลหนึ่งไปยังอีกฐานข้อมูลหนึ่ง (และข้อมูลใหม่ถูกสร้างขึ้น/อัปเดตหลังจากที่เราปรับใช้) มันไม่ใช่สถานการณ์ย้อนกลับง่ายๆ เราต้องรีบแก้ไข
เรลส์กำลังทำอะไรอยู่
ขั้นตอนแรกในการดีบักคือการดูว่าพฤติกรรมปัจจุบันเป็นอย่างไรและจะทำให้เกิดข้อผิดพลาดซ้ำได้อย่างไร ฉันลอกแบบข้อมูลการผลิตและเริ่มคอนโซล Rails ขึ้นอยู่กับการตั้งค่าของคุณ คุณอาจไม่เห็นการสืบค้น SQL Rails ทำงานโดยอัตโนมัติเมื่อคุณดำเนินการสืบค้น ActiveRecord ต่อไปนี้คือวิธีการตรวจสอบให้แน่ใจว่าคำสั่ง SQL ปรากฏบนคอนโซลของคุณ:
ActiveRecord::Base.logger = Logger.new(STDOUT)
หลังจากนั้น ฉันลองใช้คำสั่งทั่วไปของ Rails:
$ FavoriteFruit.find_by(id: 123, user_id: 456)
FavoriteFruit Load (0.6ms)
SELECT "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]
find_by
ดูเหมือนจะใช้งานได้ดี แต่แล้วฉันก็เห็นโค้ดดังนี้:
fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
...
...
fruit.reload
นั่น reload
ทำให้ฉันสงสัย ฉันจึงทดสอบด้วย:
$ fruit.reload
FavoriteFruit Load (0.3ms)
SELECT "favorite_fruits".*
FROM "favorite_fruits"
WHERE "favorite_fruits"."id" = $1
LIMIT $2
[["id", 123], ["LIMIT", 1]]
เอ่อโอ้. ดังนั้น แม้ว่าในตอนแรกเราจะดึงบันทึกที่ถูกต้องด้วย find_by
ทุกครั้งที่เราเรียก reload
จะใช้ ID ของบันทึกและทำการค้นหาแบบค้นหาโดยง่าย ซึ่งแน่นอนว่ามักจะให้ข้อมูลที่ไม่ถูกต้องเนื่องจากรหัสที่ซ้ำกันของเรา
ทำไมมันทำอย่างนั้น? ฉันตรวจสอบซอร์สโค้ดของ Rails เพื่อหาเบาะแส นี่เป็นแง่มุมที่ยอดเยี่ยมในการเขียนโค้ดด้วย Ruby on Rails ซอร์สโค้ดเป็น Ruby ธรรมดาและสามารถเข้าถึงได้ฟรี ฉันเพียงแค่ googled "ActiveRecord reload" และพบสิ่งนี้อย่างรวดเร็ว:
# File activerecord/lib/active_record/persistence.rb, line 602
def reload(options = nil)
self.class.connection.clear_query_cache
fresh_object =
if options && options[:lock]
self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
@attributes = fresh_object.instance_variable_get("@attributes")
@new_record = false
self
end
นี่แสดงว่า reload
ไม่มากก็น้อย เสื้อคลุมสำหรับ self.class.find(id)
. การสืบค้นด้วย ID เท่านั้นถูกเดินสายในวิธีนี้ เพื่อให้เราทำงานกับ ID ที่ซ้ำกันได้ เราต้องแทนที่เมธอดหลักของ Rails (ไม่แนะนำ) หรือหยุดใช้ reload
โดยสิ้นเชิง
โซลูชันของเรา
ดังนั้นเราจึงตัดสินใจที่จะดำเนินการทุก reload
ในโค้ดแล้วเปลี่ยนเป็น find_by
เพื่อดึงฐานข้อมูลผ่านหลายคีย์
อย่างไรก็ตาม นั่นเป็นเพียงข้อบกพร่องบางส่วนเท่านั้นที่ได้รับการแก้ไข หลังจากค้นคว้าเพิ่มเติม ฉันตัดสินใจทดสอบ update
โทร:
$ fruit = FavoriteFruit.find_by(id: 123, user_id: 456)
$ fruit.update(last_eaten: Time.now)
FavoriteFruit Update (43.3ms)
UPDATE "favorite_fruits"
SET "last_eaten" = $1
WHERE "favorite_fruits"."id" = $2
[["updated_at", "2020-04-16 06:24:57.989195"], ["id", 123]]
เอ่อโอ้. คุณจะเห็นได้ว่าแม้ find_by
กำหนดขอบเขตการบันทึกตามฟิลด์เฉพาะเมื่อเราเรียก update
ในบันทึก Rails มันสร้าง WHERE id = x
. อย่างง่าย แบบสอบถามซึ่งแบ่งด้วยรหัสที่ซ้ำกัน เราหลีกเลี่ยงสิ่งนี้ได้อย่างไร
เราสร้างวิธีการอัปเดตที่กำหนดเอง update_unique
ซึ่งมีลักษณะดังนี้:
class FavoriteFruit
def update_unique(attributes)
run_callbacks :save do
self.class
.where(id: id, user_id: user_id)
.update_all(attributes)
end
self.class.find_by(id: id, user_id: user_id)
end
end
ซึ่งช่วยให้เราอัปเดตระเบียนที่มีขอบเขตมากกว่า ID:
$ fruit.update_unique(last_eaten: Time.now)
FavoriteFruit Update All (3.2ms)
UPDATE "favorite_fruits"
SET "last_eaten" = '2020-04-16 06:24:57.989195'
WHERE "favorite_fruits"."id" = $1
AND "favorite_fruits"."user_id" = $2
[["id", "123"], ["user_id", "456"]]
รหัสนี้ทำให้มั่นใจได้ถึงขอบเขตที่แคบสำหรับการอัปเดตระเบียน แต่โดยการเรียก update_all
ของชั้นเรียน เมธอด เราสูญเสียการเรียกกลับที่ปกติมาพร้อมกับการอัพเดตเรกคอร์ด ดังนั้นเราจึงต้องเรียกใช้การเรียกกลับด้วยตนเองและทำการเรียกฐานข้อมูลอีกครั้งเพื่อดึงข้อมูลบันทึกที่อัปเดตตั้งแต่ update_all
ไม่ส่งคืนบันทึกที่อัปเดต ผลิตภัณฑ์ขั้นสุดท้ายไม่ใช่ เกินไป ยุ่ง แต่อ่านยากกว่า fruit.update
. แน่นอน .
ทางออกที่แท้จริง
เนื่องจากต้นทุนที่ลดลง การจัดการ และข้อจำกัดด้านเวลา โซลูชันของเราคือการนำ patch Rails มาใช้โดยใช้หลายคีย์สำหรับการเรียกฐานข้อมูลทั้งหมด วิธีนี้ใช้ได้ผล ในแง่ที่ว่าลูกค้ายังคงซื้อและใช้ผลิตภัณฑ์ แต่ก็เป็นความคิดที่ไม่ดีด้วยเหตุผลหลายประการ:
- การพัฒนาในอนาคตอาจแนะนำจุดบกพร่องซ้ำโดยไม่ได้ตั้งใจโดยใช้วิธีการทั่วไปของ Rails นักพัฒนารายใหม่จะต้องได้รับการฝึกฝนอย่างเข้มงวดเพื่อให้โค้ดไม่มีข้อบกพร่องที่ซ่อนอยู่ เช่น การใช้
reload
วิธีการ - โค้ดนี้ซับซ้อนกว่า ชัดเจนน้อยกว่า และดูแลรักษาได้น้อยกว่า นี่เป็นหนี้ทางเทคนิคที่ทำให้การพัฒนาช้าลงมากขึ้นเรื่อยๆ เมื่อโครงการดำเนินไป
- การทดสอบช้าลงมาก คุณต้องทดสอบไม่เพียงแต่ว่าฟังก์ชันใช้งานได้ แต่ยังทำงานได้เมื่ออ็อบเจ็กต์ต่างๆ มี ID ที่ซ้ำกัน ใช้เวลาในการเขียนการทดสอบมากขึ้น และทุกครั้งที่เรียกใช้ชุดการทดสอบ จะใช้เวลามากกว่าในการทดสอบเพิ่มเติมทั้งหมด การทดสอบยังอาจพลาดจุดบกพร่องได้อย่างง่ายดาย หากนักพัฒนาแต่ละรายในโครงการไม่ได้ทดสอบสถานการณ์ที่เป็นไปได้ทั้งหมดอย่างรอบคอบ
ทางออกที่แท้จริงสำหรับปัญหานี้คือการไม่มี ID ที่ซ้ำกันตั้งแต่แรก หากจำเป็นต้องถ่ายโอนข้อมูลจากฐานข้อมูลหนึ่งไปยังอีกฐานข้อมูลหนึ่ง สคริปต์ที่ทำนั้นควรรวบรวมและแทรกข้อมูลโดยไม่มี ID ทำให้ฐานข้อมูลที่รับใช้ตัวนับการเพิ่มอัตโนมัติมาตรฐานเพื่อให้แต่ละบันทึกมี ID เฉพาะของตัวเอง
อีกวิธีหนึ่งคือการใช้ UUID สำหรับเร็กคอร์ดทั้งหมด ID ประเภทนี้เป็นสตริงอักขระยาวๆ ที่สร้างขึ้นแบบสุ่ม (แทนที่จะนับแบบทีละขั้นตอน เช่นเดียวกับ ID จำนวนเต็ม) จากนั้น การย้ายข้อมูลไปยังฐานข้อมูลอื่นจะไม่เกิดข้อขัดแย้งหรือปัญหา
สิ่งสำคัญที่สุดคือ Rails สร้างขึ้นด้วยความเข้าใจว่า ID นั้นไม่ซ้ำกันต่อบันทึกและเป็นวิธีที่ง่ายและรวดเร็วในการจัดการข้อมูลเฉพาะในฐานข้อมูล Rails เป็นเฟรมเวิร์กที่มีความคิดเห็น และความสวยงามของสิ่งนี้คือความราบรื่นของทุกอย่างที่ทำงาน ตราบใดที่คุณยึดมั่นในแนวทางการทำสิ่งต่างๆ ของ Rails สิ่งนี้ไม่เพียงใช้กับ Rails เท่านั้น แต่ยังรวมถึงแง่มุมอื่นๆ ของการเขียนโปรแกรมด้วย เมื่อสิ่งต่าง ๆ ซับซ้อน เราควรทราบวิธีการระบุปัญหา อย่างไรก็ตาม หากเราเขียนโค้ดที่ชัดเจน บำรุงรักษาได้ และเป็นมาตรฐาน เราก็สามารถหลีกเลี่ยงปัญหายุ่งยากเหล่านี้ได้ตั้งแต่แรก