Jex’s Note

Rails Controller

(最後更新 : 2016-04-27)

Params

全部通過

params.require(:post).permit!

只接收特定的欄位的參數

params[:user].permit(:name, ages)

# 或這樣寫
params.require(:user).permit(:name, :ages)

permit Array

params[:user].permit(user_contacts: [:name, :ages, phone])

Protect from forgery

Rails 會在 POST, PUT/PATCH, DELETE 時檢查 authenticity token,

假如接收到假造的 token 如: {"authenticity_token"=>"g1mmeTH3brains=", "user"=>{"name"=>"Big Dummy"}}

則 protect_from_forgery 做出的反應會依照你在 controller 的設定, 有以下情況 :

  • 擲出錯誤 (default) : protect_from_forgery with: :exception
  • 相當於關掉檢查 token, 直接通過 : protect_from_forgery with: :null_session
  • 刪舊session, 建新的一條 : protect_from_forgery with: :reset_session

關掉檢查 authenticity token

測試環境下預設是不檢查的

config/environments/test.rb

config.action_controller.allow_forgery_protection = false

Skip authenticity token

即使你在 ApplicationController 有檢查 authenticity token,但有些需要接外部 POST 值的 API 接口,你可以在那個 controller 加上

skip_before_action :verify_authenticity_token

session & cookie

session

session[:locale] = 'zh-TW'

cookie

cookies[:storage_path] = params[:storage_path]
cookies[:storage_path] = {:value => params[:storage_path], exripes => 1.hour.from_now}

Cache

Write

Rails.cache.write("cache_key1" , 'Hello World!', :expires_in => 5.minutes)

Read

Rails.cache.read("cache_key1")

存入 hash 及取出

source = {
    'xx': 'Hello World!'
}
Rails.cache.write("cache_key1" , Marshal.dump(source) , :expires_in => 5.minutes)
source = Marshal.load(Rails.cache.read("cache_key1"))

render && redirect

render

render :new                                                 # render new.html.erb
render action: 'new'                                        # 同上
render text: Rails.env                                      # 輸出純文字
render json: @files, status: 200                            # same as => status: :ok
render json: user.errors, status: 422                       # return Http status code
render json: episode, status: :created, location: episode   # :created 等同 201 (新增成功)

redirect

redirect_to root_path
redirect_to post_comment_path(@post, @comment)
redirect_to @user, notice: 'Updated'                    # 等同 flash[:notice] = 'Updated' 再 redirect
redirect_to(:back)                                      # 回送出時的那一頁
redirect_to request.referer + "#user-#{@user.id}"       # back + anchor 是無效的, 必須改成這樣
redirect_to(request.env['HTTP_REFERER'])                # 效果同上
redirect_to action: 'profile'                           # 等同 redirect_to profile_path
redirect_to root_path, :anchor => "user-#{user.id}"     # url 加入 anchor : http://xxxxxx.com/#user-33

注意 redner :edit 使用 flash[:notice] 是沒用的(下個 request 才會發生), 要改用 flash.now[:notice] = '...'

輸出 json 立即 stop

return render json: post_params

format : 根據網址後面的格式輸出

link: /users/1.json /users/1.xml

def show
    @user = user.find(params[:id])
    respond_to do |format|
        format.html # show.html.erb
        format.json {render json: @user }      # Content-Type: application/json
        format.xml  {render xml: @user }       # Content-Type: application/xml

        # Render specific action
        format.html { render :action => "edit" }

        # JSONP
        format.json { render :json => @user.to_json, :callback => "process_user" }
        format.json     # 預設是 show.js.erb
    end
end

def create
    @post = Post.new(post_params)
    respond_to do |format|
        if @post.save
            format.html { redirect_to @post, notice: 'Success!' }
            format.json { render :show, status: :created, location: @post }
        else
            format.html { render :new }
            format.json { render json: @post.errors, status: :unprocessable_entity }
        end
    end
end

只要在 url 後面加上 .json 它就可以用 format.json 去做區分,你不需要在 Header 帶 json,因為它不是靠 Content-Type 判斷的,而且它支援 CORS (Cross-origin resource sharing)

Redirect with flash

flash[:notice] = "Success"
flash[:alert] = "Fail"

