21 days ago

Someone got some trouble "no implicit conversion of Hash into String" after upgraded properties field from json to jsonb type in Ahoy. Or you can follow this issue#86 or issue#120.

There is a easy way to fix it. Just add following content in ahoy related class:

# config/initializers/ahoy.rb


class Ahoy::Store < Ahoy::Stores::ActiveRecordStore
  def track_event(name, properties, options, &block)
    event =
      event_model.new do |e|
        e.id = options[:id]
        e.visit_id = ahoy.visit_id
        e.user = user if e.respond_to?(:user=)
        e.name = name
        e.properties = properties.to_json # the keypoint

        e.time = options[:time]
      end

    yield(event) if block_given?

    begin
      event.save!
    rescue *unique_exception_classes
      # do nothing

    end
  end
end

The key point is the track_event, just override it and convert the type for properties assignment.

 
3 months ago

外媒表示:Google 默默開始了 Google Cloud Functions 產品,一個要與 AWS Lambda 相較的服務,雖然還在 Alpha 版。

“Google Cloud Functions is a lightweight, event-based, asynchronous compute solution that allows you to create small, single-purpose functions that respond to cloud events without the need to manage a server or a runtime environment”

不過目前只支援 Node.js 的模組,也就是只能用 javascript 去撰寫,不像 AWS Lambda 還有 java 與 python 可以選擇。

Microsoft 的 Azure 聽說也正在開發,拭目以待

 
3 months ago

Concerns 是 Rails 拿來做模組化的一個方式,在 Rails4 之後也正式在 app/models/ 內有了 concerns 這麼一個資料夾的一席之地,也是一種 Rails Convention。

把重複的 Model 程式碼搬到 Model Concerns 內

留言功能在臉書不斷的演進下,似乎已經成為現在網站的一個基本盤功能,什麼內容都可以討論一下。Rails4 Patterns 裡面拿這個當做範例,覺得蠻適合的。

# app/models/post.rb

class Post < ActiveRecord::Base
  has_many :comments, as: :commentable
  def comments_by_user(id) # 重複了

    comments.where(user_id: id)
  end
end

# app/models/image.rb

class Image < ActiveRecord::Base
  has_many :comments, as: :commentable
  def comments_by_user(id) # 重複了

    comments.where(user_id: id)
  end
end

# app/models/comment.rb

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

以上方來說利用了 Rails 內的 Polymorphic Association 的特性來做到文章跟圖片都可以留言,但也因如此,造成文章與圖片的 Model 出現重複程式碼在所難免。
若是包成 Concern 的方式來做,不只可以重複使用、避免重複程式碼,更可做到模組化。

# app/models/concerns/commentable.rb 

module Commentable
  def self.included(base) # 這邊還可以更優化

    base.class_eval do
      has_many :comments, as: :commentable
    end
  end

  def comments_by_user(id)
    comments.where(user_id: id)
  end
end

# app/models/post.rb

class Post < ActiveRecord::Base
  include Commentable
end
# app/models/image.rb

class Image < ActiveRecord::Base
  include Commentable
end

純以 Ruby 寫法來說這樣包成 Module 完全沒有問題,但若善用 Rails 內建的 Library 會更好

# app/models/concerns/commentable.rb

module Commentable
  extend ActiveSupport::Concern # 提供了 included 等的方法,更方便將 Module 包裝成 ActiveRecord 的擴充

  
  included do
    has_many :comments, as: :commentable
  end
  
  module ClassMethods
  # 因為 ActiveSupport::Concern 的關係,讓這裡面的 ClassMethods 可以直接變成引用的 class 內的 class method

  # 像是 Image.upvote(@comment)

    def upvote(comment)
      # ...

    end
  end
  
  def comments_by_user(id)
    comments.where(user_id: id)
  end
end

Controller 一樣也有 Concerns

相似性質的 Resource 操作也容易出現重複程式碼

# app/controllers/images_controller.rb

class ImagesController < ApplicationController
  def show
    @image = Image.find(params[:id])
    file_name = File.basename(@image.path) # 重複了

    @thumbnail = "/thumbs/#{file_name}"
  end
end

# app/controllers/videos_controller.rb

class VideosController < ApplicationController
  def show
    @video = Video.find(params[:id])
    file_name = File.basename(@video.path) # 重複了

    @thumbnail = "/thumbs/#{file_name}"
  end
end

包成 Controller Concern

# app/controllers/concerns/previewable.rb

module Previewable
  def thumbnail(attachment)
    file_name = File.basename(attachment.path)
    "/thumbs/#{file_name}"
  end
end

# app/controllers/images_controller.rb

