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

การเลือก structure.sql ใน Ruby on Rails: ประโยชน์ ข้อเสีย และวิธีการสลับ

ในบทความนี้ เราจะครอบคลุมความแตกต่างและประโยชน์สำคัญของการใช้ structure.sql เทียบกับรูปแบบสคีมาเริ่มต้น schema.rb ในแอปพลิเคชัน Ruby on Rails ของคุณ ในโลกที่ขับเคลื่อนด้วยข้อมูล การรู้วิธีใช้ประโยชน์จากคุณสมบัติที่หลากหลายของฐานข้อมูลสามารถทำให้แตกต่างกันระหว่างธุรกิจที่ประสบความสำเร็จและไม่สำเร็จ

หลังจากอธิบายความแตกต่างหลักระหว่างสองรูปแบบแล้ว เราจะแนะนำวิธีการเปลี่ยนมาใช้ structure.sql และสาธิตว่า มันช่วยให้มั่นใจได้ถึงความสมบูรณ์ของข้อมูลและฟังก์ชันของฐานข้อมูลที่อาจไม่สามารถรักษาได้ด้วยวิธีอื่น

ในบทความนี้ ผมจะให้ตัวอย่างแอป Rails ที่ใช้ structure.sql กับฐานข้อมูล PostgreSQL แต่มفهومพื้นฐานสามารถนำไปใช้กับฐานข้อมูลอื่นได้เช่นกัน ไม่มีแอปพลิเคชันเว็บจริงใดที่สมบูรณ์โดยไม่มีฐานข้อมูลที่น่าเชื่อมั่นรองรับ

โดยไม่ล่าช้า เริ่มกันเลย!

ความแตกต่างระหว่าง schema.rb และ structure.sql

หนึ่งในสิ่งแรกที่ต้องทำเมื่อเริ่มโปรเจกต์ Ruby on Rails คือการรันการย้ายฐานข้อมูล (migrations) ตัวอย่างเช่น หากคุณสร้างโมเดล User Rails จะขอให้คุณรัน migrations ซึ่งจะสร้างไฟล์ schema.rb ตามมา:

rails g model User first_name:string last_name:string

Rails จะสร้าง migration ดังนี้:

class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :first_name t.string :last_name t.timestamps end end end

เมื่อรัน migration แล้ว คุณจะพบว่า Rails สร้างไฟล์ schema.rb ให้:

ActiveRecord::Schema.define(version: 2019_12_14_074018) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "users", force: :cascade do |t| t.string "first_name" t.string "last_name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end end

ไฟล์ schema.rb นี้เหมาะสำหรับแอปพลิเคชันและกรณีใช้งานที่พื้นฐาน

มีสองจุดสำคัญที่ต้องสังเกต:

  1. เป็นการแสดงโครงสร้างฐานข้อมูลในรูปแบบ Ruby; schema.rb ถูกสร้างโดยตรวจสอบฐานข้อมูลและแสดงโครงสร้างด้วย Ruby
  2. เป็นอิสระจากฐานข้อมูล (database-agnostic) (เช่น ไม่ว่าคุณจะใช้ SQLite, PostgreSQL, MySQL หรือฐานข้อมูลอื่นที่ Rails รองรับ ไวยากรณ์และโครงสร้างจะคงเหมือนเดิม)

อย่างไรก็ตาม อาจจะมีเวลาที่กลยุทธ์นี้กลายเป็นข้อจำกัดสำหรับแอปที่เติบโต

สมมติว่าคุณมีไฟล์ migration หลายร้อยหรือหลายพันไฟล์

หากคุณต้องการสร้างระบบ production ใหม่อย่างรวดเร็ว คุณอาจเผชิญสถานการณ์ที่การรัน migrations ทั้งหมดตามลำดับใช้เวลานานเกินไป หรือคุณอาจเจอสถานการ션ที่ migrations บางส่วนมีโค้ดที่เขียนไว้สำหรับเวอร์ชันฐานข้อมูลเก่า ซึ่งไม่สามารถรันได้บนเวอร์ชันปัจจุบัน หรือ migrations ถูกเขียนด้วยสมมติฐานข้อมูลที่ไม่ถูกต้องอีกต่อไป ทำให้ migrations ล้มเหลว

สถานการณ์ทั้งหมดนี้ป้องกันไม่ให้ตั้งค่าอินสแตนซ์ใหม่ของแอปพลิเคชันได้อย่างมีประสิทธิภาพ—ไม่ว่าจะเป็น production หรือสำหรับสมาชิกทีมใหม่—ด้วยคำสั่ง rails db:create db:migrate เพียงอย่างเดียว หากเป็นเช่นนั้น คุณจะจัดการอย่างไรเพื่อให้ได้โครงสร้างฐานข้อมูลที่ถูกต้อง?

