2014年1月1日 星期三

vagrant 初體驗與故障排除

1. 30 秒認識 vagrant:

Vagrant 是一個 ruby 寫的工具,它是一個 DSL 讓開發者可以輕易控制 VirtualBox 的 VM 。用它可以輕鬆管理和制作我們理想中的開發環境。
  $ gem install vagrant #安裝 vagrant
  $ vagrant box add ubuntu http://cloud-images.ubuntu.com/vagrant/quantal/current/quantal-server-cloudimg-i386-vagrant-disk1.box #安裝新的 Vagrant Package。這裡的 ubuntu 是一個預先做好的空的 ubuntu 12.10 (intel-based)
  $ vagrant init ubuntu
  $ vagrant up
box 檔可以在 http://www.vagrantbox.es/ 下載


2. 設定:

此時就可以使用 vangrant ssh 連到vm了,預設使用者/密碼為 vangrant/vangrant
接著編輯本地資料夾中的 Vagrantfile,修改 config.vm.network :hostonly, "33.33.33.33" 將ip改成自己想要的ip,這裡以 "33.33.33.33" 為例。修改完後要執行 vangrant reload


3. 故障排除:


無法以 SSH 連接 VM:

[default] Failed to connect to VM!
Failed to connect to VM via SSH. Please verify the VM successfully booted
by looking at the VirtualBox GUI.
當出現上面的錯誤訊息時,可以照 http://vagrant.wikia.com/wiki/Usage 上的步驟來排除,基本上就是將 Vagrantfile 中的 config.vm.boot_mode = :gui 設定打開。再重新 vagrant up,在產生的 GUI 中輸入 sudo dhclient eth0


Guest additions not matchs:

[default] The guest additions on this VM do not match the install version of
VirtualBox! This may cause things such as forwarded ports, shared
folders, and more to not work properly. If any of those things fail on
this machine, please update the guest additions and repackage the
box.


Reference:

http://reality.hk/posts/2011/12/21/vagrant
http://gogojimmy.net/2013/05/26/vagrant-tutorial/


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月24日 星期二

Rails 的 Assets Convension

Rails 開發者應該都知道 assets 的路徑原則

  1. app/assets/ 放自己的 assets
  2. vendor/assetes/ 放第三方assets


但是在 app/assets 底下無法 require_tree vendor底下的資料夾,只能 require 單一檔案。要達到同樣的效果只要 require vendor的檔案,這個檔案再 require_tree 即可。

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 腳本來設置資料庫。