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

ActiveRecord สำหรับฐานข้อมูลที่ไม่มีรหัสเฉพาะ

บางครั้งสถานการณ์และสิ่งต่าง ๆ ที่อยู่นอกเหนือการควบคุมของเรานำไปสู่ข้อกำหนดที่ผิดธรรมดาอย่างดุเดือด เมื่อเร็ว ๆ นี้ ฉันมีประสบการณ์ที่ฉันต้องการใช้ 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 เท่านั้น แต่ยังรวมถึงแง่มุมอื่นๆ ของการเขียนโปรแกรมด้วย เมื่อสิ่งต่าง ๆ ซับซ้อน เราควรทราบวิธีการระบุปัญหา อย่างไรก็ตาม หากเราเขียนโค้ดที่ชัดเจน บำรุงรักษาได้ และเป็นมาตรฐาน เราก็สามารถหลีกเลี่ยงปัญหายุ่งยากเหล่านี้ได้ตั้งแต่แรก