แน่นอน วิธีหนึ่งคือย้อนกลับไปแก้ migrations ที่เสียหายทั้งหมด นั่นไม่ใช่แนวทางที่แย่!

หากการแก้ไข migrations จำนวนมากมีค่าใช้จ่ายสูงเกินไป อีกวิธีหนึ่งคือรันงาน rails db:setup งานนี้จะสร้างโครงสร้างฐานข้อมูลจากไฟล์ schema.rb แต่ถ้าฐานข้อมูลของคุณมีตรรกะที่ซับซ้อนซึ่งไม่ได้แสดงใน schema.rb จะทำอย่างไร?

โชคดีที่ Rails มีทางเลือกอื่น: structure.sql

structure.sql แตกต่างจาก schema.rb ดังนี้:

  • อนุญาตให้คัดลอกโครงสร้างฐานข้อมูลได้อย่างแม่นยำ สิ่งนี้สำคัญเมื่อทำงานเป็นทีม และเมื่อต้องสร้างฐานข้อมูล production ใหม่อย่างรวดเร็วจากงาน rails db:setup
  • อนุญาตให้รักษาข้อมูลของคุณสมบัติขั้นสูงของฐานข้อมูล ตัวอย่างเช่น หากคุณใช้ PostgreSQL จะช่วยให้ใช้ views, materialized views, functions, constraints และอื่นๆ ได้

เมื่อแอปพลิเคชันถึงระดับความ成熟หนึ่ง เราต้องใช้ทุกวิธีเพื่อเพิ่มประสิทธิภาพ รักษาความถูกต้องของข้อมูล และให้ประสิทธิภาพสูงสุด การใช้ structure.sql จัดการพฤติกรรมฐานข้อมูลของ Rails ช่วยให้ผู้ใช้ทำได้

การเปลี่ยนจาก schema.rb เป็น structure.sql

การเปลี่ยนจาก schema.rb เป็น structure.sql เป็นกระบวนการที่ตรงไปตรงมา คุณเพียงต้องตั้งค่าบรรทัดต่อไปนี้ใน config/application.rb:

module YourApp class Application < Rails::Application config.load_defaults 6.0 # เพิ่มบรรทัดนี้: config.active_record.schema_format = :sql end end

จากนั้นรัน rails db:migrate และคุณควรจะเห็นไฟล์ที่ db/structure.sql โวอิล่า! Rails จะ dump โครงสร้างฐานข้อมูลโดยใช้เครื่องมือเฉพาะของฐานข้อมูลที่คุณใช้ (ในกรณีของ PostgreSQL คือ pg_dump, สำหรับ MySQL หรือ MariaDB จะมีผลลัพธ์ของ SHOW CREATE TABLE สำหรับแต่ละตาราง ฯลฯ) ขอแนะนำให้ใส่ไฟล์นี้ในระบบควบคุมเวอร์ชัน เพื่อให้ทีมอื่นมีโครงสร้างฐานข้อมูลเหมือนกัน

มุมมองแรกของไฟล์อาจดูน่ากลัว: ไฟล์ schema.rb มีเพียง 25 บรรทัด ในขณะที่ structure.sql มีถึง 109 บรรทัด! ไฟล์ขนาดใหญ่นี้จะเพิ่มประโยชน์อะไรให้การพัฒนาแอป?

การเพิ่มข้อจำกัดระดับฐานข้อมูล (Database-level Constraints)

ActiveRecord เป็นหนึ่งในส่วนที่ชอบของการใช้ Rails มันช่วยให้คุณสืบค้นฐานข้อมูลได้อย่างเป็นธรรมชาติ เหมือนภาษาพูด ตัวอย่างเช่น หากคุณต้องการหา user ทั้งหมดของบริษัทที่ชื่อ Dan ActiveRecord ช่วยให้คุณรันคำสั่งแบบนี้:

company = Company.find(name: 'Some Company') # อ่านง่ายเหมือนภาษาธรรมชาติ! company.users.where(first_name: 'Dan')

แต่มีบางกรณีที่ ActiveRecord ไม่เพียงพอ สมมติว่าคุณมีการตรวจสอบ (validation) บนโมเดล User ดังนี้:

class User < ApplicationRecord validate :name_cannot_start_with_d private def name_cannot_start_with_d if first_name.present? && first_name[0].downcase == 'd' errors.add(:first_name, "cannot start with the letter 'D'") end end end

หากคุณพยายามสร้าง user ชื่อ 'Dan' คุณจะเห็นข้อผิดพลาดเมื่อ validation ทำงาน:

