顯示具有 rails 標籤的文章。 顯示所有文章
顯示具有 rails 標籤的文章。 顯示所有文章

2014年2月19日 星期三

在development模式下,assets 與 precompiled assets 衝突的解決方法

因為自己的開發環境效能往往比production server 還好,有時候我們為了求快,會直接在local 將 assets precompile 過,上傳到 production。
但繼續在 development 模式下開發時,就有個很機車的情況出現了,在 public/assets 中已經 compile 過的 assets 也被 require 進來,造成某些 lib 被 require 兩次而造成不正常的情況,當有新的變更時也會被舊的蓋過去。

查了一下發現可以針對 development 模式對 asset 路徑加上prefix:
  config.assets.prefix = "/dev/assets"

這樣子 request 的路徑就會變成 /dev/assets/application.js ,避免被precompile的assets搞亂。



2014年2月12日 星期三

修正 PG::UniqueViolation: ERROR: Duplicate Key Value Violates Unique Constraint 'Your_table_name_pkey' 問題

PG::UniqueViolation: ERROR: Duplicate Key Value Violates Unique Constraint 'Your_table_name_pkey'

今天在新產品上遇到了這個 error,查了一下發現是因為 postgres 因為不明原因而把 seq 搞亂了,以致於新增資料時使用到了既有資料的 id 。

了解問題所在後,循以下作法順利解決了問題:
  1.  rails db production
  2.  SELECT setval('your_table_id_seq', (SELECT MAX(id) FROM your_table));
打完收工~


2014年1月8日 星期三

Rails 的 i18n fallback 問題

當在 config/application.rb 中指定了 config.i18n.fallbacks 語系順序後,別忘了要 comment 掉 produciton.rb 中的 config.i18n.fallbacks = true 。
production 下的語系順序才會乖乖實現。

2014年1月3日 星期五

Unicorn + Nginx 配置及設定

最近部屬公司的新產品 - flyTutor 的時候,嘗試了不同的架構,從以往的 passenger-plugin on Nginx 換成了Unicorn 搭配 Nginx 的「反向代理」架構

What's "Reverse Proxy Model" ?

在提起 Unicorn 及 Nginx 的配置之前,我們得先了解一下「反向代理機制」,下面這張圖應該可以幫助你了解:

(轉自: ROR實戰聖經)


在用戶端向 server 作請求的時候,不是直接與 app server 連結 (中間那三個),而是先連到 web server ,通常是 Apache 或 Nginx,再由 web server 指派 request 給 app server、或直接 serve static assets。
所以在這樣的架構下你可以想像你的系統需要常駐著以下的服務:
  1. web server
  2. 數個 app server
  3. DB server

這樣的架構是與常見的 Apache/Nginx + Passenger 架構有所不同的,需要作一點觀念上的調適。下面我們就來介紹一下如何做相關的配置。


Nginx 配置

以下是預設的 Nginx 設定檔 - nginx.conf:
user deployer; # 定義操作 nginx 的使用者
worker_processes 1; # 定義 worker 的數量

error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {

  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  access_log    /var/log/nginx/access.log;

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  keepalive_timeout  65;

  gzip  on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_proxied any;
  gzip_vary off;
  gzip_types text/plain text/css application/x-javascript text/xml application/xml application/rss+xml application/atom+xml text/javascript application/javascript application/json text/mathml;
  gzip_min_length  1000;
  gzip_disable     "MSIE [1-6]\.";

  server_names_hash_bucket_size 64;
  types_hash_max_size 2048;
  types_hash_bucket_size 64;

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;

}

上面這段預設的設定檔有兩個地方需要注意:
第一行的使用者身分必須為負責 deploy 的用戶,否則常常會出現 403 權限問題,因為未經設定的話 nginx 會沒有權限存取 deployer 的檔案,如 js, css。
第二行的 worker 數量建議別設定超過 cpu 數目,多了也沒好處。

再來就是重頭戲了,我們要開始加入 app server 的設定,這段設定需要加在 http 的括號內:
  upstream unicorn {
    server 127.0.0.1:8080 fail_timeout=0;
  }

  server {
    listen 80 default deferred;
    server_name flytutor.com;
    root /var/www/flytutor/public;

    location ^~ /assets/ {
      gzip_static on;
      expires max;
      add_header Cache-Control public;
    }

    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;

      if (!-f $request_filename) {
        proxy_pass http://unicorn;
      }

    }

    error_page 500 502 503 504 /500.html;
    client_max_body_size 4G;
    keepalive_timeout 10;
  }


upstream:

首先我們先來講解一下第一部分的 upstream。首先,upstream 後的「unicorn」並非一定的,端看你希望怎麼命名,但是要注意的是 proxy_pass 也需作對應的設定。再來,upstream 內的 server 可以不只一個,如果你有數個 app server 在不同的 host 的話,你也可以做如下的設定:
upstream flytutor.com {
  server 192.168.0.111:8080 weight=3;
  server 192.168.0.222:8080 weight=2;
  server 192.168.0.333:8080 weight=3;
}
其中 weight 代表的是「被配發 request 的權重」,當 weight 越高的時候被分配到的機率越大,可以做 loading balance。

而除了走 TCP/IP 外,如果是在 web server 和 app server 都在同一個主機的情況下,你也可以使用 socket 來做設定,基本上效能會比 TCP/IP 好一點點(因為少了 header):
  upstream unicorn {
    server unix:/tmp/unicorn.sock fail_timeout=0;
  }

server:

接下來我們要介紹一下 server 的設定,基本上這邊指的 server 就是指 app server,一台 nginx server 可以依 server 設定派發 request ,所以可以做 vhost 的應用,例如判斷 request 的不同網域 (設定檔中的 "server_name")、或是同網域但是不同 port (設定檔中的 "listen"),來派發到不同的服務。
proxy_pass 很重要,許多網友都死在這個地方,切記要將他設置成 upstream 的命名,例如 "http://<upstream_name>"。
你可以想像當一個 request 經過 location(path), server_name(domain), listen(port) 重重篩選後,你卻不指派 proxy_pass 的目的地給他,讓這個 request 就此隱沒在數據流中,成了「digital phantom」,你於心何忍?


Unicorn 配置

unicorn 的配置通常是放在 rails project 中的 config 資料夾,長得像這樣:
app_root = "/var/www"
app_name = "flytutor"
listen "127.0.0.1:10101" #, :backlog => 2048 #這邊要跟nginx虛擬主機檔中upstream內定義的務必一樣
worker_processes 2 #看情況開
preload_app false
timeout 30
 
module Rails
  class <<self
    def root
      File.expand_path(__FILE__).split('/')[0..-3].join('/')
    end
  end
end

_working_directory = File.join(app_root, app_name)
working_directory _working_directory
logs_path = "#{_working_directory}/log"
pid "#{_working_directory}/tmp/pids/unicorn.pid"
stderr_path "#{logs_path}/unicorn.stderr.log"
stdout_path "#{logs_path}/unicorn.stdout.log"
 
GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true
 
before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
  
  old_pid = "#{Rails.root}/tmp/pids/unicorn.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      puts "Send 'QUIT' signal to unicorn error!"
    end
  end
end
 
after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

這邊要注意的地方有兩個:

  1. listen 的對象必須與 nginx.conf 中 upstream 設定的一樣,unicorn 與 nginx 就是靠這行設定連起來的。還有,如果是走 TCP/IP 的設定,盡量不要使用 1024 以下的 port 以免 deployer 沒有權限啟動 unicorn 導致失敗(你總不能老用 root 權限作 deploy 吧?)。
  2. unicorn 是採用 master-slave 機制,啟動 unicorn 後叫出  process 列表你就會了解的。而這裡的 worker_processes 的數目顧名思義就是指定 worker 的數目。開多少個才好?基本上沒有一定的標準答案,還是要依你的使用情況及環境做測試後才知道,如果懶得想的話就直接設定為「CPU 數目 + 1」吧。


操作 unicorn

許多網友在網路上搜尋到的啟動法是以 unicorn_rails 來啟動:/usr/bin/unicorn_rails -c config/unicorn.rb -E $RAILS_ENV -D。但現在 unicorn 官方已經不建議使用這種作法而是直接改用 unicorn 來啟動/重啟 unicorn。

Hot restart with zero down time

許多人之所以選擇 unicorn 或許就是因為他的 hot restart 特性。如果對這有興趣的人可以參考這篇。



2013年12月31日 星期二

Rails query tips

Pluck - :

這個指令能讓搜尋結果的特定欄位獨立抽取出來放到 Array 裡,在很多情形下都很方便。
Ex: User.all.pluck :name # => ["Kevin", "Laura", "Yiya", "Diya"]


Subquery:

