Rails – Migration

1.6/5 - (44 votes)

Trong bài trước chúng ta đã xây dựng tính năng giỏ hàng, tuy nhiên chưa xử lý được thông minh lắm, giả sử bạn bấm nút ‘Add to Cart’ của một sản phẩm nhiều lần, thì khi hiển thị trên trang /carts/<id>, danh sách các sản phẩm được hiển thị ra hết trên từng dòng, có 100 sản phẩm thì có 100 dòng, cho dù chúng cùng là một thứ, bây giờ chúng ta sẽ thiết kế để in số lượng sản phẩm ngay bên cạnh tên sản phẩm luôn, chứ không cần phải in trên nhiều dòng như thế.

Để làm việc này thì chúng ta cần thêm một cột/trường mới để lưu số lượng sản phẩm trong model/bảng LineItem. Tuy nhiên việc thay thế cấu trúc các bảng trong CSDL như thêm, xóa, sửa cột sẽ gây ra nhiều rắc rối (đặc biệt là các cột khóa ngoại, khóa chính…v.v), chúng ta có thể “bảo” Rails thay thế giùm chúng ta bằng tính năng Migration của Rails.

Mỗi khi chạy migration, Rails sẽ tạo ra một phiên bản CSDL mới dựa vào các migration mà chúng ta đã tạo ra và cập nhật lại.

Migration trong Rails là các file .rb có tên tương tự như <YYYYMMDDHHMMSS>_create_products.rb, bên trong là phần định nghĩa một lớp có tên như CreateProducts được kế thừa từ lớp ActiveRecord::Migration, và một phương thức có tên là change dùng để thêm/xóa/sửa cột trong bảng. Phần YYYYMMDDHHSS là thời gian file này được tạo ra, Rails sẽ dựa vào chuỗi này để biết phần nào được cập nhật trước.

Ví dụ chúng ta thực hiện việc thêm trường quantity dùng để lưu số lượng sản phẩm trong bảng line_items bằng cách chạy lệnh rails generate migration như sau:

C:\Projects\Rails\depot>rails generate migration add_quantity_to_line_items quantity:integer
    invoke active_record
    create   db/migrate/20161123043154_add_quantity_to_line_items.rb

Chúng ta tạo một migration có tên add_quantity_to_line_items, Rails sẽ tạo một file có tên dạng như 20161123043154_add_quantity_to_line_items.rb trong thư mục db/migrate, bên trong là định nghĩa lớp AddQuantityToLineItems. Nếu lớp migration của bạn có tên dạng như Add<X>To<Y> hoặc Remove<X>From<Y> thì Rails sẽ tự động hiểu là tạo thêm trường X vào bảng Y hoặc xóa trường X trong bảng Y, và tự động thêm các đoạn code thích hợp trong phương thức change.

Ở đây Rails sẽ hiểu là thêm một trường có tên là quantity vào bảng line_items, và tự biết là trường này có kiểu dữ liệu là INTEGER do chúng ta truyền vào trong lệnh generate migration ở trên. Nội dung của file 20161123043154_add_quantity_to_line_items.rb hiện tại như sau:

class AddQuantityToLineItems < ActiveRecord::Migration
    def change
        add_column :line_items, :quantity, :integer
    end 
end

Bên trong phương thức change chỉ có một dòng duy nhất là lời gọi phương thức add_column, bạn có thể tự biết đây là phương thức thêm một cột mới trong bảng, thêm số đầu tiên là tên bảng, thâm số thứ 2 là tên cột, tham số thứ 3 là kiểu dữ liệu, các kiểu dữ liệu khác có thể đưa vào là :string, :text, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean. Ngoài ra còn có một tham số thứ 4 nữa mà chúng ta có thể đưa vào nếu muốn, đây là tham số tùy chọn, chúng ta sửa lại để cột này nhận giá trị mặc định là 1 như sau:

class AddQuantityToLineItems < ActiveRecord::Migration 
    def change 
        add_column :line_items, :quantity, :integer, :default => 1
    end
end

Các tham số tùy chọn khác có thể đưa vào là :limit (giới hạn) và :null (cột có được phép để NULL hay không), khi truyền nhiều tham số tùy chọn thì chúng ta truyền vào kiểu bảng băm ví dụ như: { :limit => 50, :null => false, :default => 1 }.

