Jex’s Note

Rails Route

command

  • rake routes : 查看所有routes
  • rake routes | grep user : 查看所有 routes 含有 user 關鍵字的

觀念

4 HTTP methods, 4 URL helper, 7 actions

Helper                      GET                 POST            PUT 或 PATCH    DELETE
event_path(@event)          /events/1                           /events/1       /events/1
                            show                                update          destroy

events_path                 /events             /events
                            index               create

edit_event_path(@event)     /events/1/edit
                            edit

new_event_path              /events/new
                            new
  • show, edit, update, destroy 是單數, 對特定元素操作
  • index, create 是複數, 對群集操作

基本 Route 語法

root 'welcome#index'

# 7 個 action 都使用
resources :info do

    # 只允許 edit, update
    resources :users, only: [:edit, :update]

    # 除了 show 其他 action 都用
    resources :products, except: [:show]

    # 指定使用 PUT
    put 'change_password', on: :member
end

# namespace 是 folder 名稱
namespace :dashboard  do
  # 原本 dashboard 在 route 的命名改為 admin, 通常是為了美化 route 或是減少不直覺的 url 造成的困惑才會使用
  resources :welcome, as: 'home'
end

# 兩者是一樣的
resources :search, only: [:show]
get '/search/:id', to: 'search#show', as: 'search'      # to: action, as: route name

Difference between member and collection

如果要帶上原本物件的 id 就用 :member

如果只是需要一個一般的頁命就用 :collection

i.e.

resources :posts do
  # on collection
  get 'search', on: :collection             # '/posts/search' and search_posts_path

  # on member
  get 'share', on: :member                  # '/posts/:id/share' and share_photo_path(@post)
end

member 與 collection 那一種寫法

resources :users do
  member do
    get :find
  end

  collection do
    get  :find
    post :freeze
  end
end

Url / Path 相關

完整 url

request.original_url
> http://127.0.0.1:3000/dashboard/admin/110/find_name

Path

request.path (= request.full.path)
> /dashboard/admin/110/find_name

判斷目前 path 是否一樣

current_page?(new_product_path)
> True / False

將 hash 轉為 query string

{ name: 'David', nationality: 'Danish' }.to_query
> name=David&nationality=Danish

在 Rails consloe 下

使用 route path 必須要先引入

include Rails.application.routes.url_helpers
> root_path

印出所有的 assets path

Rails.application.config.assets.paths

# 條列式
y Rails.application.config.assets.paths
ap Rails.application.config.assets.paths

path helper

xxxx_path(anchor: 'xx')             # /xxxx#xx
xxxx_path(format: :json)            # /xxxx.json

Dashboard 設計

config/route.rb :

root 'welcome#index'                    # 首頁

namespace :dashboard do
  root 'welcome#index'                  # dashboard 的首頁
  resources :musics, only: [:index]
end

controllers/dashboard_controller.rb

class DashboardController < ApplicationController
end

controllers/dashboard/welcome_controller.rb :

class Dashboard::WelcomeController < DashboardController
  def index
  end
end

views/dashboard/welcome/index.rb :

dashboard index

另一種寫法 dashboard_controller.rb 放在 controllers/dashboard 下

controllers/dashboard/dashboard_controller.rb

class Dashboard::DashboardController < ApplicationController

controllers/dashboard/welcome_controller.rb :

class Dashboard::WelcomeController < Dashboard::DashboardController

Rails Asset

Command

清除 assets cache

rake assets:clean

precompiled 所有 assets

rake assets:precompile
RAILS_ENV=production bundle exec rake assets:precompile

Custom assets folder

config/application.rb 在 class 裡加上 :

config.assets.paths << Rails.root.join("app", "assets", "[custom foler name]")

加入 fonts

1) config/application.rb

config.assets.paths << Rails.root.join("app", "assets", "fonts")

?) config/initializers/assets.rb, 不加也能 work, 但先保留

# Rails.application.config.assets.precompile += %w( .svg .eot .woff .ttf )

Instead of assets/fonts path

font-awesome.min.css.erb :

src:url(<%= asset_path 'dashboard/fontawesome-webfont.eot' %>);

它會被 compile 成

src:url(/assets/dashboard/fontawesome-webfont-e511891d3e01b0b27aed51a219ced5119e2c3d0460465af8242e9bff4cb61b77.eot);

如果不用預設的 application.js application.css

不用預設的 application, 在 controller 帶值給 layout load asset, 就必須要寫在 config/initializers/assets.rb :

Rails.application.config.assets.precompile += %w( welcome.js )
Rails.application.config.assets.precompile += %w( welcome.css )
Rails.application.config.assets.precompile += %w( videofrom.js )
Rails.application.config.assets.precompile += %w( videofrom.css )

或加入所有的 js 及 css

config.assets.precompile += Dir["#{__dir__}/../app/assets/stylesheets/*.css"].map{|i|i.split('/').pop}
config.assets.precompile += Dir["#{__dir__}/../app/assets/javascripts/*.js"].map{|i|i.split('/').pop}

讀取 Javascript / CSS

<%= javascript_include_tag "application" %>
<%= stylesheet_link_tag "application" %>

依 controller name 決定讀取的 JS

view : (讓 controller 讀自己的 javascript)

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

require 語法說明

//= require jquery
//= require jquery.turbolinks       # 一定要馬上在 jquery 後面 load
//= require jquery_ujs
//= require turbolinks

/app/assets/javascripts/application.js :

//= require jquery
//= require jquery_ujs      # unobtrusive JavaScript. Ajax
//= require_tree .          # include all files
//= require application     # include application.js
//= require shared          # include lib/assets/javascripts/shared.js.coffee
//= require friend          # include vendor/assets/javascripts/friend.js

/app/assets/stylesheets/application.css :

/*
 *= require reset           # Included before the content in this file
 *= require_self            # Specifies where to insert content in this file
 *= require_tree .
 */
form.new_user {
    border: 1px dashed gray;    # include before everything else
}

Helper

Assets helper 使用後都會加上 hash, 讓 browser 判斷是否拿 cache

audio_path("horse.wav")   # => /audios/horse.wav
audio_tag("sound")        # => <audio src="/audios/sound" />
font_path("font.ttf")     # => /fonts/font.ttf
image_path("edit.png")    # => "/images/edit.png"
image_tag("icon.png")     # => <img src="/images/icon.png" alt="Icon" />
video_path("hd.avi")      # => /videos/hd.avi
video_tag("trailer.ogg")  # => <video src="/videos/trailer.ogg" />

引入

src: url('/assets/fonts/myfont-webfont.ttf')
src: url('myfont-webfont.eot?#iefix') format('embedded-opentype');

在副檔名加入 .erb, ex: application.css.erb

src: url(<%= asset_path 'Chunkfive-webfont.eot' %>);

或直接用 public 路徑, 不管是 assets/fonts, assets/js, 最後都是同一層目錄結構, 所以可以在 css 下直接引入 /assets/web-icons.woff 位在 /assets/fonts/web-icons.woff 的檔案

Sass helper

將原本 .css 檔 rename 為 .scss, 並根據以下修改

image-url("rails.png")         # => url(/assets/rails.png)
image-path("rails.png")        # => "/assets/rails.png".
asset-url("rails.png", image)  # => url(/assets/rails.png)
asset-path("rails.png", image) # => "/assets/rails.png"

用法

src: asset-url('Junction-webfont.eot', font);
background-image:image-url('application/typewriter_dark.jpg');
src: url(font-path('myfont-webfont.eot')
src: font-url('icofonts.eot');                      # compile css 後 : src: url(/assets/icofonts.eot);
src: url(font-path('myfont-webfont.eot')

src:url(/assets/dashboard/web-icons/web-icons.eot?v=0.2.2)      # web-icons.eot 的路徑在 app/assets/fonts/dashboard/web-icons/web-icons.eot
改成
src:asset-url("dashboard/web-icons/web-icons.eot?v=0.2.2")

如何套入 bootstrap dashboard theme

將會引入的 css 放在 assets/stylesheets/dashboard

assets/stylesheets/dashboard.css

/*
 *= require_tree ./dashboard
 */

將會引入的 js 放在 assets/javascripts/dashboard

assets/javascripts/dashboard.js

/*
 *= require ./dashboard/jquery.min.js
 *= require_tree ./dashboard
 */

將會引入的 fonts 放在 assets/fonts/dashboard

需再做以下步驟

1) config/application.rb 加入 assets_path

2) initialize/assets.rb 加入 precompile

3) 修改 css 引入的 fonts

  1. 將 url 改成 asset-url 參考本文 Sass helper -> 用法
  2. 如果網站使用的是 HTTPS, 那麼如果 css 有引入 http 的 font 都要改成 https, 否則瀏覽器會拒絕載入

Error

ActionController::RoutingError (No route matches [GET] “/assets/dashboard/jquery.min.map”)

能將 minify 後的變數從 a b c 轉回原本的, 它在 jquery.min.js 最後一行 //# sourceMappingURL=jquery.min.map, 如果不需要可以直接刪掉

讓 js 檔可以取得 Route Path

application.js.erb : (注意! 副檔名是 .erb)

Rails.application.routes.url_helpers.*_path

# ex:
Rails.application.routes.url_helpers.edit_post_path

Rails View

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

fields_for 一對一, 一對多

籍由 user 去更新 profile(1對1), user_languages(1對多) 欄位

user.rb

has_one  :profile
has_many :user_languages
accepts_nested_attributes_for :profile
accepts_nested_attributes_for :user_languages

view

<%= form_for @user, url: user_path(@user), html: {method: :put} do |f| %>
    <%= f.fields_for :profile do |s| %>
        <%= s.text_field :about_me %>
    <% end %>

    <%= f.fields_for :user_languages do |l| %>
        <%= l.check_box :has_badge %>
        <%= l.object.from %>
        <%= l.object.to %>
    <% end %>
<% end %>

產生的 HTML Name :

user[profile_attributes][:about_me]
user[user_languages_attributes][0][has_badge]

