Computer >> Máy Tính >  >> Lập trình >> Ruby

Mổ xẻ các cuộc di chuyển đường ray

Trong bài đăng ngày hôm nay, chúng ta sẽ đi sâu hơn vào việc di chuyển Rails. Chúng tôi sẽ chia nhỏ quá trình di chuyển thành các phần khác nhau và trong quá trình này, hãy tìm hiểu cách viết một quá trình di chuyển hiệu quả. Chúng ta sẽ tìm hiểu cách viết các lần di chuyển cho nhiều cơ sở dữ liệu, cũng như cách xử lý các lần di chuyển không thành công và các kỹ thuật thực hiện quá trình khôi phục.

Để hiểu toàn bộ bài đăng, bạn cần có hiểu biết cơ bản về cơ sở dữ liệu và Rails.

Di chuyển 101

Migrations in Rails cho phép chúng tôi phát triển cơ sở dữ liệu trong suốt thời gian tồn tại của một ứng dụng. Việc di chuyển cho phép chúng ta viết mã Ruby thuần túy để thay đổi trạng thái của cơ sở dữ liệu bằng cách cung cấp một DSL thanh lịch. Chúng ta không cần phải viết SQL dành riêng cho cơ sở dữ liệu vì quá trình di chuyển cung cấp các thông tin trừu tượng để thao tác cơ sở dữ liệu và quan tâm đến các chi tiết phức tạp của việc chuyển đổi DSL thành các truy vấn SQL dành riêng cho cơ sở dữ liệu phía sau. Migrations cũng tránh xa cách của chúng tôi và cung cấp các cách thực thi SQL thô trên cơ sở dữ liệu, nếu nhu cầu đó phát sinh.

Hai vạn giải đấu vào cơ sở dữ liệu Rails Di chuyển

Chúng tôi có thể tạo bảng, thêm hoặc xóa cột và thêm chỉ mục trên các cột bằng cách sử dụng di chuyển.

Mỗi ứng dụng Rails đều có một thư mục đặc biệt— db/migrate —Có nơi lưu trữ tất cả các lần di chuyển.

Hãy bắt đầu với việc di chuyển tạo bảng events vào cơ sở dữ liệu của chúng tôi.

$ rails g migration CreateEvents category:string

Lệnh này tạo tệp có dấu thời gian 20200405103635_create_events.rb trong db/migrate danh mục. Nội dung của tệp như sau.

class CreateEvents < ActiveRecord::Migration[6.0]
  def change
    create_table :events do |t|
      t.string :category
 
      t.timestamps
    end
  end
end

Hãy chia nhỏ tệp di chuyển này.

  • Mọi tệp di chuyển mà Rails tạo ra đều có dấu thời gian có trong tên tệp. Dấu thời gian này rất quan trọng và được Rails sử dụng để xác nhận xem quá trình di chuyển đã chạy hay chưa, như chúng ta sẽ thấy ở phần sau.
  • Quá trình di chuyển chứa một lớp kế thừa từ ActiveRecord::Migration[6.0] . Khi tôi đang sử dụng Rails 6, lớp cha di chuyển có [6.0] . Nếu tôi đang sử dụng Rails 5.2, thì lớp cha sẽ là ActiveRecord::Migration[5.2] . Sau đó, chúng ta sẽ thảo luận lý do tại sao phiên bản Rails lại là một phần của tên lớp cha.
  • Việc di chuyển có một phương pháp change chứa mã DSL thao tác cơ sở dữ liệu. Trong trường hợp này, change phương thức đang tạo một events bảng có cột category thuộc loại string .
  • Quá trình di chuyển sử dụng mã t.timestamps để thêm dấu thời gian created_atupdated_at đến events bảng.

Khi quá trình di chuyển này được chạy bằng rails db:migrate , nó sẽ tạo ra một events bảng có category cột của loại string và cột dấu thời gian created_atupdated_at .