Ngoài ra còn có một số phương thức khác như:

  • create_table(name, options): tạo bảng với tên là name
  • drop_table(name): xóa bảng với tên là name
  • change_table(name, options): thay đổi cấu trúc bảng name
  • rename_table(oldname, newname): đổi tên bảng oldname thành newname
  • rename_column(table, oldname, newname): đổi tên cột oldname thành newname trong bảng table
  • change_column(table, column, options): thay đổi cột column trong bảng table
  • remove_column(table, column, type, options): xóa cột column khỏi bảng table

Để thực hiện thay đổi CSDL thì chúng ta chạy lệnh rake db:migrate là được

C:\Projects\Rails\depot>rake db:migrate
== 20161123043154 AddQuantityToLineItems: migrating ===================================
-- add_column(:line_items, :quantity, :integer, {:default=>1})
   -> 0.0205s
== 20161123043154 AddQuantityToLineItems: migrated (0.0216s) ==========================

Bây giờ chúng ta cần chỉnh sửa lại cách model tạo giỏ hàng cho đúng với CSDL mới.

Đầu tiên chúng ta sửa lại file cart.rb trong thư mục app/models như sau.

class Cart < ActiveRecord::Base 
    has_many :line_items, :dependent => :destroy
 
    def add_product(product_id)
        current_item = line_items.find_by_product_id(product_id)
        if current_item 
            current_item.quantity += 1
        else
            current_item = line_items.build(:product_id => product_id)
        end
        current_item
    end
end

Chúng ta định nghĩa phương thức add_product nhận vào id của sản phẩm để tạo hoặc chỉnh sửa đối tượng LineItems. 

Đầu tiên chúng ta tìm xem trong dách sách các đối tượng LineItems (trong biến line_items) có đối tượng nào có product_id trùng với product_id trong tham số hay không bằng cách dùng phương thức find_by_product_id, phương thức này mặc định không có trong lớp ActiveRecord::Base, nhưng chúng ta vẫn có thể sử dụng được, đó là nhờ vào tính năng Dynamic Finders (đọc thêm ở đây) trong lớp này, theo đó thì trong model/bảng của bạn có thuộc tính nào thì bạn có thể gọi một phương thức có tên find_by_<tên cột>và truyền vào tham số là một giá trị có cùng kiểu dữ liệu với cột đó, ở đây chúng ta truyền vào một số nguyên là id của Product.

Sau đó nếu tìm thấy đối tượng LineItems nào thì chúng ta tăng giá trị của thuộc tính quantity lên 1, nếu không thì chúng ta tạo một đối tượng LineItems mới. Cuối cùng chúng ta trả về đối tượng LineItems đó.

Tiếp theo chúng ta sửa lại phương thức create của lớp controller trong file line_items_controller.rb như sau:

class LineItemsController < ApplicationControler
    .
    .
    .
    # POST /line_items
    # POST /line_items.json
    def create
        @cart = current_cart
        product = Product.find(params[:product_id])
        @line_item = @cart.add_product(product.id)

        respond_to do |format|    
            if @line_item.save
                format.html { redirect_to(@line_item.cart, :notice => 'Line item was successfully created') }
                format.json { render :show, status: :created, location: @line_item }
            else
                format.html { render :new }
                format.json { render json: @line_item.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Đối tượng LineItems sẽ được tạo bằng phương thức add_product đã được định nghĩa ở trên.

Cuối cùng chúng ta sửa lại View để hiển thị số lượng như sau:

 
<%= notice %>
<h2>Your cart</h2>
<ul>
    <% @cart.line_items.each do |a| %>
    <li><%= a.quantity %> x <%= a.product.title %></li>
    <% end %>
</ul>
<%= link_to 'Edit', edit_cart_path(@cart) %> |
<%= link_to 'Back', carts_path %>

Vậy là xong, chúng ta có thể chạy được rồi.

capture

0 0 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

6 Comments
Inline Feedbacks
View all comments
Kieu Thi Anh
Kieu Thi Anh
7 năm trước

Em chao ad . Em bi loi:
ActiveRecord::RecordNotFound in LineItemsController#create
Couldn’t find Product with ‘id’=
Khi “Add to cart a”.
Em da tao 3 book trong seed roi a. Em cung thu byebug ma khong sua duoc loi a..
Em cam on ad a.

Near
Near
7 năm trước

Chào ad. mình hỏi chút
current_item = line_items.find_by_product_id(product_id)
trong dòng này thì line_items là biến hay tên class vậy? sao mình k thấy khai báo nhỉ

Near
Near
7 năm trước
Reply to  Phở Code

nếu vậy thì là line_item chứ ko phải line_items chứ ad ?