Category Archives: Ruby on Rails

Rails – Session

Session là được hiểu 1 phiên làm việc trong đó người sử dụng giao tiếp với 1 ứng dụng. Session bắt đầu khi người sử dụng truy cập vào ứng dụng lần đầu tiên, và kết thúc khi người sử dụng thoát khỏi ứng dụng.

Khi người dùng xem các sản phẩm trên một website bán hàng, có thể họ sẽ muốn mua nó, do đó chúng ta sẽ xây dựng tính năng giỏ hàng để lưu trữ các sản phẩm mà khách hàng muốn mua trong session.

Chúng ta sẽ định nghĩa một model cart (giỏ hàng), mỗi khi có người dùng nào truy cập vào website thì chúng ta sẽ tạo một đối tượng cart rồi lưu vào CSDL. Khi người dùng thoát trang web, sau đó quay lại thì chúng ta sẽ kiểm tra trong session của họ có id của cart nào hay không, nếu có thì lấy ra hiển thị, không thì lại tạo mới như ban đầu.

Đầu tiên chúng ta tạo model như sau:

C:\Projects\Rails\depot>rails generate scaffold cart
...

Chúng ta không định nghĩa cột nào ở đây cả vì sau này chúng ta sẽ liên kết với các model khác là đủ.

Tiếp theo chúng ta chạy lệnh rake db:migrate để Rails tạo bảng carts trong CSDL:

C:\Projects\Rails\depot>rake db:migrate
== 20161119034946 CreateCarts: migrating ==============================================
-- create_table(:carts)
   -> 0.0182s
== 20161119034946 CreateCarts: migrated (0.0194s)======================================

Nếu bạn còn nhớ thì Rails sẽ tự động tạo 3 trường là id,  created_atupdated_at:

C:\Projects\Rails\depot\db>sqlite3 development.sqlite3
> pragma table_info(carts);
0|id|INTEGER|1|1
1|created_at|datetime|1|0
2|updated_at|datetime|1|0

Tất cả các lớp controller do Rails tạo ra đều được kế thừa từ lớp ApplicationController (trong file app/controllers/application_controller.rb), lớp này cũng là một lớp do Rails tự tạo ra và được kế thừa từ lớp ActionController::Base, trong mỗi lớp controller bất kỳ có một đối tượng toàn cục có tên là session thuộc lớp ActionDispatch::Request::Session, đây là một lớp lưu trữ dữ liệu theo dạng từ điển, tức là cứ mỗi phần tử trong này sẽ bao gồm một cặp <khóa>-<giá trị>.

Bây giờ chúng ta sửa lại file application_controller.rb như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    protect_from_forgery with: :exception
 
    private
    
    def current_cart
        Cart.find(session[:cart_id])
    rescue ActiveRecord::RecordNotFound
        cart = Cart.create
        session[:cart_id] = cart.id
        cart
    end
end

Ở đây chúng ta định nghĩa một phương thức có tên current_cart dùng để lấy thuộc tính id của đối tượng Cart trong đối tượng session của người dùng khi người dùng truy cập vào website. Chúng ta chỉ định phương thức này có phạm vi truy xuất là private.

Đầu tiên chúng ta tìm xem trong CSDL có giỏ hàng nào có id giống với phần tử có khóa là :cart_id trong session hay không bằng phương thức Cart.find(), nếu không có thì phương thức này sẽ giải phóng một exception là ActiveRecord::RecordNotFound, chúng ta bắt lỗi này và thực hiện tạo một đối tượng Cart mới, sau đó tạo một phần tử trong đối tượng session với khóa là :cart_id và giá trị là id mới vừa được tạo (thuộc tính idcart.id).

Ngoài các lớp controller thì một khu vực khác cũng có thể đọc/ghi dữ liệu với đối tượng session là View, chúng ta sẽ xử lý session với View trong các bài sau.

Rails – Functional Testing

Trong bài Unit Testing chúng ta đã tìm hiểu cách kiểm tra code đối với model, trong phần này chúng ta sẽ tìm hiểu cách kiểm tra code trên controller – hay còn gọi là Functional Testing.

Khi chúng ta tạo một controller thì Rails sẽ tự động tạo cho chúng ta một file dùng để test controller này, trong bài Routing chúng ta đã tạo một controller là store, và Rails sẽ tạo một file test tương ứng có tên là store_controller_test.rb trong thư mục test/controllers. Ngoài ra nếu bạn còn nhớ thì khi tạo một model, Rails cũng sẽ định nghĩa controller cho model này, và dĩ nhiên Rails cũng sẽ tạo một file test cho controller này, chúng ta đã tạo model Product và file test controller là file products_controller_test.rb cũng nằm trong thư mục test/controllers.

capture

Nội dung của file products_controller_test.rb như sau:

require 'test_helper'

class ProductsControllerTest < ActionController::TestCase
    setup do
        @product = products(:one) 
    end

    test "should get index" do
        get :index
        assert_response :success
        assert_not_nil assigns(:products)
    end

    test "should get new" do
        get :new
        assert_response :success
    end

    test "should create product" do
        assert_difference('Product.count') do 
             post :create, product: { 
                     description: @product.description, 
                     image_url: @product.image_url, 
                     price: @product.price, 
                     title: @product.title } 
        end

        assert_redirected_to product_path(assigns(:product))
    end

    test "should show product" do
         get :show, id: @product
         assert_response :success
    end

    test "should get edit" do
         get :edit, id: @product
         assert_response :success
    end

    test "should update product" do
         patch :update, id: @product, product: { 
                 description: @product.description, 
                 image_url: @product.image_url, 
                 price: @product.price, 
                 title: @product.title }   
         assert_redirected_to product_path(assigns(:product)) 
    end

    test "should destroy product" do
         assert_difference('Product.count', -1) do
             delete :destroy, id: @product
         end
 
         assert_redirected_to products_path
     end
end

Trong đó đoạn setup do...end sẽ làm công việc khởi tạo, ở đây Rails khởi tạo biến @product là đối tượng one trong fixture.

Sau đó là các đoạn code test, ở đây các phương thức get, post, patch sẽ gửi các gói tin HTTP lên server, theo sau là tên các hàm trong model mà Rails đã định nghĩa trước, như index là phương thức index trong controller, các phương thức gửi sẽ tương ứng với một phương thức của giao thức HTTP như GET, POST, PATCH, PUT… sau đó chúng ta dùng các phương thức assert để kiểm tra dữ liệu trả về từ server, trong đó:

  • assert_response: nhận vào tham số mã và kiểm tra xem gói tin trả về có mã giống với tham số hay không, ở đoạn code trên các đoạn assert_response đều kiểm tra với mã là :success, tương ứng với mã từ 200-299, ngoài ra còn có :redirect tương ứng với mã từ 300-399, :missing là mã 404, :error là từ 500-599.
  • assert_not_nil: nhận vào một đối tượng và kiểm tra xem đối tượng đó có giá trị là nil hay không
  • assert_difference: nhận vào tham số là một biểu thức (phép tính, hàm, toán tử…v.v) và tính với hiệu của biểu thức đó sau khi thực hiện phần code phía sau, rồi kiểm tra xem hiệu đó có bằng 1 hay không.
  • assert_redirected_to: nhận vào tham số là một URL, kiểm tra xem URL đó có giống với hành động chuyển hướng trang web cuối cùng được thực hiện hay không.

ProductsControllerTest

Bạn có thể chạy lệnh rake test:functionals để Rails chạy những test trên. Và Bạn sẽ nhận được 2 lỗi tại test "should create product""should update product". 

Đối với "should create product" thì lý do là vì bị trùng thuộc tính title, chúng ta đã quy định thuộc tính này phải là duy nhất, nhưng Rails lại lấy dữ liệu từ fixture để chèn vào CSDL nên mới bị trùng, và Rails lại không kiểm tra trường hợp bị trùng, chúng ta có thể sửa lại như sau:

require 'test_helper'

class ProductsControllerTest < ActionController::TestCase
    .
    .
    .
    test "should create product" do 
        assert_difference('Product.count', 0) do 
            post :create, product: { 
                    description: @product.description, 
                    image_url: @product.image_url, 
                    price: @product.price,
                    title: @product.title } 
        end
 
        assert_difference('Product.count') do
            post :create, product: {
                    :description => 'Some new book',
                    :image_url => 'some_img.jpg',
                    :price => 59.99,
                    :title => 'Some new title'
            } 
        end
        assert_redirected_to product_path(assigns(:product))
    end
    .
    .
    .