Loại cột cơ sở dữ liệu thực tế sẽ là varchar hoặc text, tùy thuộc vào cơ sở dữ liệu.

Tầm quan trọng của Dấu thời gian di chuyển và Bảng schema_migration

Mỗi khi di chuyển được tạo bằng rails g migration lệnh, Rails tạo tệp di chuyển với một dấu thời gian duy nhất. Dấu thời gian nằm trong định dạng YYYYMMDDHHMMSS . Bất cứ khi nào quá trình di chuyển được chạy, Rails sẽ chèn dấu thời gian di chuyển vào một bảng nội bộ schema_migrations . Bảng này được tạo bởi Rails khi chúng tôi chạy lần di chuyển đầu tiên của mình. Bảng chỉ có cột version , cũng là khóa chính của nó. Đây là cấu trúc của schema_migrations bảng.

CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);

Bây giờ chúng ta đã chạy quá trình di chuyển để tạo events bảng, hãy xem liệu Rails có lưu trữ dấu kích thích của quá trình di chuyển này trong schema_migrations hay không bảng.

sqlite> select * from schema_migrations;
20200405103635

Nếu chúng tôi chạy lại quá trình di chuyển, trước tiên, Rails sẽ kiểm tra xem mục nhập có tồn tại trong schema_migrations hay không bảng có dấu thời gian của tệp di chuyển và chỉ thực thi nó nếu không có mục nhập như vậy. Điều này đảm bảo rằng chúng tôi có thể thêm dần các thay đổi vào cơ sở dữ liệu theo thời gian và quá trình di chuyển sẽ chỉ chạy một lần trên cơ sở dữ liệu.

Lược đồ cơ sở dữ liệu

Khi chúng tôi chạy ngày càng nhiều di chuyển, lược đồ cơ sở dữ liệu tiếp tục phát triển. Rails lưu trữ lược đồ cơ sở dữ liệu mới nhất trong tệp db/schema.rb . Tệp này là bản trình bày Ruby của tất cả các quá trình di chuyển trên cơ sở dữ liệu của bạn trong suốt vòng đời của ứng dụng. Do tệp này, chúng tôi không cần phải giữ các tệp di chuyển cũ trong cơ sở mã. Rails cung cấp các tác vụ để dump lược đồ mới nhất từ ​​cơ sở dữ liệu vào schema.rbload lược đồ thành cơ sở dữ liệu từ schema.rb . Vì vậy, các di chuyển cũ hơn có thể được xóa khỏi codebase một cách an toàn. Việc tải lược đồ vào cơ sở dữ liệu cũng nhanh hơn so với việc chạy mỗi lần di chuyển mỗi khi chúng tôi thiết lập ứng dụng.

Rails cũng cung cấp một cách để lưu trữ lược đồ cơ sở dữ liệu ở định dạng SQL. Chúng tôi đã có một bài viết để so sánh hai định dạng. Bạn có thể đọc thêm về nó tại đây.

Phiên bản Rails trong Di chuyển

Mỗi lần di chuyển mà chúng tôi tạo ra đều có phiên bản Rails như một phần của lớp cha. Vì vậy, quá trình di chuyển được tạo bởi ứng dụng Rails 6 có lớp cha ActiveRecord::Migration[6.0] Di chuyển whereasa được tạo bởi ứng dụng Rails 5.2 có lớp cha ActiveRecord::Migration[5.2] . Nếu bạn có ứng dụng anold với Rails 4.2 trở xuống, bạn sẽ nhận thấy rằng không có phiên bản nào trong lớp cha. Lớp cha chỉ là ActiveRecord::Migration .

Phiên bản Rails đã được thêm vào lớp siêu di chuyển trong Rails 5. Điều này về cơ bản đảm bảo rằngAPI di chuyển có thể phát triển theo thời gian mà không phá vỡ các quá trình di chuyển được tạo bởi các phiên bản Rails cũ hơn.

Hãy xem xét kỹ hơn vấn đề này bằng cách xem xét quá trình di chuyển tương tự để tạo events bảng trong ứng dụng Rails 4.2.