User.create!(first_name: 'Dan') Traceback (most recent call last): ActiveRecord::RecordInvalid (Validation failed: First name cannot start with the letter 'D')

นี่ดี แต่สมมติว่าคุณหรือสมาชิกทีมเปลี่ยนข้อมูลโดยข้าม validation ของ ActiveRecord:

u = User.create(first_name: 'Pan') # เมธอด update_attribute ข้าม validation ของ ActiveRecord u.update_attribute :first_name, 'Dan' u.first_name => "Dan"

เช่นที่แสดง ง่ายมากที่จะข้าม validation

สิ่งนี้อาจสร้างผลลัพธ์ร้ายแรงต่อแอปพลิเคชัน ActiveRecord อาจเป็นพรการและคำสาป—ในขณะที่มี DSL ที่สะอาดและเป็นธรรมชาติ ทำให้ทำงานสนุก แต่ก็มักอนุญาตให้ validation ระดับโมเดลผ่านไปได้ง่ายเกินไป วิธีแก้ไขคือเพิ่มข้อจำกัดระดับฐานข้อมูล

rails g migration AddFirstNameConstraintToUser

จะสร้างไฟล์ที่คุณสามารถแก้ไขตรรกะเพื่อห้ามชื่อจริงขึ้นต้นด้วยตัวอักษร 'D':

class AddFirstNameConstraintToUser < ActiveRecord::Migration[6.0] def up execute "ALTER TABLE users ADD CONSTRAINT name_cannot_start_with_d CHECK (first_name !~* '^d')" end def down execute "ALTER TABLE users DROP CONSTRAINT IF EXISTS name_cannot_start_with_d" end end

สำคัญมากที่ต้องเพิ่มโค้ดที่ย้อนกลับ migration ได้สำเร็จ ในตัวอย่างข้างต้น มีคำสั่ง up และ down up จะรันเมื่อ migration ทำงาน down จะรันเมื่อ rollback migration หากไม่มีการย้อนกลับโครงสร้างฐานข้อมูลอย่างถูกต้อง คุณอาจต้องทำความสะอาดด้วยมือภายหลัง ขอแนะนำให้มีไฟล์ migration ที่รันได้ทั้ง up และ down เพื่อหลีกเลี่ยงปัญหาในอนาคต

ตอนนี้ รัน migration และตรวจสอบว่าข้าม constraint ได้หรือไม่:

rails db:migrate
user = User.create first_name: 'Pan' user.update_attribute :first_name, 'Dan' ActiveRecord::StatementInvalid (PG::CheckViolation: ERROR: new row for relation "users" violates check constraint "name_cannot_start_with_d") DETAIL: Failing row contains (2, Dan, null, 2019-12-14 09:40:11.809358, 2019-12-14 09:40:41.658974).

สมบูรณ์แบบ! Constraint ทำงานตามที่ตั้งใจ แม้เราจะข้าม validation ของ ActiveRecord ก็ยังพึ่งพาฐานข้อมูล—ผู้รักษาประตูสุดท้ายของเรา—เพื่อรักษาความสมบูรณ์ของข้อมูล

สิ่งนี้เกี่ยวข้องกับ structure.sql อย่างไร?

หากคุณดู structure.sql จะเห็นว่ามีการเพิ่มดังนี้:

CREATE TABLE public.users ( id bigint NOT NULL, first_name character varying, last_name character varying, created_at timestamp(6) without time zone NOT NULL, updated_at timestamp(6) without time zone NOT NULL, CONSTRAINT name_cannot_start_with_d CHECK (((first_name)::text !~* '^d'::text)));

Constraint ของคุณอยู่ในสคีมาเอง!

ในขณะที่ schema.rb รองรับข้อจำกัดระดับฐานข้อมูลได้ ยังจำได้ว่า มันไม่ได้แสดงทุกอย่างที่ฐานข้อมูลรองรับ เช่น triggers, sequences, stored procedures หรือ check constraints ตัวอย่างเช่น นี่คือสิ่งที่จะเกิดกับไฟล์ schema หากคุณใช้ schema.rb กับ migration เดียวกัน (AddFirstNameConstraintToUser):

ActiveRecord::Schema.define(version: 2019_12_14_074018) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "users", force: :cascade do |t| t.string "first_name" t.string "last_name" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end end

ไฟล์ไม่เปลี่ยนแปลง! Constraint ไม่ได้ถูกเพิ่ม

หากคุณนำสมาชิกทีมใหม่เข้ามา คุณอาจทำงานภายใต้กฎระเบียบฐานข้อมูลที่ต่างกัน