end

Đối với trường hợp bị trùng, chúng ta truyền thêm tham số thứ 2 vào phương thức assert_difference, đây là tham số hiệu, như đã nói ở trên mặc định phương thức này kiểm tra hiệu bằng 1, ở đây chúng ta kiểm tra hiệu bằng 0, tức là nếu không chèn thêm bản ghi mới vào thì số lượng các bản ghi không đổi, còn với trường hợp chèn thành công thì chúng ta kiểm tra một yêu cầu redirect có tồn tại không.

Tương tự, "should update product" cũng bị 2 lỗi vì Rails chưa kiểm tra trường hợp cập nhật thất bại, nếu bạn còn nhớ thì trong Fixtures có 2 đối tượng dữ liệu mẫu có title giống nhau, và cả 2 đều có thuộc tính image_url không có đuôi hợp lệ, chúng ta sửa lại như sau:

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
 title: MyString
 description: MyText
 price: 9.99
 image_url: MyString

two:
 title: MyString2
 description: MyText
 price: 9.99
 image_url: MyString.jpg

ruby:
 title: Ruby on Rails 4
 description: Zzzzzzz....
 price: 9.99
 image_url: img.jpg

Chúng ta sửa dữ liệu của two sao cho hợp lệ, còn one thì giữ nguyên. Tiếp theo chúng ta sửa đoạn code test như sau:

class ProductsControllerTest < ActionController::TestCase
    .
    .
    .
    test "should update product" do
        patch :update, id: @product, product: { 
            description: @product.description, 
            image_url: @product.image_url, 
            price: @product.price, 
            title: @product.title 
        }
        assert_response 200
 
        @product = products(:two)
        patch :update, id: @product, product: {
            :description => @product.description,
            :image_url => @product.image_url,
            :price => @product.price,
            :title => @product.title
        } 
        assert_redirected_to product_path(assigns(:product))
     end
    .
    .
    .
end

Đầu tiên chúng ta kiểm tra đối với fixture mặc định được tạo trong phần setup là  one, đây là dữ liệu không hợp lệ, Rails sẽ trả về response có thông báo lỗi, chú ý là response ở đây mang theo một thuộc tính thông báo lỗi, còn bản thân response lại là một gói tin HTTP được gửi thành công, nên chúng ta mới so sánh với mã trạng thái là 200. Sau đó chúng ta đổi lại biến @product lấy giá trị của fixture two, đây là fixture hợp lệ, khi cập nhật xong thì trang web của chúng ta sẽ được chuyển đến đường dẫn /products/<id>, do đó chúng ta kiểm tra với phương thức assert_redirected_to.

StoreControllerTest

Đối với controller store chỉ có một phương thức index thì có file test như sau:

require 'test_helper'

class StoreControllerTest < ActionController::TestCase
    test "should get index" do
        get :index
        assert_response :success
    end

end

Ở đây Rails chỉ kiểm tra xem khi trỏ đến hàm index này thì có nhận được một gói tin trả về với mã từ 200-299 (thành công – success) hay không.

Chúng ta kiểm tra thêm một số thứ bằng cách sửa lại như sau:

require 'test_helper'

class StoreControllerTest < ActionController::TestCase
    test "should get index" do
        get :index
        assert_response :success
        assert_select '#columns #side a', :minimum => 4
        assert_select '#main .entry', 3
        assert_select 'h3', 'Ruby on Rails 4'
        assert_select '.price', /\$[,\d]+\.\d\d/
    end
end

Phương thức assert_select sẽ kiểm tra xem trong nội dung HTML trả về có chứa những thành phần mà chúng ta quy định hay không.

Chẳng hạn như assert_select '#columns #side a', tức là kiểm tra xem trong nội dung có thẻ nào có id là columns hay không, và trong đó phải có một thẻ có id là side, rồi phải có thẻ <a>, ngoài ra thuộc tính minimum còn yêu cầu là phải có ít nhất 4 thẻ <a>.

Dòng assert_select '#main .entry', 3 yêu cầu nội dung trả về phải có một thẻ có id là main, trong đó có 3 thẻ có class là entry.

Dòng assert_select 'h3', 'Ruby on Rails 4' kiểm tra xem có thẻ <h3> nào có nội dung là Ruby on Rails 4 hay không.

Cuối cùng dòng assert_select '.price', /\$[,\d]+\.\d\d/ kiểm tra xem thẻ nào mà có class là price có nội dung khớp với đoạn biểu thức chính quy hay không, ở đây đoạn biểu thức chính quy này mô tả số tiền, có ký tự $, có 2 chữ số sau phần thập phân.

Bạn có thể chạy lệnh rake test:functionals để Rails kiểm tra tất cả các đoạn test trên. Có tổng cộng 8 đoạn test trong 2 lớp StoreControllerTestProductsControllerTest.

C:\Project\Rails\depot>rake test:functionals
Run options: --seed 46620

# Running:

........

Finished in 0.874736s, 9.1456 runs/s, 21.7208 assertions/s

8 runs, 19 assertions, 0 failures, 0 errors, 0 skips

Rails – Helper

Helper ở đây là các phương thức tiện ích mà chúng ta có thể gọi trong View (ERB).

Ví dụ

Chúng ta đã sử dụng các phương thức helper nhiều rồi, chẳng hạn như phương thức image_tag dùng để tạo thẻ <img>, phương thức sanitize dùng để loại bỏ kí tự lạ trong chuỗi…v.v.

Bây giờ chúng ta sửa file index.html.erb trong thư mục app/views/store như sau:

<% if notice %>
    <p id="notice"><%= notice %></p>
<% end %>
 
<h1>Product List</h1>
 
<% @products.each do |product| %>
    <div class="entry">
        <%= image_tag(product.image_url) %>
        <h3><%= product.title %></h3> 
        <%= sanitize(product.description) %>
        <div class="price_line">
            <span class="price"><%= number_to_currency(product.price) %></span>
        </div>
    </div>
<% end %>

Phương thức number_to_currency sẽ chuyển một số sang một chuỗi và chèn thêm kí tự định dạng tiền tệ vào trước. Mặc định là U.S. Dollars (ký tự $)

capture

Chúng ta có thể tùy chỉnh để hàm này hiển thị theo loại tiền tệ tùy thuộc vào người dùng truy cập từ đâu (chẳng hạn như hiển thị ‘đ’ nếu người dùng truy cập từ Việt Nam), chúng ta sẽ tìm hiểu chủ đề này sau.

Bạn có thể tìm hiểu các phương thức helper thường dùng tại đây:

http://guides.rubyonrails.org/action_view_overview.html

Rails – Tùy chỉnh layout

Như chúng ta đã biết, trong Rails có một file chứa code HTML có chức năng chính là định nghĩa cách các thành phần trong toàn bộ website được hiển thị như thế nào, chẳng hạn như trong blog Phở Code, cho dù bạn có đọc bài nào thì giao diện của website vẫn luôn có phần tiêu đề, thanh navigation (hay menu), vùng sidebar, footer…v.v file này được gọi là file Layout. Khi cần chỉnh sửa lại giao diện thì chúng ta chỉ cần chỉnh sửa file này là được.

Trong một project Rails thì file này là file application.html.erb nằm trong thư mục app/views/layout, chúng ta sẽ chỉnh sửa file này.

Chúng ta sửa lại nội dung file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <a href="#">Home</a><br />
            <a href="#">FAQ</a><br />
            <a href="#">News</a><br />
            <a href="#">Contact</a><br />
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta định nghĩa một số id cho thẻ <div> để tùy chỉnh giao diện cho từng phần khác nhau.

Ở trong thẻ <div id="banner">, chúng ta gọi đến hàm image_tag() để tạo một thẻ <img> chứa thuộc tính src dẫn đến file tương ứng. File này bạn có thể tự tạo hoặc lấy ở đâu đó (lấy hàng miễn phí ấy 🙂 ) rồi bỏ vào thư mục app/assets/images.

Sau đó chúng ta gọi đến biến @page_title để hiển thị tiêu đề website, biến này sẽ được gửi từ một controller nào đó, ở đây chúng ta chưa khai báo, do đó chúng ta thực hiện phép toán OR (toán tử ||) với chuỗi “Books Store” để hiển thị chuỗi này nếu biến @page_title không tồn tại.

