Trong bài đăng hôm nay, chúng tôi sẽ đề cập đến những khác biệt đáng kể và lợi ích của việc sử dụng structure.sql
so với schema.rb
mặc định các định dạng lược đồ trong ứng dụng Ruby on Rails của bạn. Trong một thế giới theo hướng dữ liệu, biết cách khai thác tất cả các tính năng phong phú của cơ sở dữ liệu của bạn có thể tạo ra sự khác biệt giữa một doanh nghiệp thành công và không thành công.
Sau khi nêu ra những điểm khác biệt chính giữa hai định dạng, chúng tôi sẽ phác thảo cách chuyển sang structure.sql
và chứng minh cách nó có thể giúp đảm bảo tính toàn vẹn của dữ liệu cũng như chức năng cơ sở dữ liệu mà bạn có thể không bảo quản được.
Trong bài đăng, tôi sẽ đưa ra các ví dụ về ứng dụng Rails sử dụng structure.sql
với cơ sở dữ liệu PostgreSQL, nhưng các khái niệm cơ bản cũng có thể được chuyển sang các cơ sở dữ liệu khác. Không có ứng dụng web trong thế giới thực nào thực sự hoàn chỉnh nếu không có cơ sở dữ liệu đáng tin cậy để hỗ trợ nó.
Không cần thêm lời khuyên nào nữa, chúng ta hãy đi sâu vào ngay!
Sự khác biệt giữa schema.rb và structure.sql
Một trong những điều đầu tiên bạn cần làm khi bắt đầu một dự án Ruby on Rails là chạy quá trình di chuyển cơ sở dữ liệu. Ví dụ:nếu bạn tạo mô hình Người dùng, Rails chắc chắn sẽ yêu cầu bạn chạy quá trình di chuyển, điều này sẽ tạo ra một schema.rb
nộp cho phù hợp:
rails g model User first_name:string last_name:string
Rails sẽ tạo ra quá trình di chuyển sau:
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
Khi quá trình di chuyển được thực hiện, bạn sẽ thấy rằng Rails đã tạo ra một schema.rb
tệp cho bạn:
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
này tệp tuyệt vời cho các ứng dụng và trường hợp sử dụng tương đối cơ bản.
Có hai điều chính cần lưu ý ở đây:
- Đây là một đại diện Ruby cho cơ sở dữ liệu của bạn;
schema.rb
được tạo bằng cách kiểm tra cơ sở dữ liệu và thể hiện cấu trúc của nó bằng Ruby. - Đây là cơ sở dữ liệu bất khả tri (tức là cho dù bạn sử dụng SQLite, PostgreSQL, MySQL hay bất kỳ cơ sở dữ liệu nào khác mà Rails hỗ trợ, thì cú pháp và cấu trúc sẽ vẫn giữ nguyên phần lớn)
Tuy nhiên, có thể có lúc chiến lược này trở nên quá hạn chế đối với ứng dụng đang phát triển của bạn.
Ví dụ:giả sử bạn có hàng trăm hoặc hàng nghìn tệp di chuyển.
Nếu bạn cần nhanh chóng khởi động một hệ thống sản xuất mới, bạn có thể gặp phải tình huống trong đó việc chạy tất cả chúng theo trình tự mất quá nhiều thời gian. Hoặc bạn có thể gặp phải tình huống trong đó một số quá trình di chuyển chứa mã được thực thi trên phiên bản cũ hơn của cơ sở dữ liệu của bạn, nhưng mã đó không còn thực thi được trên phiên bản hiện tại. Bạn có thể gặp phải tình huống trong đó quá trình di chuyển được viết với một số giả định dữ liệu nhất định không còn hợp lệ, điều này sẽ khiến quá trình di chuyển không thành công.
Tất cả các tình huống này ngăn cản việc thiết lập hiệu quả phiên bản mới của ứng dụng của bạn — có thể là phiên bản sản xuất hoặc cho một thành viên mới trong nhóm — với một rails db:create db:migrate
đơn giản yêu cầu. Nếu đúng như vậy, bạn sẽ làm cách nào để bắt kịp tốc độ với một lược đồ cơ sở dữ liệu chính xác?
Chắc chắn, một cách sẽ là quay lại và sửa tất cả các lần di chuyển bị hỏng. Đó không bao giờ là một ý tưởng tồi!
Nếu việc quay lại và sửa một loạt các lần di chuyển là quá tốn kém, một cách khác sẽ là chạy rails db:setup
nhiệm vụ. Tác vụ này sẽ tạo một lược đồ cơ sở dữ liệu từ schema.rb
của bạn tập tin. Tuy nhiên, điều gì sẽ xảy ra nếu cơ sở dữ liệu của bạn chứa logic phức tạp không được trình bày trong schema.rb
đại diện cho cơ sở dữ liệu của bạn?
May mắn thay, Rails cung cấp một giải pháp thay thế:structure.sql
structure.sql
khác với schema.rb
theo những cách sau:
- Nó cho phép tạo một bản sao chính xác của cấu trúc cơ sở dữ liệu. Điều này rất quan trọng khi làm việc với nhóm, cũng như nếu bạn cần nhanh chóng tạo cơ sở dữ liệu mới trong quá trình sản xuất từ
rails db:setup
nhiệm vụ. - Nó cho phép lưu giữ thông tin của các tính năng cơ sở dữ liệu nâng cao. Ví dụ:nếu bạn đang sử dụng PostgreSQL, nó cho phép sử dụng các khung nhìn, các khung nhìn cụ thể hóa, các hàm, các ràng buộc, v.v.
Khi một ứng dụng đạt đến một mức độ hoàn thiện nhất định, chúng tôi phải sử dụng mọi thủ thuật trong sách để tăng hiệu quả, duy trì tính chính xác của dữ liệu và đảm bảo hiệu suất nhanh như chớp. Sử dụng structure.sql
để quản lý hành vi của cơ sở dữ liệu Rails cho phép người dùng làm như vậy.
Chuyển từ schema.rb
thành structure.sql
Thực hiện thay đổi từ schema.rb
thành structure.sql
là một quá trình tương đối đơn giản. Tất cả những gì bạn cần làm là đặt dòng sau trong 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
Sau đó, chạy rails db:migrate
và bạn sẽ thấy tệp trong db/structure.sql
. Voilà! Rails sẽ kết xuất cấu trúc cơ sở dữ liệu bằng công cụ dành riêng cho cơ sở dữ liệu bạn đang sử dụng (trong trường hợp của PostgreSQL, công cụ đó là pg_dump
, đối với MySQL hoặc MariaDB, nó sẽ chứa đầu ra của SHOW CREATE TABLE
cho mỗi bảng, v.v.). Bạn nên đảm bảo rằng tệp này được kiểm soát phiên bản để những người còn lại trong nhóm của bạn sẽ có cùng cấu trúc cơ sở dữ liệu.
Cái nhìn đầu tiên về tệp đó có thể khiến bạn nản lòng:schema.rb
tệp chỉ có 25 dòng, trong khi structure.sql
tập tin là một con số khổng lồ 109 dòng! Những lợi ích nào mà một tệp lớn như vậy có thể thêm vào việc phát triển ứng dụng?
Thêm các ràng buộc cấp cơ sở dữ liệu
ActiveRecord là một trong những phần yêu thích của tôi khi sử dụng Rails. Nó cho phép bạn truy vấn cơ sở dữ liệu theo cách tự nhiên, gần giống như trong ngôn ngữ nói. Ví dụ:nếu bạn muốn tìm tất cả người dùng của công ty có tên Dan, thì ActiveRecord cho phép bạn chỉ cần chạy một truy vấn như sau:
company = Company.find(name: 'Some Company')
# Reads just like in a natural language!
company.users.where(first_name: 'Dan')
Tuy nhiên, có một số trường hợp ActiveRecord bị thiếu. Ví dụ:giả sử bạn có xác thực sau trên mô hình Người dùng của mình:
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
Nếu bạn cố gắng tạo người dùng có tên 'Dan', bạn sẽ thấy lỗi khi xác thực chạy:
User.create!(first_name: 'Dan')
Traceback (most recent call last):
ActiveRecord::RecordInvalid (Validation failed: First name cannot start with the letter 'D')
Điều này không sao cả, nhưng giả sử bạn hoặc một trong các thành viên trong nhóm của bạn đã thay đổi dữ liệu bằng cách bỏ qua xác thực của ActiveRecord:
u = User.create(first_name: 'Pan')
# The update_attribute method bypasses ActiveRecord validations
u.update_attribute :first_name, 'Dan'
u.first_name
=> "Dan"
Như đã chứng minh, rất dễ dàng bỏ qua xác thực.
Điều này có thể gây ra hậu quả tai hại cho ứng dụng của chúng tôi. ActiveRecord có thể là một may mắn cũng như một lời nguyền — trong khi nó có một DSL rất rõ ràng và tự nhiên khiến bạn cảm thấy thích thú khi làm việc cùng, nó thường quá dễ dãi khi thực thi xác thực cấp mô hình. Giải pháp, như bạn có thể đã biết, là thêm các ràng buộc cấp cơ sở dữ liệu.
rails g migration AddFirstNameConstraintToUser
Thao tác này sẽ tạo một tệp mà bạn có thể chỉnh sửa theo logic để không cho phép các tên bắt đầu bằng chữ '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
Lưu ý rằng nó rất quan trọng là thêm mã hoàn nguyên thành công quá trình di chuyển. Trong ví dụ trên, tôi có up
và down
các chỉ thị. up
phương thức được thực thi khi quá trình di chuyển chạy, down
được thực thi khi quá trình di chuyển được khôi phục lại. Nếu không hoàn nguyên đúng cấu trúc cơ sở dữ liệu của bạn, bạn có thể phải thực hiện một số công việc dọn dẹp nhà cửa thủ công sau đó. Tôi khuyên bạn nên luôn có một tệp di chuyển có thể được thực thi cả up
và down
để tránh đau đầu trong tương lai.
Bây giờ, hãy chạy quá trình di chuyển và kiểm tra xem bạn có thể bỏ qua ràng buộc đó hay không:
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).
Hoàn hảo! Ràng buộc của chúng tôi đang hoạt động như dự định. Ngay cả khi, vì bất kỳ lý do gì, chúng tôi bỏ qua xác thực của ActiveRecord, chúng tôi vẫn có thể dựa vào cơ sở dữ liệu — thủ môn cuối cùng của chúng tôi — để bảo toàn tính toàn vẹn dữ liệu của chúng tôi.
Điều này liên quan gì đến structure.sql
?
Nếu bạn nhìn vào nó, bạn sẽ thấy rằng phần sau đã được thêm vào:
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)));
Ràng buộc của bạn nằm trong chính lược đồ!
Trong khi schema.rb
cũng hỗ trợ các ràng buộc cấp cơ sở dữ liệu, điều quan trọng cần nhớ là nó không thể hiện mọi thứ mà cơ sở dữ liệu của bạn có thể hỗ trợ như trình kích hoạt, trình tự, thủ tục được lưu trữ hoặc ràng buộc kiểm tra. Ví dụ:đây là điều sẽ xảy ra với tệp giản đồ của bạn với cùng một lần di chuyển chính xác (AddFirstNameConstraintToUser
) nếu bạn chỉ sử dụng 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
Tập tin không thay đổi! Ràng buộc không được thêm vào.
Nếu bạn đã giới thiệu một nhà phát triển mới để làm việc trong dự án của mình, bạn có thể đang hoạt động theo các quy định cơ sở dữ liệu khác nhau.
Đang cam kết structure.sql
kiểm soát phiên bản sẽ giúp đảm bảo rằng nhóm của bạn ở trên cùng một trang. Nếu bạn chạy rails db:setup
có structure.sql
, cấu trúc cơ sở dữ liệu của bạn sẽ chứa các ràng buộc trên. Với schema.rb
không có bảo đảm như vậy.
Điều này cũng có thể nói về một hệ thống sản xuất. Nếu bạn cần thiết lập nhanh một phiên bản mới của ứng dụng với cơ sở dữ liệu mới — và việc chạy tất cả các quá trình di chuyển tuần tự sẽ mất nhiều thời gian — việc thiết lập cơ sở dữ liệu từ structure.sql
tệp sẽ nhanh hơn rất nhiều. Chúng tôi có thể yên tâm rằng structure.sql
sẽ tạo cơ sở dữ liệu của chúng tôi với cấu trúc giống hệt như trong các trường hợp khác.
Tăng lợi nhuận
Quản lý schema.rb
ngắn gọn tệp trong nhóm là một nhiệm vụ dễ dàng hơn nhiều so với việc quản lý structure.sql
dài dòng tệp.
Một trong những khó khăn ngày càng tăng khi di chuyển sang structure.sql
đảm bảo rằng chỉ những thay đổi bắt buộc mới được cam kết đối với tệp đó, điều này đôi khi có thể khó thực hiện.
Ví dụ:giả sử bạn kéo nhánh của ai đó và chạy các di chuyển cụ thể cho nhánh đó. structure.sql
của bạn bây giờ sẽ chứa một số thay đổi. Sau đó, bạn quay lại làm việc trên chi nhánh của riêng mình và tạo một quá trình di chuyển mới. structure.sql
của bạn tệp bây giờ sẽ chứa cả các thay đổi của chi nhánh của bạn và chi nhánh khác. Điều này có thể hơi phức tạp để giải quyết và chắc chắn có một chút đường kinh nghiệm khi nói đến việc quản lý những xung đột này.
Bằng cách sử dụng cách tiếp cận này, chúng tôi đang thực hiện một sự cân bằng. Chúng tôi phải đối phó với một chút phức tạp của mã từ trước cho phép chúng tôi duy trì chức năng nâng cao của cơ sở dữ liệu của chúng tôi. Đổi lại, chúng ta cũng phải đối phó với một biểu diễn giản đồ đơn giản hơn cũng như không có tất cả sức mạnh của cơ sở dữ liệu trong tầm tay của chúng ta, ví dụ:nếu chúng ta muốn thiết lập một bản sao lưu từ db:setup
nhiệm vụ. Tôi cho rằng tốt nhất bạn nên giải quyết một chút rắc rối về kiểm soát phiên bản hơn là phải chịu đựng việc sửa dữ liệu bị hỏng / không chính xác trong hệ thống sản xuất hoặc không thể sử dụng tất cả các chức năng nâng cao mà cơ sở dữ liệu của bạn cung cấp.
Nói chung, có hai chiến lược tôi đã sử dụng để đảm bảo structure.sql
của mình tệp chỉ chứa những thay đổi cần thiết đối với một nhánh cụ thể:
- Khi bạn đã hoàn thành công việc trên một nhánh có chứa các di chuyển, hãy đảm bảo rằng bạn chạy
rails db:rollback STEP=n
ở đâun
là số lần di chuyển trong nhánh đó. Điều này sẽ đảm bảo cấu trúc cơ sở dữ liệu của bạn trở lại trạng thái ban đầu. - Bạn có thể quên khôi phục sau khi làm việc trên một nhánh. Trong trường hợp đó, khi làm việc trên một nhánh mới, hãy đảm bảo rằng bạn kéo một
structure.sql
nguyên sơ tệp từ trang cái trước khi tạo bất kỳ quá trình di chuyển mới nào.
Theo quy tắc chung, structure.sql
của bạn tệp chỉ nên chứa các thay đổi liên quan đến nhánh của bạn trước khi được hợp nhất thành tệp chính.
Kết luận
Nói chung, khi các ứng dụng Rails nhỏ hoặc không cần một số tính năng nâng cao hơn mà cơ sở dữ liệu cung cấp thì bạn có thể sử dụng schema.rb
một cách an toàn. , rất dễ đọc, ngắn gọn và dễ quản lý.
Tuy nhiên, khi một ứng dụng phát triển về quy mô và độ phức tạp, thì điều cốt yếu là phản ánh chính xác cấu trúc cơ sở dữ liệu. Nó sẽ cho phép một nhóm duy trì các ràng buộc, mô-đun cơ sở dữ liệu, chức năng và toán tử phù hợp mà nếu không sẽ không thể thực hiện được. Học cách sử dụng Rails với structure.sql
được duy trì tốt tệp sẽ cung cấp một lợi thế mà schema.rb
đơn giản hơn đơn giản là không thể.
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!