controller

user = User.find(params[:id])
user.update(admin_update_params)

def admin_update_params
    params[:user].permit(profile_attributes: [:id, :about_me], user_languages_attributes: [:id, :has_badge])
end

它就會更新一對一或一對多了

接下來看一下它到底是怎麼更新的

params[:user] 參數 :

profile_attributes: {
    about_me: "Hello world!",
    id: "66"
},
user_languages_attributes: {
    0: {
        has_badge: "1",
        id: "143"
    }
}

原來 params 會帶該 record 的 id, 所以上面 controller 取參數時要記得 permit :id

Rails 會依照 User model 的關聯下對應的 sql, 注意的是它會先用 user_id + IN (..上面的id參數..), 所以可以確定是這名 User 的資料才可以被 Update

Profile Load (0.3ms)  SELECT  "profiles".* FROM "profiles" WHERE "profiles"."user_id" = ? LIMIT 1  [["user_id", 69]]
UserLanguage Load (0.3ms)  SELECT "user_languages".* FROM "user_languages" WHERE "user_languages"."user_id" = ? AND "user_languages"."id" IN (143, 141, 142)  [["user_id", 69]]
SQL (2.8ms)  UPDATE "profiles" SET "about_me" = ?, "updated_at" = ? WHERE "profiles"."id" = ?  [["about_me", "Hello world!"], ["updated_at", "2015-08-18 09:01:20.299243"], ["id", 66]]
SQL (0.6ms)  UPDATE "user_languages" SET "has_badge" = ?, "updated_at" = ? WHERE "user_languages"."id" = ?  [["has_badge", "f"], ["updated_at", "2015-08-18 09:01:20.305686"], ["id", 143]]

測試它是否真的安全, 將 fields_for 產生的 HTML 隱藏欄位的值改為一個不屬於此 User 的 id

<input type="hidden" value="143" name="user[user_languages_attributes][0][id]" id="user_user_languages_attributes_0_id">
                            改成
<input type="hidden" value="123" name="user[user_languages_attributes][0][id]" id="user_user_languages_attributes_0_id">

送出後, 可以看到 Rails 噴錯了:D , 符合預期結果

Couldn't find UserLanguage with ID=123 for User with ID=69

Partial

Pass a variable into a partial

如果是 instance variable (ex: @user), 不需要特別傳入到 partial, 如果是一般變數才需要 (ex: user)

<%= form_for @user, url: users_path(@user) do |f| %>
    <%= render 'partial file', f: f %>
<% end %>

partial file :

<%= f.text_field :name %>

注意!! 判斷傳入 partial 的變數, 要用 local_assigns 去判斷, 如果直接用

if name   <= 會噴錯 undefined local variable or method `name`

OK :
    if local_assigns.has_key? :name
    if local_assigns[:name]

代入 template 取得 html

<% @progress_bar = render :partial => 'template/progress_bar', :locals => { :num => num, :part_num => part_num, :progress_bar_status => "progress-bar-danger" } %>
<%= render :partial => 'template/url_item', :locals => { :num => num, :progress_bar => @progress_bar } %>

注意檔名要底線 _progress_bar.html.erb, _url_item.html.erb

layout Template

views/layouts/application.html.erb :

<!DOCTYPE html>
<html>
<head>
  <title>Template</title>
  <meta charset="utf-8">
  <%= favicon_link_tag '/favicon.ico' %>        # 要注意一定要 / 因為是從根目錄開始讀, 否則到其他 show 的頁面會把它當 id 在 load
  <%= stylesheet_link_tag @controller_name, media: 'all', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body id="page-top" class="index">
  <%= render 'layouts/header' %>
  <div class="content">
    <%= yield %>
  </div>
  <%= render 'layouts/footer' %>
  <%= javascript_include_tag @controller_name, 'data-turbolinks-track' => true %>
</body>
</html>
  • layouts/header.html.erb, footer.html.erb
  • yield 是替換的內容的, ex: users/edit.html.erb

不同頁面引入相同 partial, path 如何根據不同 route 顯示

很簡單, ex:

link_to '評價', request.path

引入個別 view 定義的 CSS / JS

像有些共用的 js 你可以寫在 appication.js,但有些只有這一頁才會用到的 js 可以直接寫在該 view,這樣開發及維護都很方便

layout/application.html.erb

<%= yield :js %>

在個別 view 寫 javascript

<% content_for :js do %>
    <script>
        $(document).ready(function () {
            alert(1);
        })
    </script>
<% end %>

讓表單送出錯誤後返回資料不清除

controller

def update

  ...(省略)...

  if 驗證錯誤
    render :edit
  end
end

edit.html.erb

<input type="text" class="form-control" name="xxx" value="<%= params[:xxx] %>">

不要用 redirect_to action: :edit, 因為 post 的資料不會被帶到 edit 頁面, 它相當於是一個”重新”的 request 請求。 用 render 直接在 update 生出 edit 頁面, post 資料也會被保存下來

或讓 rails 自動幫你綁定回填

<%= f.text_field :nickname, class: 'form-control' %>

但它有個問題是, 一定要執行到 model 裡, 例如 @post.update(params[:post]) 最後再 redner :edit, 如果還沒執行到 model 就 render 那麼資料就不會回填

刪除 validate 錯誤產生多的 div wrapper

正常 :

<%= f.text_field :present_price  %>

錯誤時 :

<div class="field_with_errors"><input type="text" value="" name="product[present_price]" id="product_present_price"></div>

解決方法 : 拿掉它, 在 config/environment.rb 最後面加上

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  html_tag.html_safe
end

JSONP (jbuilder - 預設包在 Rails 的 Gemfile 裡)

ajax 送出請求給 show, show 會去返回 show.js.erb 的 js code 回去給 browser 執行

ajax 以 bootstrap modal 顯示 show action

index.html.erb

<div class="modal fade" id="myModal"
    ..略..
</div>

link_to '訂單明細', order_path(o), remote: true, data: {toggle: 'modal', target: '#myModal'}

show.js.erb

$(".modal-title").html("訂單明細");

// 填入 html, escape_javascript 可簡寫成 j
$(".modal-body").html("<%= escape_javascript render(partial: 'orders/show')%>");

_show.html.erb

html + ruby code

ajax submit form

view

form_for 加上 remote: true

controller

respond_to do |format|
  format.js
end

create.js.erb

alert('Success');

JSON 檔

index.json.jbuilder

json.array!(@posts) do |post|
    json.extract! post, :id, :title, :content
    json.url post_url(post, format: :json)
end

Form helper

radio_button

<% UserDomain.domains.each do |k, v| %>
  <div>
    <%= radio_button_tag 'user[domain]', v %>
    <%= label_tag "user_domain_#{v}", t("domain.#{k}") %>
  </div>
<% end %>

radio_button("post", "category", "rails")
radio_button("post", "category", "java")


f.radio_button :gender, 'male', checked: true

collection_radio_buttons(:item, :owner_id, Owner.all, :id, :name)

select

未指定值 : 頁面上及 option value 皆顯示一樣

f.select :gender, ['male', 'female', 'others']

有指定值 : 頁面上顯示 male/female/others; option value 為 1 / 2 / 3

f.select :city, {male: 1, female: 2, others: 3}

取出 name 及 id 欄位,直接放入

f.select :category, Category.pluck(:name, :id)        要注意順序是 :name, :id, 否則 select 下拉顯示的會是 id
f.select :category, Category.map { |s| [s.name, s.id] }

空白值, 給 default 值及 class name

f.select :product_spec_id, @product.product_specs.map { |s| [s.name, s.id] }, { include_blank: ture, selected: params[:spec] }, class: 'form-control'

設定 Default

f.select :parent_id, @parent_categories.unshift(["不選擇", 0])

select_tag

select_tag :category_id, options_from_collection_for_select(Category.all, "id", "name")

<select name="category_id" id="category_id">
    <option value="1">Music, Games & Kids</option>
    <option value="2">Games, Movies & Baby</option>
</select>

collection_select

User has_many UserLocation, 將 User 住的 Location 每項以 Select 列出來

current_user.user_locations.each do |l|
    collection_select('user_locations', '', UserLocation.locations_i18n, :last, :first, { include_blank: false, selected: l.location }, { class: 'location', id: nil } )
end
  • 第一個參數 : HTML name 名稱
  • 第二個參數 : user_locations (第二個參數), 我將它留空
  • 第三個參數 : Hash (key: value)
  • :last, :first : 會讓 hash 的 key 顯示在 option 的名字, hash 的 value 則是 option 的值
  • include_blank : 一定要設定它, 否則 class, id 不會 work
  • selected : 如果去掉, 就是未選擇的 <select>

備註

1) f.collection_select(:category_id, Category.all, :id, :name)
等於
f.select :category_id, Category.all.map{ |c| [c.name, c.id] }

2)
s.select :recommended, options_for_select(Translator.recommendeds)

checkbox

f.check_box :rotting

<%= check_box_tag 'user_domains[]', k, @user_domains.has_key?(k), id: "domain-#{k}" %>

Object :
<div class="field">
    <%= f.label "Categories" %><br />
    <% for category in Category.all %>
        <%= check_box_tag 'user[category_ids][]', category.id, @user.category_ids.include?(category.id), :id => dom_id(category) %>
        <%= label_tag dom_id(category), category.name, :class => "check_box_label" %>
    <% end %>
</div>

Hash : dom_id 無法用, 它只吃 object
<% UserDomain.domain.each do |k, v| %>
  <div>
    <%= check_box_tag 'user_domains[]', k, @user_domains.has_key?(k), id: "domain-#{k}" %>
    <%= label_tag "domain-#{k}", t("domain.#{k}") %>
  </div>
<% end %>

參數:

  1. 第一個參數 name
  2. 第二個參數 value
  3. 第三個參數 是否 checked

collection_check_boxes

models/owner.rb

has_many :items

models/item.rb

belongs_to :owner

view :