Tiếp theo là các vùng columns, side, main… đây chỉ là các thẻ <div> bình thường. Ở vùng main, chúng ta có dòng code gọi phương thức yield, phương thức này sẽ gọi controller gắn với đối tượng root mà chúng ta đã bàn đến trong bài trước, dùng để lấy nội dung hiển thị của controller đó, tức là phương thức yield này sẽ gọi đến phương thức index trong controller store, và nội dung trả về là danh sách các sản phẩm.

Cuối cùng chúng ta cần định nghĩa CSS cho các thẻ <div> trên bằng cách chèn thêm đoạn code sau vào trong file depot.css như sau:

...
#banner {
    background: #9c9;
    padding-top: 10px;
    padding-bottom: 10px;
    border-bottom: 2px solid;
    font: small-caps 40px/40px "Times New Roman", serif;
    color: #282;
    text-align: center;
}

#banner img {
    float: left;
    padding-left: 5px;
}

#columns {
    background: #141;
}

#main {
    margin-left: 17em;
    padding-top: 4ex;
    padding-left: 2em;
    background: white;
}

#side {
    float: left;
    padding-top: 1em;
    padding-left: 1em;
    padding-bottom: 1em;
    width: 16em;
    background: #141;
}

#side a {
    color: #bfb;
    font-size: small;
}

Kết quả chúng ta có trang web như sau:

capture

Rails – Routing

Routing là tính năng điều hướng một URL vào một phương thức nhất định, tính năng routing có trong hầu hết các web framework phổ biến như Django, Node.js…

Ví dụ

Chúng ta sẽ tạo một trang hiển thị sản phẩm và hiển thị trang này lên URL '/'.

Đầu tiên chúng ta tạo một controller có tên là store và có một phương thức là index như sau:

C:\Project\Rails\depot>rails generate controller store index
    create app/controllers/store_controller.rb
     route get 'store/index'
    invoke erb
    create   app/views/store
    create   app/views/store/index.html.erb
    invoke test_unit
    create   test/controllers/store_controller_test.rb
    invoke helper
    create   app/helpers/store_helper.rb
    invoke   test_unit
    invoke asserts
    invoke   coffee
    create     app/assets/javascripts/store.coffee
    invoke   scss
    create     app/assetss/stylesheets/store.scss

Nếu bạn còn nhớ thì mặc định Rails sẽ tự động điều hướng các URL dựa vào controller và các phương thức của chúng, chẳng hạn chúng ta vừa tạo controller tên store, trong đó có một phương thức tên index, thì Rails sẽ trỏ (hay route) URL /store/index về phương thức này, bạn có thể chạy server và test thử.

Thông tin về các routing này được lưu trong file routes.rb trong thư mục config như sau:

Rails.application.routes.draw do
    get 'store/index'   

    resources :products
    # The priority is based upon order of creation: first created -> highest priority.
    # See how all your routes lay out with "rake routes".

    # You can have the root of your site routed with "root"
    # root 'welcome#index'

    # Example of regular route:
    #     get 'products/:id' => 'catalog#view'

    # Example of named route that can be invoked with purchase_url(id: product.id)
    #     get 'products/:id/purchase' => 'catalog#purchase', as: :purchase

    # Example resource route (maps HTTP verbs to controller actions automatically):
    #     resources :products

    # Example resource route with options:
    #     resources :products do
    #         member do
    #             get 'short'
    #             post 'toggle'
    #         end
    #
    #         collection do
    #             get 'sold'
    #         end
    #     end

    # Example resource route with sub-resources:
    #     resources :products do
    #         resources :comments, :sales
    #         resource :seller
    #     end

    # Example resource route with more complex sub-resources:
    #     resources :products do
    #         resources :comments
    #         resources :sales do
    #             get 'recent', on: :collection
    #         end
    #     end

    # Example resource route with concerns:
    #     concern :toggleable do
    #         post 'toggle'
    #     end
    #     resources :posts, concerns: :toggleable
    #     resources :photos, concerns: :toggleable

    # Example resource route within a namespace:
    #     namespace :admin do
    #         # Directs /admin/products/* to Admin::ProductsController
    #         # (app/controllers/admin/products_controller.rb)
    #         resources :products
    #     end
end

File này do Rails tự tạo, dòng get 'store/index' rất trực quan, Rails định nghĩa đoạn URL /store/index sẽ được chuyển đến phương thức index trong controller store.

Tuy nhiên có một điều khó hiểu là trong các bài trước chúng ta có thể trỏ đến /product, /product/1, /product/1/edit ..v.v nhưng trong file routes.rb này lại không có dòng nào dạng như get 'product', lý do chúng ta có thể trỏ đến các URL product kia là nhờ vào dòng:

resources :products

Thông thường khi tạo một model chúng ta hay định nghĩa các phương thức thêm, sửa, xóa, cập nhật..v.v đây được gọi chung là các thao tác CRUD (CreateReadUpdateDelete), và như lẽ thông thường thì chúng ta sẽ phải tự định nghĩa controller cũng như các phương thức để thực hiện các thao tác đó, rồi khai báo các dòng get trong file routes.rb.

Chính vì thao tác CRUD quá phổ biến nên Rails đã đơn giản hóa việc này cho chúng ta, khi chúng ta khai báo products với resources, Rails sẽ hiểu là phải tự động xử lý các URL /product/... cho chúng ta luôn mà không cần phải khai báo ở đâu cả. Ngoài ra các lời gọi URL còn mang theo cả thông tin về phương thức nữa, như GET, POST, PUT…v.v Mặc định thì Rails định nghĩa 6 phương thức cùng với URL cho một model tương ứng như sau:

Phương thức gởi URL Controller#Phương thức xử lý Ý nghĩa
GET /products/new /products#new Trả về form HTML tạo một đối tượng mới
POST /products /products#create Tạo một đối tượng mới
GET /products /products#show Hiển thị thông tin về một đối tượng cụ thể
GET /products/edit /products#edit Trả về form chỉnh sửa một đối tượng
PATCH/PUT /products /products#update Cập nhật một đối tượng
DELETE /products /products#destroy Hủy một đối tượng

Tiếp theo, nếu bạn còn nhớ thì URL '/' sẽ được trả về một trang mẫu như sau:

capture

Chúng ta có thể sửa lại để Rails trỏ đến trang do chúng ta quy định như sau:

Rails.application.routes.draw do
    get 'store/index'

    resources :products
 
    root :to => 'store#index'
end

Bằng cách định nghĩa thuộc tính to của đối tượng root tới một URL như trên (controller và phương thức ngăn cách nhau bởi dấu '#') .

Tiếp theo chúng ta tạo một đối tượng để lưu trữ toàn bộ bản ghi trong controller, chúng ta sửa file store_controller.rb như sau:

class StoreController < ApplicationController
    def index
        @products = Product.all
    end
end

Chúng ta tham chiếu tới model Product và gọi phương thức all là sẽ lấy được danh sách các đối tượng (hay các bản ghi trong CSDL) của model đó.

Bây giờ chúng ta sẽ sửa lại view cho trang này một tí để hiển thị danh sách các sản phẩm, chúng ta sửa file view tương ứng là file app/views/store/index.html.erb như sau:

<% if notice %>
   <p id="notice"><%= notice %></p>
<% end %>

<h1>Product List</h1>

<% @products.each do |product| %>
    <div class="entry">
        <%= image_tag(product.image_url) %>
        <h3><%= product.title %></h3> 
        <%= sanitize(product.description) %>
        <div class="price_line">
            <span class="price"><%= product.price %></span>
        </div>
    </div>
<% end %>

Biến notice lưu giữ một chuỗi text thông báo, chẳng hạn như thông báo tạo thành công, hủy thành công… nếu có tồn tại thì chúng ta hiển thị ra.

Hàm image_tag sẽ tạo một thẻ <img> và nhận tham số là đường dẫn tham chiếu đến file ảnh.

Hàm sanitize() có tác dụng loại bỏ một số thẻ ký tự lạ trong một chuỗi. Chúng ta sẽ tìm hiểu sau.

capture

Rails – Unit Testing

Trong bài này chúng ta sẽ thực hiện kiểm tra code bằng phương pháp Unit Testing.

Nếu bạn chưa biết thì có thể nói đơn giản Unit Testing là phương pháp kiểm tra xem các đoạn code chúng ta viết ra có chạy chính xác hay không. Rails có riêng một nền tảng hỗ trợ Unit Testing rất mạnh, chúng ta sẽ tìm hiểu cách sử dụng nền tảng này.