redirect_to root_path, notice: "Success"

Actions

before_action

before_action :set_person,          except: [ :index, :new, :create ]           # except
before_action :ensure_permission,   only: [ :edit, :update ]                    # only
before_action :set_menu, if: :devise_controller?                                # 只有特定 controller 才讀

其他 actions

  • prepend_before_action
  • skip_before_action
  • append_before_action
  • after_action
  • prepend_after_action
  • skip_after_action
  • append_after_action
  • around_action
  • prepend_around_action
  • skip_around_action
  • append_around_action

Exception Handling

begin
    @cart = Cart.new(cart_params)
    @cart.save

    @user = User.find(3)

rescue ActiveRecord::RecordNotUnique
    logger.info('Unique key 已重覆')

rescue ActiveRecord::RecordNotFound
    logger.info('沒有這筆資料')

rescue => e
    # 如果以上沒有符合的 error, 都會進這裡
    logger.info(e.class)            # i.e. ActiveRecord::RecordNotUnique
    # retry                         # 下 retry 要注意,不小心可能會形成無窮迴圈

ensure
    "無論是否發生例外都會執行"
end

補捉自訂 exception

def create
  @order = check_cart
rescue CartService::CartIsEmpty
  flash[:alert] = 'Cart is empty'
rescue ActiveRecord::ActiveRecordError
  flash[:alert] = "Something Wrong:#{$!}"
end

class CartIsEmpty < StandardError; end
def check_cart
  raise CartIsEmpty, '購物車裡無任何商品' if current_user.carts.empty?
end

擲出其他錯誤

吐 404
raise ActionController::RoutingError.new('Not Found')

$! (例外物件)

  • .class, ex: ZeroDivisionError
  • .message, ex: divided by 0
  • .backtrace (等同於$@), 程式出錯的位置, ex: /tmp/test.rb:2:in

concerns - controller 之間共同 method

controllers/concerns 與 models/concerns 是共通的

controllers/concerns/example.rb

module Example
  def test
    logger.info('TEST TEST TEST')
  end
end

controllers/carts_controller.rb

class CartsController < ApplicationController
  include CheckCart

  def show
    test
  end
end

includes 避免 n+1 queries 問題

若 posts 有 user_id 欄位, 顯示 post list 時, 每筆 post 後面也要顯示 user name 怎麼撈會比較好 ?

因為要透過 user_id 去關聯 users TABLE 的 name 欄位, 以下是有用 includes 及沒有使用的解析

沒有用 includes :

@posts = Post.all
> Post Load (1.4ms)  SELECT "posts".* FROM "posts"

執行了第一次 query

執行迴圈 :

@posts.each do |post|
  post.title %> written by <%= post.user.email
end
> User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
> User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 2]]
> User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 2]]

迴圈每一次執行都會跑一次, 所以執行了 n 次 query

總共是 n+1 次

使用 includes

@posts = Post.includes(:user).all
> Post Load (0.7ms)  SELECT "posts".* FROM "posts"
> User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2)

@posts.each do |post|
  post.title %> written by <%= post.user.email
end
(不會再產生 query)

只執行了第兩次 query

總共只會執行 2 次 query, 之後迴圈每一次執行都會跟 cache 拿, 所以不會有額外的 query 產生

結論

  • 如果要顯示的 list 沒有關聯的問題, 就不需要用 includes
  • 如果有的話, 則需要

Logger

如果在 model 或 concerns 下會無法直接取到 logger,需改用 Rails.logger

幾種 log 的方法, 按照越來越嚴重的等級排序 :

  • logger.debug : 在 production 下不會紀錄
  • logger.info : 一般等級的 log,在 production 也會紀錄
  • logger.warn : 警告訊息
  • logger.error : 誤訊息,但還不到網站無法執行的地步
  • logger.fatal : 嚴重錯誤到網站無法執行的訊息

注意! log 檔案會越來越大,記得要用 logrotate 控制它的檔案大小,可參考此篇

其他

在 controller 使用 NumberHelper 需要另外引入

include ActionView::Helpers::NumberHelper
number_with_delimiter(1000000)

Get controller and action name

controller_name
action_name

在 controller 取得上傳檔名

params[:user][:avatar].original_filename

Comments