ลูกค้าไปที่นั่นได้อย่างไร
ก่อนจะลงลึกในรายละเอียด เรามาทำความเข้าใจกันก่อนว่าแอพจะจบลงอย่างไรในสถานะนี้ เราเริ่มต้นด้วย users
. ง่ายๆ โต๊ะ. หลังจากผ่านไปสองสามสัปดาห์ เราต้องสามารถระบุเวลาลงชื่อเข้าใช้ครั้งสุดท้ายได้ เราจึงเพิ่ม users.last_sign_in_at
. จากนั้นเราต้องรู้ชื่อผู้ใช้ เราเพิ่ม first_name
และ last_name
. ทวิตเตอร์จัดการ? คอลัมน์อื่น โปรไฟล์ GitHub? หมายเลขโทรศัพท์? หลังจากผ่านไปสองสามเดือน ตารางก็กลายเป็นเรื่องเหลือเชื่อ
เกิดอะไรขึ้นกับสิ่งนี้?
ตารางที่มีขนาดใหญ่แสดงถึงปัญหาหลายประการ:
users
มีหน้าที่หลายอย่างที่ไม่เกี่ยวข้องกัน ทำให้เข้าใจ เปลี่ยนแปลง และทดสอบได้ยากขึ้น- การแลกเปลี่ยนข้อมูลระหว่างแอปและฐานข้อมูลต้องใช้แบนด์วิดท์เพิ่มเติม
- แอปต้องการหน่วยความจำเพิ่มเติมเพื่อจัดเก็บโมเดลขนาดใหญ่
แอปดึงข้อมูล users
ในทุกคำขอเพื่อวัตถุประสงค์ในการรับรองความถูกต้องและการอนุญาต แต่มักจะใช้เพียงไม่กี่คอลัมน์เท่านั้น การแก้ไขปัญหาจะช่วยปรับปรุงทั้งการออกแบบและประสิทธิภาพ
การแยกตาราง
เราสามารถแก้ปัญหาได้โดย แยกคอลัมน์ที่ไม่ค่อยได้ใช้ไปยังตารางใหม่ (หรือตาราง) . ตัวอย่างเช่น เราสามารถดึงข้อมูลโปรไฟล์ (first_name
ฯลฯ) ลงใน profiles
ด้วยขั้นตอนดังต่อไปนี้:
- สร้าง
profiles
ด้วยคอลัมน์ที่ซ้ำกันของคอลัมน์ที่เกี่ยวข้องกับโปรไฟล์ในusers
. - เพิ่ม
profile_id
ถึงusers
. ตั้งค่าเป็นNULL
สำหรับตอนนี้ - สำหรับแต่ละแถวใน
users
ให้แทรกแถวไปที่profiles
ที่ซ้ำกับคอลัมน์ที่เกี่ยวข้องกับโปรไฟล์ - ชี้
profile_id
ของแถวที่เกี่ยวข้องในusers
ไปยังแถวที่แทรกใน 3. - อย่า ไม่ ทำ
users.profile_id
ไม่ใช่-NULL
. แอปยังไม่ทราบถึงการมีอยู่ของมันจึงพัง
เราจำเป็นต้องแทนที่การอ้างอิงถึง users.first_name
ด้วย profiles.first_name
และอื่นๆ หากเราแยกข้อมูลอ้างอิงเพียงไม่กี่คอลัมน์ เราขอแนะนำให้คุณดำเนินการด้วยตนเอง แต่ทันทีที่เรานึกขึ้นได้ "โอ้ ไม่ นี่เป็นงานที่แย่ที่สุดที่เคยมีมา!" เราควรมองหาทางเลือกอื่น
อย่าละเลยปัญหา ส่วนหนึ่งของรหัสที่ทุกคนหลีกเลี่ยงจะยิ่งแย่ลงไปอีกและยิ่งไม่ใส่ใจมากขึ้นไปอีก . วิธีที่ง่ายที่สุดในการทำลายวงจรอุบาทว์คือการเริ่มต้นจากจุดเล็กๆ
อ่านต่อ หากคุณสงสัยว่าลูกค้าของฉันแก้ปัญหาอย่างไร
แก้ไขโค้ดทีละบรรทัด
แนวทางที่เพิ่มขึ้นที่สุดคือการแก้ไขการอ้างอิงคอลัมน์เก่าครั้งละหนึ่งรายการ เน้นย้าย first_name
จาก users
ไปยัง profiles
.
ขั้นแรก สร้าง profiles
ด้วย:
rails generate model Profile first_name:string
จากนั้นเพิ่มข้อมูลอ้างอิงจาก users
ไปยัง profiles
และคัดลอก users.first_name
ไปยัง profiles
:
class ExtractUsersFirstNameToProfiles < ActiveRecord::Migration
# Redefine the models to break dependency on production code. We need
# vanilla models without callbacks, etc. Also, removing a model in the future
# might break the migration.
class User < ActiveRecord::Base; end
class Profile < ActiveRecord::Base; end
def up
add_reference :users, :profile, index: true, unique: true, foreign_key: true
User.find_each do |user|
profile = Profile.create!(first_name: user.first_name)
user.update!(profile_id: profile.id)
end
change_column_null :users, :profile_id, false
end
def down
remove_reference :users, :profile
end
end
เนื่องจากบังคับให้ผู้ใช้แต่ละรายมีโปรไฟล์เดียว ข้อมูลอ้างอิงจาก users
ไปยัง profiles
ดีกว่าการอ้างอิงที่ตรงกันข้าม
ด้วยโครงสร้างฐานข้อมูล เราสามารถมอบหมาย first_name
จาก users
ไปที่ profiles
. ลูกค้าของฉันมีข้อกำหนดหลายประการ:
- Accessors ควรใช้
Profile
ที่เกี่ยวข้อง . พวกเขาควรบันทึกว่ามีการเรียกตัวเข้าถึงจากที่ใดที่เลิกใช้แล้ว - กำลังบันทึก
users
ควรบันทึกprofiles
. โดยอัตโนมัติ เพื่อหลีกเลี่ยงการทำลายรหัสโดยใช้ตัวเข้าถึงที่เลิกใช้แล้ว User#first_name_changed?
และActiveModel::Dirty
. อื่นๆ วิธีการยังคงใช้งานได้
หมายถึง users
ควรมีลักษณะดังนี้:
class User < ActiveRecord::Base
# We need autosave as the client code might be unaware of
# Profile#first_name and still reference User#first_name.
belongs_to :profile, autosave: true
def first_name
log_backtrace(:first_name)
profile.first_name
end
def first_name=(new_first_name)
log_backtrace(:first_name)
# Call super so that User#first_name_changed? and similar still work as
# expected.
super
profile.first_name = new_first_name
end
private
def log_backtrace(name)
filtered_backtrace = caller.select do |item|
item.start_with?(Rails.root.to_s)
end
Rails.logger.warn(<<-END)
A reference to an obsolete attribute #{name} at:
#{filtered_backtrace.join("\n")}
END
end
end
หลังจากการเปลี่ยนแปลงเหล่านี้ แอปจะทำงานเหมือนเดิม แต่อาจช้าลงเล็กน้อยเนื่องจากมีการอ้างอิงเพิ่มเติมถึง profiles
(หากประสิทธิภาพกลายเป็นปัญหา ให้ใช้เครื่องมือเช่น AppSignal) รหัสจะบันทึกการอ้างอิงทั้งหมดไปยังแอตทริบิวต์ดั้งเดิม แม้กระทั่งรายการที่ไม่สามารถแก้ไขได้ (เช่น user[attr] = ...
หรือ user.send("#{attr}=", ...)
) ดังนั้นเราจะสามารถระบุตำแหน่งทั้งหมดได้แม้ในขณะที่ grep
ก็ไม่มีประโยชน์
ด้วยโครงสร้างพื้นฐานนี้ เราสามารถมุ่งมั่นที่จะแก้ไขการอ้างอิงหนึ่งรายการไปยัง users.first_name
ตามกำหนดเวลาปกติ เช่น ทุกเช้า (เพื่อเริ่มต้นวันใหม่ด้วยชัยชนะอย่างรวดเร็ว) หรือประมาณเที่ยงวัน (เพื่อทำงานบางอย่างที่ง่ายกว่าหลังจากเช้าที่มุ่งเน้น) ความมุ่งมั่นนี้มีความสำคัญเนื่องจากเป้าหมายของเราคือการลดอุปสรรคทางจิตในการแก้ไขปัญหา . การวางโค้ดด้านบนไว้โดยไม่ดำเนินการใดๆ จะทำให้แอปแย่ลงไปอีก
หลังจากลบการอ้างอิงที่เลิกใช้แล้วทั้งหมด (และยืนยันด้วย grep
และบันทึก) ในที่สุดเราก็สามารถวาง users.first_name
:
class RemoveUsersFirstName < ActiveRecord::Migration
def change
remove_column :users, :first_name, :string
end
end
เราควรกำจัดโค้ดที่เพิ่มใน User
เพราะไม่จำเป็นอีกต่อไป
ข้อจำกัด
วิธีการนี้อาจใช้กับกรณีของคุณ แต่โปรดทราบถึงข้อจำกัดบางประการ:
- ไม่รองรับการสืบค้นจำนวนมาก เช่น
User.update_all
. - ไม่รองรับการสืบค้นข้อมูลดิบของ SQL
- มันอาจทำลายแพทช์ของลิง (โปรดจำไว้ว่าการพึ่งพาอาจแนะนำพวกเขาด้วย)
users
และprofiles
อาจไม่ซิงค์หากprofiles.first_name
อัปเดตแล้ว แต่users.first_name
ไม่ได้
คุณอาจสามารถเอาชนะบางคนได้ ตัวอย่างเช่น คุณอาจให้โมเดลซิงค์กับออบเจ็กต์บริการหรือการเรียกกลับใน Profile
. หรือหากคุณใช้ PostgreSQL คุณอาจลองใช้มุมมองที่เป็นรูปธรรมในระหว่างนี้
แค่นั้น!
บทเรียนที่สำคัญที่สุดของบทความคือ อย่าหลีกเลี่ยงโค้ดที่มีกลิ่น แต่ควรจัดการกับมันแทน . หากงานล้นหลาม ให้ทำงานซ้ำตามกำหนดเวลา บทความนำเสนอ a วิธีพิจารณาในการแยกตารางทำได้ยาก ถ้าใช้ไม่ได้ก็มองหาอย่างอื่น หากคุณไม่ทราบว่าเป็นอย่างไรก็วางสายให้ฉัน ฉันจะพยายามช่วย อย่าปล่อยให้เศษอาหารเน่าเสีย