Dev With Ethan

Blog Lập trình cùng Ethan

[Ruby on Rails] Migration trong Rails: Tạo mới Models với Active Record

1. Active Record

Để đảm bảo chúng ta có cùng 1 cách hiểu về Migration trong Ruby on Rails, mời các bạn xem qua 1 vài tài liệu về Active Record từ trang hướng dẫn chính thức của Ruby on Rails.

Để hiểu 1 cách đơn giản, Active Record là 1 khái niệm được dùng trong các công nghệ làm web back-end hiện đại theo mô hình Model - View - Controller (viết tắt là MVC), trong đó Model là các luồng làm việc với dữ liệu, View là các xử lý liên quan đến giao diện, tương tác đối với người dùng cuối (end-user) hoặc front-end (web app, mobile app), còn Controller là tất cả những logic cầu nối giữa ModelView.

Khi mới ra đời, việc sử dụng các raw query (các câu lệnh query trực tiếp dữ liệu từ database) ngay trong các Controller là khá phổ biến, nhưng bộc lộ rất nhiều nhược điểm như:

  • Không tường minh: các câu lệnh dài dằng dặc, nối vài bảng với nhau gây rối rắm và không rõ ràng về chức năng
  • Khó bảo trì: vì lý do ở trên nên thường việc debug một đoạn code viết bằng raw query rất đau đầu và mất nhiều thời gian, đôi khi còn không hiệu quả bằng đoạn code viết ban đầu (?!)
  • Không an toàn: SQL injection là 1 thủ thuật (thủ đoạn ?!) được các hacker đời đầu ưa chuộng, vì nó đơn giản, đánh vào các câu query được viết ẩu & các server không được đầu tư kỹ về bảo mật

Do vậy, cần có 1 tầng logic nằm giữa các nhu cầu xử lý dữ liệu của lập trình viên và các lệnh cấp thấp tương tác trực tiếp với database engine (như MySQL hoặc PostgreSQL). Active Record là 1 kỹ thuật nhằm giải quyết vấn đề này.

Trong Ruby on Rails, Active Record được thiết kế rất gần với cấu trúc và đặc tính của các SQL database, vậy nên đối với các bạn đã thành thạo 1 SQL database từ trước là 1 lợi thế. Ngược lại, những ai đã từng làm việc với NoSQL như MongoDB trong Ruby on Rails (thông qua 1 Object-Relational Mapping - ORM tên là Mongoid), cũng không khó khăn lắm để làm quen với các khái niệm trong Active Record (vì Mongoid được làm ra dựa trên Active Record nhưng chỉ dành cho NoSQL).

Tuy nhiên có 1 thứ mà bất kỳ ai cũng phải vượt qua, đó là Migration. Về cơ bản, các SQL database đều quy định khá chặt chẽ về cấu trúc các bảng dữ liệu, được gọi là schema. Mỗi khi chúng ta định nghĩa 1 bảng dữ liệu, 1 schema được sinh ra để lưu lại cấu trúc của bảng đó. Tuy nhiên trong quá trình phát triển dự án, schema cần luôn luôn thay đổi 1 cách linh hoạt. Nếu chỉ tiến hành thay đổi tại Active Record, các database engine sẽ báo lỗi do sự sai khác về định nghĩa. Lúc này có 2 cách để tiếp tục làm việc:

  • Drop database: tức là xóa toàn bộ dữ liệu của bảng đó đi, khi đó database engine sẽ ghi dữ liệu theo schema mới
  • Viết các Migration: toàn bộ dữ liệu vẫn được dữ nguyên, nhưng database engine biết được cột (column) nào sẽ được thay đổi và cần thay đổi như thế nào.

Cách thứ nhất rất nhanh và dễ, nhưng là 1 thói quen xấu, đặc biệt khi dự án đã ra mắt người dùng cuối, chạy và ghi nhận dữ liệu thật, thì không có cách nào xóa trắng dữ liệu được cả. Vậy nên chúng ta cần đến cách thứ 2, đó là viết Migration.

2. Bài toán cụ thể

Migration là cách để chúng ta định nghĩa 1 schema cần thay đổi ở đâu và như thế nào.

