ในโพสต์ของวันนี้ เราจะพูดถึงความแตกต่างที่สำคัญและประโยชน์ของการใช้ structure.sql
เทียบกับ schema.rb
default ที่เป็นค่าเริ่มต้น รูปแบบสคีมาในแอปพลิเคชัน Ruby on Rails ของคุณ ในโลกที่ขับเคลื่อนด้วยข้อมูล การรู้วิธีใช้ประโยชน์จากฟีเจอร์ที่หลากหลายของฐานข้อมูลสามารถสร้างความแตกต่างระหว่างองค์กรที่ประสบความสำเร็จและไม่ประสบความสำเร็จได้
หลังจากที่เห็นความแตกต่างหลักๆ ระหว่างทั้งสองรูปแบบแล้ว เราจะสรุปวิธีการเปลี่ยนไปใช้ structure.sql
และสาธิตวิธีการช่วยให้มั่นใจถึงความสมบูรณ์ของข้อมูลรวมถึงฟังก์ชันของฐานข้อมูลที่คุณอาจเก็บรักษาไว้ไม่ได้
ในโพสต์ ผมจะยกตัวอย่างแอป Rails ที่ใช้ structure.sql
ด้วยฐานข้อมูล PostgreSQL แต่แนวคิดพื้นฐานสามารถย้ายไปยังฐานข้อมูลอื่นได้เช่นกัน ไม่มีเว็บแอปพลิเคชันในโลกแห่งความเป็นจริงที่สมบูรณ์อย่างแท้จริงหากไม่มีฐานข้อมูลที่เชื่อถือได้รองรับ
เพื่อไม่ให้เป็นการเสียเวลา มาดำน้ำกัน!
ความแตกต่างระหว่าง schema.rb และ structure.sql
สิ่งแรกที่คุณต้องทำเมื่อเริ่มต้นโปรเจ็กต์ Ruby on Rails คือการเรียกใช้การย้ายฐานข้อมูล ตัวอย่างเช่น หากคุณสร้างโมเดลผู้ใช้ Rails จะขอให้คุณเรียกใช้การย้ายข้อมูลอย่างหลีกเลี่ยงไม่ได้ ซึ่งจะสร้าง schema.rb
ไฟล์ตามลำดับ:
rails g model User first_name:string last_name:string
Rails จะสร้างการโยกย้ายต่อไปนี้:
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
เมื่อดำเนินการย้ายข้อมูล คุณจะพบว่า 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
นี้ เป็นไฟล์ที่ยอดเยี่ยมสำหรับแอปพลิเคชันและกรณีการใช้งานที่ค่อนข้างพื้นฐาน
มีสองสิ่งสำคัญที่ควรสังเกตที่นี่:
- เป็นตัวแทน Ruby ของฐานข้อมูลของคุณ
schema.rb
ถูกสร้างขึ้นโดยการตรวจสอบฐานข้อมูลและแสดงโครงสร้างโดยใช้ Ruby - เป็นฐานข้อมูลที่ไม่เชื่อเรื่องพระเจ้า (เช่น คุณใช้ SQLite, PostgreSQL, MySQL หรือฐานข้อมูลอื่นใดที่ Rails รองรับ ไวยากรณ์และโครงสร้างส่วนใหญ่จะยังคงเหมือนเดิม)
อย่างไรก็ตาม อาจมีบางครั้งที่กลยุทธ์นี้จำกัดเกินไปสำหรับแอปที่กำลังเติบโตของคุณ
ตัวอย่างเช่น คุณมีไฟล์การย้ายข้อมูลนับร้อยหรือหลายพันไฟล์
หากคุณต้องการสร้างระบบการผลิตใหม่อย่างรวดเร็ว คุณอาจพบสถานการณ์ที่การเรียกใช้งานทั้งหมดตามลำดับใช้เวลานานเกินไป หรือคุณอาจเผชิญกับสถานการณ์ที่การย้ายข้อมูลบางส่วนมีโค้ดที่ตั้งใจให้เรียกใช้บนฐานข้อมูลเวอร์ชันเก่าของคุณ แต่ไม่สามารถดำเนินการได้อีกต่อไปในเวอร์ชันปัจจุบัน คุณอาจมีสถานการณ์ที่การย้ายข้อมูลถูกเขียนขึ้นโดยมีการสันนิษฐานว่าข้อมูลบางอย่างใช้ไม่ได้อีกต่อไป ซึ่งจะทำให้การย้ายข้อมูลล้มเหลว
สถานการณ์ทั้งหมดเหล่านี้ป้องกันการตั้งค่าอินสแตนซ์ใหม่ของแอปพลิเคชันของคุณอย่างมีประสิทธิภาพ——ไม่ว่าจะเป็นในเวอร์ชันที่ใช้งานจริงหรือสำหรับสมาชิกในทีมใหม่——ด้วย rails db:create db:migrate
ที่เรียบง่าย สั่งการ. หากเป็นกรณีนี้ คุณจะดำเนินการตามสคีมาฐานข้อมูลที่ถูกต้องได้อย่างไร
แน่นอน วิธีหนึ่งคือการกลับไปแก้ไขการโยกย้ายที่เสียหายทั้งหมด นั่นไม่ใช่ความคิดที่เลวเลย!
หากการกลับไปแก้ไขการย้ายข้อมูลจำนวนมากมีค่าใช้จ่ายสูงเกินไป อีกวิธีหนึ่งก็คือการเรียกใช้ rails db:setup
งาน. งานนี้จะสร้างสคีมาฐานข้อมูลจาก schema.rb
. ของคุณ ไฟล์. อย่างไรก็ตาม จะเกิดอะไรขึ้นหากฐานข้อมูลของคุณมีตรรกะที่ซับซ้อนซึ่งไม่ได้แสดงใน schema.rb
เป็นตัวแทนของฐานข้อมูลของคุณหรือไม่
โชคดีที่ Rails มีทางเลือกอื่น:structure.sql
structure.sql
แตกต่างจาก schema.rb
ด้วยวิธีดังต่อไปนี้:
- อนุญาตให้คัดลอกโครงสร้างฐานข้อมูลได้อย่างแม่นยำ นี่เป็นสิ่งสำคัญเมื่อทำงานกับทีม เช่นเดียวกับถ้าคุณต้องการสร้างฐานข้อมูลใหม่ในการผลิตจาก
rails db:setup
อย่างรวดเร็ว งาน - ช่วยให้สามารถรักษาข้อมูลคุณลักษณะฐานข้อมูลขั้นสูงได้ ตัวอย่างเช่น หากคุณใช้ PostgreSQL จะทำให้สามารถใช้มุมมอง มุมมองที่เป็นรูปธรรม ฟังก์ชัน ข้อจำกัด และอื่นๆ
เมื่อแอปพลิเคชันถึงระดับที่กำหนด เราต้องใช้ทุกเคล็ดลับในหนังสือเพื่อเพิ่มประสิทธิภาพ รักษาความถูกต้องของข้อมูล และรับประกันประสิทธิภาพการทำงานที่รวดเร็ว การใช้ 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
# Add this line:
config.active_record.schema_format = :sql
end
end
จากนั้นเรียกใช้ rails db:migrate
และคุณควรเห็นไฟล์ใน db/structure.sql
. โว้ว! Rails จะดัมพ์โครงสร้างฐานข้อมูลโดยใช้เครื่องมือเฉพาะสำหรับฐานข้อมูลที่คุณใช้ (ในกรณีของ PostgreSQL เครื่องมือนั้นคือ pg_dump
สำหรับ MySQL หรือ MariaDB จะมีผลลัพธ์ของ SHOW CREATE TABLE
สำหรับแต่ละโต๊ะ ฯลฯ) ขอแนะนำให้ตรวจสอบให้แน่ใจว่าไฟล์นี้อยู่ภายใต้การควบคุมเวอร์ชัน เพื่อให้ทีมที่เหลือของคุณมีโครงสร้างฐานข้อมูลเดียวกัน
แวบแรกที่เห็นไฟล์นั้นอาจดูน่ากลัว:schema.rb
ไฟล์มีเพียง 25 บรรทัด ในขณะที่ structure.sql
ไฟล์มหันต์ 109 บรรทัด! ไฟล์ขนาดใหญ่เช่นนี้มีประโยชน์อย่างไรต่อการพัฒนาแอป
การเพิ่มข้อจำกัดระดับฐานข้อมูล
ActiveRecord เป็นหนึ่งในส่วนที่ฉันโปรดปรานในการใช้ Rails ช่วยให้คุณสามารถสืบค้นฐานข้อมูลในลักษณะที่เป็นธรรมชาติ เกือบจะเหมือนกับในภาษาพูด ตัวอย่างเช่น หากคุณต้องการค้นหาผู้ใช้ของบริษัททั้งหมดที่ชื่อ Dan ActiveRecord จะให้คุณเรียกใช้แบบสอบถามได้ง่ายๆ ดังนี้:
company = Company.find(name: 'Some Company')
# Reads just like in a natural language!
company.users.where(first_name: 'Dan')
มีบางกรณีที่ ActiveRecord สั้นแม้ว่า ตัวอย่างเช่น สมมติว่าคุณมีการตรวจสอบความถูกต้องต่อไปนี้ในโมเดลผู้ใช้ของคุณ:
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.create!(first_name: 'Dan')
Traceback (most recent call last):
ActiveRecord::RecordInvalid (Validation failed: First name cannot start with the letter 'D')
เป็นเรื่องปกติ แต่สมมติว่าคุณหรือสมาชิกในทีมของคุณเปลี่ยนข้อมูลโดยข้ามการตรวจสอบความถูกต้องของ ActiveRecord:
u = User.create(first_name: 'Pan')
# The update_attribute method bypasses ActiveRecord validations
u.update_attribute :first_name, 'Dan'
u.first_name
=> "Dan"
ดังที่ได้แสดงให้เห็นแล้ว การเลี่ยงผ่านการตรวจสอบทำได้ง่ายมาก
สิ่งนี้สามารถส่งผลร้ายต่อการสมัครของเรา ActiveRecord สามารถให้พรเช่นเดียวกับคำสาป——ในขณะที่มี DSL ที่สะอาดและเป็นธรรมชาติมากที่ทำให้การทำงานด้วยมีความสุข แต่ก็มักจะอนุญาตมากเกินไปเมื่อบังคับใช้การตรวจสอบระดับแบบจำลอง วิธีแก้ปัญหาอย่างที่คุณอาจทราบแล้วคือการเพิ่มข้อจำกัดระดับฐานข้อมูล
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
โปรดทราบว่า มาก สิ่งสำคัญคือต้องเพิ่มรหัสที่คืนค่าการโยกย้ายได้สำเร็จ ในตัวอย่างข้างต้น ฉันมี up
และ down
คำสั่ง up
เมธอดจะถูกดำเนินการเมื่อมีการย้ายข้อมูล down
ได้รับการดำเนินการเมื่อมีการย้อนกลับการย้ายข้อมูล หากไม่คืนค่าโครงสร้างฐานข้อมูลของคุณอย่างถูกต้อง คุณอาจต้องทำความสะอาดบ้านด้วยตนเองในภายหลัง ฉันขอแนะนำให้มีไฟล์การโยกย้ายที่สามารถดำเนินการได้ทั้ง up
และ down
เพื่อหลีกเลี่ยงอาการปวดหัวในอนาคต
ตอนนี้ เรียกใช้การย้ายข้อมูลและตรวจสอบว่าคุณสามารถข้ามข้อจำกัดนั้นได้หรือไม่:
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).
สมบูรณ์แบบ! ข้อจำกัดของเราทำงานตามที่ตั้งใจไว้ แม้ว่าเราจะหลีกเลี่ยงการตรวจสอบความถูกต้องของ ActiveRecord ไม่ว่าด้วยเหตุผลใดก็ตาม เรายังคงสามารถพึ่งพาฐานข้อมูล——ผู้รักษาประตูขั้นสูงสุดของเรา—เพื่อรักษาความสมบูรณ์ของข้อมูลของเรา
สิ่งนี้เกี่ยวข้องกับ 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)));
ข้อจำกัดของคุณอยู่ในสคีมาเอง!
ขณะที่ schema.rb
ยังรองรับข้อจำกัดระดับฐานข้อมูลด้วย สิ่งสำคัญคือต้องจำไว้ว่ามันไม่ได้แสดงทุกอย่างที่ฐานข้อมูลของคุณอาจสนับสนุน เช่น ทริกเกอร์ ลำดับ กระบวนงานที่เก็บไว้ หรือข้อจำกัดการตรวจสอบ ตัวอย่างเช่น นี่คือสิ่งที่จะเกิดขึ้นกับไฟล์สคีมาของคุณด้วยการย้ายข้อมูลแบบเดียวกัน (AddFirstNameConstraintToUser
) หากคุณเพียงแค่ใช้ 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
ไฟล์ไม่เปลี่ยน! ไม่ได้เพิ่มข้อจำกัด
หากคุณต้องจ้างนักพัฒนารายใหม่มาทำงานในโครงการ คุณอาจอยู่ภายใต้ระเบียบข้อบังคับของฐานข้อมูลที่แตกต่างกัน
คอมมิต structure.sql
การควบคุมเวอร์ชันจะช่วยให้แน่ใจว่าทีมของคุณอยู่ในหน้าเดียวกัน หากคุณต้องเรียกใช้ rails db:setup
มี structure.sql
โครงสร้างฐานข้อมูลของคุณจะมีข้อจำกัดข้างต้น ด้วย schema.rb
ไม่มีการค้ำประกันดังกล่าว
สามารถพูดได้เช่นเดียวกันเกี่ยวกับระบบการผลิต หากคุณต้องการสร้างอินสแตนซ์ใหม่ของแอปพลิเคชันของคุณอย่างรวดเร็วด้วยฐานข้อมูลใหม่——และการเรียกใช้การย้ายข้อมูลทั้งหมดตามลำดับจะใช้เวลานาน——การตั้งค่าฐานข้อมูลจาก structure.sql
ไฟล์จะเร็วกว่ามาก เราวางใจได้ว่า structure.sql
จะสร้างฐานข้อมูลของเราด้วยโครงสร้างเดียวกันกับในกรณีอื่นๆ
ความเจ็บปวดที่เพิ่มขึ้น
การจัดการ schema.rb
ที่กระชับ ไฟล์ทั่วทั้งทีมนั้นง่ายกว่าการจัดการ structure.sql
อย่างละเอียด ไฟล์.
ปัญหาใหญ่ที่สุดอย่างหนึ่งเมื่อย้ายไปยัง structure.sql
คือการสร้างความมั่นใจว่าเฉพาะการเปลี่ยนแปลงที่จำเป็นเท่านั้นที่จะถูกส่งไปยังไฟล์นั้น ซึ่งบางครั้งอาจทำได้ยาก
ตัวอย่างเช่น คุณดึงสาขาของใครบางคนและเรียกใช้การย้ายข้อมูลเฉพาะสำหรับสาขานั้น structure.sql
ของคุณ ตอนนี้จะมีการเปลี่ยนแปลงบางอย่าง จากนั้นคุณกลับไปทำงานในสาขาของคุณเองและสร้างการย้ายข้อมูลใหม่ structure.sql
ของคุณ ไฟล์จะมีทั้งการเปลี่ยนแปลงของสาขาและสาขาอื่น นี่อาจเป็นเรื่องยุ่งยากในการจัดการ และแน่นอนว่าต้องมีช่วงการเรียนรู้เล็กน้อยเมื่อต้องจัดการกับความขัดแย้งเหล่านี้
ด้วยการใช้แนวทางนี้ เรากำลังทำการประนีประนอม เราต้องจัดการกับความซับซ้อนของโค้ดล่วงหน้า ซึ่งช่วยให้เราสามารถรักษาฟังก์ชันการทำงานขั้นสูงของฐานข้อมูลของเราไว้ได้ ในทางกลับกัน เรายังต้องจัดการกับการแสดงสคีมาที่ง่ายกว่า รวมถึงการไม่มีพลังของฐานข้อมูลเพียงปลายนิ้วสัมผัส เช่น หากเราต้องการตั้งค่าการสำรองข้อมูลจาก db:setup
งาน. ฉันคิดว่าควรอดทนต่อความยุ่งยากในการควบคุมเวอร์ชันสักเล็กน้อย ดีกว่าต้องทนกับการแก้ไขข้อมูลที่เสียหาย/ไม่ถูกต้องในระบบที่ใช้งานจริง หรือไม่สามารถใช้ฟังก์ชันขั้นสูงทั้งหมดที่ฐานข้อมูลของคุณนำเสนอได้
โดยทั่วไป มีสองกลยุทธ์ที่ฉันเคยใช้เพื่อให้แน่ใจว่า structure.sql
file มีเพียงการเปลี่ยนแปลงที่จำเป็นต่อสาขาเฉพาะ:
- เมื่อคุณทำงานในสาขาที่มีการโยกย้ายเสร็จแล้ว ตรวจสอบให้แน่ใจว่าคุณเรียกใช้
rails db:rollback STEP=n
โดยที่n
คือจำนวนการย้ายถิ่นในสาขานั้นๆ เพื่อให้แน่ใจว่าโครงสร้างฐานข้อมูลของคุณจะเปลี่ยนกลับเป็นสถานะเดิม - คุณอาจลืมย้อนกลับหลังจากทำงานในสาขา ในกรณีนั้น เมื่อทำงานกับสาขาใหม่ ตรวจสอบให้แน่ใจว่าคุณได้ดึง
structure.sql
ที่เก่าแก่ จากมาสเตอร์ก่อนสร้างการย้ายข้อมูลใหม่
ตามหลักการทั่วไป structure.sql
. ของคุณ ไฟล์ควรมีเฉพาะการเปลี่ยนแปลงที่เกี่ยวข้องกับสาขาของคุณก่อนที่จะรวมเข้ากับมาสเตอร์
บทสรุป
โดยทั่วไปแล้ว เมื่อแอปพลิเคชัน Rails มีขนาดเล็กหรือไม่ต้องการคุณลักษณะขั้นสูงบางอย่างที่ฐานข้อมูลนำเสนอ ก็สามารถใช้ schema.rb
ได้อย่างปลอดภัย ซึ่งอ่านง่าย กระชับ และจัดการง่าย
อย่างไรก็ตาม เนื่องจากแอปพลิเคชันมีขนาดและความซับซ้อนเพิ่มขึ้น การสะท้อนโครงสร้างฐานข้อมูลที่แม่นยำจึงเป็นสิ่งสำคัญ จะช่วยให้ทีมสามารถรักษาข้อจำกัดที่เหมาะสม โมดูลฐานข้อมูล ฟังก์ชันและตัวดำเนินการที่ไม่เช่นนั้นจะไม่สามารถทำได้ เรียนรู้การใช้ Rails ด้วย structure.sql
. ที่ได้รับการดูแลเป็นอย่างดี ไฟล์จะนำเสนอขอบที่ง่ายกว่า schema.rb
ทำไม่ได้
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!