collection_check_boxes(:item, :owner_id, Owner.all, :id, :name)

file

一般用法

<%= form_for @user, url: users_path, method: :post, html: { class: 'form-inline' } do |f| %>
  <%= f.file_field :name %>
<% end %>

Rename resource

<%= form_for @user, as: 'man', url: users_path, method: :post, html: { class: 'form-inline' } do |f| %>
  <%= f.file_field :name %>
<% end %>

<input type="text" name="man[name]" id="man_name">

label + text_field

<%= f.label :nickname %>
<%= f.text_field :nickname %>

# 讓 text input disable
disabled: true

# 重新給值或 format datetime
value: @case.deadline.strftime("%Y-%m-%d")

數字

<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>

text_area

f.text_area :bio

button

<button>

<%= button_tag(type: 'submit', class: "btn btn-primary") do %>
 <i class="icon-ok icon-white"></i> Save
<% end %>

<input type="submit">

f.submit "Submit", :disable_with => 'Submiting...'
submit_tag "Submit", id: "foo-submit", data: { disable_with: "Please wait..." }

form

<%= form_for @user, url: user_path(@user), html: { method: :put, id: 'edit-user' } do |f| %>

取值

@user.email 或
f.object.email

刪除 hidden 欄位 - utf8

<%= form_for (略), enforce_utf8: false %>

link_to

兩者是一樣意思的

<%= link_to user.contacts.name, contact_path(user.contact) %>
<%= link_to user.contacts.name, user.contact %>

block 寫法

<%= link_to edit_user_registration_path do %>
    <span class="glyphicon glyphicon-user" aria-hidden="true"></span>
    Edit profile
<% end %>

表單 ajax (使用 Unobtrusive JavaScript - UJS)

<%= link_to 'ajax show', event_path(event), :remote => true %>
form_for @user, :remote => true

ajax 送出加上 remote: true

Ajax Link

link_to 'Del', post_path(post), class: 'btn btn-danger', method: :delete, data: {confirm: 'Are you sure?', disable_with: 'Removing'}

label

f.label :name

tag

<%= label_tag "domain-#{k}", t("domain.#{k}") %>

參數:

  1. id
  2. 顯示名稱

div

<% @users.each do |u| %>
    ...
    <%= div_for u do %>                 <div id="user_<%= u.id %>" class="user">
        <%= u.ages %>      same as =>     <%= u.ages %>
    <% end %>                           </div>
    ...
<% end %>

dom id

dom_id(@user) => user_2

輸出

nil 即顯示預設值

<%= @xxx || 'none' %>

輸出換行字元,Replace \n<br>

<%= h(c.text).gsub("\n", "<br>").html_safe %>

截斷指定長度的字串

truncate(user.about_me, length: 100)
 => Stella appositus odio cilicium. Adopto quia magni textus stips libero vergo enim. Iste delibero c...

脫逸

脫逸

<%= @user.ages %>

不脫逸

<%= raw @user.ages %>

脫逸危險標籤

<%=raw sanitize "<script>alert(1);</script>" %>             # alert(1);

(?)脫逸 javascript

escape_javascript()

# 可以縮寫為
j()

數字及日期

數字口語化

number_to_human 1234567890                                      # 1.2 十億

數字 3 位一撇

number_with_delimiter(8400)                                     # 8,400

貨幣符號

number_to_currency(8400)                                        # NT$ 8,400.00  # 如果 locale 為 en => $8,400.00


# 強制指定 locale 及小數位數
number_to_currency(8400, precision: 0, locale: 'zh-TW')         # NT$ 8,400

在 model 裡面用 :

  1. ActiveSupport::NumberHelper::number_to_currency(self.price, unit: '$', precision: 0)
  2. extend ActionView::Helpers::NumberHelper

Percent

number_to_percentage("98")                                       # => 98.000%
number_to_percentage(100, precision: 0)                          # => 100%

日期

time_ago_in_words current_user.created_at                       # 大約 6 小時

語意化

名詞單複數

pluralize(3, "user")                                            # 3 users

標題化 - 單字的開頭大寫

"man from the boondocks".titleize                               # Man From The Boondocks

連接詞 (and)

['one', 'two', 'three'].to_sentence                             # one, two, 和 three

Debug

It will return a <pre> tag that renders the object using the YAML format

<%= debug @article %>

Displaying an instance variable, or any other object or method, in YAML format

<%= simple_format @article.to_yaml %>

Displaying object values

<%= [1, 2, 3, 4, 5].inspect %>

其他

判斷目前所在頁面

current_page?(root_path)

Rails DB & Migration

(最後更新: 2016-05-01)

連接 sqlite3 設定

rails 預設連接的 DB,開發階段才使用

config/database.yml

development:
  <<: *default
  database: db/development.sqlite3

有個小缺點,即使欄位有限制字數,但 sqlite 仍然可以超出字數且 insert 成功

連接 MySQL 設定

  • ubuntu 可能要再安裝sudo apt-get install libmysqlclient-dev
  • 安裝 MySQL 可參考這

1) 設定好 config/database.yml

production:
  adapter: mysql2
  encoding: utf8
  database: myapp_production
  username: root
  password:
  host: 127.0.0.1
  port: 3306
  strict: false                     # 關閉此模式, 否則存超過 size 的資料會噴 error, 讓它自動截斷

2) Gemfile

gem 'mysql2', '~> 0.4.3'
  • ubuntu 需要再安裝 libmysqlclient-dev, 否則 bundle 安裝到 mysql2 時會噴錯
  • 要強制指定版號, 不然會噴 error : Specified 'mysql2' for database adapter, but the gem is not loaded. Addgem ‘mysql2’to your Gemfile (and ensure its version is at the minimum required by ActiveRecord). (Gem::LoadError)

3) 建立資料庫

RAILS_ENV=production rake db:create

4) 執行 migrate

rake db:migrate

沒有的話, 建立 table : rails g migration create_videos

註) 連到 MySQL console

rails dbconsole

連接 PostgreSQL 設定

安裝 PostgreSQL 可參考這

1) 設定好 config/database.yml

default: &default
  adapter: postgresql
  pool: 5
  timeout: 5000

development:
  <<: *default
  adapter: postgresql
  encoding: unicode
  database: myapp_development

2) Gemfile

gem 'pg'

3) 建立資料庫 (使用 Postgres 或 rails 的 command 都可以)

createdb myapp_development
或
rake db:create

4) 執行 migrate

rake db:migrate

註) 連到 MySQL console

rails dbconsole

Command

rake db:create                                  # 建立目前 RAILS_ENV 環境的資料庫
rake db:create:all                              # 建立所有環境的資料庫
rake db:drop                                    # 刪除目前 RAILS_ENV 環境的資料庫
rake db:drop:all                                # 刪除所有環境的資料庫
rake db:migrate                                 # 執行 migration
rake db:migrate RAILS_ENV=development           # 指定 development 環境執行 migration
rake db:migrate:up VERSION=20150713155732       # 執行特定版本的 migration
rake db:migrate:down VERSION=20150713155732     # 回復特定版本的 migration
rake db:rollback                                # To rollback the previous migration
rake db:rollback STEP=3                         # 回復前 3 個 migration
rake db:version                                 # 顯示 Current version: 20150713155732
rake db:seed                                    # 執行 db/seeds.rb (種子資料)
rake db:schema:dump                             # Dump the current db state. 產生 db/schema.rb
rake db:setup                                   # Creates the db, loads schema, & seed.    ( When you start working on an existing app
  • 不加上 RAILS_ENV 預設就是 development
  • rails 的 db 可以分為 production 跟 development, 所以可以用 RAILS_ENV 指定哪個 DB

欄位

欄位型態

  • :string : 有限的字串長度, 如果不指定長度, 預設是 varchar(255)
  • :text : 不限的字串長度
  • :integer : 整數
  • :float : 浮點數
  • :decimal : 十進位數
  • :datetime : 日期時間
  • :timestamp : 時間戳章, 型態為 datetime
  • :date : 日期
  • :time : 時間
  • :binary : 二進位, blob
  • :boolean : 布林值
  • :references : 用來參照到其他 Table 的外部鍵, 型態為 integer

tinyint 與 boolean

1) MySQL 有 tinyint 欄位, 但 Rails 不支援, 但使用 integer + limit 來取代, 如下

t.integer :status, :limit => 2

2) MySQL 沒有 boolean 的型態, Rails 是用 tinyint(1) 來支援 MySQL 的 boolean

integer 補充

如果沒有指定 limit 的話, 預設會建立 int(11), 但如果需要建立 BIGINT,

又該指定多少的 limit 呢? 參考以下對照表

 :limit     Numeric Type    Column Size     Max Value
-----------------------------------------------------------------------
    1       TINYINT         1 byte          127
    2       SMALLINT        2 bytes         32767
    3       MEDIUMINT       3 bytes         8388607
    4       INT(11)         4 bytes         2147483647
    8       BIGINT          8 bytes         9223372036854775807

預設 INT(11) = limit 4

add_column :users, :money, :integer

BIGINT = limit 5~8

add_column :users, :money, :integer, limit: 8

Migrate

產生 migration 指令及命名慣例

rails g migration create_users                      # 建立 TABLE
rails g migration add_confirmable_to_devise         # 新增欄位
rails g migration change_comment_field_name         # 修改欄位

語法

migrate 的方式

def up          # migrate 執行的
def down        # rollback 執行的
def change      # migrate 執行的, 要嘛就 up + down, 不想那麼麻煩就選擇 change

Create Table & 欄位

create_table(:users) do |t|

    t.column :id, 'INT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (id)'
    t.string :id, primary_key: true     # create_table :users, id: false
    t.string :video_id, :limit => 50, :null => false               # primary key 但要自己產生
    t.integer :kind , :limit => 1 , :default => 0 , :null => false , :unsigned => true
    t.boolean  :is_hidden, :default => false   ,:null => false
    t.datetime "delete_at"
    t.string   "title" limit: 100,  default: 0, null: false           (VARCHAR)
    t.text     "body"                                               (TINYTEXT, TEXT, MEDIUMTEXT, or LONGTEXT2)
    t.timestamps                                                    (same as : t.datetime :created_at, :updated_at)
    t.references :article, index: true