Ví dụ, ta có 1 ứng dụng tên là SecretMessenger. Ứng dụng này cần có 3 models:

  • User: các dữ liệu liên quan đến người dùng đăng nhập vào hệ thống
  • Conversation: các cuộc hội thoại tạo ra giữa 2 người dùng với nhau
  • Message: các tin nhắn mà 2 người dùng trao đổi trong 1 cuộc hội thoại

Đây là 1 thiết kế khá giống với hầu hết các ứng dụng nhắn tin hiện nay như Messenger của Facebook, Viber hay Telegram. Giả sử thiết kế ban đầu của chúng ta có dạng như thế này:

SecretMessenger - Database schema - v1.0
Database schema v1.0

Đây là thiết kế cơ bản nhất, cho phép ứng dụng SecretMessenger của chúng ta có thể:

  • Lưu trữ thông tin người dùng trong bảng User
  • Lưu trữ các cuộc hội thoại trong bảng Conversation, trong đó biết được ai là người gửi (thông qua trường from_user_id), ai là người nhận (to_user_id)
  • Lưu trữ các tin nhắn trong bảng Message, trong đó cũng biết được ai là người gửi (thông qua trường from_user_id), ai là người nhận (to_user_id), tin nhắn nào nằm trong cuộc hội thoại nào (conversation_id)
  • Tất cả các Model đều có các trường created_atupdated_at gọi là các timestamps lưu trữ các thông tin ngày giờ được khởi tạo và ghi vào bảng (created) và ngày giờ cập nhật giá trị mới (updated)

Chúng ta sẽ đi vào chi tiết từng bước các cách Migration trong Ruby on Rails.

3. Các thao tác với Migration trong Ruby on Rails

3.1. Thêm mới 1 Model

Ban đầu, khi chưa có gì cả, ta cần thêm mới Model. Rails cung cấp cho chúng ta 1 bộ công cụ dòng lệnh (command line tools) tên là rails generate với các lệnh con (gọi là generators) như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ rails generate
Usage: rails generate GENERATOR [args] [options]

General options:
  -h, [--help]     # Print generator's options and usage
  -p, [--pretend]  # Run but do not make any changes
  -f, [--force]    # Overwrite files that already exist
  -s, [--skip]     # Skip files that already exist
  -q, [--quiet]    # Suppress status output

Please choose a generator below.

Rails:
  channel
  controller
  generator
  integration_test
  job
  mailer
  migration
  model
  resource
  responders_controller
  scaffold
  scaffold_controller
  serializer
  task

ActiveRecord:
  active_record:devise
  active_record:migration
  active_record:model

Devise:
  devise
  devise:controllers
  devise:install
  devise:views

Mongoid:
  mongoid:devise

Responders:
  responders:install

TestUnit:
  test_unit:controller
  test_unit:generator
  test_unit:helper
  test_unit:integration
  test_unit:job
  test_unit:mailer
  test_unit:model
  test_unit:plugin
  test_unit:scaffold
Shell snippet 1
Rails generators

Đây là bộ công cụ cực kỳ hữu dụng vì nó làm giúp chúng ta hầu hết các phần đơn giản như: tạo các file với tên theo chuẩn, có sẵn các đoạn mã cơ bản để khởi tạo. Lệnh này cũng có cách viết tắt là rails g thay cho viết đầy đủ là rails generate

Đúng ra để tạo mới 1 Model, ta sẽ dùng lệnh rails generate model:

1
2
3
4
$ rails generate model conversation title:string from_user:references to_user:references
      invoke  active_record
      create    db/migrate/20160704073539_create_conversations.rb
      create    app/models/conversation.rb
Shell snippet 2
Generate model

Tuy nhiên lệnh này chỉ tạo ra Model, chúng ta sẽ cần đến nhiều thành phần hơn:

  • Controller để xử lý các request liên quan đến các Conversation
  • Serializer để tự động hóa việc trả kết quả bằng JSON
  • Routes để cấu hình các URI (gọi là Routing) liên quan đến các Conversation
  • Test để viết các kiểm thử