class CreateEvents < ActiveRecord::Migration
  def change
    create_table :events do |t|
      t.string :category
 
      t.timestamps null: false
    end
  end
end

Nếu chúng ta nhìn vào lược đồ của events bảng được tạo bởi quá trình di chuyển Rails 6, chúng ta có thể thấy rằng NOT NULL ràng buộc cho các cột dấu thời gian tồn tại.

sqlite> .schema events
CREATE TABLE IF NOT EXISTS "events" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "category" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

Điều này là do, bắt đầu từ Rails 5 trở đi, API di chuyển tự động thêm NOT NULL ràng buộc các cột dấu thời gian mà không cần phải thêm nó một cách rõ ràng trong tệp di chuyển. Phiên bản Rails trong tên lớp cha đảm bảo rằng quá trình di chuyển sử dụng API di chuyển của phiên bản theRails mà quá trình di chuyển được tạo ra. Điều này cho phép Rails duy trì khả năng tương thích ngược với các di chuyển cũ hơn, đồng thời phát triển API di chuyển.

Thay đổi lược đồ cơ sở dữ liệu

change phương thức là phương thức chính trong quá trình di chuyển. Khi một quá trình di chuyển được chạy, nó sẽ gọi change và thực thi mã bên trong nó.

Cùng với create_table , Rails cũng cung cấp một phương thức mạnh mẽ khác— change_table Như tên cho thấy, nó được sử dụng để thay đổi lược đồ của một bảng hiện có.

def change
  change_table :events do |t|
    t.remove :category
    t.string :event_type
    t.boolean :active, default: false
  end
end

Việc di chuyển này sẽ xóa category cột từ events bảng, thêm một cột chuỗi mới events_type và một cột boolean mới active với giá trị mặc định là false .

Rails cũng cung cấp rất nhiều phương thức trợ giúp khác có thể được sử dụng trong quá trình di chuyển, chẳng hạn như:

  • change_column
  • add_index
  • remove_index
  • rename_table

và nhiều cái khác. Bạn có thể tìm thấy tất cả các phương pháp có thể được sử dụng với sự thay đổi tại đây

Dấu thời gian

Chúng tôi thấy rằng t.timestamps đã được thêm vào quá trình di chuyển bởi Rails và nó đã thêm các cột created_atupdated_at đến events bàn. Các cột đặc biệt này được Railsto sử dụng để theo dõi thời điểm bản ghi được tạo và cập nhật. Rails thêm giá trị vào các cột này khi bản ghi được tạo và đảm bảo cập nhật chúng khi bản ghi được cập nhật. Các cột này giúp chúng tôi theo dõi vòng đời của bản ghi cơ sở dữ liệu.

updated_at cột không được cập nhật khi chúng tôi thực thi updated_all phương thức từ Rails.

Xử lý lỗi

Di chuyển không có khả năng chống đạn. Họ có thể thất bại. Lý do có thể là sai cú pháp hoặc một truy vấn cơ sở dữ liệu không hợp lệ. Dù lý do là gì, chúng ta phải xử lý lỗi và khôi phục nó để cơ sở dữ liệu không đi vào trạng thái không nhất quán. Rails giải quyết vấn đề này bằng cách chạy từng lần di chuyển bên trong một giao dịch. Nếu quá trình di chuyển không thành công, thì giao dịch sẽ được khôi phục lại. Điều này đảm bảo rằng cơ sở dữ liệu không chuyển sang trạng thái không nhất quán.

Điều này chỉ được thực hiện đối với cơ sở dữ liệu hỗ trợ các giao dịch để cập nhật lược đồ cơ sở dữ liệu. Chúng được gọi là các giao dịch Ngôn ngữ Định nghĩa Dữ liệu (DDL). MySQL và PostgreSQL đều hỗ trợ các giao dịch DDL.