class ImagesController < ApplicationController
  include Previewable
  def show
    @image = Image.find(params[:id])
    @thumbnail = thumbnail(@image)
  end
end

# app/controllers/videos_controller.rb

class VideosController < ApplicationController
  include Previewable
  def show
    @video = Video.find(params[:id])
    @thumbnail = thumbnail(@video)
  end
 end

從這些範例與程式碼演進過程,也可以知道一個小知識。就是 concerns 跟 ActiveSupport::Concern 是不一樣的,concerns 是指 Rails4 裡面將重複程式碼模組化的資料夾位置,ActiveSupport::Concern 是從 Rails3 開始,當做是 module 包成擴充可使用的方法。

 
3 months ago

Extracting Queries

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def index
    @posts = Post.where('published = ? AND published_on > ?', true, 2.days.ago)
  end
end

這是 controllers 內會常見的程式碼,以結果來說完全沒問題,但考量到維護性與可讀性,有可以改進的地方:

  • 在 controllers 內做了太多細節
  • 容易產生不必要重複的程式碼
  • 測試較難撰寫

改進後

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def index
     @posts = Post.recent
  end
end

# app/models/post.rb

class Post < ActiveRecord
  def self.recent
    where('published = ? AND published_on > ?', true, 2.days.ago)
  end
end

這是標準改進作法,使用了 ActiveRecord 的 scope 特性,有其他作法嗎?有的,可以使用以往 Class Methods 的方式

#app/models/post.rb

class Post < ActiveRecord
  def self.recent
    where('published = ? AND published_on > ?', true, 2.days.ago)
  end
  
  def self.by_author(author)
    where(author: author)
  end
end

一樣都是可以被串連起來使用的沒有問題!

author = "Carlos"
Post.by_author(author).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."author" = 'Carlos' AND (published_on > '...')
Read on →
 
3 months ago

今天起春節希望每天介紹 Rails4 Patterns 內的一個章節給大家,這是一個在 Code School 的課程,原址:https://www.codeschool.com/courses/rails-4-patterns

肥肥的 Controller 是不好的

原因有幾個:

  • 會難以理解
  • 商業邏輯難以統整
  • 程式碼間容易衝突
  • 難以開發新功能

像是以下程式碼

class ItemsController < ApplicationController
  def publish
    if @item.is_approved?
      @item.published_on = Time.now

      if @item.save
        flash[:notice] = "Your item published!"
      else
        flash[:notice] = "There was an error."
      end
    else
      flash[:notice] = "There was an error."
    end

    redirect_to @item
  end
end

當時間的推進,應用的發展,條件式只會越來越複雜

if @item.is_approved? # 原先的條件式

# ->

if @item.is_approved? && @iterm.user != "Loblaw" # 新增條件

這裡切記一個原則:「Tell, Don't Ask」,應該要直接的告訴物件它要做什麼事情。而不是透過問句的方式去確認目前它的狀態是什麼後,再決定作什麼事情。

商業邏輯該丟到 Models 內

如剛剛的 controller,我們可以將邏輯性的功能包裝丟至 model 內,那對 controller 而言就可以符合 Tell, Don't Ask 原則。

# app/controllers/items_controller.rb

class ItemsController < ApplicationController
  def publish
    if @item.publish # 明確知道這邊的動作是將 item 發佈

      flash[:notice] = "Your item published!"
    else
      flash[:notice] = "There was an error."
    end

    redirect_to @item
  end
end
# app/models/item.rb

class Item < ActiveRecord::Base
   belongs_to :user

   def publish
     if !is_approved? || user == 'Loblaw'
       return false
     end

     self.published_on = Time.now
     self.save
   end
end

函式為確保是簡潔與可讀性的程式碼,特別將 if-else 的邏輯單純化,如果一個 if 就能乾淨俐落,為什麼還要來個 else,大家都愛直線,不要拐彎抹角阿!

所以這邊才會是

if !is_approved? || user == 'Loblaw'
  return false
end
...

而非

# wrong case

if is_approved? || user != 'Loblaw'
  ...
else
  return false
end

避免在 Callbacks 中呼叫其他功用的物件

因為 callback 本身就像是一個物件生命週期的一個環節,如果牽扯到其他的物件,不只讓人在看程式碼會覺得有點跳,甚至因為交流密切,間接影響到物件的資料庫運作週期。

class User < ActiveRecord::Base
  before_create :set_token

  protected
    def set_token
      self.token = TokenGenerator.create(self) # another class object

  end
end
為此建立一個客製化的函式
# app/models/user.rb 

class User < ActiveRecord::Base
  def register # 另外拉一個函式,專作註冊

    self.token = TokenGenerator.create(self)
    save
  end