Do đó ta sẽ dùng scaffold để tạo tất cả bằng 1 lệnh duy nhất:

1
2
3
4
5
6
7
8
9
10
$ rails generate scaffold conversation title:string from_user:references to_user:references
      invoke  active_record
      create    db/migrate/20160704073626_create_conversations.rb
      create    app/models/conversation.rb
      invoke  resource_route
       route    resources :conversations
      invoke  serializer
      create    app/serializers/conversation_serializer.rb
      invoke  scaffold_controller
      create    app/controllers/conversations_controller.rb
Shell snippet 3
Generate model với scaffold

Mặc định, rails generate sẽ tạo ra rất nhiều các file phục vụ các tính năng khác nhau, ta có thể cấu hình bật/tắt các tính năng này bằng cách thêm lệnh config.generators trong file configs/application.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module SecretMessengerApi
  class Application < Rails::Application
    # ...

    config.generators do |g|
      g.orm             :active_record
      g.template_engine nil
      g.test_framework  nil
      g.stylesheets     false
      g.javascripts     false
    end

    # ...
  end
end
Ruby code snippet 1
Cấu hình generators

(trong trường hợp này, chúng ta sẽ không sinh ra bất kỳ file nào phục vụ cho web front-end như template_engine, stylesheets, javascripts,…)

Sau khi tạo scaffold, chúng ta có 1 file mới tên là db/migrate/20160704073626_create_conversations.rb có nội dung:

1
2
3
4
5
6
7
8
9
10
11
class CreateConversations < ActiveRecord::Migration[5.0]
  def change
    create_table :conversations do |t|
      t.string :title
      t.references :from_user, foreign_key: true
      t.references :to_user, foreign_key: true

      t.timestamps
    end
  end
end
Ruby code snippet 2
File migration tự động sinh

Ở đây chúng ta có 1 class tên là CreateConversations, kế thừa từ ActiveRecord::Migration[5.0]. Class này có 1 hàm tên change, với nội dung là tạo mới (create_table) một bảng tên là conversations, trong bảng conversations:

  • Tạo 1 trường kiểu string tên là title
  • Tạo 1 trường liên kết ngoài bảng (foreign_key) tên là from_user
  • Tạo 1 trường liên kết ngoài bảng (foreign_key) tên là to_user
  • Tạo các trường thời gian created_atupdated_at (thông qua timestamps)

Tuy nhiên, việc dùng references như ở trên không đúng ý đồ của chúng ta lắm, vì nó không thể hiện được rõ ràng mối quan hệ n-1 giữa ConversationUser. Ta sẽ sửa lại file Migration này 1 chút như sau:

1
2
3
4
5
6
7
8
9
10
11
class CreateConversations < ActiveRecord::Migration[5.0]
  def change
    create_table :conversations do |t|
      t.string :title
      t.belongs_to :from_user
      t.belongs_to :to_user

      t.timestamps
    end
  end
end
Ruby code snippet 3
Thêm quan hệ belongs_to

Ngoài ra, trong file app/models/conversation.rb, chúng ta cũng có nội dung như sau:

1
2
3
4
class Conversation < ApplicationRecord
  belongs_to :from_user
  belongs_to :to_user
end
Ruby code snippet 6
Quan hệ mặc định trong model

Lúc này, Active Record sẽ hiểu rằng, Conversation có quan hệ n-1 với 2 Model FromUserToUser. Trong khi chúng ta chỉ có 1 Model là User mà thôi. Để Active Record hiểu đúng, ta cần sửa lại như sau:

1
2
3
4
class Conversation < ApplicationRecord
  belongs_to :from_user, class_name: "User"
  belongs_to :to_user, class_name: "User"
end
Ruby code snippet 4
Thêm quan hệ trong model

Cuối cùng, ta chạy lệnh migrate để thực hiện các thay đổi:

1
$ rails db:migrate
Shell snippet 4
Chạy lệnh migrate

Trong bài viết sau, ta sẽ làm việc sâu hơn với Migration trong Rails với các thao tác thêm mới, đổi tên, đổi kiểu dữ liệu, đặt giá trị mặc định, xóa trường & đánh index cho các trường trong Model.