Đôi khi, chúng tôi không muốn thực hiện một số chuyển đổi nhất định bên trong một giao dịch. Một ví dụ đơn giản là khi thêm chỉ mục dòng điện trong PostgreSQL. Việc di chuyển như vậy không thể được thực hiện bên trong một giao dịch DDL dưới dạng PostgreSQLtries để thêm chỉ mục mà không có khóa trên bảng để chúng tôi có thể thêm chỉ mục trên cơ sở dữ liệu sản xuất trực tiếp mà không cần gỡ cơ sở dữ liệu xuống. Rails cung cấp một cách để chọn không tham gia các giao dịch bên trong quá trình di chuyển dưới dạng disable_ddl_transactions! .

def change
  disable_ddl_transactions!
 
  add_index :events, :user_id, algorithm: :concurrently

Điều này sẽ không chạy quá trình di chuyển bên trong một giao dịch. Nếu quá trình di chuyển như vậy không thành công, chúng tôi cần phải tự khôi phục nó. Trong trường hợp này, chúng ta có thể REINDEX hoặc xóa chỉ mục và thử thêm lại.

Di chuyển có thể đảo ngược

Rails cho phép chúng tôi khôi phục các thay đổi đối với cơ sở dữ liệu bằng lệnh sau.

rails db:rollback

Lệnh này hoàn nguyên lần di chuyển cuối cùng đã được chạy trên cơ sở dữ liệu. Nếu quá trình di chuyển đã thêm một cột event_type thì việc khôi phục sẽ loại bỏ cột đó. Nếu quá trình di chuyển đã thêm một chỉ mục, thì quá trình khôi phục sẽ xóa chỉ mục đó.

Ngoài ra còn có một lệnh để quay lại quá trình di chuyển trước đó và chạy nó. Đó là rails db:redo .

Rails đủ thông minh để biết cách đảo ngược hầu hết các đợt di chuyển. Nhưng chúng tôi cũng có thể cung cấp gợi ý cho Railson cách hoàn nguyên quá trình di chuyển bằng cách cung cấp updown thay vì sử dụng change method.The up phương thức sẽ được sử dụng khi quá trình di chuyển được chạy trong khi down phương thức sẽ được sử dụng khi quá trình di chuyển được hoàn nguyên.

def up
  change_table :events do |t|
    t.change :price, :string
  end
end
 
def down
  change_table :events do |t|
    t.change :price, :integer
  end
end

Trong ví dụ này, chúng tôi đang thay đổi price cột events từ integer thành string . Chúng tôi chỉ định cách nó sẽ được khôi phục trong down phương pháp.

Quá trình di chuyển tương tự này cũng có thể được viết bằng cách sử dụng change phương pháp.

def change
  reversible do |direction|
    change_table :events do |t|
      direction.up { t.change :price, :string }
      direction.down { t.change :price, :integer }
    end
  end
end

Rails cũng cung cấp một cách để hoàn nguyên quá trình di chuyển trước đó hoàn toàn bằng cách sử dụng revert phương pháp.

def change
  revert CreateEvents
 
  create_table :events do
   ...
  end
end

revert phương thức cũng chấp nhận một khối để hoàn nguyên một phần quá trình di chuyển.

def change
  revert do
    reversible do |direction|
      change_table :events do |t|
        direction.up { t.remove :event_type }
        direction.down { t.string :event_type }
      end
    end
  end
end

Thực thi nó thô

Đôi khi, chúng tôi muốn thực thi SQL phức tạp bên trong quá trình di chuyển. Trong những trường hợp như vậy, chúng ta có thể quên DSL di chuyển điển hình và thay vào đó thực thi SQL thô như sau.

def change
  execute <<-SQL
    ....
  SQL
end

Nhiều Cơ sở dữ liệu và Di chuyển

Rails 6 đã thêm hỗ trợ để sử dụng nhiều cơ sở dữ liệu trong một ứng dụng Rails. Nếu chúng ta muốn sử dụng nhiều cơ sở dữ liệu, chúng ta định cấu hình chúng trong database.yml tệp.

