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

วิธีเชื่องตารางผู้ใช้ที่กำลังเติบโตของคุณ

ลูกค้าไปที่นั่นได้อย่างไร

ก่อนจะลงลึกในรายละเอียด เรามาทำความเข้าใจกันก่อนว่าแอพจะจบลงอย่างไรในสถานะนี้ เราเริ่มต้นด้วย users . ง่ายๆ โต๊ะ. หลังจากผ่านไปสองสามสัปดาห์ เราต้องสามารถระบุเวลาลงชื่อเข้าใช้ครั้งสุดท้ายได้ เราจึงเพิ่ม users.last_sign_in_at . จากนั้นเราต้องรู้ชื่อผู้ใช้ เราเพิ่ม first_name และ last_name . ทวิตเตอร์จัดการ? คอลัมน์อื่น โปรไฟล์ GitHub? หมายเลขโทรศัพท์? หลังจากผ่านไปสองสามเดือน ตารางก็กลายเป็นเรื่องเหลือเชื่อ

เกิดอะไรขึ้นกับสิ่งนี้?

ตารางที่มีขนาดใหญ่แสดงถึงปัญหาหลายประการ:

  1. users มีหน้าที่หลายอย่างที่ไม่เกี่ยวข้องกัน ทำให้เข้าใจ เปลี่ยนแปลง และทดสอบได้ยากขึ้น
  2. การแลกเปลี่ยนข้อมูลระหว่างแอปและฐานข้อมูลต้องใช้แบนด์วิดท์เพิ่มเติม
  3. แอปต้องการหน่วยความจำเพิ่มเติมเพื่อจัดเก็บโมเดลขนาดใหญ่

แอปดึงข้อมูล users ในทุกคำขอเพื่อวัตถุประสงค์ในการรับรองความถูกต้องและการอนุญาต แต่มักจะใช้เพียงไม่กี่คอลัมน์เท่านั้น การแก้ไขปัญหาจะช่วยปรับปรุงทั้งการออกแบบและประสิทธิภาพ

การแยกตาราง

เราสามารถแก้ปัญหาได้โดย แยกคอลัมน์ที่ไม่ค่อยได้ใช้ไปยังตารางใหม่ (หรือตาราง) . ตัวอย่างเช่น เราสามารถดึงข้อมูลโปรไฟล์ (first_name ฯลฯ) ลงใน profiles ด้วยขั้นตอนดังต่อไปนี้:

  1. สร้าง profiles ด้วยคอลัมน์ที่ซ้ำกันของคอลัมน์ที่เกี่ยวข้องกับโปรไฟล์ใน users .
  2. เพิ่ม profile_id ถึง users . ตั้งค่าเป็น NULL สำหรับตอนนี้
  3. สำหรับแต่ละแถวใน users ให้แทรกแถวไปที่ profiles ที่ซ้ำกับคอลัมน์ที่เกี่ยวข้องกับโปรไฟล์
  4. ชี้ profile_id ของแถวที่เกี่ยวข้องใน users ไปยังแถวที่แทรกใน 3.
  5. อย่า ไม่ ทำ 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 . ลูกค้าของฉันมีข้อกำหนดหลายประการ:

  1. Accessors ควรใช้ Profile ที่เกี่ยวข้อง . พวกเขาควรบันทึกว่ามีการเรียกตัวเข้าถึงจากที่ใดที่เลิกใช้แล้ว
  2. กำลังบันทึก users ควรบันทึก profiles . โดยอัตโนมัติ เพื่อหลีกเลี่ยงการทำลายรหัสโดยใช้ตัวเข้าถึงที่เลิกใช้แล้ว
  3. 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 วิธีพิจารณาในการแยกตารางทำได้ยาก ถ้าใช้ไม่ได้ก็มองหาอย่างอื่น หากคุณไม่ทราบว่าเป็นอย่างไรก็วางสายให้ฉัน ฉันจะพยายามช่วย อย่าปล่อยให้เศษอาหารเน่าเสีย