Software engineering notes

Rails Basics

Rails 指令

console :

Routes

command

觀念

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

基本 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

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 的設定, 有以下情況 :

關掉檢查 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

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

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')

$! (例外物件)

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 產生

結論

Logger

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

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

注意! 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

Database

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

連接 sqlite3 設定

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

config/database.yml

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

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

連接 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’

  1. 建立資料庫

    RAILS_ENV=production rake db:create

  2. 執行 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

欄位

欄位型態

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

DB Migration

產生 migration 指令及命名慣例

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

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

語法

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

操作欄位

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"

欄位參數

unsigned: true

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

Assets

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 )

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, 否則瀏覽器會拒絕載入

Troubleshootings

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

Custom helper

app/helpers/event_helper.rb

module EventsHelper
  def do_something
  end
end

controller :

class BadgeController < ApplicationController
  include EventHelper

  ....
end

Mailer

SMTP 設定

config/application.rb

config.action_mailer.default_url_options = { host: $settings[:host] }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
    address: "smtp.mailgun.org",
    port: 587,
    user_name: "postmaster@example.com",
    password: "9******************************d",
}

寄信範例

app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  # 如果有設定的話, 在寄信時就不用特別指定
  default from: 'contact@gmail.com'
  default to: 'test@gmail.com'

  layout 'mailer'
end

app/mailers/my_mailer.rb

class MyMailer < ApplicationMailer
  def welcome(user)
    @user = user

    mail(to: @user.email, subject: 'Welcome')
  end
end

寄給多位 User : to: 'aaa@gmail.com,bbb@gmail.com'

app/views/layouts/mailer.text.erb

<%= @user.nickname %> 您好~

<%= yield %>

如果有任何疑問,歡迎隨時聯絡我們

謝謝您選擇我們,期待與您的合作。

app/views/my_mailer/welcome.text.erb

Welcome to example.com, <%= @user.name %>

app/controllers/user_controller.rb

MyMailer.welcome(current_user).deliver_later

寫完一封 mail 可以在 console 下執行, 測試信件是否發送出去

Send to multiple recipients

emails = @recipients.collect(&:email).join(",")
mail(to: emails, subject: "A replacement clerk has been requested")

cc & bcc

cc 看到的所有副本的收件人 email, bcc 則看不到

mail(to: recipient.email_address_with_name, bcc: ["bcc@example.com", "Order Watcher <watcher@example.com>"])

reply-to

當收件人按下回覆,預設回覆是給寄件人,但寄件人的 email 往往是客服的信箱,所以可以透過指定 reply-to 去指定收件人

mail(to: user_email, reply_to: [email_1, email_2])

Config

為每條 log 前加上 user_id

Started GET "/" for 127.0.0.1 at 2016-03-31 14:12:52 +0800

加上後 :

[user_id:2] Started GET "/" for 127.0.0.1 at 2016-03-31 14:12:52 +0800

如何加上 :

config/application.rb

config.middleware.delete(ActionDispatch::Cookies)
config.middleware.delete(ActionDispatch::Session::CookieStore)
config.middleware.insert_before(Rails::Rack::Logger, ActionDispatch::Session::CookieStore)
config.middleware.insert_before(ActionDispatch::Session::CookieStore, ActionDispatch::Cookies)

config/initializers/logging.rb

Rails.configuration.log_tags = [
  proc do |req|
    if req.session["warden.user.user.key"].nil?             # 這個 key 是 devise 的
      "Anonym"
    else
      "user_id:#{req.session["warden.user.user.key"][0][0]}"
    end
  end
]

ref : http://stackoverflow.com/questions/10811393/how-to-log-user-name-in-rails

其他

Difference between .length, .count and .size

這三個共同點都是算一個集合的數量, 如果對像是 Array 這兩者的行為無差別

有差別的是有時候我們會利用這兩者去判斷從 DB 撈出來的筆數如果 >0 再做相關的處理, 從 DB 撈出來的是 ActiveRecord::Relation

a = User.all
User Load (115.4ms)  SELECT "users".* FROM "users"

使用 .length, .size 會直接算出數量

a.length
 => 203

a.size
 => 203

使用 .count 就會使用 SQL count, 成本相對是比較高的

a.count
(2.5ms)  SELECT COUNT(*) FROM "users"<F6>
 => 203