development:
  primary:
    <<: *default
    database: db/development.sqlite3
  analytics:
    adapter: sqlite3
    database: db/analytics_dev.sqlite3

Cấu hình này cho Rails biết rằng chúng tôi muốn sử dụng hai cơ sở dữ liệu— primaryanalytics Như chúng ta đã thấy trước đó, quá trình di chuyển được lưu trữ trong db/migrate thư mục theo mặc định. Nhưng trong trường hợp này, chúng tôi không thể thêm di chuyển của cả hai cơ sở dữ liệu bên trong một thư mục. Chúng tôi không muốn chạy quá trình di chuyển analytics cơ sở dữ liệu trên primary cơ sở dữ liệu và ngược lại. Nếu chúng tôi đang sử dụng nhiều cơ sở dữ liệu, cần thiết phải cung cấp một đường dẫn để lưu trữ các di chuyển cho cơ sở dữ liệu thứ hai. Điều này có thể được thực hiện bằng cách cung cấp migrations_paths trong database.yml .

development:
  primary:
    <<: *default
    database: db/development.sqlite3
  analytics:
    adapter: sqlite3
    database: db/analytics_dev.sqlite3
    migrations_paths: db/analytics_migrate

Sau đó, chúng tôi có thể tạo di chuyển cho analytics cơ sở dữ liệu như sau.

rails generate migration AddExperiments rule:string active:boolean --db=analytics

Điều này sẽ tạo ra sự di chuyển bên trong db/analytics_migrate và chúng tôi có thể chạy nó như sau.

rails db:migrate --db=analytics

Nếu chúng ta chỉ chạy rails db:migrate , nó sẽ thực hiện di chuyển cho tất cả các cơ sở dữ liệu.

analytics cơ sở dữ liệu sẽ có schema_migrations của riêng nó bảng để theo dõi quá trình di chuyển nào được chạy và không chạy.

Chạy di chuyển trong quá trình triển khai

Vì quá trình di chuyển có thể thay đổi trạng thái của cơ sở dữ liệu và mã của chúng tôi có thể phụ thuộc vào những thay đổi đó, điều cực kỳ quan trọng là quá trình di chuyển phải được chạy trước trước khi áp dụng mã mới.

Trong triển khai dựa trên Heroku, quá trình di chuyển có thể được chạy trong release giai đoạn của Procfile .

# Profile
web: bin/puma -C config/puma.rb
release: bundle exec rake db:migrate

Điều này đảm bảo rằng quá trình di chuyển được chạy trước khi khởi động lại ứng dụng dynos.

Trong triển khai dựa trên Capistrano, quá trình di chuyển sẽ chạy trước khi máy chủ được khởi động lại.

Trong triển khai dựa trên docker, chúng tôi có thể chạy một vùng chứa sidecar để chạy quá trình di chuyển trước khi ứng dụng được khởi động lại. Điều này rất quan trọng vì nếu không, các vùng chứa mới có thể chuyển sang trạng thái không nhất quán nếu chúng bắt đầu sử dụng mã mới trước khi áp dụng các thay đổi cơ sở dữ liệu cho mã mới đó.

Kết luận

Trong bài đăng này, chúng ta đã thấy các khía cạnh khác nhau của việc viết chuyển đổi cơ sở dữ liệu trong Rails. Chúng tôi cũng đã xem điều gì tạo nên sự di cư cũng như cách xử lý các thất bại và khôi phục các cuộc di cư nếu cần. Rails 6 cho phép chúng tôi sử dụng nhiều cơ sở dữ liệu và việc di chuyển cho từng nhu cầu được thêm vào riêng biệt. Cuối cùng, chúng ta đã biết sơ qua về cách chạy di chuyển trong quá trình triển khai để các thay đổi cơ sở dữ liệu được áp dụng đúng cách trước khi bất kỳ mã mới nào bắt đầu sử dụng chúng.

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!