end

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.register # 使用客製化的函式

      redirect_to @user, notice: 'Success'
    else
      ...
    end
  end
end
Callbacks 應該是用來作為更改欄位值或其他內在因素為主
# app/models/user.rb

class User < ActiveRecord::Base
  before_create :set_name

  protected
    def set_name
      self.name = self.login.capitalize if name.blank?
    end
end

也要特別留意,callbacks 放在 protected 內是 Rails 的 Convention,務必要遵守

不是所有的 Models 都一定要是 ActiveRecord

以被檢舉的使用者需要的停權動作為例,裡面牽扯到許多要考慮的層面,例如停權後就不能發言、不能登入等等

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  def suspend
    @user = User.find(params[:id])
    @user.suspend!
    
    redirect_to @user, notice: 'Successfully suspended.'
  end
end

# app/models/user.rb

class User < ActiveRecord::Base
  has_many :items
  has_many :reviews
  
  def suspend!
    self.class.transaction do # transaction 包起來可以讓這些動作對於資料庫來說只是一件事情

      self.update!(is_approved: false)
      self.items.each { |item| item.update!(is_approved: false) }
      self.reviews.each { |review| review.update!(is_approved: false) }
    end
  end
end

以上例來說,問題是同個函式內包含太多的邏輯

同個 class 裡面太多邏輯,可以拆成各自負責的工作。也要注意函式牽扯到太多其他的物件,可能會導致「God Object」現象發生,就是指作太多事情的意思。
class User < ActiveRecord::Base
  ...
  def suspend!
    self.class.transaction do
      disapprove_user!
      disapprove_items!
      disapprove_reviews!
    end
  end
  
  def disapprove_user!
  def disapprove_items!
  def disapprove_reviews!
end

抽象化:不是所有跟使用者有關係就得都在使用者的 Model 內

以停權使用者這件事情來說,抽象化有幾個好處:

  • PORO: Plain Old Ruby Object
  • 只需一個正規的 instance method
  • 這個類別就只需要專注一件事情就好
# app/models/user_suspension.rb

class UserSuspension
  def initialize(user)
    @user = user
  end

  def create!
    @user.class.transaction do
      disapprove_user!
      disapprove_items!
      disapprove_reviews!
    end
  end
  
  private
    def disapprove_user!
    def disapprove_items!
    def disapprove_reviews!
end

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  def suspend
    @user = User.find(params[:id])
    suspension = UserSuspension.new(@user)
    suspension.create!
    
    redirect_to @user, notice: 'Successfully suspended.'
  end
end

其他常見的抽象化:

  • UserRegistration
  • PlanSubscription
  • CreditCardCharge
  • ShoppingCart

小結一下,所以常會使用非 ActiveRecord 的方式將獨立且唯一的商業邏輯搬出來成一個新的類別。

 
7 months ago

