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



60fps scrolling using "pointer-events: none"



Reference: http://www.thecssninja.com/javascript/pointer-events-60fps

Eager & Lazy Loading

Eager Loading 就是事先將可能會用到的 resource 給準備好。
Lazy Loading 則相反,等到用到時才去fetch。

這兩個技術名詞聽起來沒什麼學問,但他代表了兩種不同的策略。何時該用哪一種是需要用經驗來判斷的,沒有孰好孰壞的問題。

2013年12月6日 星期五

用ngrok將公開網址指向你的localhost

在本機端開發網站的過程中,我們會遇到幾個情況:
要給夥伴看目前的進度
要接第三方的Oauth驗證
但如果是在家用adsl上網,會發生沒有實體ip可以接的窘境,這時我們可以使用 ngrok 的服務來產生一個公開的internet網址。

註冊Ngrok

當你沒有註冊會員時,只能隨機產生subdomain,例如:
ngrok 80 #http://580ef28f.ngrok.com

在你註冊過Ngrok的免費會員後,可以在後台取得authtoken,進而指定自己想要的subdomain:
ngrok -subdomain <your subdomain> -authtoken <your authtoken> 3000