Khi chúng ta tạo một project thì ngoài thư mục app, Rails còn tạo ra một thư mục có tên là test, trong thư mục này cũng có thư mục models, controllers và một vài thư mục cũng như file khác tương tự như trong thư mục app. Ý nghĩa của thư mục này là tạo ra một môi trường giả lập giống hệt như môi trường “chính” dùng để kiểm tra tất cả mọi thứ, môi trường này có models riêng, controller riêng và cả CSDL riêng nữa.

Khi chúng ta tạo một model bằng lệnh generate scaffold, thì trong thư mục gốc của project có một thư mục tên là test/models, trong đó sẽ có một file có tên theo dạng <tên model>_test.rb, trong ứng dụng depot mà chúng ta đã làm thì file này sẽ là product_test.rb, file này có nội dung như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    # test "the truth" do
    # assert true
    # end
end

Ở đây Rails định nghĩa lớp ProductTest kế thừa từ lớp ActiveSupport::TestCase, lớp này thực ra cũng chỉ là kế thừa từ lớp Test::Unit::TestCase có trong Ruby thôi. Trong lớp này có một khối lệnh (đã được mặc định là comment) có cú pháp test <tên test> do...end, bạn cứ hình dung đây cũng giống như là một định nghĩa hàm/phương thức thôi cho đơn giản, chúng ta sẽ gọi chúng là “test”, khi kiểm tra thì Rails sẽ gọi những “test” này để chạy.

Tiếp theo trong khối lệnh đó có dòng lệnh assert true, assert là một phương thức, phương thức assert được dùng để:

  • Kiểm tra thuộc tính này có giá trị như thế kia không?
  • Kiểm tra đối tượng có phải là nil hay không.
  • Có phải đoạn code kia sẽ giải phóng exception hay không.

Nói tóm lại giá trị trả về của phương thức assert là true hoặc false, và cứ mỗi lần có một phương thức assert nào đó trả về false thì Rails báo lỗi.

Trước khi đi vào phần kiểm tra thì chúng ta phải chạy lệnh rake db:migrate RAILS_ENV=test:

C:\Project\Rails\depot>rake db:migrate RAILS_ENV=test
== 20161106144824 CreateProducts: migrating ===========================================
-- create_table(:products)
   -> 0.0024s
== 20161106144824 CreateProducts: migrated (0.0066s)===================================

Lệnh này cũng giống như lệnh rake db:migrate thôi, tức là sẽ tạo các bảng trong CSDL, chỉ khác là tạo cho môi trường test, không đụng chạm gì tới file CSDL của app cả, mặc định Rails sử dụng CSDL SQLite3 nên file được tạo ra là file test.sqlite3 nằm trong thư mục db.

Ví dụ Unit Testing

Chúng ta sửa file product_test.rb như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        product = Product.new
        assert product.invalid?
        assert product.errors[:title].any?
        assert product.errors[:description].any?
        assert product.errors[:price].any?
        assert product.errors[:image_url].any?
    end
end

Trong đoạn code trên, chúng ta định nghĩa một test có tên là “Thuoc tinh khong duoc de trong”, trong đó chúng ta tạo một đối tượng Product và không có thuộc tính nào có giá trị nào cả. Tiếp theo chúng ta gọi phương thức assert kiểm tra giá trị của phương thức invalid?, phương thức invalid? là một phương thức của lớp ActiveRecord::Base, phương thức này sẽ kiểm tra xem các thuộc tính của lớp tương ứng có hợp lệ hay không dựa trên các phương thức validates (mà chúng ta đã làm trong bài trước), tất nhiên là không hợp lệ vì không có thuộc tính nào có giá trị nào cả, và Rails sẽ làm một việc là gán một chuỗi thông báo lỗi ứng với từng thuộc tính vào thuộc tính errors (đây là một mảng và cũng là một thuộc tính của lớp ActiveRecord::Base).

Tiếp theo chúng ta lại kiểm tra xem các phần tử của mỗi thuộc tính tương ứng trong thuộc tính errors có tồn tại hay không thông qua phương thức any?, phương thức này kiểm tra xem một thuộc tính nào đó có tồn tại hay không, ở đây là có tồn tại và phương thức assert sẽ trả về true.

Bây giờ chúng ta có thể chạy test bằng lệnh rake test:units và chúng ta sẽ có đoạn output tương tự như thế này:

C:\Project\Rails\depot>rake test:units
Run options: --seed 26133

# Running:

Finished in 0.123351s, 8.1069 runs/s, 40.5347 assertions/s.

1 runs, 5 assertions, 0 failures, 0 errors, 0 skips

Đoạn output trên thông báo là việc test đã thành công, các đoạn code kiểm tra không bị lỗi (0 failures), tức là chúng ta đã viết các phương thức validates đúng.

Bây giờ chúng ta tạo một test khác như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        ...
    end 
 
    test "Price phai lon hon 1.0" do
        product = Product.new(:title => 'Hmm...',
                              :description => 'Err...',
                              :image_url => 'Something.jpg')
 
        product.price = -1
        assert product.invalid? 
        assert_equal "must be greater than or equal to 1.0",
                     product.errors[:price].join('') 
 
        product.price = 0
        assert product.invalid?
        assert_equal "must be greater than or equal to 1.0",
                     product.errors[:price].join('')
 
        product.price = 5
        assert product.valid?
 
    end
end

Chúng ta tạo một test có tên "Price phai lon hon 1.0" để kiểm tra xem phương thức validates với thuộc tính price có hoạt động chính xác hay không.

Ở đây chúng ta tạo một đối tượng Product với các thuộc tính còn lại có đầy đủ giá trị hợp lệ, còn thuộc tính price sẽ lần lượt có 3 giá trị là -1, 0, và 5 để kiểm tra.

Đối với giá trị -1 và 0, nếu hàm validates chúng ta viết chạy đúng thì sau khi gọi hàm invalid?, trong thuộc tính errors sẽ có một phần tử có khóa là :price và có giá trị là "must be greater than or equal to 1.0" (phải lớn hơn hoặc bằng 1.0). Chúng ta gọi phương thức assert_equal, phương thức này nhận vào 2 chuỗi và so sánh xem 2 chuỗi đó có bằng nhau hay không, ở đây chúng ta kiểm tra xem chuỗi trong mảng errors có giống với chuỗi "must be..." ở trên hay không. Ở đây thực chất giá trị trong mảng errors được lưu dưới dạng một mảng khác nữa nên chúng ta gọi phương thức join() để chuyển giá trị đó thành một chuỗi thì mới so sánh được.

Đối với giá trị 5 thì hiển nhiên là giá trị này hợp lệ (nếu chúng ta viết validates đúng) nên chúng ta kiểm tra bằng phương thức valid?.

Bạn có thể chạy rake test:units để kiểm tra, nếu có lỗi thì tức là các đoạn validates viết sai.