end

操作欄位

  • 改default : change_column_default :users, :is_admin, default: true
  • 建立table : create_table :videos do |t|
  • 刪除table : drop_table :people
  • 變更欄位 : change_column :table_name, :field_name, :integer, :limit => 8, :unsigned => true, :null => false, :auto_increment => true
  • 增加欄位 : add_column :table_name, :balance, :integer, default: 0, null: false, unsigned: true
  • 刪除欄位 : remove_column :table_name, :created_at
  • rename欄位 : rename_column :table_name, :id, :udid
  • 增加索引 : add_index :table_name, :email, unique: true
  • 增加組合索引 : add_index :user_views, [:user_id, :article_id]
  • 執行SQL : execute "ALTER TABLE users modify COLUMN id int(8) AUTO_INCREMENT"

Example

change_column :videos, :source_website, :string, :limit => 50, :null => false
add_column :videos, :file_name, :string, :limit => 100, :null => false
add_index :videos, :file_name, :unique => true
add_index :videos, :source_website
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
add_index "users_roles", ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"

欄位參數

  • default: 'Hello'
  • limit: 30
  • null: false
  • first: true # position
  • after: :email # position

unsigned: true

當做 migration 時,這個屬性 rails 本身是不支援的,雖然執行 migrate 不會噴錯誤,但是你可以去 db/schema.rb 看,unsigned: true 是沒有被寫進去的

Rails Model

Model 與 Table 的 Naming Conventions

Model : 兩個單字以上不使用 _ 連接, 並且開頭字母大寫, ex: BookClub Table : 兩個單字以上使用 _ 連接, ex: book_clubs

Model / Class       Model 檔名          Table / Schema
--------------------------------------------------------
Article             article.rb          articles
LineItem            line_item.rb        line_items
Deer                deer.rb             deers
Mouse               mouse.rb            mice
Person              person.rb           people

外連鍵命名 : item_id, order_id

Console 下 Reload model

如果在 model 改的情況下, console 會一直是吃舊的 model 設定, 除非重新啟動 console, 或直接下 reload

> reload!
Reloading...
 => true

clone 一筆 record

a = User.find(103).dup              # 會自動把 id 為 nil
User.create(a.dup.attributes)
或
a = User.find(103)
User.create(a.attributes.except("id"))

CRUD

Read

User.find(3)
User.find(3, 4, 5)
User.first
User.first(5)                                               # 取前5個
User.last
User.all
User.count
User.limit(10)
User.where(name: "ash").order(status: desc).limit(10)       # complex
User.where('name LIKE ?', "%Bob%")                          # Like
User.order(:created_at)                                     # orderd by created_at
User.order(created_at: :asc)                                # orderd by created_at ASC
User.order("user_extension.company desc")                   # SQL
User.order("RAND()")                                        # random
User.order("age DESC, RAND()")                              # 先遞減再 random
User.column_names                                           # 取得欄位名稱
User.where("DAY(created_at) = ?", DateTime.now.day)         # 本日
User.where("MONTH(created_at) = ?", DateTime.now.month)     # 本月
User.where("YEAR(created_at) = ?", DateTime.now.year)       # 本年

Select 某個欄位挑出並 Alias

User.joins(:user_groups).select('user_groups.name AS group_name')

AND

where 預設就是 AND 了

你也可以用另一種寫法 :
User.where('`age` >= ? AND `age` <= ?', 10, 20)

OR

User.where('`grade` = ? OR `level` = ?', 80, 3)