結論 : 如果只是需要單純判斷目前 DB 資料的筆數用 .count, 如果不是或不確定用哪個就用 .length.size

nil vs empty vs balnk

首先 blank 是 rails 才有的, ruby 本身是沒有的

nil 可以用在任何物件上, 即使物件為 nil, 當 Object 為 nil 的話為 true

empty 可以用在 strings, arrays and hashes, True 的話有以下三種情況

如果在某個為 nil 的物件上問 .empty? 會擲出 NoMethodError

.blank? 可以解決這個問題, 它不會引發 NoMethodError, 用法跟 empty 幾乎一樣

nil.blank? = true       # empty? 會引發錯誤
false.blank? = true     # empty? 會引發錯誤
[].blank? = true
[].empty? = true
{}.blank? = true
{}.empty? = true
"".blank? = true
"".empty? = true
5.blank? = false       # empty? 會引發錯誤
0.blank? = false       # empty? 會引發錯誤

有一個 space 的情況

" ".blank? = true
" ".empty? = false

Array 是空的情況

[ nil, '' ].blank? == false
[ nil, '' ].all? &:blank? == true

區分環境變數

config/settings.yml

:development:
  :host: '127.0.0.1'

:production:
  :host: 'example.com'

config/application.rb

# 載入 settings.yml
require 'yaml'
$settings = YAML.load(File.open("#{__dir__}/settings.yml"))[Rails.env.to_sym]

要記得先重啟 rails server 才能讀取新的 config,在程式裡使用,會依照你的環境讀取相對的設定

$settings[:host]

只顯示 date 就好

如果網站上常使用 date 顯示, 一般輸出 created_at 都要用 created_at.strftime('%F %T') 很不方便

可以建一個 config/initializers/time_format.rb 或直接寫在 application.rb

Time::DATE_FORMATS[:default] = "%Y-%m-%d %H:%M:%S"

將物件儲存成字串

Marshal

a = {qq: 'xxx', ff: {cc: 'ccc', dd: 'ddddd'}}

# 轉成字串
Marshal.dump(a)
 => "\x04\b{\a:\aqqI\"\bxxx\x06:\x06ET:\aff{\a:\accI\"\bccc\x06;\x06T:\addI\"\nddddd\x06;\x06T"

# 轉回成物件
Marshal.load("\x04\b{\a:\aqqI\"\bxxx\x06:\x06ET:\aff{\a:\accI\"\bccc\x06;\x06T:\addI\"\nddddd\x06;\x06T")
 => {:qq=>"xxx", :ff=>{:cc=>"ccc", :dd=>"ddddd"}}

JSON

# 轉成 JSON
a.to_json
 => "{\"qq\":\"xxx\",\"ff\":{\"cc\":\"ccc\",\"dd\":\"ddddd\"}}"

# 轉成 json ( hash key)
JSON.parse("{\"qq\":\"xxx\",\"ff\":{\"cc\":\"ccc\",\"dd\":\"ddddd\"}}")
 => {"qq"=>"xxx", "ff"=>{"cc"=>"ccc", "dd"=>"ddddd"}}

# 轉成 <F10>
JSON.parse("{\"qq\":\"xxx\",\"ff\":{\"cc\":\"ccc\",\"dd\":\"ddddd\"}}", symbolize_names: true)
 => {:qq=>"xxx", :ff=>{:cc=>"ccc", :dd=>"ddddd"}}

Marshal 會佔比較多的字符 55 : 43

解開 session

Marshal.load(Base64.decode64(cookie_token.split("--")[0]))

devise 登入使用記住我,cookie_token 才能解

簡化 console 指令

~/.irbrc

class T
  def self.mail(mail)
    User.where(email: mail)
  end
end

Rails console :

T.mail('me@gmail.com)

console 下自動 include (好像沒用了 updated at 2015/9/3)

config/application.rb

config.console do
  include Rails.application.routes.url_helpers
end

找出 Gem 的真實路徑

In console

 > Gem.loaded_specs['rails'].full_gem_path
 => "/usr/local/rvm/gems/ruby-2.2.1/gems/rails-4.2.3"

Rails lib 真實路徑

ex: upload
/usr/local/rvm/gems/ruby-2.2.1/gems/actionpack-4.2.3/lib/action_dispatch/http/upload.rb