Tiếp theo chúng ta viết đoạn test kiểm tra tên file trong thuốc tính image_url có hợp lệ hay không như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        ...
    end 
 
    test "Price phai lon hon 1.0" do
        ...
    end
 
    def new_product(image_url)
        Product.new(:title => 'Hmm...',
                    :description => 'Err...',
                    :image_url => image_url,
                    :price => 5)
    end
 
    test "image url" do
        url_hop_le = %w{ img.gif img.jpg img.png IMG.JPG IMG.Jpg http://a.b.c/x/y/z/img.gif }
        url_khong_hop_le = %w{ img.doc img.gif/more img.gif.more }
 
        url_hop_le.each do |name|
            assert new_product(name).valid?, "#{name} phai hop le"
        end
 
        url_khong_hop_le.each do |name|
            assert new_product(name).invalid?, "#{name} khong hop le"
        end
    end
end

Đầu tiên chúng ta viết phương thức new_product() để tạo một đối tượng Product mới có thuộc tính image_url được truyền vào từ tham số. Tiếp theo là đoạn test có tên “image url”, trong này chúng ta tạo 2 mảng url_hop_leurl_khong_hop_le chứa các tên file hợp lệ và không hợp lệ. Sau đó chúng ta duyệt qua từng mảng, cứ mỗi lần duyệt thì chúng ta tạo một đối tượng Product mới từ hàm new_product và gọi phương thức assert để kiểm tra. Ở đây phương thức assert được truyền thêm một tham số thứ 2 nữa, đây chẳng qua chỉ là một chuỗi và chuỗi này sẽ được nối vào thuộc tính lỗi trong mảng errors để có gì thì tìm cho dễ thôi.

Fixtures

Trong số các điều kiện kiểm tra mà chúng ta đã viết thì còn một điều kiện nữa là kiểm tra xem thuộc tính title có phải là duy nhất hay không. Để kiểm tra thì có một cách là mỗi lần test chúng ta tạo một đối tượng cụ thể, sau đó lưu vào CSDL, rồi lại một đối tượng khác có thuộc tính giống như đối tượng vừa tạo và kiểm tra, tuy nhiên cách này có hơi rắc rối, do đó Rails cung cấp cho chúng ta cơ chế Fixtures.

Để hiểu một cách đơn giản thì fixtures là các dữ liệu mẫu đã được lưu trong CSDL test, mỗi khi chạy test thì chúng ta có thể sử dụng các dữ liệu này.

Trong Rails thì các dữ liệu fixture được lưu trong thư mục mặc định là test/fixtures dưới định dạng CSV hoặc YAML. Khi tạo model thì Rails cũng tạo một file fixture mẫu trong thư mục này với định dạng YAML, ở đây chúng ta tạo model Product và file fixture tương ứng là products.yml, và chúng ta cũng sẽ dùng định dạng này luôn cho tiện. File products.yml mặc định có nội dung như sau:

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
    title: MyString
    description: MyText
    image_url: MyString
    price: 9.99

two:
    title: MyString
    description: MyText
    image_url: MyString
    price: 9.99

Lưu ý là các file .yml này luôn phải trùng với tên một bảng nào đó, không phải ngẫu nhiên mà file này lại có tên là products.yml. Ngoài ra cứ mỗi lần chúng ta chạy lệnh rake test:units thì các bảng trong file test.sqlite3 luôn được tạo mới và được chèn sẵn các dữ liệu fixtures này.

Cú pháp YAML có lẽ cũng không khó hiểu lắm nên mình sẽ không giải thích đoạn code trên 🙂 Chỉ có một lưu ý là các từ như one, two, đây là các tên các dòng để chúng ta có thể tham chiếu khi viết test, chứ các tên này không được lưu vào CSDL.

Bây giờ chúng ta sẽ thêm một bản ghi khác vào fixture như sau:

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
 title: MyString
 description: MyText
 image_url: MyString
 price: 9.99

two:
 title: MyString
 description: MyText
 image_url: MyString
 price: 9.99

ruby:
 title: Ruby on Rails 4
 description: Zzzzzzz....
 price: 9.99
 image_url: img.jpg

Lưu ý là các dòng được thụt vào bằng một dấu cách, không phải là dấu tab.

Tiếp theo chúng ta viết test như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        ...
    end 
 
    test "Price phai lon hon 1.0" do
        ...
    end
 
    def new_product(image_url)
        ...
    end
 
    test "image url" do
        ...
    end
 
    fixtures :products
 
    test "title khong duoc trung" do 
        product = Product.new(:title => products(:ruby).title,
            :description => "Hmm...",
            :image_url => "img.jpg",
            :price => 5)
        assert !product.save
        assert_equal "has already been taken",
                     product.errors[:title].join('')
     end
end

Dòng fixtures :products sẽ chỉ định Rails lấy những bản ghi được định nghĩa trong file products.yml từ bảng products trong file test.sqlite3. Sau đó Rails sẽ tạo một phương thức có tên trùng với tên bảng, phương thức này sẽ nhận vào tên fixtures để tìm các bản ghi có tên tương ứng và tạo thành một đối tượng thuộc model đó, nhờ đó chúng ta có thể đọc dữ liệu từ bản ghi này.

Trong ví dụ trên, chúng ta tạo test có tên “title khong duoc trung”, trong đó chúng ta tạo một đối tượng Product có thuộc tính title được lấy từ đối tượng tạo ra từ phương thức products(:ruby). Khi chúng ta gọi phương thức save để lưu đối tượng mới vào CSDL thì phương thức này sẽ trả về false, tức là không hợp lệ và assert sẽ tạo thông báo lỗi vào thuộc tính errors.

Bạn có thể chạy lệnh rake test:units để kiểm tra.

Rails – Kiểm tra dữ liệu gửi lên form

Trong bài này chúng ta sẽ tìm hiểu cách kiểm tra sự đúng đắn của dữ liệu.

Kiểm tra dữ liệu ở đây là kiểm tra xem dữ liệu được gửi lên có đúng với yêu cầu hay không, cũng có thể xem đây là câu lệnh catch trong quá trình kiểm tra lỗi exception vậy, chẳng hạn như chúng ta yêu cầu người dùng nhập vào email nhưng người dùng nhập sai cấu trúc, hay upload ảnh avatar mà lại gửi lên một file PDF, nguy hiểm hơn nữa là up file ảnh giả – tức file có đuôi .jpg, .png… nhưng lại không chứa dữ liệu ảnh mà chứa mã độc… vì thế chúng ta nên kiểm tra dữ liệu được gửi lên trước khi lưu chúng vào CSDL.

Validator

Trong ứng dụng depot mà chúng ta vừa làm trong các bài trước, chúng ta có tạo một model có tên Product, khi tạo model thì Rails sẽ tạo một lớp cũng có tên Product trong một file là product.rb, và đặt file này trong thư mục app/models, mặc định thì Rails không định nghĩa gì trong lớp này cả:

class Product < ActiveRecord::Base
end

Lớp Product được kế thừa từ lớp ActiveRecord::Base của Rails, trong lớp ActiveRecord::Base có một phương thức tên là validates dùng để kiểm tra dữ liệu. Ví dụ chúng ta sửa lại đoạn code trên như sau:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
end

Phương thức validates nhận vào danh sách các thuộc tính do chúng ta định nghĩa và các phương thức kiểm tra dữ liệu, ở đây title, descriptionimage_url là các thuộc tính do chúng ta định nghĩa, presence là một phương thức kiểm tra dữ liệu, ý nghĩa của phương thức validates này là kiểm tra xem các thuộc tính có khớp với điều kiện trong phương thức kiểm tra hay không. Ở đây presence nhận vào tham số true tức là bắt buộc các thuộc tính kia không được để trống.

Bạn có thể thử chạy server, tạo một product mới và không điền gì vào 3 trường title, description, image_url thì server sẽ thông báo lỗi:

capture

Chúng ta thử kiểm tra một điều kiện khác như sau:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
end

Ở đây phương thức numericality nhận vào giá trị là một đối tượng khác, đối tượng này là greater_than_or_equal_to có giá trị 1.0, ý nghĩa là giá trị phải là số và lớn hơn 1.0.

capture

Một phương thức kiểm tra khác là uniqueness, ví dụ:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
    validates :title, :uniqueness => true
end

Ý nghĩa của phương thức này là thuộc tính phải là duy nhất, tức là không có 2 đối tượng/bản ghi nào có thuộc tính này giống nhau.

capture

Ví dụ tiếp theo là phương thức format:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
    validates :title, :uniqueness => true
    validates :image_url, :format => {
        :with => %r{\.(gif|jpg|png)\Z}i,
        :message => 'Chi nhan file GIF, JPG, PNG'
    }
end

Phương thức format này nhận vào tham số with có giá trị là một chuỗi Regex (Regular Expression – Biểu thức chính quy), chuỗi này sẽ so sánh chuỗi giá trị với một mẫu nào đó, ở đây mẫu Regex của chúng ta có nghĩa là đường dẫn file ảnh phải kết thúc bằng .jpg, .png hoặc .gif. Ngoài ra chúng ta có thể sửa lại thuộc tính message, thuộc tính này sẽ in thông báo lỗi ra theo ý chúng ta muốn, thuộc tính này không bắt buộc phải có.

capture

Ngoài ra còn có rất nhiều phương thức kiểm tra khác mà mình không đề cập ở đây, bạn có thể tham khảo tại:

http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html

Bạn cũng có thể tự viết phương thức kiểm tra riêng cho mình, nhưng mình sẽ đề cập ở bài khác.

Rails – Tùy chỉnh View

Chúng ta sẽ chỉnh sửa giao diện người dùng (View) với ứng dụng depot đã làm trong bài trước.

Tạo dữ liệu với seeds.rb

Trước tiên chúng ta sẽ cần đến một số dữ liệu mẫu để sử dụng. Nếu bạn còn nhớ thì trong bài trước chúng ta biết là Rails sẽ tạo sẵn một form để chúng ta thực hiện thêm bản ghi vào model rất dễ dàng. Tuy nhiên việc ngồi gõ từng dòng dữ liệu như thế rất chán và mệt mỏi, do đó Rails cung cấp cho chúng ta một cách tạo dữ liệu khác.

Ở trong thư mục db có một file có tên là seeds.rb, đây là file Rails dành cho chúng ta để ghi đoạn code tạo đối tượng từ model, và Rails sẽ đọc các dòng code đó rồi insert thành các bản ghi trong CSDL luôn. Ví dụ chúng ta mở file này ra rồi chèn vào đoạn code sau:

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first)
Product.delete_all
Product.create(:title => 'Learn Web Development with Rails',
    :description => 
    %{
        Ruby on Rails Tutorial book and screencast series 
        teach you how to develop and deploy real, 
        industrial-strength web applications with Ruby on Rails.
    },
    :image_url => 'rails_book.png',
    :price => 29.99)
 