การ commit structure.sql เข้าระบบควบคุมเวอร์ชันจะช่วยให้ทีมอยู่ในความเข้าใจเดียวกัน หากคุณรัน rails db:setup ด้วยไฟล์ structure.sql โครงสร้างฐานข้อมูลจะมี constraint ดังกล่าว ด้วย schema.rb ไม่มีการรับประกันเช่นนั้น

สิ่งเดียวกันก็ใช้ได้กับระบบ production หากคุณต้องสร้างอินสแตนซ์ใหม่ของแอปด้วยฐานข้อมูลใหม่—และการรัน migrations ทั้งหมดตามลำดับใช้เวลานาน—การตั้งค่าจาก structure.sql จะเร็วกว่ามาก เราสามารถมั่นใจได้ว่า structure.sql จะสร้างฐานข้อมูลด้วยโครงสร้างเดียวกับอินสแตนซ์อื่นๆ

ความเจ็บปวดในการเติบโต (Growing Pains)

การจัดการไฟล์ schema.rb ที่กระชับทั่วทีมเป็นงานที่ง่ายกว่าการจัดการไฟล์ structure.sql ที่冗长

หนึ่งในความเจ็บปวดใหญ่เมื่อย้ายมาใช้ structure.sql คือการรับรองว่ามีเฉพาะการเปลี่ยนแปลงที่จำเป็นเท่านั้นที่ถูก commit เข้าไฟล์นั้น ซึ่งบางครั้งทำได้ยาก

สมมติว่าคุณ pull branch ของคนอื่นและรัน migrations ของ branch นั้น structure.sql ของคุณจะมีการเปลี่ยนแปลงบางส่วน คุณกลับไปทำงานบน branch ของตัวเองและสร้าง migration ใหม่ structure.sql จะมีทั้งการเปลี่ยนแปลงของ branch คุณและ branch อื่น นี่อาจเป็นความยุ่งยาก และมี learning curve ในการจัดการ conflict นี้

ด้วยวิธีนี้ เรากำลังทำ trade-off เราต้องจัดการกับความซับซ้อนของโค้ดเบื้องหน้าเพื่อรักษาคุณสมบัติขั้นสูงของฐานข้อมูล แต่กลับต้องจัดการกับการแสดงสคีมาที่ง่ายกว่าและไม่มีอำนาจทั้งหมดของฐานข้อมูลใน متناولมือ เช่น หากต้องการสร้าง backup จากงาน db:setup ผมมองว่าการทนต่อความยุ่งยากของ version control ดีกว่าการทนทรมานกับข้อมูลที่เสียหาย/ไม่ถูกต้องใน production หรือไม่สามารถใช้คุณสมบัติขั้นสูงของฐานข้อมูลได้

โดยทั่วไป มีสองกลยุทธ์ที่ผมใช้เพื่อให้ structure.sql มีเฉพาะการเปลี่ยนแปลงที่เกี่ยวข้องกับ branch:

  • เมื่อทำงานเสร็จบน branch ที่มี migrations ให้รัน rails db:rollback STEP=n โดย n คือจำนวน migrations ใน branch นั้น จะทำให้โครงสร้างฐานข้อมูลกลับสู่สถานะเดิม
  • คุณอาจลืม rollback หลังทำงานบน branch ในกรณีนั้น เมื่อเริ่ม branch ใหม่ ให้ pull structure.sql ที่สะอาดจาก master ก่อนสร้าง migrations ใหม่

กฎทั่วไป: ไฟล์ structure.sql ควรมีเฉพาะการเปลี่ยนแปลงที่เกี่ยวข้องกับ branch ก่อน merge เข้า master

สรุป

โดยทั่วไป เมื่อแอป Rails เล็กหรือไม่ต้องการคุณสมบัติขั้นสูงของฐานข้อมูล การใช้ schema.rb ปลอดภัย อ่านง่าย กระชับ และจัดการง่าย

อย่างไรก็ตาม เมื่อแอปเติบโตในขนาดและความซับซ้อน การสะท้อนโครงสร้างฐานข้อมูลอย่างแม่นยำเป็นสิ่งสำคัญ จะช่วยให้ทีมรักษา constraints, โมดูลฐานข้อมูล, functions และ operators ที่ไม่เป็นไปได้ด้วย schema.rb การเรียนรู้การใช้ Rails ด้วย structure.sql ที่ดูแลดีจะให้ข้อได้เปรียบที่ schema.rb เรียบง่ายไม่สามารถให้ได้

P.S. หากคุณต้องการอ่านบทความ Ruby Magic ทันทีที่เผยแพร่ ให้สมัครสมาชิก newsletter Ruby Magic และจะไม่พลาดบทความใดๆ!