以第一個query的結果作為條件做搜尋,Ex:
Order.where( :product_id => Product.where("price<1000") )
(Reference: http://ruby-china.org/topics/10771)


Update with condition:

Ex: Model.where("state == 'decline'").update_all(:state => 'deny')


has_many relation with condition:

當兩個 model 彼此為一對多關係時,我們可以利用 condition 讓這個關係有所區分,我拿以下的情形為例:user 有很多訂單 (orders) ,有些是取消的、有些是處理中的、有些是已經結帳的。在這個情形下,用 user.orders 撈所有的訂單後再進行判斷顯然沒有效率、用 Order.where("{條件}") 的方法也就失去了 relation 的方便與直觀。這時我們可以做加上這幾個 relation:
has_many :cancled_orders, :class_name=>"Order", :conditions=>proc{ "status = 'Cancled'" }
has_many :handling_orders, :class_name=>"Order", :conditions=>proc{ "status = 'Handling'" }
has_many :paid_orders, :class_name=>"Order", :conditions=>proc{ "status = 'Paid'" }

這樣就可以透過如 current_user.cancled_orders 的方式取到「取消的訂單」了,相當直觀。


OR condition using Arel

Ex:
t = Post.arel_table

results = Post.where(
  t[:author].eq("Someone").
  or(t[:title].matches("%something%"))
)



2013年12月30日 星期一

動手寫一個 Rails plugin

因為開發上方便,需要一個能快速切換登入身分的小 widget,正好趁這個機會搞懂怎麼開發一個 Rails plugin。成品在這:https://github.com/kevin-shu/user_switch
以下就是步驟囉:

1. rails plugin new my_gem

首先執行 rails plugin new <plugin_name>,會產生 plugin 資料夾,結構為:
my_plugin
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.rdoc
├── Rakefile
├── lib
│   ├── my_plugin
│   │   └── version.rb
│   ├── my_plugin.rb
│   └── tasks
│       └── my_plugin_tasks.rake
├── my_plugin.gemspec
└── test
    │
    ...(本篇不提到,忽略)
接下來就要介紹這些資料夾、檔案的用途


2. my_plugin.gemspec

這是一個 manifest 檔案,用來告訴 gem 該怎麼打包這個專案。


3. lib 資料夾

my_plugin.rb:

這就是 plugin 的核心,當裝上 plugin 後,被執行的就是這個檔案。你當然可以將所有 code 都寫在裡面,但是這樣很亂。一個功能稍微複雜且完整的 plugin 一定會有系統的做模組化。所以我們往往不在裡面寫很多 code ,而是將一個個寫好的模組 require 近來。

my_plugin 資料夾:

剛剛說的模組化的 code 通常會被分門別類放在這裡。

my_plugin/version.rb:

在初始化後,my_plugin 資料夾一開始只有這個檔案,用來記錄版本號。你如果有看一下 gemspec 的內容的話,會發現這個檔案會被 gemspec require 到。

Engine

如果你的 plugin 需要一些自定的 controller、view、assets,首先要加入這段程式碼:
module ZurbFoundation
  class Engine < Rails::Engine
  end
end
你可以將他獨立成一個檔案由 my_plugin.rb 載入,或是直接寫到 my_plugin.rb 裡。有了這段程式碼,Rails就會自動將 app 及 config 兩個資料夾下的所有檔案載入。
請注意,載入 engine 的 code 必須放在 my_plugin.rb 的最後,才能正常運作


4. app 資料夾

當你希望在安裝這個套件的專案中使用客製的 controller、assets 等檔案,就要在根目錄下建立一個 app 資料夾。結構基本上跟你熟悉的 rails 的 app 資料夾是一樣的。要注意的是,裡面的 controller 或 model 必須在 專案的 namespace 下,例如:
# CURRENT FILE :: app/controllers/my_plugin/my_controller.rb
module MyPlugin
  class MyController < ::ApplicationController
    def index
      ...
    end
  end
end


5. config

這個資料夾跟app資料夾一樣需要手動建立,最常見的用途是設定 routing,例如:
# CURRENT FILE :: config/routes.rb
Rails.application.routes.draw do
  get "team" => "team_page/team#index" , :as => :team_page
end


6. generators 資料夾

這個資料夾也是需要時才要自行建立。裡面放了一些設定檔等的 template 以及 install method,讓這個 gem 提供 install 功能,執行後會複製一份設定檔到專案中。這邊不會對 generators 多做介紹。


7. pack & push

當你的 plugin 完成後,就可以用以下指令將他打包起來,gem 會按照 gemspec 的設定做打包。
gem build /project_path/user_switch.gemspec
打包後的 gem 可以 push 到 rubygems.org:
gem push project_name-0.0.1.gem


8. 在 Rails 專案中掛上剛寫好的plugin push

我們有可以在 Gemfile 中依不同來源來做來源的設定,其中當來源是來自 local 或 git 時,是不用打包就可以直接掛上的,開發的時候來源通常是設定在 local。

(1) local: 

gem "my_plugin", :path=>"/path/to/your/plugin"

(2) git:

gem "my_plugin", :git=>"https://path/to/your/repo"

(3) rubygem.org:

gem "my_plugin"


Reference:

http://zurb.com/article/814/yetify-your-rails-new-foundation-gem-and-
http://coding.smashingmagazine.com/2011/06/23/a-guide-to-starting-your-own-rails-engine-gem/
http://guides.rubyonrails.org/plugins.html

2013年12月17日 星期二

warning: toplevel constant Model referenced by NS::Model


今天遇到了這個問題,在網上搜一下找到一個滿詳盡的解釋,茅塞頓開:
Your User::File class is not loaded. You have to require it (e.g. in user.rb).
The following happens when ruby/rails sees User::Info and evaluates it (simplified; only User is defined yet).
  • check if User::Info is defined - it is not (yet)
  • check if Info is defined - it is not (yet)
  • uninitialized constant -> do rails magic to find the user/info.rb file and require it
  • return User::Info
Now lets do it again for User::File
  • check if User::File is defined - it is not (yet)
  • check if File is defined - it is (because ruby has a built in File class)!
  • produce a warning, because we've been asked for User::File but got ::File
  • return ::File

裡面提到了要手動 Require NameSpace 下的 Model,但是如果 NS 下的 model 本身有繼承到 NS 外的 Model,要確保在父類別宣告後建立才行。所以最好是在父類別檔案的結尾處 require。

2013年12月12日 星期四

以 STI (Single Table Inheritance) 及 NameSpace 提高專案維護及可讀性

正在開發的產品中,有個「個人資料維護功能」需要記錄使用者的學歷。我採用的架構是:
  • user has_many educations
  • education belongs_to school

而且 educations 和 schools 資料表中各有 type 和 stage 欄位記錄這個 學歷/學校 是屬於哪個階段 (ex. 國中、高中、大學)。(題外話,type 在 Rails 中是個欄位的保留名稱,專門給 STI 使用)

在這樣的情況下,當我使用 user.educations 取得使用者學歷後,還需要再進行 Array.detect 來找對應的階段來顯示「國中讀哪裡」、「高中讀哪裡」,這樣的邏輯實作出來相當冗餘也不好看。


用 STI 來讓多型別的 relation 更直觀


於是我便將架構做一點改變,新增幾個 education 的 sub-class,放在 "education" 的 NameSpace 下避免與既有的 model 撞名。並將 stage 改為 type,以建立 STI 的從屬關係,例如:
class Education::HighSchool < Education
  ...
end

這樣一來,我們只要再建立以下的 relation,就可以使用 user.high_school 和 user.junior_high_school 直接拿到對應學歷,相當直覺:
  • has_one :junior_high_school, :class_name=>"Education::JuniorHighSchool"
  • has_one :high_school, :class_name=>"Education::HighSchool"


STI name hack


但在這樣的情況下,因為 Rails 的 convention,他會預設抓取 type 為 "Education::JuniorHighSchool" 或 "Education::HighSchool" 的 education。但是這樣很醜,並不直覺。我希望在type中顯示 "JuniorHighSchool" 或 "HighSchool" 就好。這時我們可以在他們的父類別,也就是 education.rb 加入以下程式碼來 hack :
class Education < ActiveRecord::Base

  ...

  class << self
    def find_sti_class(type_name)
      ("Education::"+type_name).constantize()
    end

    def sti_name
      name.demodulize
    end
  end

end

find_sti_class 的用途是:在 table 中找到一個 entry 時,決定用什麼 model 來表達他,而傳入的 type_name 當然就是 type 欄位中的資料了。當我們如範例程式碼重寫 find_sti_class 後,如果傳入的 type_name 為 「HighSchool」,find_sti_class 會返回 Education::HighSchool 這個Class。

sti_name 則是在 new 一個繼承 STI 的物件時,將 return 的值塞進 type 欄位。經過範例程式法的改寫後,新增一筆 Education::HighSchool資料時,Rails 會在type欄位中存入經過 demodulize 的字串「HighSchool」,而不是 Education::HighSchool。

這樣一來 Rails 就會乖乖找type為 "HighSchool" 的 Education::HighSchool model了。



2013年12月11日 星期三

Rails 的 db:migration 及 db:schema:load

相信 rails 開發者對 rake db:migrate 這個指令相當熟悉,但是可能比較少機會用到 rake db:schema:load,他是拿來幹嘛的?

db:migrate 的用途


在多個環境或多人開發的時候,我們會頻繁的使用 db:migrate ,為得是將各個環境的資料庫結構給同步起來,而不用使用同一個資料庫來開發。使用同一個資料庫除了會弄髒彼此的還境外,也會造成每改一次資料庫,就要確認其他開發者的程式碼是 workable 的,也就是「schema和程式碼不同步」的情形。

在這樣的情形下,將 migration 寫成腳本並加上 time stamp ,在各自環境的資料庫各自進行 migration 就成了一個最合理的 solution 了。而因為 migration 腳本就是整個專案程式碼的一部分,也就解決了schema 及程式碼的同步問題。每個開發者可以依照自己的進度 commit、pull、merge,再執行 rake db:migrate ,資料庫就同步了。

但是在開發過程,尤其是初期的 scratch 階段,常常會有一堆冗餘的動作,例如:
  1. 創建 users 資料表
  2. 加兩個欄位到 users 資料表
  3. 刪除 users 的其中一個欄位
  4. 再加一個欄位到 users 資料表

這時就會在系統中留下四個針對 users 資料表的 migration 腳本。這時如果有一個新加入的開發者,將程式碼拉到自己建立的新環境,使用 rake db:migrate 這個指令後, rake 就會乖乖的幫你分四個步驟做完,其實很沒必要也很笨。


db:schema:load 幫你直接從 schema.rb 初始化資料庫結構


其實在你進行migration的時候,rake 也產出了一份資料庫的架構檔案,叫做「schema.rb」,這個檔案記錄了這個專案的資料庫目前的架構。檔案中也會有一個 timestamp 記錄這是什麼時候的結構,用以區分先後順序。

而當有新的環境要被建立,或是重置資料庫的時候,我們可以利用 rake db:shema:load 這個指令讓資料庫「一步到位」,而不用一步步的照 migration 腳本來設置資料庫。



2013年9月27日 星期五

Rails 的 Counter Cache 機制

雖然很基本,但是一定要做一下記錄。
我們先想像一個情況:你目前的專案中,有user及comment兩種model:

class User < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :user
end

如果在一個頁面需要將使用者列表,並同時顯示他所擁有的回應數時,我們直覺上會在迴圈中,一個個進行user.comments.count,並同時產生一個db query,這樣的情況就是經典的「N+1」問題。


讓我們把counter cache住吧!

在系統架構上還滿常用到的一個手法是,將「讀取機會比寫入機會大很多」的數值記錄起來,例如這次會講到的counter cache,就是乾脆在user資料表上開一個comments_count欄位,在新增或刪除comment的同時,記錄他目前所擁有的comment數。
如果你對rails有一定程度的了解,應該會想到使用ActiveRecord callback的機制在新增或刪除comment後進行user.comments_count的更新。但是也因為這是一個還算常見的作法,Rails本身就內建了這個功能,能夠讓我們所做的工減少很多。


進入正題

1. 新增counter欄位

首先我們要先在user資料表中開一個專門儲存comment數的欄位,由於rails「Convention over configuration」的核心精神,這個欄位應該被命名為 [resource複數]_count ,否則rails可能會認不得他。以這個例子來說,就是要新增一個叫 comments_count 的欄位,並且預設值為0。新增欄位的過程就不贅述了。


2. 修改relation選項

接下來要做的是在comment.rb中的relation上加上一個選項::counter_cache ,告知Rails這個relation是要做counter的同步動作的。程式碼變成:

class Comment < ActiveRecord::Base
  belongs_to :user, :counter_cache => true
end

如果目前的專案增加這個欄位時,並沒有存在任何資料,也就是所有的user所擁有的comment是0,這樣的情況下其實這樣已經ok了。但如果之前就已經有一些user有創造comment,這時我們就要做counter的更新。


更新counter cache

可能有些人會想說,直接遍尋所有user,讓他們的 comments_count = user.comments.count 就好。
很不幸的,Rails對於counter欄位有保護機制,我們不能這樣直接修改他,而必須使用 update_counters(id, counters) 或 reset_counters(id, *counters) 兩種方法,以reset_counter來說,migration的程式碼就像這樣:

User.find_each do |user|
  User.reset_counters user.id, :comments
end



2013年8月17日 星期六

Rails排程工作套件 - whenever

專案中或多或少會需要排程的功能,不免俗的,Rails強大的生態圈中當然也出現了幾套好用的排程gem,其中最多人使用的就是「whenever」。

安裝

gem 'whenever', :require => false

其中":require => false"的變數是為了不讓這個gem被專案require,因為其實他本身並不會被專案所呼叫,那也就沒有必要被載入專案,佔用記憶體。

原理:
linux中本身就有排程的功能(參見鳥哥的linux私房菜),whenever只是以更親切的方式來提供開發者使用,但實際上還是由os來執行cronjob。


初始化

在專案底主目錄下:
wheneverize .
這樣會產生config/schedule.rb,我們需要在裡面設定隔多久執行特定的job。


設定cronjob

上一個步驟中產生的schedule.rb中其實已經有一些範例可以做參考,基本上格式就是:

every 1.day, :at => '4:30 am' do
  runner "MyModel.task_to_run_at_four_thirty_in_the_morning"
end

以上述程式碼來說,要執行的method就寫在my_model.rb中,這樣應該是足夠明瞭了。


最後一步

但可不是這樣就搞定了,因為對os來說,crontab中仍然沒有任何排程工作。可以用以下指令來檢視:
crontab -l
所以我們要先執行whenever的一段指令讓我們寫在schedule.rb的排程工作實際加到crontab中才行:
whenever --update-crontab store
這時我們再執行 crontab -l 就可以看到剛剛加入的行程了。


2013年7月28日 星期日

如何在Rails中重置所有user的session

前陣子因為遇到devise更改了他的session配置,使得專案中一個功能出了點問題(改版前登入使用者的session配置與改版後登入的使用者不同),所以最近申請的user會引發一個bug。為了讓所有的使用者統一session格式,我必須強制清除所有user的session,讓他們登出才行。

重設secret_token

要做到這樣的目標很簡單,只需要將環境中的secret_token更新在重啟即可,方法如下:

  1. 在terminal中,專案資料夾下輸入 rake secret,會拿到一串亂碼
  2. 將上一步驟中得到的亂碼替換掉「config/initializers/secret_token.rb」中的MyApp::Application.config.secret_token
  3. 重啟伺服器

這樣就完成了

2013年6月26日 星期三

Rails的db:seed小技巧

在開發Rails專案時,在db的sync上,常常會遇到下面兩個情況:

1. 開發環境與正式環境的初始資料
2. 多人開發時的local DB同步問題

第二個問題其實只要開一個遠端的伺服器來共用就解決了,但是在使用sqlite或是沒有網路的情況下,這麼做實在很麻煩,而且有時候我們也不希望別人的資料弄髒資料庫。這時候我們可以使用seed.rb來初始化資料庫中的資料,我們來看看怎麼做:
# seed.rb

areas = [
  {name:"台北"},
  {name:"高雄"}
]
Area.create areas
然後下rake db:seed這個指令就行了


進階需求


但是需要初始化的資料往往都是系統的固定選項(如縣市等不應被隨意增加、減少的資料),這時常常會有需要指定id的需求,這時我們就要稍微修改一下作法:
areas = [
  {id:1, name:"台北"},
  {id:2, name:"高雄"}
]
areas.each do |area|
  s = Area.new area
  s.id = area[:id]
  s.save
end

如果是確定此資料表中只會出現seed中所寫入的資料的話,我們可以加上delete_all來先清空資料表再做插入資料的動作:
Area.delete_all

使用csv匯入資料


在只有少量資料需要被處理時,直接將資料寫在seed.rb十分方便,但是當資料量很大時,我們希望能以報表的形式事先整理好再做匯入。而csv是一個格式簡單,也能被Excel, Numbers等報表軟體支援的檔案格式,因此我選擇它作為外部資料匯入的媒介。

但是在Excel中比較難去設定成我們想要的utf-8編碼,比較建議使用Apple的Numbers軟體,可以直接輸出成utf-8的csv檔。

來一段簡單的程式碼大家應該就能看懂了:
# seed.rb

require "csv"
SysSchool.delete_all
CSV.foreach("#{Rails.root}/db/school.csv") do |row|
 school = {
  id:row[0],
  name:row[1],
  sys_area_id:row[2]
 }
 s = SysSchool.new school
 s.id = school[:id]
 s.save
end

這段程式碼也是要貼在seed.rb中的,這樣一來在執行"rake db:seed"時,就會先刪除資料表中的資料,並將db/school.csv裡的資料一列一列插進資料表中。

2013年5月8日 星期三

[ROR] ActiveRecord效能調校

雖然在使用Rails的過程中感受到orm的方便,但是ActiveRecord用爽爽的同時,許多對sql不是很瞭解的開發者可能會犯下了許多問題導致無謂的浪費了sql資源。在系統剛做好,資料量不大的時候不太是問題,但在使用者及資料量多了以後,問題就會一一浮現了,我們先來看一下一些常見的問題:


Resource.find(id).child_resources


這樣的寫法很直覺,也很方便,但是會造成兩個query:
Resource Load (x ms)  SELECT "resource".* FROM "resource" where id = n
ChildResource Load (x ms)  SELECT "child_resource".* FROM "child_resource" WHERE "child_resource"."resource_id" = n

如果改成這樣的寫法只需要一次的query:
ChildResources.where("resource_id=#{id}")

雖然只少了一次的query,但最多能減少50%的sql操作時間



Resource.all (沒有加select)


當你可能只是很單純的要Resource的id及name,而不需要肥肥的"content"欄位,使用select只拉出需要的欄位,可以幫以省下不少傳輸時間和記憶體。
雖然可以使用slim_scrooge這種gem,不過別這麼懶,自己養成好習慣吧!


利用joins將需要的資料一次拿完

剛開始開發rails的時候,常常會拿出model後就直接往view丟,相關的relation到時候再拿就好,例如這樣:
<%= @category.products.counts %>
但是這樣常常會產生無謂的sql query,少量還好,當你在前台對一堆@catgories跑each的時候會發生很可怕的情形....也就是所謂的 "N+1 Query"
所以盡量養成一個習慣,就是要用到的資料一次拉進來就好,最嚴謹的檢查方式就是將產出的資料用attributes轉成hash,所有要拿的資料都在裡面了,沒有query進來的relation絕對拿不到。不過這樣的作法可能有點矯枉過正就是XD
要一次拿完所需資料最簡單就是用join+select操作,例如:
@products=Category.joins(:products=>:photo).select("categories.name cat_name, products.name pro_name, photos.url")
<% @products.each do |product| %>
  <%= product.cat_name %>
  <%= product.pro_name %>
  <%= image_tag product.url %>
<% end %>
而不是
@products=Product.all
<% @products.each do |product| %>
  <%= product.category.name %>
  <%= product.name %>
  <%= image_tag product.photo.url %>
<% end %>


適時使用counter_cache

相信這種情境一定很常見:
進入網站後,螢幕顯示出你有幾個朋友、幾個相簿、幾篇部落格
這時,在controller裡面....

Bad code:

  @friend_count = current_user.friends.count
  @album_count = current_user.albums.count
  @blog_count = current_user.blogs.count


這時我們該使用counter cache把他cache起來:
在users增加friends_count、albums_count、blogs_count等欄位
在friend.rb、album.rb、blog.rb中的
belongs_to :user
改成
belongs_to :user, :counter_cache => true
如果之前已經有資料了,記得把user.xxx_count給update到目前的count數,但為了保護counter的正確性,counter是read-only的,我們只能使用Base.reset_counter來設定他。
而counter的reset程式碼最好是寫在migration.rb裡,讓各種環境下migrate時順便reset counter:
class AddCounterCacheToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :comments_count, :integer, :default => 0
    User.find_each do |user|
      User.reset_counter user.id, :friends, :albums, :blogs
    end
  end

  def self.down
    remove_column :users, :comments_count
  end
end

Good code:

@counts = User.find(current_user.id).select(:friends_count, :albums_count, :blogs_count).attributes



善用Transaction


利用transaction


find.each

2012年11月8日 星期四

[宅] 宅男臥軌日記(9) - Filters in controller

好像在前面的筆記裡有提到before_filter,但是沒有特別深入研究,拜讀了ihower大大的部落格並上網查了一下相關文章以後,在這裡做一個筆記整理。


filters

在Rails中,我們可以定義method在進入action之前、之中、之後進行,分別使用before_filter、after_filter和around_filter來實做。其中最常見的應該就是用before_filter來做權限驗證了:
before_filter :authenticate
上面的例子中接在before_filter後的是一個symbol,代表著要呼叫哪個method
before_filter最主要的功能還是在跨action的共用參數傳遞,在前面關於layout & style的文章中就有呼叫method,然後method回傳一個instance varible的例子。

在ihower大大的部落格中有提到,除了method以外,還能傳入code block以及物件。若傳入的是物件,Rails就會執行物件中的filter方法。

那code block呢?


在filter中傳入參數

前面有提到要在filter傳入method,要用symbol來指定method的名稱,但是我們沒辦法在symbol中加入參數("before_filter authenticate(arg)"明顯是個錯誤的寫法Orz),所以要傳入參數就要用以下兩個方法了:

1. code block

before_filter do |c|
c.send(:authorize, args)
end
或
before_filter :only => :show do |controller|
  controller.instance_eval do
    authorize(args)
  end
end

以上兩個方法我也還沒驗證過,改天來試試看XD


2. inline procs

這種方法最直覺,不過我一樣還沒驗證過能不能用XD
before_filter {authorize(args)}





2012年11月6日 星期二

[宅] 宅男臥軌日記(8) - 關於layout & style



依不同controller來assign Layout

要依不同controller來指定對應Layout(template),可以用layout選項來指定。就拿開發中所遇到的問題來舉例:
在還沒登入或登入錯誤時,我想指定一個特別的layout來呈現,因為會這些頁面都會經過devise的controller,基於這樣的前提我們可以在application controller下做以下的調整:
class ApplicationController < ActionController::Base
  protect_from_forgery
  
  layout :layout_by_resource

  protected

  def layout_by_resource
    if devise_controller?
      "guest"
    else
      "application"
    end
  end
end

"devise_controller?"是devise所提供的helper,用以判斷所在的controller是否為devise。
其實在一般情況下我們是不用直接修改application controller的,直接修改要呈現不一樣Layout的controller就可以。但是因為我們看不到devise controller,無法對他做修改,所以就寫在他的父類別讓他繼承這個特性。
而指定layout時其實還可以再做一些微調,例如:
layout "special", :only => :index
或
layout "special", :except => [:show, :edit, :new]

大家應該可以注意到以上的兩行和一開始的範例寫法有點不一樣,一個是使用symbol、一個是使用字串,這在意義上是不一樣的。使用symbol是call function,而使用字串就是直接去找這個名字的layout了。

Style

在一開始的application.css.erb中,我們可以在comments中找到以下兩行設定
*= require_self
*= require_tree .
雖然這兩行是寫在註解中,但他們還是有功用的(要把他disable掉只要刪掉"="就好了)。
require_self代表最後匯整的內容要包含"這個檔案"
require_tree代表最後匯整的內容要包含"所在資料夾下所有的檔案"

我們也可以很單純的require單一css,ex:
require bootstrap


同一個controller下為不同的view分配style sheet

不同controller對應各別layout在前面已經提過,但是有時候我們想用一樣的layout,但是是不同的style該怎麼辦呢?這時我們可以在template中動態的載入style sheet:

在View中:
<%=stylesheet_link_tag @foo %>

在Controller中:
before_filter :get_css_file
def get_css_file
  @foo = defined?(@style_type) ? 'normal' : 'special'
end

或是在你自己的邏輯定義@foo。

2012年11月2日 星期五

[宅] 宅男臥軌日記(7) - .nil? , .empty?, .blank? 的區別


.nil? 和.empty? 是ruby的方法,而.blank? 是rails的方法
這三個method都是判斷是否為空值。


.nil?

判斷對象是否存在(nil)。不存在的對像都是nil的

.empty?

對象已經存在,判斷是否為空字串。
比如一個字符串是否為空串,或者一個數組中是否有值。

.blank?

具有以上兩個method的特性,railsAPI:
An object is blank if it’s false, empty, or a whitespace string. For example, “”, “ ”, nil, [], and {} are all blank.

2012年10月29日 星期一

[宅] 宅男臥軌日記(6) - %Q, %q, %W, %w, %x, %r, %s

%Q( )

能將括弧中的內容轉成雙引號框成的字串,
而%Q後面未必要用括弧,也可以是!、[ ]、+等符號,例如:
%Q! !、%Q[ ]、%Q+ +

%q( )

功能跟大寫Q差不多,只是將內容換成單引號框成的字串。

※Notice: 在Ruby中定義字串時最好使用雙引號,因為只有在雙引號中才能使用#{var}來嵌入程式碼

%W( )

將括弧中的內容轉換成雙引號的string array,看下面的程式碼會更容易理解
>> %W(#{foo} Bar Bar\ with\ space)
=> ["Foo", "Bar", "Bar with space"]

%w( )

和大寫W是差不多的,只是將雙引號變單引號


%r( )

正規表示式,如下:
>> %r(/home/#{foo})
=> "/\\/home\\/Foo/"

%s( )

用於符號(symbol)
>> %s(foo)
=> :foo
>> %s(foo bar)
=> :"foo bar"
>> %s(#{foo} bar)
=> :"\#{foo} bar"

[宅] 宅男臥軌日記(5) - 資料表關聯方法: includes & joins

用一行程式碼大家應該就能明白:
user = User.includes(:authorizations).where("authorizations.provider" => 'facebook' , "authorizations.uid" => uid).first

includes跟joins功能挺像,引用ihower大大的部落格內的說法:
透過joins抓出來的event物件是沒有包括其關連物件的。如果需要其關連物件的資料,會使用includes。(出處:http://ihower.tw/rails3/activerecord-relationships.html)
但有一個差別在於includes所找到的model內的readonly屬性會是false,而joins所回傳的將是true。這造成了很大的影響:"用joins所找到的model不能被修改",所以當試圖修改model的值時,會回傳ActiveRecord::ReadOnlyRecord error.

2012年10月28日 星期日

[宅] 宅男臥軌日記(4) - class及instance的method定義

在定義class中的method時,要注意究竟是給instance使用還是class

當以如下方式定義method時,代表這個method是要給class的instance使用
class User
  def say_hello
    puts "Hello"
  end
end
所以當執行User.say_hello時就會發生找不到method的情況,但在以下情形會是正常的:
user = User.new
user.say_hello
=>"Hello"

所以如果一個method希望能被class所使用,必須以以下方式定義:
class User
  def User.say_hello
    puts "Hello"
  end
end
或是
class User
  def self.say_hello
    puts "Hello"
  end
end
當然也可以用extend從module中mixin進來