Product.create(:title => 'The Ruby Programming Language',
    :description =>
    %{
        The Ruby Programming Language is the authoritative guide 
        to Ruby and provides comprehensive coverage 
        of versions 1.8 and 1.9 of the language.
    },
    :image_url => 'ruby_book.png',
    :price => 39.99)

Trong đoạn code trên chúng ta tạo 2 đối tượng Product. Dòng đầu tiên chúng ta gọi phương thức delete_all, phương thức này sẽ xóa toàn bộ bản ghi có trong CSDL, tiếp theo chúng ta gọi phương thức create và đưa vào các tham số dữ liệu để tạo mới đối tượng. Có một lưu ý là trong thuộc tính description chúng ta dùng cú pháp %{...}, cú pháp này cho phép chúng ta ghi chuỗi trên nhiều dòng, thay vì truyền vào trong dấu nháy kép thì phải nối chuỗi.

Sau đó để thực hiện việc tạo các bản ghi này trong CSDL thì chúng ta mở command prompt lên rồi gõ lệnh rake db:seed là được.

C:\Project\Rails\depot>rake db:seed

Bạn có thể chạy server rồi trỏ đến URL /products hoặc dùng trình sqlite3 để kiểm tra nội dung trong CSDL đã được chèn vào hay chưa, tất nhiên là chẳng có gì đặc biệt cả.

Tùy chỉnh View

Trước tiên chúng ta tìm hiểu sơ qua về khái niệm Layout. Layout ở đây có nghĩa là một đoạn code được dựng sẵn để dùng chung cho toàn bộ website, các trang View khác trong website sẽ copy code trong layout vào code của chính nó rồi chèn thêm các đoạn code đặc trưng của nó vào. Vậy đoạn code layout dùng chung đó nằm ở đâu? câu trả lời la file application.html.erb trong thư mục app/views/layouts, file này được Rails tạo ra tự động và có nội dung như sau:

<!DOCTYPE html>
<html>
<head>
    <title>Depot</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

Chúng ta sẽ tìm hiểu về layout sau, ở đây chúng ta chỉ cần để ý các đoạn nằm trong thẻ <%= %>. Các đoạn code nằm trong cặp thẻ này được gọi chung là template, chúng ta có thể gọi một số hàm, thực hiện một số câu lệnh đơn giản như if, vòng lặp… hoặc tham chiếu đến biến trong cặp thẻ này… và những gì nằm trong cặp thẻ này sẽ được trình biên dịch Rails dịch ra thành code HTML rồi chèn vào đó. Ở đây dòng:

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>

có nghĩa là tạo các thẻ <link> trỏ đến toàn bộ file CSS có trong thư mục app/assets/stylesheets, bạn có thể mở thư mục này ra và thấy trong đó có sẵn một số file CSS do Rails tự tạo ra rồi, một số file có thể có đuôi .scss, nhưng cũng không quan trọng mấy.

Tương tự, các file Javascript và các file ảnh sẽ được đặt vào trong thư mục app/assets/javascriptsapp/assets/images.

Bây giờ chúng ta tạo một file có tên depot.css chứa nội dung như sau:

#product_list table tr td {
    padding: 5px;
    vertical-align: top;
}

#product_list .list_image {
    width: 60px;
    height: 70px;
}

#product_list .list_description {
    width: 60%;
}

#product_list .list_description dl {
    margin: 0;
}

#product_list .list_description dt {
    color: #244;
    font-weight: bold;
    font-size: larger;
}

#product_list .list_description dd {
    margin: 0;
}

#product_list .list_actions {
    font-size: x-small;
    text-align: right;
    padding-left: 1em;
}

#product_list .list_line_even {
    background: #e0f8f8;
}

#product_list .list_line_odd {
   background: #f8b0f8;
}

#store .entry {
    overflow: auto;
    margin-top: 1em;
    border-bottom: 1px dotted #77d;
}

#store .title {
    font-size: 120%;
    font-family: sans-serif;
}

#store .entry img {
    width: 80px;
    margin-right: 5px;
    margin-bottom: 5px;
    float: left;
}


#store .entry h3 {
    margin-top: 0;
    margin-bottom: 2px;
    color: #227;
}

#store .entry p {
    margin-top: 0.5em; 
    margin-bottom: 0.8em; 
}

#store .entry .price_line {
    clear: both;
    margin-bottom: 0.5em;
}

#store .entry .add_to_cart {
    position: relative;
}

#store .entry .price {
    color: #44a;
    font-weight: bold;
    margin-right: 2em;
}

Chúng ta định nghĩa vài class CSS đơn giản để hiển thị trang /products theo giao diện mới một tí. Và tất nhiên file này sẽ được file layout tham chiếu tới như đã nói ở trên.

Tiếp theo chúng ta sửa lại file index.html.erb trong thư mục app/views/products như sau:

<div id="product_list">
    <h1>Products</h1>
 
    <table>
    <% @products.each do |product| %>
        <tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
 
            <td>
                <%= image_tag(product.image_url, :class => 'list_image') %>
            </td>
 
            <td class="list_description">
                <dl>
                    <dt><%= product.title %></dt>
                    <dd><%= truncate(strip_tags(product.description),
                            :length => 80) %></dd>
                </dl>
            </td>
 
            <td class="list_actions">
                <%= link_to 'Show', product %><br/>
                <%= link_to 'Edit', edit_product_path(product) %><br/>
                <%= link_to 'Destroy', product, 
                    data: { confirm: 'Are you sure?' },
                    :method => :delete %>
            </td>
        </tr>
    <% end %>
    </table>
</div>

<br />

<%= link_to 'New product', new_product_path %>

Có một số điểm lưu ý trong đoạn code trên như sau:

  • Hàm cycle() có tác dụng duyệt qua lần lượt từng phần tử trong danh sách, khi đến hết danh sách thì lại quay về phần tử đầu tiên, trong ví dụ trên thì class của thẻ tr đầu tiên sẽ là list_line_odd hoặc list_line_even tùy vào từng lần lặp.
  • Hàm truncate() có tác dụng lấy một lượng kí tự nhất định, ở đây chúng ta lấy 80 kí tự, ngoài ra chúng ta còn dùng hàm strip_tags() để loại bỏ các thẻ HTML trong chuỗi cho trước.
  • Các dòng link_to <tên phương thức>  chẳng hạn như link_to 'Destroy' có nghĩa là gọi đến phương thức destroy trong file controller, tức là file app/controllers/products_controller.rb, ngoài ra theo sau chúng ta còn có thể truyền vào các tham số, ví dụ như data: { confirm: 'Are you sure?'} tức là truyền vào đối tượng data có thuộc tính confirm có giá trị là chuỗi "Are you sure?" (dưới dạng JSON). Dòng này sẽ được Rails dịch thành thuộc tính data-confirm trong thẻ <a>, nếu bạn rành HTML5 thì bạn sẽ biết là thuộc tính này sẽ tạo một hộp thoại để xác nhận.

Bây giờ bạn có thể chạy server và trỏ đến localhost:3000/products để xem giao diện mới.

capture

Rails – Xây dựng ứng dụng MVC

Chúng ta sẽ xây dựng một website bán hàng đơn giản, sử dụng cơ sở dữ liệu là SQLite3

SQLite3

SQLite3 là một cơ sở dữ liệu SQL, đặc điểm của SQLite3 là rất nhỏ, nhẹ, dễ cài đặt, phù hợp với nhiều loại ứng dụng. Điểm đặc biệt của SQLite3 là chúng ta không cần một server, không cần các bước cài đặt phức tạp, rườm rà.