把 assets 包成 rails 的 gem 非常簡單,只要簡單4個步驟!(更多 rails assets gems 請參照 rails-assets

1. Create Gem Framework

bundle gem  momentjs-rails

2. Start Your Engine

bundler 產生的 gem 是一個標準 ruby 模組,但我們希望他是 rails engine

# lib/momentjs-rails.rb


require "momentjs-rails/version"

module Momentjs
  module Rails
    class Engine < ::Rails::Engine
    end
  end
end

3. Add The Assets

下載需要放進 gem 的 assets,建議放沒有壓縮也沒有編譯過的版本,因為可以讓 assets-pipeline 來幫我們做壓縮就好。

mkdir -p vendor/assets/javascripts
cp ~/Downloads/moment.js vendor/assets/javascripts/moment.js

補充說明:app/assets, lib/assets, vendor/assets 有什麼不同?

  • app/assets: 一般在開發調整的 js, css(會一直修正的)
  • lib/assets: 自有專案內打包模組化的 js, css(例如:backme-bootstrap.js)
  • vendor/assets: 第三方的 js, css,不會去變動的(例如:bootstrap.js)

4. Complete The Gemspec

裡面的 TODOs 都要解決,不然會沒辦法成功的編譯成 gem,然後拿掉 spec.executables 跟 spec.test_files 這兩行,因為裡面沒有 .rb 檔案需要被執行,只是單純打包起來,所以用不到。

把 spec.files 改掉:

spec.files = Dir["{lib,vendor}/**/*"] + ["MIT-LICENSE", "README.md"]

接著再下 gem build momentjs-rails.gemspec 即可完成。

實際使用

# Gemfile

gem "momentjs-rails", path: "{your gem file directory}"

# app/assets/javascripts/application.js

//= require moment

外譯:Gemify Assets for Rails

 
7 months ago

awesome-rails-gem

前陣子整理的 gem 上了 rubyweekly,也算是生活中的小確幸吧!當時只是單純的想法,簡單整理了一下去年一路走來,在開發 rails 專案上學到的這些 gem,也沒想過會變成這樣 XDD

過去因緣際會碰過不少程式語言,但這些過往,確也成為去年開始重新接觸 Ruby on Rails 這個框架的意識阻礙,嘗試用過去一些功能撰寫上的 tricky 想法理解這個框架背後的設計。直到一再又一再的發現很多 gem(前人做過 best practice 的可再利用套件),一再又一再的改架構,覺得怎麼寫更好的過程,有種驚醒將過去意識拋下重新體會 Rails 的藝術。

或許這些 gem 整理沒辦法表象 Rails 的設計巧思(大概得從 rebuilding rails 這本電子書中才能深刻體會),但卻能讓有些許經驗的人可以知道有哪些武器可以使用,建構相對較完整的 Rails 網站。

Reference:
RubyWeekly Issue#265
Screen Shot 2015-09-25 at 12.33.39 PM.png

 
7 months ago

專案不斷的成長,需要更多的是模組化的開發與管理,在 Rails 中的模組是 gem,但一般我們常用 gem source 是 rubygems,不過這是 public 給大家都可以使用的,私密的、公司機密的該怎辦?

Gemfury

Gemfury 是一個供 public 與 private gem 存放與串接的服務,可以如一般我們在使用 rubygems 這個 source 一樣,只差在網址的部份,並且也完整支援 gem 的 deployment。

使用方法很簡單,只要申請完把 gem 丟上去就可以,如範例的 Gemfile(主要修改 source):
Screen Shot 2015-09-21 at 1.53.41 PM.png

  • 若想要自己建一個 Gemfury,可以參考 heroku 推出的 gemgate
  • 若想要簡單點透過 git token 方式安裝 private gem,可以參考這個 gist

Rails Assets

rails-assets 是 Rubygem 版的 bower,而 javascript 與 bower 的關係就像是 ruby 與 rubygem 的關係。使用方法就像裝 gem 一樣,然後就直接可以在 js 中 require 了:
Screen Shot 2015-09-21 at 4.31.34 PM.png
Screen Shot 2015-09-21 at 5.03.32 PM.png

 
8 months ago

In this year, we go through over 10 rails projects. Rails is a awesome framework design for web development. And the community grows stronger.

In the initial stage, we often reconstruct and redesign db schema on our projects, because we found feature related gems in our troubleshooting process.

So we just collect those gems we ever found and felt awesome after trying. Hope other rails developers can easily to find useful gems before starting development. This list called "rails-gem-list".

Now this gem have those sections as below:

  • User
  • Active Record
  • Plugins
  • API
  • File Upload
  • Search
  • Scheduled/Recurrence Jobs
  • View Helper
  • Environment Variables
  • Admin Panel
  • Logging
  • Debug
  • Coding Style
  • Testing
  • Production

link: https://github.com/hothero/rails-gem-list

If you feel useful for your, please give us your star. :D

 
8 months ago

正常在實作 Rails 的 show action 時都會是:

@post = Post.find(params[:id])

根據傳入的 id 用 find 去找到物件,但若沒有這個 id 的紀錄呢?就會跑出 ActiveRecord::RecordNotFound 的錯誤,是錯誤喔不是 warning 而已

但其實這事情也不是挺嚴重,可能就不小心打錯 ID 或是被爬蟲亂爬導致,不算是 bug,只是沒有特別處理(尤當裝 rollbar 後,一直累計噴錯也不是辦法...)

那 rescue_from 是 ActiveController 裡面非常有用的一個函式,可以在一個 controller 的範圍內將某個 error 都 catch 起來統一處理,尤其是 ActiveRecord::RecordNotFound 這個~

class PostController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
  
  private
  def record_not_found
    message = "Post with ID #{params[:id]} not found."
    logger.error message
    redirect_to not_found_url, info: message
  end
end

依照上述作法,當遇到 ActiveRecord::RecordNotFound 時就會自動記錄哪個 id 找不到 Post,並 redirect 到相對應告知找不到記錄的頁面。當然 rescue_from 本身也有 block 供使用,可以改成:

class PostController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found do |exception|
    message = "Post with ID #{params[:id]} not found."
    logger.error message
    redirect_to not_found_url, info: message
  end
end

如果是一般自己寫的 class 想要 handle 特殊例外狀況的話~可以直接 mixin ActiveSupport::Rescuable 就好

class Foo
  include ActiveSupport::Rescuable
end