over 3 years ago

Rails 的 ORM 提供很便利的語法讓工程師可以輕鬆地對資料庫作查詢,但是在某些場景裡仍讓工程師們感到就有些美中不足,例如: 聯集(UNION)

假定系統需要一個 my tracking list 的頁面,這個頁面必需列出 manager 和 tracking product,並且針對它們去做更進一步的查詢操作,如列出 publish 的 product,你會怎麼設計?

似乎這一切聽起來令人頭疼,但看完後面的範例,你也能暸解實現聯集的作法。

product.rb
#  name         :string(255)

#  description  :text

#  status       :string(255)

#  manager_id   :integer

 
class Product < ActiveRecord::Base 
  belongs_to :manager, :foreign_key => :manager_id, :class_name => "User"
  
  has_many :tracking_lists
  has_many :tracking_users, :through => :tracking_lists, :source => :user  
end
user.rb
 
class User < ActiveRecord::Base
  has_many :products 
  
  has_many :tracking_lists
  has_many :tracking_products, :through => :tracking_lists, :source => :product
end
track_list.rb
#  id           :integer

#  user_id      :integer

#  product_id   :integer

 
class TrackList < ActiveRecord::Base
  belongs_to :user
  belongs_to :product
end
起初我們會想到 Array 的簡單作法
@products = Set.new
manage_products = @user.products.is_publish
tracking_products = @user.tracking_products.is_publish

@products.merge(manage_products)
@products.merge(tracking_products)

維護性: 低

  • (優) 寫起來快,幾乎不用思考就寫完了...
  • (劣) 需要下兩道 query,且無法合併一起查詢
  • (劣) Arrayinstance 無法做更進一步的查詢操作,例: order(:created_at)...etc 。
  • (劣) 速度比較慢

但我們始終覺得這樣的做法效率太差,且維護性非常的低...