Nếu bạn đã có trình sqlite3.exe trên máy thì bạn có thể bỏ qua phần này và kéo xuống phần tạo cơ sở dữ liệu để đọc tiếp. Nếu chưa thì đầu tiên bạn tải công cụ của sqlite3 tại địa chỉ https://sqlite.org/download.html

Bạn tải các tool về và giải nén theo hệ điều hành mà bạn đang dùng, chẳng hạn như bạn dùng Windows thì tải file sqlite-tools-win32-x86-3140200.zip (1.52 MiB) về:

capture

Tiếp theo bạn nên (hoặc phải) đưa đường dẫn đến thư mục chứa các file vừa được giải nén này vào biến môi trường PATH để tiện sử dụng sau này:

Ví dụ với Windows 10:

Bạn bấm chuột phải vào My Computer → Properties → Advanced system settings → Environment Variables, sau đó click chọn biến Path trong phần System variables rồi chèn thêm đường dẫn đến thư mục sqlite3 vừa giải nén vào. Chẳng hạn ở đây mình giải nén vào thư mục F:\DevSoft\sqlite3 thì mình có hình như sau:

capture

Bây giờ chúng ta kiểm tra xem sqlite3 đã được đưa vào biến môi trường Path chưa thì bạn mở terminal lên (Cmd trong Windows) rồi gõ lệnh sqlite3 -version để xem phiên bản sqlite3.

capture

Nếu bạn ra được giống như hình trên thì đường dẫn đến thư mục chứa sqlite3 của bạn đã hoàn toàn nằm trong biến môi trường Path rồi. Ở đây mình dùng SQLite3 phiên bản 3.14.2 như trong hình.

Tạo project

Chúng ta tạo project với tên là depot (nghĩa là nhà kho).

C:\Projects\Rails>rails new depot
...

Tiếp theo chúng ta sẽ tạo CSDL. Chúng ta chuyển thư mục trong cmd vào thư mục của project (bằng lệnh cd) và gõ lệnh sau:

C:\Project\Rails\depot>rails generate scaffold Product title:string description:text 
image_url:string price:decimal
    invoke active_record
    create   db/migrate/20161106144824_create_products.rb
    create   app/models/product.rb
    invoke test_unit
    create   test/models/product_test.rb
    ...

Lệnh trên có cú pháp như sau:

rails generate scaffold <tên bảng> <tên trường 1:kiểu dữ liệu> <tên trường 2:kiểu dữ liệu>...<tên trường n:kiểu dữ liệu>

Lệnh này sẽ định nghĩa các model để tạo CSDL, ở đây chúng ta tạo model Product (sản phẩm) với các trường title, description, image_url, price. Tuy nhiên lệnh này chưa tạo bảng và model ngay mà chỉ tạo một thư mục có tên migrate trong thư mục db và một file có có tên tương tự như 20161106144824_create_products.rb trong thư mục đó, file này sẽ lưu thông tin của các bảng sẽ được tạo sau này:

class CreateProducts < ActiveRecord::Migration
    def change
        create_table :products do |t|
            t.string :title
            t.text :description
            t.string :image_url
            t.decimal :price

            t.timestamps null: false
        end
    end
end

Chúng ta sửa lại file này để quy định trường price có độ lớn 8 chữ số và số phần thập phân là 2 chữ số như sau:

class CreateProducts < ActiveRecord::Migration
    def change
        create_table :products do |t|
            t.string :title
            t.text :description
            t.string :image_url
            t.decimal :price, :precision => 8, :scale => 2

            t.timestamps null: false
        end
    end
end

Tiếp theo để thực sự tạo CSDL và tạo bảng thì chúng ta chạy lệnh rake db:migrate như sau:

C:\Project\Rails\depot>rake db:migrate
== 20161106144824 CreateProducts: migrating ===========================================
-- create_table(:products)
   -> 0.0041s
== 20161106144824 CreateProducts: migrated (0.0066s)===================================

Lệnh này sẽ tạo CSDL, mặc định Rails sử dụng cơ sở dữ liệu SQLite3 và file chứa CSDL sẽ nằm trong thư mục db và có tên là development.sqlite3. Bạn có thể cấu hình để Rails sử dụng CSDL khác hoặc đổi tên file CSDL mặc định thành tên khác bằng cách chỉnh sửa trong file database.yml trong thư mục config. Cách cấu hình thì mình sẽ nói ở bài khác.

Model mà chúng ta đã tạo bằng lệnh rails generate scaffold có tên là Product, tuy nhiên Rails sẽ tự động chuyển tên model thành viết thường và thêm ký tự ‘s‘ vào cuối tên và dùng tên đó để tạo bảng, tức là ở đây bảng của chúng ta sẽ có tên là products. Một điều khác nữa là Rails sẽ tự động thêm trường id có kiểu dữ liệu là INTEGER tự động tăng và dùng trường này làm khóa chính, và tạo 2 trường created_atupdated_at có kiểu dữ liệu DATETIME dùng để lưu thời điểm tạo bản ghi, thời điểm cập nhật bản ghi.

Ngoài việc tạo CSDL, Rails cũng sẽ tạo luôn model, controller và một số view mẫu để chúng ta sử dụng. Bây giờ chúng ta chạy server bằng lệnh rails server sau đó trỏ đến địa chỉ localhost:3000/products là sẽ được trang có hình như sau:

capture

Bạn có thể thực hiện các thao tác thêm (click vào link New Product), sửa, xóa, cập nhật sản phẩm một cách dễ dàng, tất cả các thao tác cơ bản này đều do Rails tạo ra tự động.

Khi click vào đường dẫn New Product, bạn sẽ được chuyển đến trang http://localhost:3000/products/new, trang này có giao diện được dựng từ file app/views/products/_form.html.erb, chúng ta có thể tùy ý chỉnh sửa để giao diện hiển thị theo ý chúng ta, ở đây chúng ta sửa file này để hiển thị số dòng trong trường description là 6 dòng như sau:

<%= form_for(@product) do |f| %>
    <% if @product.errors.any? %>
        <div id="error_explanation">
            <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2>

            <ul>
            <% @product.errors.full_messages.each do |message| %>
                <li><%= message %></li>
            <% end %>
            </ul>
        </div>
    <% end %>

    <div class="field">
        <%= f.label :title %><br>
        <%= f.text_field :title %>
    </div>
    <div class="field">
        <%= f.label :description %><br>
        <%= f.text_area :description, :rows => 6 %>
    </div>
    <div class="field">
        <%= f.label :image_url %><br>
        <%= f.text_field :image_url %>
    </div>
    <div class="field">
        <%= f.label :price %><br>
        <%= f.text_field :price %>
    </div>
    <div class="actions">
        <%= f.submit %>
    </div>
<% end %>

Save lại file này, sau đó refresh lại trang products/new, bạn có thể tự tạo một sản phẩm (hay 1 bản ghi) mẫu nào đó.

capture

Bạn sẽ nhận được thông báo sản phẩm được tạo thành công. Bây giờ bạn có thể quay lại trang /products và sản phẩm sẽ được hiển thị lên.

capture

Ngoài ra bạn sẽ thấy còn có 3 đường link là Show, Edit được route với các trang /products/<id>, /products/<id>/edit dùng để hiển thị thông tin chi tiết sản phẩm và chỉnh sửa sản phẩm. Đường link Destroy sẽ hiển thị một hộp thông báo hỏi chúng ta có muốn xóa sản phẩm không, nếu bấm OK thì sản phẩm đó sẽ bị xóa.

Các chức năng thêm, sửa, xóa, cập nhật được gọi chung là thao tác CRUD (Create – Read – Update – Delete), hầu hết mọi ứng dụng quản lý (như quản lý cửa hàng, quản lý khách sạn, quản lý sinh viên…v.v) đều phải code các chức năng này. Vậy nên Rails đã hỗ trợ chúng ta bằng cách tạo sẵn các thao tác này, do đó phần việc của chúng ta được tinh giảm đi rất nhiều. Việc cần làm còn lại chỉ là chỉnh sửa giao diện, thêm các chức năng đặc thù như phân quyền, xác thực, bảo mật…v.v

Rails – Kiến trúc của Rails