注意! 使用 ` 括起來

IN

@users = User.where(name: [value1, value2]).all
> SELECT * FROM users WHERE name IN (value1, value2, and so on...);

LIKE

Product.where("name LIKE ?", "%#{@key_words}%")

無法 LIKE 的關鍵字 \ % _

是否有改變 attribute

person = Person.find_by_name('Uncle Bob')
person.changed?       # => false

person.name = 'Bob'
person.changed?       # => true
person.name_changed?  # => true
person.name_was       # => 'Uncle Bob'
person.name_change    # => ['Uncle Bob', 'Bob']

Create

先 new 再 save

u = User.new
u.name = 'Jex'
u.save

u = User.new(name: 'Jex')
u.save

Create

User.create(name: 'Jex', ages: '26')

沒有的話就新增

user.profile.where(user_id: user.id).first_or_create

Update

以下這幾種 update 方法會有不同的 validation 及 callback 行為, 請參考本文下 Validations & Callbacks 的整理圖表

先改再 save

user.name = 'Jex'
user.save

user.attributes = { name: 'Jex' }
user.save

Update

user.update('name', 'Jex')

update_attribute(name, value) 更新單一欄位

user.update_attribute('name', 'Jex')

update_attribute(attributes) 更新多個欄位

user.update_attributes(name: 'Jex')

update_column(name, value) 更新單一欄位

user.update_column('name', 'Jex')

update_columns(attributes) 更新多個欄位

user.update_columns(name: 'Jex')

+1 / +n 的簡潔寫法

a.increment!(:case_count)           # case_count += 1
a.increment!(:case_count, 3)        # case_count += 3

Delete

刪除單筆

User.find(3).destroy

刪除全部

User.destroy_all

! 與沒有的差別(i.e. create vs create!)

create 失敗時不會擲出 exception, 但 create! 會 (Raises a RecordInvalid error if validations fail)

create
create!
save
save!
update
update!

Validations & Callbacks

Validation examples

# 只要是數字就行
validates :price, numericality: true

# 特定範圍內的值
validates :price, inclusion: { in: 1..999999999, message: '必須 > 0' }
                               in: [true, false]
                               in: %w( male female )

# 除了特定範圍內的值
validates :price, exclusion: { in: 1..10 }

# 大於 等於 小於
validates :price, numericality: {
      greater_than: 0                                 # 大於
      less_than_or_equal_to: 100                      # 小於等於
      less_than_or_equal_to: :original_price          # 小於等於 "某個欄位的值"
      greater_than_or_equal_to: 100                   # 大於等於
    }

# 限制字串長度
validates :name, length: {minimum:2, maximum: 20}

# 必填
validates :name, presence: true

# Custom error message
validates :name, presence: { message: 'been eaten' }

# Regex
validates :name, format: { with: /.*/ }

# on: create
validates :email, format: { with: /\/i, on: :create }

# using a string:
validates :name, presence: true, if: 'name.present?'

# using a Proc:
validates :email, presence: true, if: Proc.new { |user| user.approved? }

# using a method:
validates :address, presence: true, if: :some_complex_condition
def some_complex_condition
  true
end

# using a lambda  假設你希望某個 enum 的值有存在才驗證這個,可以這樣寫
validates :discount, inclusion: { in: 0.00..0.99 }, if: lambda { self.enum_discount? }

當 validate 失敗時取得 Error Messages

在 controller 可以用以下截取錯誤訊息

@user.errors.full_messages.join(',')

Custom validation / Custom errors message

如果有個 enabled 欄位, 判斷是否為 0 或 1, 錯誤的話給錯誤訊息

validate :check_enabled

def check_enabled
  if ! enabled.to_i.between?(0,1)
    errors.add(:enabled, 'Error message')
    false       # 當錯誤發生時要記得一定要回傳 FALSE
  end
end

即使在 controller 也可以

 @user.errors.add(:email, "Not valid")
 @user.errors[:email] << "Not valid"

幾種 update 方法中不同的 validation 及 callback 行為

  • save
    • 行為同 update
  • update(attributes)
    • Validation and Callback 都會執行
    • updated_at 會更新
  • update_attributes(attributes)
    • Validation 不會執行
    • Callback 會執行
    • updated_at 會更新
  • update_attribute(name, value)
    • 行為同 update_attributes
  • update_columns(attributes)
    • 直接執行 SQL statement
    • Skip Validation and Callback
    • updated_at 也不會被 update
  • update_column(name, value)
    • 行為同 update_columns

Skip callback

attr_accessor :skip_callbacks
after_create :assign_role, unless: :skip_callbacks

user.skip_callbacks = true
user.save

Skip validation

user.save(validate: false)

針對特定情況才 validate

attr_accessor :need_validation
validates :real_name, presence: true, on: :update, if: :need_validation

當 update 需要 validate 時指定 need_validation: true 就會執行 validate 了

@user.update(real_name: 'Jex Lin', need_validation: true)

Custom field 驗證

有時候會有一些特殊需求, 例如需要一個欄位, 但這些欄位只在 model 的 before_create 前做處理才使用到, 不會存在在 DB 裡, 也因為會需要 validation, 所以放在 model

models/post.rb

attr_accessor :role
validates :role, presence: true, on: :create

限制只有在 create 才會做 validate 驗證, 否則 update 也會觸發到

controllers/posts_controller.rb

# 在 create 前的參數取得要允許 role
params.require(:post).permit(:title, :content, :role)

views/posts/new.html.erb

<%= f.input :role, input_html: { class: 'form-control' } %>

before* / after*

If any return false then everything stops.

before_validation
after_validation

before_save
before_save

before_create
after_create

before_update
after_update

before_destroy
after_destroy

強制存的 float 只能有一位小數

before_save do
  if self.price_changed?
    self.price = self.price.round(1)
  end
end

在每次儲存前檢查特定欄位是否有變動,有的話記 flag

before_save do |f|
  fields = [
    'name',
    'email',
  ]
  fields.each do |field|
    if f.send("#{field}_changed?")
      f.edit_flag = true
      break
    end
  end
end

before_destroy 刪除前檢查是否有其他關聯

category.rb :

has_many :products
before_destroy :check_for_products

def check_for_products
  if products.any?
    errors.add("無法刪除", "此類別下還有其他商品")
    false
  end
end

Associating models

through 用法

models/user.rb

    has_many :groups  througth => user_groups

User_groups Table 關聯 User.id 及 Group.id

User.groups 會藉由 through 取得 User 擁有的 Groups

官方 example

class Physician < ActiveRecord::Base
  has_many :appointments
  has_many :patients, through: :appointments
end

class Appointment < ActiveRecord::Base
  belongs_to :physician
  belongs_to :patient
end

class Patient < ActiveRecord::Base
  has_many :appointments
  has_many :physicians, through: :appointments
end

如果 User 與 Translator 的關係為 1 對 1, TranslatorLanguage 而是用 user_id 的話, 當 Translator 與 TranslatorLanguage 以 user_id 關聯寫法

Translator

has_many :translator_languages, primary_key: 'user_id', foreign_key: 'user_id'

TranslatorLanguage

belongs_to :translator, primary_key: 'user_id', foreign_key: 'user_id'

就可以直接用 Translator.user_id 與 TranslatorLanguage.user_id 關聯

Translator.first.translator_languages
TranslatorLanguage.first.translator

如果 Order.uuid 與 OrderItem.order_uuid 關聯

假設 OrderItem 沒有 order_id,關聯必須這樣寫

Order :

has_many :order_items, primary_key: 'uuid', foreign_key: 'order_uuid'

使用 Order.uuid 去關聯 OrderItem.order_uuid

OrderItem :

belongs_to :order, primary_key: 'uuid', foreign_key: 'order_uuid'

關聯回去

primary_key: 是指自已的 key; foreign_key: 是指要關聯到那個表的 key

ThirdCategory.id 與 Product.category_id 關聯

Product

belongs_to :third_category, foreign_key: 'category_id'

ThirdCategory

has_many :products, foreign_key: 'category_id'

可以雙向關聯

Product.first.third_category
ThirdCategory.first.products

FirstCategory 透過 SecondCategory 關聯 ThridCategory

FirstCategory

has_many :second_categories
has_many :third_categories, through: :second_categories

SecondCategory

belongs_to :first_category
has_many :third_categories

ThridCategory

belongs_to :second_category

includes with where

Article.includes(:comments).where(comments: { visible: true })
User.includes(:posts).references(:posts).where('posts.author = ?', 'Jex')
User.includes(:posts).references(:posts).where('posts.id IS NOT NULL')

class_name : has_many 不一定要跟 class name 一樣

has_many :profile
等同於
has_many :info, class_name: 'Profile'

Join + where

User.all.joins(:user_languages).where(user_languages: { from: 1, to: 2 } )

或
User.all.joins(:user_languages).where('`from` = ? AND `to` = ?', 1, 2)

LEFT JOIN

joins('LEFT JOIN categories AS b ON (a.id = b.parent_id)')

Alias table name

Category.from('categories AS a').where('a.parent_id = ?', 0)

將結果的 AssociationRelation 集合截取特定欄位

顯示 DB 真實的值

Book.pluck(:type)
    > [1, 2, 3]

Book.pluck(:type, :name)

如果有 Enum 會自動被轉換, 兩者效果相同 :

Book.all.map(&:name)
    或
Book.collect(&:type)
    > [tech, food, sport]

scope

Scope (基本用法)

scope :sold, -> { where(state: 'sold') }                        # 一般用法
scope :teenager, -> { where("age >= 13 AND age <= 19") }        # 下 where SQL
scope :recent, ->{ where(published_at: 2.weeks.ago) }           # 使用 proc object 來避免 date 固定的問題
scope :recent, -> { order("created_at desc").limit(3) }         # Order 完再 Limit
scope :recent_red, ->{ recent.where(color: 'red') }             # 處理完再 where
scope :unpublished, -> {self.published}                         # 使用其他 scope

可傳入參數

scope :starts_with, -> (name) { where("name like ?", "#{name}%")}
@products.starts_with(params[:starts_with])

Default scope

default_scope { is_admin }                                      # 也可以是 enum 的值
default_scope {where(deleted_at: nil)}

# 使用
Post.all                # Fires "SELECT * FROM posts WHERE deleted_at IS NULL"
Post.unscoped.all       # Fires "SELECT * FROM posts"

Join scope

scope :feature_products, -> { joins(:products).merge(Product.features) }

Join 完要注意的是要記得 includes
@c = Category.feature_product
@c.products  會變成 category 重新撈底下的 products
應該改成
@c = Category.feature_product.includes(:products)
就會撈 join 裡面的 products 了

Includes scope

# 傳入 where 的值 `Person.parent_last_name('John Smith')`
scope :parent_last_name, ->(name) { includes(:parents).where(last_name: name) }

Includes nested relationships

Category 可以直接撈 product, 但 product_specs 不行,product_specs 只有跟 product 關聯, 所以用下面的方式 includes products 及 product_specs

Category.first.includes(products: [:product_specs])

joins 與 includes 的差別

joins

a = User.joins(:translator)
 => SELECT "users".* FROM "users" INNER JOIN "translators" ON "translators"."user_id" = "users"."id"

a[1].translator
  Translator Load (0.3ms) SELECT ...(略)

看這個 SQL 結果知道當兩邊 TABLE 的都有資料的 user (user_id 一樣) 才放入集合, 所以它是沒有關聯到 translator 的, 如果 each print translator 的資料的話會有 N+1 query 的問題

includes

a = User.includes(:translator)
  User Load (1.3ms)  SELECT "users".* FROM "users"
SELECT "users".* FROM "users"
  Translator Load (2.3ms)  SELECT "translators".* FROM "translators" WHERE "translators"."user_id" IN (1, 2,  ...(略)... )

這邊會一次將需要的 translator 資料都取出來, 但如果某個 user 沒有 translator 他仍然會在集合裡, 只是要取 translator 時是 nil

a[1].translator

就會直接從 cache 取, 不會再下 query 了

先 joins 後 includes 結果不符合預期問題

假如我們要先取出 group_id = 3 的資料, 然候再用 includes 避免 each 有 n+1 query

users = User.with_role(:user)
users = users.joins(:user_groups).where(user_groups: { group_id: 3 })
users = users.includes(:user_groups)

但會發現最後 user_groups 只會剩 group_id = 3 那筆, 最後一行不使用 includes 改成 preload, 即可避免

users = users.preload(:user_groups)

eager_load vs includes vs preload

TODO …

Enum

models/users.rb

enum gender: {female: 0, male: 1}
# 也等於 : enum gender: [:female, :male]

撈出來就能直接取得對應的值

顯示 enum 對應表 (也就是 model 裡設定的 enum)

User.genders
 => {"female"=>0, "male"=>1}

直接取得對應後的結果

u = User.find(1)
u.gender
 => "female"

如果指定 gender 一個不存在在 enum 的值會引發錯誤, ex: u.gender = 131

更可以直接取得值

u.male?
 => true

where 使用 enum 對應

User.where(gender: User.genders[:male])

那麼 view 該如何呈現呢?

# simple_form 版
<%= f.input :gender, as: :radio_buttons, collection: User.genders.keys %>

某些時候需要真實的值而不是 Enum 轉義過的值

User.find(4).read_attribute('status')

Enum + i18n + Radio button

models/user.rb

enum gender: {female: 0, male: 1}
def self.genders_i18n(hash = {})
   genders.keys.each { |key| hash[I18n.t("genders.#{key}")] = key }
   hash
end

locales/zh-TW.yml

zh-TW:
  genders:
    female: '女'
    male: '男'

到 Rails consle 結果是否正確

I18n.locale = 'zh-TW'
User.genders_i18n
 => {"女"=>"female", "男"=>"male"}

view : 顯示 radio button (with simple_form)

<%= f.input :gender, as: :radio_buttons, collection: User.genders_i18n %>

view : 顯示 gender + i18n

Gender: <%= t('genders.' + current_user.gender) %>

Enum 同值問題, 用 prefix 解決

Enum 預設不支援同值, 如果有兩個不同 column 但值相同, 會引發錯誤

ex :

hash = { private: 0, public: 1 }

enum post: hash
enum name: hash

因為 enum 可以這樣用, 如果有兩個一樣值的 enum, 就會產生問題, 不能明確指的是哪一個 column

a.post = 1
a.public?
 => true

解決方法 : 加上 prefix

己經有人發現這個問題也己經修復了, 使用 enum_prefix 參數即可解決,

但目前 Rails 4.2.3 版還沒有, 只能等下一版發佈才能使用,

在那之前只能先用其他方式達成 :

status = { private: 0, public: 1 }

enum post: Hash[status.map{|k,v| ["post_#{k}",v]}]
enum name: Hash[status.map{|k,v| ["name_#{k}",v]}]

concerns - model 之間共用 method

controllers/concerns 與 models/concerns 是共通的

/models/concerns/example.rb

module Example
  def self.location
    {
        /* something else */
    }
  end
end

/models/user.rb

enum location: Example.location

transaction

確保在 transaction block 裡的程式都成功,最後才會寫進資料庫

最經典的例子 - A 匯款給 B

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

Different ActiveRecord classes in a single transaction

雖然 transaction 是被 ActiveRecord Calss 呼叫的,但 block 裡面不一定都要是那個 Class 的 instance

你可以混著不同的 model 在同一個 transaction block, This is because transactions are per-database connection, not per-model.

Client.transaction do       # 或 @client.transaction do
  @client.users.create!
  @user.clients.first.destroy!
  Product.first.destroy!
end

注意!! 要使用 The bang modifier (!) - save! 而不是 save, 原因是 transaction reset 是經由 rollback 觸發後才重置的, 然而 rollback 只會被 Exception 觸發,所以當你執行如 update_attribute 等等的 method 時,當它失敗它會擲出 false, 但它並不會觸發 rollback,所以一定要加上 !,讓它發生錯誤時擲出 Exception

基於上述的觀念也可以得出另個結論 - 如果有 exception 表示有 rollback,沒有的話代表 transaction 是成功的,可以進一步針對失敗時要做的處理

def test
  ActiveRecord::Base.transaction do
    # do something ...
  end

  rescue => e
    render json: {updated: false}
end

Transactions are not distributed across database connections

一個 transaction 只會作用在單一的 database connection. 如果你有多個指定用途的 databases,那麼你應該使用 nested transaction 來解決

Client.transaction do
  Product.transaction do
    product.buy(@quantity)
    client.update_attributes!(:sales_count => @sales_count + 1)
  end
end

一般情況都是規劃一個 database 運行網站,這樣則不需要用到 nested transaction

Lock row - .lock!

這個 example 先將某筆 lock 鎖起來,並且 sleep 後十秒,在這期間內去 update 它觀察它的變化

為了方便直接用 Rails console 測 (要開兩個 console 才能測)

第一個 console

Case.transaction do; @case = Case.find(112); @case.lock!; sleep(10); end

# Case.transaction do
#   @case = Case.find(112)
#   @case.lock!
#   sleep(10)
# end

第二個 console

Case.find(112).update(message: 'VVV')

結果你會看到 : 一筆資料被 lock 了十秒鐘, 如果其他地方 update 這筆資料的話會等在那邊, 直到解除 (如果是更新同個 TABLE 不同筆資料則不會被 lock)

Lock row - .with_lock

作用與 lock! 一樣,不同的是它是個 block :

account = Account.first
account.with_lock do
  # This block is called within a transaction,
  # account is already locked.
  account.balance -= 100
  account.save!
end

ref : http://markdaggett.com/blog/2011/12/01/transactions-in-rails/

Lock row - .lock.find(id)

user = User.lock.find(1)
# do something
user.save!

Permalink

Permalink 原生 to_param

讓你的 URL 更 Friendly (i.e. /article/environment-protection), 使用原生就有支援

def to_param
  "#{id} #{name}".parameterize
end

id, 與 name 是欄位名稱, 當 url 產生如 : 3-peter

但中文是不行的, 需要用其他套件處理 i.e. Babosa

Permalink - friendly_id

它能將原本的 auto increament 改成使用中文或英文(i.e. environment-protection-is-important) 這種標題, 功能較強大

Install
gem 'friendly_id', '~> 5.1.0'
[1] Migrate
add_column :products, :slug, :string
add_index :products, :slug, unique: true
[2] 在 Product Model 加上
extend FriendlyID
friendly_id :name, use: :slugged

當建立資料後,欄位 slug 的值就會是 product 的 name

但原本已經存在的資料沒有 slug 值,我們也要更新它 :

Product.find_each(&:save)
[3] 設定 friendly.find 自動/手動 2選1

1) 自動 - 讓 find 自動找到 friendly_id

目前你應該可以看到 product 的 path 都是產品名稱了,但你可能會發現連結都是壞的,因為 find 還沒有自動對應,我們要來設定它

rails generate friendly_id --skip-migration     # 產生 config/initializers/friendly_id.rb

config/initializers/friendly_id.rb

config.use :finders                             # 將原本註解取消

如果你用 .find(params[:id]) 它就會自動用 params[:id] 去找 :slug 欄位

2) 手動 - 你不想設定自動對應你也可以每次都寫

Product.friendly.find(params[:id])
[4] 完成

改善 URL 不要顯示 ID Auto-Increament

有時我們不希望我們的 id 曝露在外,如何將 order/3 改為 order/20160415-ORDER-B9DC88E23A4CE439C13A

1) Migration - 首先我們先加上 uuid 欄位, 把它當作另一個主 Key

t.string :uuid, limit: '100'

add_index :orders, [:uuid], unique: true

2) Model - create 前亂數產生 uuid

before_create :generate_uuid

def generate_uuid
    self.uuid = "#{Time.now.strftime('%Y%m%d')}-ORDER-#{SecureRandom.hex(10).upcase}"
end

3) Model - 設定 param, 讓 path 顯示 uuid (讓 uuid 代替 id)

def to_param
  self.uuid
end

4) View - 不需要特別改,就會顯示 uuid 而不是 id

edit_product_path(p)

4) Controller - find by uuid

@order = current_user.orders.find_by_uuid(params[:id])

Paranoia 軟刪除 soft-deleted

軟刪除意思是有些資料需要刪除不顯示在頁面上,但又想保留資料,因為不知道未來哪天會需要到;一般作法是透過 deleted_at 欄位來判斷是否已刪除

雖然實作軟刪除很簡單,但如果很多 model 都要寫的話,就會挺煩人的,這是一個軟刪除的套件,幫你這些重覆的事情做掉

Gemfile 安裝

gem "paranoia", "~> 2.1.5"

安裝完要記得重啟 rails

對想要軟體除的 Model 及 Table 建立必備資料

migration

class AddDeletedAtToClients < ActiveRecord::Migration
  def change
    add_column :clients, :deleted_at, :datetime
    add_index :clients, :deleted_at
  end
end

model

class Client < ActiveRecord::Base
  acts_as_paranoid

end

基本操作

軟刪除

Client.find(5).destroy
client.destroy

軟刪除後,Client.allClient.find(5) 都會撈不到,必須用 Client.only_deleted 才剛的到

恢復被軟刪除的資料

Client.restore(5)
Client.only_deleted.find(5).restore

顯示所有資料(包含已刪除的資料)

Client.with_deleted

顯示被刪除的資料

Client.only_deleted

真實刪除(要注意, 是真的刪掉!)

client.really_destroy!

其他

i18n

zh-TW:
  activerecord:
    attributes:
      user:
        name: '姓名'

姓名 必填

model 裡使用 asset helper

# dev production 都 work
default_url: ->(attachment) { ActionController::Base.helpers.image_path('icons/avatar.png') },

# 後來發現在 dev work, 但 production 不 work
default_url: ActionController::Base.helpers.image_path('default/avatar-150x150.png')

顯示 SQL

User.where(:id => 3).to_sql

Rails 上傳 Upload

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

介紹

  • paperclip, 上傳檔案或處理圖片, 對 avatar 可以處理的很乾淨

Paperclip

安裝 paperclip

gem "paperclip", "~> 4.3"

安裝 imagemagick

Mac

brew install imagemagick

Ubuntu

sudo apt-get install imagemagick -y
設定 ImageMagick utilities 的指令路徑

可用以下方法確認你的 convert 指令找的到, 有的話可以先不用設定

$ which convert
/usr/local/bin/convert

如果不能正常運作, 再設定 config/environments/development.rb :

Paperclip.options[:command_path] = "/usr/local/bin/"

上傳 avatar 圖片

1) models/user.rb
has_attached_file :avatar,
  styles: { medium: "300x300>", thumb: "100x100>" },
  # default_url: "/images/:style/missing.png",
  default_url: ->(attachment) { ActionController::Base.helpers.image_path('icons/avatar.png') },      # 在 production 才會正確顯示出來
  url: "/:class/:attachment/:id/:style.:extension",
  path: ":rails_root/public:url"
validates_attachment :avatar, presence: true,
  content_type: { content_type: /\Aimage\/.*\Z/ },                          # 或特定類型 content_type: "image/jpeg" or content_type: ['image/jpeg', 'image/png', 'image/gif']
  size: { in: 0..20.megabyte }                                             # 或 KB { in: 0..20.kilobytes }
  • validates_attachment_content_type :avatar, :content_type => /\Aimage\/.*\Z/
  • 儲存路徑是 : /public/system/users/avatar/000/000/001/original/xxxx.jpg
  • 要注意 nginx 的上傳檔案大小 client_max_body_size 50M;
2) 新增 avatar 所需的 DB 欄位
rails generate paperclip user avatar

產生 db/migrate/20150713155732_add_attachment_avatar_to_users.rb, 內容為

class AddAttachmentAvatarToUsers < ActiveRecord::Migration
  def self.up
    change_table :users do |t|
      t.attachment :avatar
    end
  end

  def self.down
    remove_attachment :users, :avatar
  end
end

執行 rake db:migrate

它會在你的 users 加上這些欄位

t.string   "avatar_file_name"
t.string   "avatar_content_type"
t.integer  "avatar_file_size"
t.datetime "avatar_updated_at"

如果只是要增加欄位

add_attachment :users, :avatar
3) 加上 avatar 相關程式

views/dashboard/welcome/index.html.erb

<%= form_for @user, :url => users_path, :html => { :multipart => true } do |form| %>
  <%= form.file_field :avatar %>
<% end %>

或 simple_form 版

<%= simple_form_for @user, url: dashboard_welcome_update_avatar_path do |form| %>
  <%= form.input :avatar, as: :file %>
  <%= form.submit '上傳' %>
<% end %>

顯示原圖, medium and thumb 大小圖片

<%= image_tag @user.avatar.url %>
<%= image_tag @user.avatar.url(:medium) %>
<%= image_tag @user.avatar.url(:thumb) %>

controllers/dashboard/welcome_controller.rb

class Dashboard::WelcomeController < ApplicationController
  def index
    @user = current_user
  end

  def update_avatar
    if User.update(current_user, avatar_params)
      redirect_to action: :index
    end
  end

  private

  def avatar_params
    params.require(:user).permit(:avatar)
  end
end

config/routes.rb

namespace :dashboard do
  root 'welcome#index'
  patch 'welcome/update_avatar'
end

參數說明

  • :url : host 後面那段 url 路徑,也是檔案相對路徑
  • :path : 檔案儲存位置的完整路徑
  • :default_url : 如果找不到圖片時的預設圖
  • :styles : A hash of thumbnail styles with geometries. If you need copies of uploaded files with particular dimensions then specify them here.

hash

has_attached_file :avatar, {
    :url => "/system/:hash.:extension",
    :hash_secret => "longSecretString"
}

image

has_attached_file :avatar, :styles => {:thumb => 'x100', :croppable => '600x600>', :big => '1000x1000>'}
has_attached_file :cover, :styles => {:small => 'x100', :large => '1000x1000>'}
has_attached_file :sample, :styles => {:thumb => 'x100'}

Dynamic Processor

has_attached_file :avatar, :styles => lambda { |attachment| { :thumb => (attachment.instance.boss? ? "300x300>" : "100x100>") } }

has_attached_file :avatar, :processors => lambda { |instance| instance.processors }
attr_accessor :processors

不檢查檔案格式一定要加上

do_not_validate_attachment_file_type :client_uploading

尺寸的符號

:styles => { :medium => "300x300>", :thumb => "100x100>" }

  • > : 等比例, 將圖片的大小縮到小於這尺寸, 較常用
  • < : 等比例, 將圖片的大小縮到大於這尺寸
  • # : 等比例, 設定的最長邊與圖片的最長邊相接, 裁切多餘部分, 一般用於縮圖或頭像
  • ! : 非等比, 強制圖片長寬和該尺寸一樣大
  • ^ : 等比例, 圖片的大小最小要那麼大
  • : 等比例, 圖片的大小最大要那麼大

儲存路徑參數

  • :style : original
  • :basename : 檔名
  • :id : TABLE primary key
  • :id_partition :
  • :fingerprint
  • :attachment : 欄位名稱 (複數), ex: avatars
  • :extension : .jpeg
  • :class : model name, ex: users

不同大小的 avatar

url: "/:class/:id/avatar/:style.:extension",
path: ":rails_root/public:url",

/users/4/avatars/original.jpeg?1436890118
/users/4/avatars/medium.jpeg?1436890118
/users/4/avatars/thumb.jpeg?1436890118

檔案不公開, 在根目錄建立 private (與 public 同層)

url: "/:class/:id/:basename.:extension",
path: ":rails_root/private:url"

/users/3/profile.png

Custom path

url: "/:class/:uuid/avatar/:style.:extension"

Paperclip.interpolates :uuid do |attachment, style|
  attachment.instance.uuid              # uuid 是欄位名稱
end

Default url

預設

default_url: "/images/:style/missing.png",

如果要使用 assets/images 下的圖片,必須這樣設置,在 production 才會正確顯示出來

default_url: ->(attachment) { ActionController::Base.helpers.image_path('icons/avatar.png') },

其他

刪除檔案

u.avatar = nil
u.save(validate: false)

Private 檔案下載

if @case && @case.original_file && File.exist?(@case.original_file.path)
    send_file @case.original_file.path
end

判斷是否已上傳檔案

@users.avatar.exists?

必須上傳圖片

validates :photo, presence: true

Paperclip + Crop

簡易 crop

加上 convert_options

has_attached_file :avatar,
styles: { medium: "300x300>", thumb: "100x100>" },
url: "/:class/:id/avatar/:style.:extension",
path: ":rails_root/public:url",
convert_options: {
  #the gravity parameter takes "Center" and directions like "northeast, nort, west"
  :thumb => "-gravity northwest -crop 100x100+0+0 +repage", #crop to 100x100 starting at upper left corner (northwest)
  :medium => "-gravity northwest -crop 101x100+100+0 +repage", #crop to 100x100 starting 100 pixels to the right of the upper left corner
}

使用 papercrop

Gemfile

gem 'papercrop', '~> 0.3.0'

application.js

//= require jquery.jcrop
//= require papercrop

application.scss

*= require jquery.jcrop

controller

def upload_avatar
  current_user.update(avatar_params)
  redirect_to action: :edit
end

def crop_avatar
  current_user.update(crop_params)
  redirect_to action: :edit
end

def avatar_params
  params[:user].permit(:avatar)
end

def crop_params
  params[:user].permit(:avatar_original_w, :avatar_original_h, :avatar_aspect, :avatar_box_w, :avatar_crop_x, :avatar_crop_y, :avatar_crop_w, :avatar_crop_h)
end

model

has_attached_file :avatar,
  styles: { medium: "300x300>", thumb: "100x100>" },
  url: "/:class/:id/avatar/:style.:extension",
  path: ":rails_root/public:url",
  processors: [:papercrop]              # 加上它
validates_attachment :avatar, content_type: { content_type: ['image/jpeg', 'image/png', 'image/gif'] }, size: { in: 0..10.megabyte }
crop_attached_file :avatar              # 加上它

view

上傳的 view
<%= form_for @user, url: upload_avatar_info_user_path(@user), method: :PATCH do |f| %>
  <%= image_tag @user.avatar.url %>
  <%= image_tag @user.avatar.url(:thumb) %>
  <%= image_tag @user.avatar.url(:medium) %>
  <%= f.file_field :avatar, as: :file %>
  <%= f.submit 'Save' %>
<% end %>

crop 的 view
<%= form_for @user, url: crop_avatar_info_user_path(@user), method: :PATCH do |f| %>
  <%= f.cropbox :avatar, width: 500 %>
  <%= f.crop_preview :avatar, width: 100 %>
  <%= f.submit 'Save' %>
<% end %>

crop 不會修改 original 的圖片只會改 thumb 及 medium 的圖

Paperclip 上傳到 S3

安裝 & 設定

Install aws-sdk

gem "paperclip", "~> 5.0.0.beta2"           # 注意! 5 版以上才支援 aws-sdk 2 版
gem 'aws-sdk', '~> 2.2.37'

1) 先到 IAM 建立一個有 S3 權限的 User, 並記下 Access Key ID Secret Access Key

2) 到 S3 -> Create Bucket -> 設定 Policy, 設定後簡單使用 command 上傳測試是否 work, 請參考此篇 - AWS-SDK / AWS-CLI 上傳及下載

3) 設定給 paperclip 讀取 s3 的 config。可以設定在 config/application.rb

config.paperclip_defaults = {
  storage: :s3,
  s3_region: 'ap-northeast-1',
  s3_credentials: {
    bucket: 'my-bucket',
    access_key_id: 'Access Key ID',
    secret_access_key: 'Secret Access Key',
  }
}

4) Model 的 Paperclip 設定

s3_host_name: "s3-ap-northeast-1.amazonaws.com",
url: "/:class/:attachment/:id/:style.:extension",
path: ":url"    # path 跟 url 一樣就好了

5) 輸出 @product.photo.url(:thumb) 它就會自動組出正確的 url 了

http://s3-ap-northeast-1.amazonaws.com/my-bucket/products/photos/1/thumb.jpeg?1461739010

6) 完成

區分 Development 使用本機空間, Production 使用 S3

1) 加上環境判斷, config/application.rb

if Rails.env.production?
  config.paperclip_defaults = {
    ...
  }
end

2) (選項)區分 path

# 如果是 development 上傳到網站根目錄的 /public
path: (Rails.env.production?) ? ":url" : ":rails_root/public:url"

# s3_host_name 不需處理它, 即使 development 存在這個參數也無妨
s3_host_name: "s3-ap-northeast-1.amazonaws.com",

3) 完成! 記得要重啟動 Rails

Rails Oauth

FB Oauth

如果使用 Devise,請不要用下列的方法,請到這篇搜尋 oauth-facebook

申請 FB APP

1) Developers

2) Add a New App 選擇 Website (網站)

3) 建立 App -> 輸入一個 Name -> 按步驟 -> Create a New App ID

4) 建立成功, 下面有個欄位輸入 Domain : http://test.jex.tw:3000/

如果沒有 domain 可以暫時用 ngrok,但注意!FB 登入時要用 http://e2191881.ngrok.io/ 而不是 http://127.0.0.1:3000/

5) 右上角大頭像的 My Apps 選剛剛建立的 App

6) 將 App ID 及 App Secret Copy 貼到安裝完 omniauth 的設定檔 config/initializers/omniauth.rb

如果日後要換 domain 一樣在 App Dashboard 頁面, 在 App ID 下面那塊, 選擇 Choose a Platform -> WWW -> 就可以改 domain 了

Install

Gemfile

gem 'omniauth', '~> 1.3.1'
gem 'omniauth-facebook', '~> 3.0.0'

安裝完重啟動 rails

新增 User model

rails g model User provider uid name oauth_token oauth_expires_at:datetime

或直接建立

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :provider
      t.string :uid
      t.string :name
      t.string :oauth_token
      t.datetime :oauth_expires_at

      t.timestamps null: false
    end
  end
end

app/models/user.rb

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.name = auth.info.name
    user.oauth_token = auth.credentials.token
    user.oauth_expires_at = Time.at(auth.credentials.expires_at)
    user.save!
  end
end

寫入後資料如下

:id => 1,
:provider => "facebook",
:uid => "860********7924",
:name => "Jex Lin",
:oauth_token => "CAAF5japY********************n7hjii",
:oauth_expires_at => Sat, 10 Oct 2015 14:26:09 UTC +00:00,

新增 Sessions Controller

app/controllers/application_controller.rb

private
def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(env["omniauth.auth"])
    session[:user_id] = user.id
    redirect_to root_url
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url
  end
end

新增 routes

config/routes

match 'auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
match 'auth/failure', to: redirect('/'), via: [:get, :post]
match 'signout', to: 'sessions#destroy', as: 'signout', via: [:get, :post]

手動建立 Omniauth initializer

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, '41512******2503', '1791f7944e*********7ebaa65989414'
end

登入 / 登出

app/views/welcome/index.html.erb

<div id="user-widget">
  <% if current_user %>
    <%= link_to "Sign out", signout_path, id: "sign_out" %>
  <% else %>
    <%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %>
  <% end %>
</div>

(選項, 可不加) JS :

$(document).ready(function () {

    $('body').prepend('<div id="fb-root"></div>')

      $.ajax
        url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js"
        dataType: 'script'
        cache: true


    window.fbAsyncInit = ->
      FB.init(appId: 'YOUR-APP-ID', cookie: true)

      $('#sign_in').click (e) ->
        e.preventDefault()
        FB.login (response) ->
          window.location = '/auth/facebook/callback' if response.authResponse

      $('#sign_out').click (e) ->
        FB.getLoginStatus (response) ->
          FB.logout() if response.authResponse
        true
});

ref : https://coderwall.com/p/bsfitw/ruby-on-rails-4-authentication-with-facebook-and-omniauth

Rails I18n

介紹

當網站會需要多國語言顯示的話, 就會用到 i18n

使用 UI 介面方便翻譯人員協助翻譯

基本用法

config/locales/en.yml :

en:
  posts: 'Posts'

config/locales/zh-TW.yml :

'zh-TW':
  posts: '文章'

controllers/welcome_controller.rb :

before_action :set_locale
def set_locale
  if params[:locale] && I18n.available_locales.include?(params[:locale].to_sym)
    session[:locale] = params[:locale]
  end

  I18n.locale = session[:locale] || I18n.default_locale
  @current_lang = session[:locale].nil? ? 'zh-TW' : session[:locale]
end

view :

<%= link_to "繁體中文", locale: "zh-TW" %>
<%= link_to "English", locale: "en" %>
<p><%= t('posts') %></p>

對應 DB 欄位

activerecord:
  attributes:
    user:
      real_name: '真實姓名'
      nickname: '暱稱'

當 validate 失敗後的 errors.full_messages 就會把它的 i18n 帶上了

Lazy Lookup

app/views/books/index.html.erb

<%= t '.title' %>

config/locales/en.yml

es:
  books:
    index:
      title: "Título"

支援子目錄

假如語系分成子目錄, ex: locales/zh-TW/welcome.yml

config/application.rb

class Application < Rails::Application
    config.i18n.load_path += Dir["#{Rails.root.to_s}/config/locales/**/*.{rb,yml}"]
end

tolk

Install

gem 'will_paginate'
group :development do
    gem 'tolk'
end

1) Init

$ rake tolk:setup

    ?  Do you want to install the optional configuration file (to change mappings, locales dump location etc..) ? Press <enter> for [Y] >
        create  config/initializers/tolk.rb
        create  db/migrate/20150712075442_create_tolk_tables.rb
    ?  Where do you want to mount tolk? Press <enter> for [tolk] >
        gsub  config/routes.rb
        gsub  config/routes.rb
        route  mount Tolk::Engine => '/tolk', :as => 'tolk'
    (自動執行 db:migrate)
    (讀取 zh-TW.yml)
(選) 限制 tolk 只能用在 dev 環境, routes.rb
if Rails.env.development?
    mount Tolk::Engine => '/tolk', :as => 'tolk'
end
(選) 將 zh-TW 為主要的語言(取決開發上方便使用的語言)

config/initializers/tolk.rb (貼在 Tolk.config do |config| 外面)

Tolk::Locale.primary_locale_name = 'zh-TW'

2) 重新啟動 Rails, 到 http://127.0.0.1:3000/tolk

看是否可以正常看到 tolk 頁面

更新翻譯

它會將主要語言的 yml 檔為主做更新(例如我設 zh-TW 我主要), 如果 key 不存在了, tolk 的資料庫也會清除

rake tolk:sync

再到 /tolk 頁面, 如果沒有看到顯示未翻譯的數量 (蠻常發生這個狀況), 就重新啟動 Rails

翻譯完成後按 save change, 最後把 yml 檔匯出

rake tolk:dump_all

它會產生所有非 tolk 預設語言的 yml 檔, 匯出在 config/locales

後記

如果 zh-TW 是子目錄裡面檔案依 controller name 分 i18n, 在 dump 後的 en 會是將所有 key 合在 en.yml 的

建議

因為 tolk dump 出來的東西會包含其他 gem 的 i18n, 並且以英文字母作為排序,

假如我是以 zh-TW 做主要開發的語言, 那麼就不要 dump zh-TW,

以免這份檔被加入其他 gem 的 i18n 給搞髒了

(其他, 不常用) dump 指定語言的 yaml

rake tolk:dump_yaml["zh-TW"]

(其他, 不常用) 重新 import

rake tolk:import

lit

介紹

當翻譯需要別人參與且需要讓他可以使用 Web UI 協助翻譯, 可以使用這個套件

Get started

1) Gemfile :
gem 'lit'

Init :

rails g lit:install
What's the authentication function, ie. :authenticate_user! : :authenticate_user!           # 使用 devise 做驗證
What's the key value engine? ([hash] OR redis): hash

for production/staging environment redis is suggested as key value engine. hash will not work in multi process environment

2) 接著它會自動做以下這些事
  • 產生 lit 所需的 migrate 檔並執行 db:migrate
  • routes 會自動地被加上 lit 的 path
  • create config/initializers/lit.rb
3) 讓 lit 知道哪些語言需要翻譯

config/application.rb 加上 :

config.i18n.available_locales = [:"zh-TW", :en]

之後如果要增/減支援的語言也是改這個地方, 如果減一個語言, 頁面上沒有更新, 就把整個 db 環境重 build 再重新 rails

3) 重啟 Rails, 再回到首頁

在 Rails startup 時會預先載入 config/locale/*.yml 裡的 Keys

接著點擊到的頁面上如果有使用到 I18n.t() 的地方 (例如 devise 的 users/sign_in), 也會建立 key / value 到 lit 的資料庫裡

4) 到 http://127.0.0.1:3000/lit/, 看是否頁面正常顯示

因為先前使用 :authenticate_user! 驗證是否登入, 沒登入會被導到 users/sign_in

登入後裡面只會看到 enzh-TW, 兩種語言

5) 測試

1) 新增 comments i18n key

2) 到 view 顯示, 它會自動進到 lit db

3) 在 lit 後台就會看到了, 翻譯完後, 雖然頁面正常顯示, 但 zh-TW.yml 還是一樣沒變

因為 lit 只負責幫你把翻譯的字串寫到它的資料表中

4) 匯出在 lit 後台翻譯好的結果:LOCALES=en,zh-TW rake lit:export

$ LOCALES=en,zh-TW rake lit:export

I, [2015-07-12T14:19:13.790566 #14791]  INFO -- : initializing Lit
Successfully exported config/locales/lit.yml.

Rails - 分頁 Pagination

介紹

will_paginate 如何使用

controller : posts/index

@posts = Post.includes(:user)
改成
@posts = Post.includes(:user).paginate(page: params[:page], per_page: 5)

view : posts/index

<% @posts.each do |post| %>
  <%= post.title %>
<% end %>
<%= will_paginate @posts %>

will_paginate not working

如果傳入的是 ActiveRecord_Associations_CollectionProxy 而不是 ActiveRecord_Relation 可能會造成此狀況,在 controller 要額外引入

require 'will_paginate/array'

bootstrap 樣式

view :

<%= will_paginate @posts, :renderer => PaginationLinkRenderer %>

config/initializers/pagination_link_renderer.rb :

require 'will_paginate/view_helpers/link_renderer'
require 'will_paginate/view_helpers/action_view'

class PaginationLinkRenderer < ::WillPaginate::ActionView::LinkRenderer
  protected
  def page_number(page)
    unless page == current_page
      tag(:li, link(page, page, :rel => rel_value(page)))
    else
      tag(:li, link(page, '#', :rel => rel_value(page)), :class => "active disabled")
    end
  end

  def gap
    tag(:li, link('...' , '#') , :class => "disabled")
  end

  def previous_or_next_page(page, text, classname)
    tag(:li, link(text , page || '#'), :class => page ? classname : classname + ' disabled')
  end

  def html_container(html)
    tag(:div , tag(:ul, html , container_attributes) , :class => 'pagination_label col-xs-12 center')
  end
end

小技巧 - 簡化 view 的分頁

在 controller 多指定變數給 @paginate

@users = @paginate = User.all.paginate(:page => params[:page] , :per_page => 15)

不必擔心 @users@paginate 佔兩份記憶體, 它們都指向同一個 object_id

view/layout

<%= raw(will_paginate(@paginate ,:renderer => PaginationLinkRenderer)) if @paginate %>

在 layout 放這段, 讓 renderer 覆寫掉原生的 gem (寫在 initializers 裡, 可參考本篇 bootstrap 那邊寫法), 就不用每一個 view 都要寫 will_paginate

之後只要在需要分頁的 view 直接 <%= @paginate %> 就好

其他

取得總數

@products.total_entries

Rails Seeds 建立測試資料

介紹

Fabrication

一個 model 對應一個 fabrication

首先先建立模型

Fabricator(:person) do
  age { rand(20..45) }
end

一定要用 { } 包起來,因為有遇過一些奇怪的 bug, 而 seed 不成功

Insert 資料

Fabricate(:person)
Fabricate(:person, email: 'xxx@ff.com')     # 指定欄位

Faker 使用上非常簡單, 直接看 Example

建立 4 個 users, 並且每名 user 指定一個 role, 隨機 user 建立共 50 篇文章, 隨機 user 對隨機挑的 50 篇文章回覆共 200 筆留言

user = User.create({email: "user@gmail.com", password: "00000000", confirmed_at: Time.now})

Fabricator(:post) do
  title { Faker::Lorem.sentence(3, true, 4) }
  content { Faker::Lorem.paragraph(4, true, 7) }
end

50.times { Fabricate(:post, user_id: user.id) }

Fabricator 的 Table name 要用單數

Seed file with paperclip

Fabricator(:product) do
  photo { File.new("app/assets/images/logo.png") }

  或傳入一個 url
  photo { open("http://lorempixel.com/300/300/") }
end

建議可以區分 Seed 的環境, 避免 Seed 到 Production

case Rails.env
when "development"
   ...
when "production"
   ...
end

Locale

Faker 在不同機器可能會讀取到不同的 locale, 例如我在開發機是 en 的, 但到 production 卻變成 zh-TW, 而 Faker::Internet.free_email 卻變得不 work 了

但可以在 config 裡設定他預設的 locale

config/environments/development.rb :

Faker::Config.locale = 'en'

Error

ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved

有可能會發生在當一筆資料沒有建立成功(可能是 validation 沒有通過),造成資料沒有建立,但又直接建立關聯資料造成的錯誤:

c = Category.create(...)
c.products.create(...)

建議加上 ! 讓錯誤當下直接擲出 Exception 中斷程序

c = Category.create!(...)
c.products.create!(...)