然後我們想到 find_by_sql + left join 的作法
@products = Product.find_by_sql(["
    SELECT p0.* FROM product as p0 
    LEFT JOIN track_lists as t1 on t1.product_id = p0.id AND t1.user_id = p0.manager_id
    where p0.manager_id = ? AND p0.status = 'published';
    ", user.id])

維護性: 中

  • (優) 只有一道 query
  • (劣) 需以 product 的 query 條件為主,當 user 沒有被 assigned manage 某樣商品時,結果就會為空
  • (劣) 由於 find_by_sql 出來的結果是 Array,無法做更進一步的查詢操作,例: order(:created_at)...etc 。

然而這種解法還是不夠方便,且當有不同的 query 需求,例如: is_draft?,我們就必須再重寫一次 method。

最後我們發現可以這樣做 from + union
manage_products_sql = @user.products.to_sql
tracking_products_sql = @user.tracking_products.to_sql

@products = Product.from("( #{ manage_products_sql } UNION #{ tracking_products_sql } ) AS products").is_publish

維護性: 高

  • (優) 只有一道 query
  • (優) from 出來的結果是 ActiveRecord::Relation,我們可以續用 QueryMethods 做更進一步的查詢操作
  • (優) 簡單好維護

備註:

  • to_sql 只會產生 query 的字串,並不會實際去下 query
  • UNION 是 mysql 的聯集語法,且預設的 distinct 只會取一次相同的資料

參考資料 ActiveRecord::QueryMethods

 
over 3 years ago

RSpec 是 Rails 的其中一種測試框架,深受國外社群的愛戴,它的撰寫方式簡單易懂,非常好上手;語意化的設計讓測試代碼變得直觀,即便是初次學習的工程師也能夠迅速理解。

然而,台灣關於 RSpec 的文獻實在不多(找過比較完整的介紹是 ihower 的實戰聖經),加上 RSpec 還在發展中,不同派別的語法和 convention 很容易混在一起,要釐清它們並整理出一個最簡單可行的版本實在有些因擾。 但其實都沒想像中的困難,看完下面的敘述,你也能立刻寫出一個可以運行的測試代碼。

設定一個 RSpec 測試環境

由於 RSpec 不是 Rails 預設的 test framework ,在進行測試代碼開發前,我們必需先準備好測試環境。

安裝 rspec-rails 套件

在 Gemfile 裡將 rspec-rails 設定在 development 和 test 的 group 底下,並執行 bundle install 的指令,它會自動安裝好所需的套件。

Gemfile
group :development, :test do
  gem 'rspec-rails', '~> 3.0.0.beta'
end
建立 rspec-rails 基本設定

執行 rails generate rspec:install 的指令,它會自動新增 RSpec 所需的檔案和設定。

Terminal
#=> create  .rspec
#=> create  spec
#=> create  spec/spec_helper.rb
建立一個 test 專用的 database

在 database.yml 中設定 test db 的參數

database.yml
development: &default

  adapter: mysql2

  encoding: utf8

  database: blog_development

  host: localhost

  username: root

  password: ""



test: &TEST

  <<: *default

  database: blog_test

接著執行 rake db:test:prepare ,rake 會根據 schema 新增一個測試用的 database。

學會 RSpec 的基礎語法

RSpec 提供很多 sugar syntax 和 features 讓我們寫出漂亮的測試代碼,這些語法經常會對初學者的腦袋造成混亂。 其實只要幾個基本的語法,就可以寫出簡單的測試。 下面將以 board 為例,說明如何運用它們

board.rb
#  title         :string(255)

#  description   :text

 
class Board < ActiveRecord::Base
end

執行 rails g rspec:model board 指令,它會在 spec 的 model 資料夾底下新增一個 board 的 spec 檔案。

board_spec.rb
require 'spec_helper'

describe Board do
  pending "add some examples to (or delete) #{__FILE__}"
end
context 與 describe

context 和 describe 都是用來宣告 example group 的語法,一般會將 describe 當作測試 feature 的 group。

board_spec.rb
describe "#is_news?"  #=> 井號代表 instance method 的測試

describe ".find_by_title"  #=> 句號代表 class method 的測試

而 context 通常當作 condition 的 group。

context "when title is empty" 
context "when title is News" 
it

每個 it 都代表一個 test example,而測試代碼都必需在 it 的 block 裡宣告才會被執行 ; 在尚未填入 block 之前,it 只會列在 pending 的清單裡。

board_spec.rb
describe Board do
  describe "#is_news?"  do
    it "returns true when eq news"
  end
end

OUTPUT

expect && eq

expect 和 eq 是 test example 裡很常被用到的語法,通常是 expect(預期的對象).to eq(預期的結果)。
由於 blog_test 裡面完全沒有資料,我們必需先產生 Board 的資料再來驗證。

board_spec.rb
it "returns true when eq news" do
  @board = Board.create!(:title => "news")
  expect(@board.is_news?).to eq(true) #=> 在這裡也可以用 be_true 來表示

end

OUTPUT


當 example 測試跑完,RSpec 會顯示 example 的數量和失敗的數量,failures 等於 0 的情況才算測試成功。

學會判斷 RSpec 的測試失敗 message

稍早我們已經知道測試成功的 message,另一方面也必需了解如何從錯誤的訊息中判別,以下圖為例

Red 代表某些 example 失敗了

message 會將失敗的代碼上色並且提供相關的訊息,只要見到紅色的文字出現,就代表有 example 失敗了。

列出失敗的行數與目的

message 會提供失敗的代碼行數和該代碼的測試目的,讓 RD 能夠迅速理解代碼的邏輯。 如圖中的 describe,我們很清楚的知道,當 @board 是 news 的時候 is_news? 應該要 return true。

expect 的 value

message 會印出 expect 的真實 value,例如 @board.is_news? 預期會 return true,但實際上獲得的 value 卻是 false。

 
over 3 years ago

國外的 Rails 社群認為寫測試已經像是呼吸一樣再自然不過的事情,他們認為測試給工程師帶來許多好處,例如:可以即時驗證程式碼的結果、可以釐清程式設計的思考邏輯、增加網站的維護性等。

然而,DHH 認為任何測試都是需要成本的 (Testing like the TSA)

Every line of code you write has a cost. It takes time to write it, it takes time to update it, and it takes time to read and understand it. Thus it follows that the benefit derived must be greater than the cost to make it.

DHH

站在專案公司的角度來看,每個專案在時間與金錢有限之下,在時程內 deliver 網站並且上線已經是 First Priority 的目標,測試的程度和涵蓋範圍似乎從來不在考量之內,肉測 (肉眼測試、人體手動測試) 也一樣可以達到驗證的效果。 運氣好的人可以順利過關,運氣差一點的則陷入時程 delay 的泥沼裡。 為確保專案的維護性能夠維持在某個程度之上,我們應該集中火力針對某些情況寫測試。

單一行為串聯太多 association

我們以購物車下訂單的例子來看

# cart model

class Cart < ActiveRecord::Base
  has_many :cart_items
  has_many :cart_budnels
end

# order model

class Order < ActiveRecord::Base
 has_many :order_items
 has_many :order_budnels
end

當購物車的品項需要轉換成訂單的時候,此時必需根據cart 的 cart_items 資訊來新增 order 底下的 order_items
這行為可能的結果有:

  • 如果有 Cart Item,沒有Cart Bundle,就只新增 Order Item
  • 如果有 Cart Bundle,沒有Cart Item,就只新增 Order Bundle
  • 如果有 Cart Item、Cart Bundle,就兩個都新增
  • 如果沒有 Cart Item、Cart Bundle,就都不要新增

我們必需手動產生符合 四種結果 的資料來測試。

但萬一今天客戶需要加價購的功能呢?

# cart model

class Cart < ActiveRecord::Base
  has_many :cart_items
  has_many :cart_budnels
  has_many :cart_extra_purchases
end

# order model

class Order < ActiveRecord::Base
 has_many :order_items
 has_many :order_budnels
 has_many :order_extra_purchases
end

我們必需手動產生符合 更多不同的結果 的資料來測試。
假設後續客戶還需要修改相關的行為,又必需再用同樣耗時的手法測試。

單一行為需要判斷多重條件

我們以驗證 coupon 有無效果的例子來看

class Coupon < ActiveRecord::Base
  def self.valid?(code, amount)
    response = {:valid => false}
    
    coupon = find_by_code(code)
  
    if coupon
      if coupon.quantity > 0
        if (coupon.start_date.beginning_of_day...coupon.end_date.end_of_day).cover?(DateTime.now)
          if amount > coupon.price_minimum
            response = {:valid => true, :msg => "Coupon code valid"}
          else
            response = {:msg => "This coupon code has minimum price of NTD #{coupon.price_minimum}"}
          end
        else
          response = {:msg => "This coupon code has been expired."}
        end
      else
        response = {:msg => "This coupon code is not available."}
      end
    else
      response = {:msg => "This coupon code is not valid."}
    end
    
    response
  end  
end

當使用者結帳時填入 coupon ,系統必需驗證這個 coupon 的有效性。
這行為可能的結果有:

  • 如果沒有 coupon
  • 如果 coupon 數量不夠
  • 如果 coupon 過期
  • 如果 coupon 低於消費金額門檻
  • 如果 coupon 有效

我們同樣必需耗費許多時間手動產生符合 五種結果 的資料來測試。

API 的串接

系統需要串接第三方服務的時候

class AlipayController < ApplicationController
  def complete
    if params[:alipay].present?
      if Order.find([:alipay][:order_id])
        process_order_paying
        redirect_to checkout_complete_path, :notice => "支付寶 付款成功"
      else
        redirect_to checkout_failed_path, :notice => "支付失敗, 請與本站聯絡."
      end
    else
      redirect_to root_path, :notice => result[:msg]
    end
  end
end

系統得根據外部回傳的 response 進行結帳後的動作。
我們得把時間花在不斷地執行結帳流程來獲得 response 以便修改程式碼。

註:DHH 的 Testing like the TSA 另外談論到 7 點關於 test 的想法,有興趣的可以去看一下。

 
about 4 years ago

Proc 與 yield 的差別在於,proc 會將 block 給暫存起來,等到需要用的時候,再以 block.call 的方式呼叫出來,yield 則是自動執行 block.call 。

新增一個 proc object 的做法有兩種,proc & lambda。
  #Proc New

  my_proc = Proc.new { puts "tweet" }
  my_proc.call
  
  #Lambda

  my_proc = lambda { puts "tweet" }  #1.9以上版本也可以寫成 my_proc = -> { puts "tweet" }

  my_proc.call      
  
  #result

  "twitter"
Proc 也可讓你執行兩個以上的 block
  class Tweet
    def post(success, error)
      if current_user
        success.call
      else
        error.call
      end
    end
  end

  tweet = Tweet.new
  success = lambda { puts "Sent!"}
  error = lambda { puts "No user login"}
  tweet.post(success, error)
Proc 如果要轉換成 block
  tweets = ["First tweet", "Second tweet"]
  printer = lambda { |tweet| puts tweet }
  tweets.each(printer) # 會噴錯誤,因為each吃的是block

  tweets.each(&printer) # 加上 '&' 就可將proc轉換成block
而 proc 和 lambda 到底有哪些差異的地方?

Lambdas 會檢查傳入的參數,而Procs不會

  def puts_parameters(code)
    code.call(1, 2)
  end
  
  l = lambda { |a, b, c| puts "#{a} is a #{a.class}, #{b} is a #{b.class}, #{c} is a #{c.class}" }
  p = Proc.new { |a, b, c| puts "#{a} is a #{a.class}, #{b} is a #{b.class}, #{c} is a #{c.class}" }
  
  puts_parameters(l)
  #result

  "ArgumentError: wrong number of arguments (2 for 3)"
  
  puts_parameters
  #result

  "1 is a Fixnum, 2 is a Fixnum,  is a NilClass"

Lambdas 會直接skip return,而 Procs不會

  def return_using_proc
    Proc.new { return "Hi from proc!"}.call
    puts "end of proc call"
  end
  
  def return_using_lambda
    lambda {return "Hi from lambda!"}.call
    puts "end of lambda call"
  end

  return_using_proc
  #result

  "Hi from proc!"
  
  return_using_lambda
  #result

  "end of lambda call"
 
about 4 years ago
Yield

Yield的概念其實就是block.call,把block裡的內容帶到method中。

快速看以下例子就可以瞭解。

def call_this_block_twice
  yield
  yield
end

call_this_block_twice { puts "twitter" }

#result

"twitter"
"twitter"

當call_this_block_twice加上了{ puts "twitter" } 後,這個block就會被傳遞到method裡。

Yield也可以使用變數
def call_this_block
  arg = "twitter"
  yield arg
end

call_this_block { |arg| puts arg } 

#result

"twitter"

call_this_block { |arg| arg.upcase }

#result

"TWITTER"

加上變數後,block在被呼叫的時候就可以使用這個變數。

Yield也可以直接回傳value
def puts_this_block
  puts yield
end

puts_this_block { "twitter" }

#result

"twitter"

block裡即使只有純文字,也會被傳遞到yield。

利用yield讓code變得更簡潔
class Timeline
    def list_tweets
      @user.friends.each do |friend|
        friend.tweets.each { |tweet| puts tweet }
      end
    end
      
    def store_tweets
      @user.friends.each do |friend|
        friend.tweets.each { |tweet| tweet.cache }
      end
    end
end

list_tweets 和 store_tweets的code幾乎一模一樣,用yield可以做的更漂亮

class Timeline
  def each
    @user.friends.each do |friend|
      friend.tweets.each { |tweet| yield tweet }
    end
  end
end
    
timeline = Timeline.new(user)
timeline.each { |tweet| puts tweet }
timeline.each { |tweet| tweet.cache }
 
about 4 years ago
include 和 extend 的用法
  • include 讓module的method作用在實例變數上
  • extend 讓module的method作用在類別變數上
看以下兩個例子
include
module Foo
  def foo
    puts 'heyyyyoooo!'
  end
end
    
class Bar
  include Foo
end

include module Foo 後,foo只能使用在Bar的實例變數上

Bar.new.foo # heyyyyoooo!

Bar.foo # NoMethodError: undefined method ‘foo’ for Bar:Class
extend
module Foo
  def foo
   puts 'heyyyyoooo!'
  end
end
    
class Bar
  extend Foo
end

extend module Foo 後,foo只能使用在Bar的類別變數上

Bar.new.foo  # NoMethodError: undefined method ‘foo’ for #<Baz:0x1e708>

Bar.foo # heyyyyoooo!
 
about 4 years ago
Super主要是呼叫parent method的內容
  • 不帶括號的 super 會呼叫parent method 的內容
  • 帶括號的 super() 則是呼叫parent method 的內容,並且帶入括號內的參數
接下來看幾個例子
不帶括號的super
class Parent 
  def my_method
    puts "Parent is awesome"
  end
end
 
class Kid < Parent
  def my_method
    puts "Kid is awesome"
    super
  end
end

當呼叫 b 的method時,super會再次呼叫 a method的內容

kid = Kid.new
kid.my_method
 
#Output

"Kid is awesome"
"Parent is awesome"
帶括號的super()
class Parent 
  def my_method(family = "Father")
    puts "#{family} is awesome"
  end
end
 
class Kid < Parent
  def my_method(string = "Mother")
    
    puts "Kid is awesome"
    super(string)
  end
end

當呼叫 b 的method時,super會再次呼叫 a method的內容,並且帶入 b 的參數

kid = Kid.new
kid.my_method
 
#Output

"Kid is awesome"
"Mother is awesome"
 
about 4 years ago
Remote Repositories
git remote add origin git@github.com:onenight/try_git.git
Pushing Remotely
git push -u origin master
Pulling Remotely
git pull origin master
Cloning Repository
git clone repository_url 
Differences
git diff HEAD
Add To Stage
git add file_name
Add To Last Commit
git commit --amend -m "message"
Staged Differences (stage)
git diff --staged
Resetting the Stage
git reset file_name
Resetting Commit To Stage
git reset --soft HEAD^
Resetting all changes
git reset --hard HEAD^
Undo
git checkout file_name
Branching Out
git branch branch_name
Switching Branches
git checkout branch_name
Branching & Switching
git checkout -b branch_name
Branch Clean Up
git branch -d branch_name
Removing All The Things
git rm '*.txt'
Merge Branch
git merge branch_name
Delete Branch
git branch -d branch name
Delete Branch (Force)
git branch -D branch name
Pushing Branch To Remote
git push origin branch_name
Listing All Remote Branches
git branch -r
Remote Show
git remote show origin
Deleting A Remote Branch
git push origin :branch_name
Clean Up Deleted Remote Branches
git remote prune origin
Listing Tags
git tag
Checkout Code At Tag
git checkout tag_name
Adding A New Tag
git tag -a tag_name -m "message"
Pushing Tags
git push --tags
Rebase
  1. Move all changes to master which are not in origin/master to a temporary area
  2. Run all origin/master commits
  3. Run all commits in the temporary area, one at a time

    git rebase 
    

Configuration

Colorizing The Log
git config --global color.ui true
The Log
git log
One Line Log
git log --pretty-oneline
Log Format
  • %ad - author date
  • %an - author name
  • %h - SHA hash
  • %s - subject
  • %d - ref names

    git log --pretty-format:"%h %ad- %s [%an]"
    
Patch(Showing Log Changes)
git log --oneline -p
Stats(Showing Log State)
git log --oneline --stat
Graph(Showing Log As Graph)
git log --oneline --graph
Date Ranges
git log --until=1.minute.ago
git log --since=1.day.ago
git log --since=1.hour.ago
git log --since=1.month.ago --until=2.weeks.ago
git log --since=2000-01-01 --until=2012-12-21
Uncommitted Changes
git diff HEAD
git diff HEAD^
git diff HEAD~5
git diff HEAD^..HEAD
Earlier Commits
git diff SHAs..SHAs
git diff master branch_name
Blame(Showing history of commits)
git blame file_name --date short
Excluding Files
  • Adding folder/file_name to .git/info/exclude
Excluding From All Copies
  • Adding pattern to .gitignore
Removing Files
git rm file_name
Untraching Files
git rm --cached file_name
Config
git config --global user.name "user name"
git config --global user.email "user email"

Use opendiff for merging conflicts

git config --global merge.tool opendiff
Local Config

Sets email for current repo

git config user.email
git config --list
Aliases
git config --global alias.mylog \ 
"log --pretty=format: '%h %s [%an]' --graph"

git config --global alias.lol \
"log --graph --decorate --pretty=online --abbrev-commit --all"

git config --global alias.st status

git config --global alias.co checkout

git config --global alias.br branch

git config --global alias.ci commit