Một trong những đặc điểm của Rails là bạn bị buộc phải viết chương trình theo một loạt các quy tắc nhất định, tức là bạn phải theo mô hình kiến trúc mà Rails đã đề ra, tuy nhiên những quy tắc này lại khiến việc phát triển ứng dụng trở nên dễ dàng hơn rất nhiều.

Mô hình MVC – Model, View, Controller

Mô hình MVC được Trygve Reenskaug đề ra vào năm 1979, mô hình này chia ứng dụng làm 3 phần: model, viewcontroller.

Trong đó model sẽ chịu trách nhiệm duy trì dữ liệu của ứng dụng. Đôi khi dữ liệu này chỉ tồn tại trong một thời gian ngắn, đôi khi lại được lưu trữ lâu dài trong cơ sở dữ liệu. Ở đây model không đơn thuần chỉ là dữ liệu, ứng dụng sẽ buộc phải chạy theo các quy tắc đã áp đặt lên dữ liệu đó. Chẳng hạn như chúng ta định nghĩa phiếu giảm giá cho các đơn hàng không lớn hơn 200.000đ, đó là một quy tắc và ứng dụng sẽ phải tuân thủ quy tắc đó. Và nhờ vào các quy tắc này mà dữ liệu của chúng ta không bị biến đổi một cách bất hợp lệ.

View sẽ chịu trách nhiệm tạo ra giao diện người dùng, và giao diện này sẽ dựa trên model. Ví dụ chúng ta có một website bán hàng, trên trang “Sản phẩm” chúng ta liệt kê danh sách các sản phẩm, vậy thì danh sách này sẽ lấy dữ liệu là hàng hóa được định nghĩa bởi model, view sẽ lấy dữ liệu từ model và chuyển đổi thành giao diện hiển thị lên cho người dùng. Công việc của view là chỉ có hiển thị chứ không xử lý bất kỳ thao tác nào của người dùng. Một model có thể được truy cập bởi nhiều view, chẳng hạn như website bán hàng có 2 trang là “Sản phẩm” và “Chỉnh sửa sản phẩm”, trang “Sản phẩm” sẽ truy cập model và lấy danh sách các sản phẩm rồi hiển thị cho người dùng, trong khi đó trang “Chỉnh sửa sản phẩm” cũng lấy danh sách các sản phẩm nhưng hiển thị cho người quản lý website.

Controller sẽ chịu trách nhiệm vận hành ứng dụng. Controller sẽ nhận các sự kiện từ bên ngoài, thông thường là từ người dùng, sau đó tương tác với model và gọi view tương ứng để hiển thị.

capture

Bộ ba Model, ControllerView hợp thành thành mô hình MVC. Mô hình MVC cho phép chúng ta tách ứng dụng thành các bộ phận riêng biệt, nhờ đó chúng ta có thể dễ dàng phát triển và bảo trì ứng dụng. Ruby on Rails cũng được phát triển theo mô hình MVC.

Bất cứ ứng dụng Rails nào cũng có 3 phần model, view và controller. Việc kết nối giữa 3 thành phần này đã được Rails giải quyết tự động rồi, do đó bạn chỉ cần quan tâm đến việc phát triển từng thành phần thôi.

Trong một ứng dụng Rails, một gói tin HTTP được gửi từ trình duyệt sẽ được chuyển tới các router trước tiên, đây là nơi vận chuyển các gói tin HTTP đến các phương thức nhất định (trong Rails thì phương thức hay hàm còn có tên khác là action). Phương thức đó sẽ đọc các dữ liệu có trong gói tin và có thể sẽ tương tác với model, hoặc gọi thêm các phương thức/action khác. Cuối cùng thì phương thức đầu tiên được gọi đó sẽ tính toán ra các dữ liệu khác rồi gửi cho một view nào đó để hiển thị cho người dùng.

Tính năng Object Relational Mapping

Thông thường chúng ta sẽ lưu dữ liệu trong một cơ sở dữ liệu quan hệ. Chẳng hạn như một hệ thống đặt hàng sẽ gồm có các bảng đơn hàng, sản phẩm, khách hàng

Tuy nhiên cơ sở dữ liệu quan hệ lại thường không mấy liên quan tới lập trình hướng đối tượng, chẳng hạn như trong SQL bạn có thể thực hiện các câu truy vấn để trích xuất các tập dữ liệu có liên quan với nhau rất dễ dàng, nhưng để làm việc này trong hướng đối tượng rất khó. Chẳng hạn việc tìm một tập các sản phẩm có giá < 2.000.000đ thuộc danh mục máy tính được đăng vào năm 2016 rất dễ trong SQL, nhưng lại rất khó trong code Ruby.

Chính vì vậy mà nhóm phát triển đã cho ra đời một tính năng hỗ trợ việc chuyển đối dữ liệu quan hệ sang các đối tượng một cách dễ dàng đó là tính năng Object Relational Mapping (ORM)

Các thư viện ORM sẽ làm công việc chuyển đổi các bảng trong CSDL sang các lớp. Ví dụ như trong CSDL có bảng Order (đơn hàng) thì ORM sẽ tạo một lớp có tên Order trong Ruby, các dòng trong bảng sẽ là các đối tượng của lớp đó, các thuộc tính của lớp sẽ tương ứng với các cột trong bảng. Ngoài ra ORM sẽ định nghĩa các phương thức để chúng ta có thể đọc ghi dữ liệu một cách dễ dàng. Ví dụ chúng ta sẽ có các đoạn code dạng như sau:

order = Order.find(1)
...
Order.where(:name => 'phocode').each do |order|
    order.payment = "Paypal"
    order.save
end

Active Record

Active Record là bộ phận thực hiện chức năng ORM trong Rails, bao gồm việc tạo lớp từ các bảng, tạo đối tượng từ các dòng trong bảng, tạo thuộc tính từ các cột. Dưới đây là đoạn code ví dụ:

require 'active_record'

class Order < ActiveRecord::Base
end

order = Order.find(1)
order.pay_type = "Paypal"
order.save

Trong ví dụ trên, lớp Order có phương thức find() dùng để tìm một đối tượng có id1, ngoài ra chúng ta còn có thể chỉnh sửa thuộc tính pay_type, đoạn code trên chỉ là ví dụ nhỏ, trên thực tế chúng ta còn phải làm nhiều thứ nữa.

Action Pack

Như chúng ta đã biết, controller sẽ gửi dữ liệu cho view, nhận và xử lý sự kiện từ view, do đó view và controller khá “thân thiết” với nhau. Chính vì vậy mà bộ phận xử lý controller và view được gộp lại làm một và được gọi là Action Pack. Bạn đừng hiểu lầm là chúng ta sẽ viết  code cho view và controller chung một chỗ, ở đây chẳng qua chỉ là chúng được vận hành bởi cùng một thứ mà thôi.

View

Trong Rails thì view có nhiệm vụ tạo ra các đoạn code HTML để hiển thị lên trình duyệt của người dùng, thông thường code HTML này có kèm theo dữ liệu được tạo ra từ các phương thức trong controller.

Các dữ liệu được tạo ra sẽ được tạo ra từ các template, bạn cứ hình dung đây giống như là một trình thông dịch nhỏ khác là được. Hiện có 3 loại template trong Rails là Embedded Ruby (ERb), XML Builder và RJS.

Trong đó ERb là phổ biến nhất, code ERb sẽ được nhúng chung với code HTML, nếu bạn đã từng làm việc với PHP hay JSP (Java) thì bạn sẽ thấy quen thuộc. Mặc dù code kiểu này rất linh hoạt nhưng đa phần người ta nghĩ rằng việc trộn chung công việc giữa các thành phần khác nhau là không nên.

Controller

Phần controller trong Rails là bộ phận trung tâm, có nhiệm vụ xử lý tương tác giữa người dùng, view và model. Tuy nhiên chúng ta sẽ chỉ tập trung vào việc phát triển các tính năng của website, còn việc kết nối giữa các thành phần này sẽ được Rails giải quyết, chúng ta không cần quan tâm.

Cơ bản thì controller hỗ trợ các tính năng sau:

  • Định tuyến (routing) các URL tới các phương thức/hàm/action tương ứng. Ngoài ra controller còn định nghĩa các URL có cấu trúc thân thiện, dễ nhìn
  • Quản lý cache, giúp tăng hiệu năng của hệ thống
  • Quản lý các module hỗ trợ, giúp mở rộng các tính năng của template
  • Quản lý session, giúp theo dõi các hoạt động đang diễn ra trên ứng dụng