きにきじ

今日の気になる記事とか学びとか

『パーフェクト Ruby on Rails』を読んだ

| Comments


 Ruby on Rails, , , Rails  Rails Ruby 2  Rails 4  Ruby  Rails 

9使1


1 Ruby on Rails 


Rails Rails 

 Mac  VMware Fusion  Linux OS  Windows 

2 Ruby on Rails  MVC


MVC Rails  MVC 

Rails  MVC  MVC 

使 Rails 4 Rails 3 4




Arel



ActiveRecord enums







StrongParameters





variants

HTML  raw 



3 


JavaScriptStylesheet Asset Pipeline 

 Asset Pipeline CoffeeScript  Sass 

2 Tip test.js.coffee.erb CoffeeScript  ERB 
test.js.coffee.erb
1
2
3
4
<% 5.times do |i| %>
  notice<%= i %> = ->
    alert("<%= i %>")
<% end %>

それと、アセット関連の読み込み改善ということで、Turbolinks についても触れられています。概要や注意点について書かれているので、こちらも入門用としてはよさそうです。

第4章 Rails のロードパスとレイヤーの定義方法

モデル、ビュー、コントローラ以外のレイヤーを追加する方法と、その際に必要になるロードパスの考え方について書かれた章です。ワーカーやデコレーターといったレイヤーの追加の仕方を gem を紹介しながら説明しています。

個人的にはこういう設計に関するところは興味があったのでおもしろかったです。以下はメモ。

lib/autoload ディレクトリをオートロードさせるには、config/application.rb に以下のように記述します。

config/application.rb
1
2
3
4
5
class Application < Rails::Application
  config.autoload_paths += %w[#{config.root}/lib/autoload)

  ....
end

Sidekiq gem で非同期処理


Sidekiq  gem 使Rails  Rails Redis 

 include Sidekiq::Worker  perform perform JSON 使

5  gem


使 gem 

Pry: irb  REPL 



ls: 

cd: 調

show-method: C

show-doc: C

byebug gem 


pry-rails 使Rails console  Pry 使


recognize-path:  action  controller 

show-middleware:  Rack Middleware 

show-model: 

show-models: 

show-routes: 

Hirb: 


ActiveRecord::Base  hirb-unicode gem 

Pry  Hirb  .pryrc 便

Better Errors: 


binding_of_caller gem 使便




 REPL 

Spring: 


rails  rake Windows 

Rails ERD: ER 


rake  erd Graphviz 使ER PDF Graphviz R 使

6 Rails 


 Rails RESTful Bootstrap OAuth Twitter Kaminari gem + kaminari-bootstrap gem 使ransack gem 使carrierwave gem 使



OAuth lvm.me

AbstractController::Helpers::ClassMethods#helper_method 使

./bin/rails g resources config/routes.rb  resources:events 

ActiveRecord::Base.find_by!  ! Rails  find_by_name  Dynamic Finder  find 

ransack gem 使 gem 1where 使 search  SQL  ransackable_attributes  ransackable_associations 使
1
2
Event.search(name_cont: "JavaScript")    # equal to Event.where("name like ?", "%JavaScript%")
Event.search(start_time_gteq: Time.now)  # equal to Event.where("start_time >= ?", Time.now)

carrierwave-magic gem 使 mime-type 









Rails 使


Rails 調Rails Web 使



7 Rails 


Rails 使TDDminitest  RSpec 1capybara gempoltergeist 使CIe.g., Brakeman gemrails_best_practices gemCode Climate 



















 RSpec 使 Spec 

RSpec 使 shoulda-matchers gem 使



assign @hoge = fuga  instance_variable_set 調

capybara 使


capybara 

capybara  selenium



 GUI capybara-webkit  poltergeist 





Database Cleaner gem DatabaseCleander.strategy poltergeist  truncation 使

describe 2 js: true capybara  JavaScript 使

8 Rails 


Vagrant + Chef 使Capistrano 使New Relic 使

/


Airbrake:  SaaS

Exception notification: 

Fluentd: Rails rack-common_logger-fluent gem 使便

Kibana: Fluentd 



9 使


Skinny Controllers, Fat Models Rals ActiveModel 使 RDB ActiveRecord 

Rails 





1






 ActiveRecord::Callbacks 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BankAccount < ActiveRecord::Base
  before_save      EncryptionWrapper.new("credit_card_number")
  after_save       EncryptionWrapper.new("credit_card_number")
  after_initialize EncryptionWrapper.new("credit_card_number")
end

class EncryptionWrapper
  def initialize(attribute)
    @attribute = attribute
  end

  def before_save(record)
    record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
  end

  def after_save(record)
    record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
  end

  alias_method :after_initialize, :after_save

  private
    def encrypt(value)
      # Secrecy is committed
    end

    def decrypt(value)
      # Secrecy is unveiled
    end
end

バリデーションの分離










ActiveModel::EachValidator 使 validates 


 validate_each 

validate_each 3

HogeValidator  Validator  underscore HogeValidator  hoge





lib/autoload/must_praise_validator.rb
1
2
3
4
5
6
7
class MustPraiseValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /すばらしい|さすが|感動|ありがとう/
      record.errors.add attribute, (options[:message] || "は必ず褒めてください。")
    end
  end
end
app/models/comment.rb
1
2
3
class Comment < ActiveRecord::Base
  validates :content, presence: true, must_praise: true
end

ActiveModel::Validator 1


 validate 

validate 

使 validates_with 


Event 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# == Schema Information
#
# Table name: events
#
# id          :integer     not null, primary key
# name        :string(255)
# start_time  :datetime
# finish_time :datetime
# created_at  :datetime
# updated_at  :datetime
#
class Event < ActiveRecord::Base
  validates_with RangeableValidator, if: [:start_time, :finish_time]
end

class RangeableValidator < ActiveModel::Validator
  def validate(record)
    unless start_time < finish_time
      record.errors.add :base, "終了時刻は開始時刻よりもあとにしてください。"
    end
  end
end

クラスを分離するメリット、デメリット










使


: 使

: 




 Concern 

ActiveModel: 


 ActiveRecord 便ActiveModel 使便













ActiveModel 

ActiveModel::AttributeMethods


ActiveModel::AttributeMethods  attr_accessor 

ActiveModel::AttributeMethods  include 


attribute_method_suffix

attribute_method_prefix

attribute_method_affix

define_attribute_methods

alias_attribute


 使
  • Person#upcase_first_name
  • Person#upcase_family_name
  • Person#upcase_first_name!
  • Person#upcase_family_name!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person
  include ActiveModel::AttributeMethods

  attribute_method_prefix "upcase_"
  attribute_method_affix  prefix: "upcase_", suffix: "!"
  define_attribute_methods :first_name, :family_name

  attr_accessor :first_name, :family_name

  def attributes
    {
      "first_name"  => @first_name,
      "family_name" => @family_name,
    }
  end

  private
    def upcase_attribute(attr)
      send(attr).upcase
    end

    def upcase_attribute!(attr)
      send("#{attr}=", upcase_attribute(attr))
    end
end

person = Person.new
person.first_name  = "Jonathan"
person.family_name = "Joestar"
person.upcase_first_name   #=> "JONATHAN"
person.upcase_family_name  #=> "JOESTAR"

ActiveModel::Callbacks


ActiveModel::Callbacks  before_save  after_create ActiveRecord  ActiveSupport::Callbacks 使

使 run_callbacks 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Hero
  extend ActiveModel::Callbacks
  define_model_callbacks :appear                   # `before_appear` と `after_appear` を定義
  define_model_callbacks :defeat, only: [:before]  # `before_defeat` のみ定義

  attr_accessor :skill_name

  before_appear :transform
  before_defeat :shout_skill_name
  after_appear :lose

  def transform
    puts "変身!!"
  end

  def appear
    run_callbacks :appear do
      puts "参上!!"
    end
  end

  def lose
    puts "何……だと……"
  end

  def shout_skill_name
    puts <<-EOS
_人人人人人人人人人_
>  #{skill_name}   <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
    EOS
  end

  def defeat(enemy)
    run_callbacks :defeat do
      puts "とどめだ、#{enemy.name}!!"
    end
  end
end

ActiveModel::Dirty

ActiveModel::Dirty を使うと、属性値の変化を追跡できるようになります。hoge_changed? などのメソッドが使えるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class User
  include ActiveModel::Dirty

  define_attribute_methods :password

  def password
    @password
  end

  def password=(value)
    password_will_change! unless value == @password
    @password = value
  end

  def save
    @previously_changed = changes
    @changed_attributes.clear
  end
end

user = User.new
user.password_changed?  #=> false
user.changed            #=> []
user.changes            #=> {}
user.password = "hoge"
user.password_changed?  #=> true
user.changed            #=> ["password"]
user.changes            #=> { "password" => ["", "hoge"] }
user.save
user.password_changed?  #=> false
user.changed            #=> []
user.changes            #=> {}

define_attribute_methods 使ActiveModel::Dirty  ActiveModel::AttributeMethods 

 hoge_will_change! 使 @changed_attributes  @previously_changed  changes 

ActiveModel::Naming


ActiveModel::Naming 使 model_name 使model_name  ActiveModel::Name  Rails  I18n 
1
2
3
class User
  extend ActiveModel::Naming
end

似たモジュールとして、以下のようなものもあります。

モジュール名 内容
ActiveModel::Translation I18n を利用するためのヘルパーメソッドを定義してくれる
ActiveModel::Conversion オブジェクトを URL のパラメータとして利用したりファイルパスの検索キーとして利用しやすい形に変換してくれる

ActiveModel::Serialization

ActiveModel::Serialization は、json や xml 形式でオブジェクトをシリアライズする機能を追加します。実際にシリアライズする機能は ActiveModel::Serializers::JSON と ActiveModel::Serializers::XML に定義されているので、利用する際はこちらを include して使うことになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Comment
  include ActiveModel::Serializers::XML

  attr_accessor :user, :article, :content, :created_at, :updated_at

  def attributes
    {
      "user"       => @user,
      "article"    => @article,
      "content"    => @content,
      "created_at" => @created_at,
      "updated_at" => @updated_at,
    }
  end

  def attributes=(hash)
    hash.each do |key, value|
      instance_variable_set("@#{key}", value)
    end
  end
end

ActiveModel::Serializer::JSON  from_json  to_json 使attributes  attributes=(hash) 

ActiveModel::Validations


ActiveModel::Validations 使ActiveRecord 使 ActiveRecord::Validations ActiveModel::Validations  ActiveRecord 使
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Article
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks

  attr_accessor :author, :category, :url, :title, :content, :created_at, :updated_at

  validates_presence_of :author
  validates :url, presence: true, format: { with: /\A[-_a-z0-9]*\z/ }

  before_save :set_title, unless :title?

  private
    def set_title
      self.title = url.gsub(/[^a-z0-9]+/, " ").split(" ").map(&:capitalize).join(" ")
    end
end

ActiveModel::Validations 使


validates_absence_of

validates_acceptance_of

validates_confirmation_of

validates_exclusion_of

validates_format_of

validates_inclusion_of

validates_length_of

validates_numericality_of

validates_presence_of

validates_size_of


before_validation 使 ActiveModel::Validations::Callbacks  include 

ActiveModel::Model


ActiveModel::Model  Rails 4  include  include  ActiveRecord 


ActiveModel::Naming

ActiveModel::Translation

ActiveModel::Validations

ActiveModel::Conversion


ActiveModel::Model  include  Rails  ActiveRecord 便



種類 意味
エンティティ システムにおいてオブジェクトの同一性が重要な意味を持つもの。Rails のモデルが持つ id のような識別情報を持つ。識別情報が同じなら同じエンティティ User(id, login_id, name, email, address) クラスのオブジェクト
値オブジェクト 「何である」かが重要で、値が同じであればアプリケーション上は同一とみなしていいもの。オブジェクトが持つ値が同じかどうかが同一性を決める メールアドレス、住所

ActiveRecord オブジェクトは基本的にはエンティティですが、その属性値の中には値オブジェクトとして扱うと便利なものもあります。

例えば以下のような、名前と(なぜか)和暦の日付を管理する Holiday クラスがあったとします(ねーよw)。日付は年号 era_name と年月日から成ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# == Schema Information
#
# Table name: holidays
#
# id         :integer     not null, primary key
# name       :string(255)
# era_name   :string(255)
# year       :integer
# month      :integer
# day        :integer
# created_at :datetime
# updated_at :datetime
#
class Holiday < ActiveRecord::Base
  def same_date?(other)
    return false unless other

    same_era_name?(other) && same_year?(other) && same_month?(other) && same_day?(other)
  end

  private
    def same_era_name?(other)
      era_name == other.era_name
    end

    def same_year?(other)
      year == other.year
    end

    def same_month?(other)
      month == other.month
    end

    def same_day?(other)
      day == other.day
    end
end

日付が同じかどうかを判別する機能を Holiday クラスに直接実装すると、↑のコードように Holiday クラスが持つ責任範囲が広くなりすぎてしまいます。また、例えば誕生日(Birthday)など他のクラスにも日付を持つオブジェクトが存在するかもしれません。

このような場合、値オブジェクトとして日付を表現すると便利です。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Holiday < ActiveRecord::Base
  def date
    @date ||= JapaneseCalendarDate.new(era_name, year, month, day)
  end

  def date=(date)
    self.era_name = date.era_name
    self.year     = date.year
    self.month    = date.month
    self.day      = date.day
    @date = date
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class JapaneseCalendarDate
  attr_accessor :era_name, :year, :month, :day

  def initialize(era_name = nil, year = nil, month = nil, day = nil)
    @era_name = era_name
    @year     = year
    @month    = month
    @day      = day
  end

  def hash
    era_name.hash + year.hash + month.hash + day.hash
  end

  def ==(other)
    return false unless other.kind_of?(JapaneseCalendarDate)

    same_era_name?(other) && same_year?(other) && same_month?(other) && same_day?(other)
  end

  private
    def same_era_name?(other)
      era_name == other.era_name
    end

    def same_year?(other)
      year == other.year
    end

    def same_month?(other)
      month == other.month
    end

    def same_day?(other)
      day == other.day
    end
end

このように値オブジェクトを使うことで、JapaneseCalendarDate というコンパクトな範囲に責任を限定し、実装を適切な場所に移すことができました。もし日付を扱うクラスが増えても同じ実装を再利用すれば OK です。

値オブジェクトは、責任を適切に分割することに加えて、業務知識の語彙を実装にマッピングする点でも意味があります。これにより実装と業務知識のモデルの差を小さくできます。

なお、Rails では compose_of メソッドを使うことで簡単に値オブジェクトを扱うことができます。さきほどの Holiday クラスの例では以下のような実装になります。

1
2
3
class Holiday < ActiveRecord::Base
  compose_of :japanese_calendar_date, mapping: [%w[era_name era_name], %w[year year], %w[month month], %w[day day]]
end

compose_of はいくつかオプションをとれます。本書には各オプションの解説と以下のような IP アドレスの例も載っています。

1
2
3
4
5
compose_of :ip_address,
  class_name: "IPAddr",
  mapping: %w[ip to_i],
  constructor: Proc.new{|ip| IPAddr.new(ip, Socket::AF_INET) },
  converter: Proc.new{|ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }

compose_of 使

Concern: 


Rails 4  app/models/concerns  app/controllers/concerns Concern 

 Concern 

ActiveRecord  gem  gem  Concern 使便

ActiveSupport::Concern


Rails 使 ActiveSupport::Concern  Concern 


included include 






便orz

ActiveSupport::Concern 




ActiveSupport::Concern 

ActiveSupport::Concern 使

 Concern 

Module#concerning


Rails 4.1  Module#concerning 使
1
2
3
4
5
6
7
8
9
class User < ActiveRecord::Base
  concerning :Segment do
    included do
      scope :male, ->{ where(sex: "male") }
      scope :female, ->{ where(sex: "female") }
      scope :young, ->{ where("age < ?", 20) }
    end
  end
end

Module#concerning  concerning  


/



 module Hoge; ....; end; include Hoge 

Module#concerning 使


1使




Rails 












 Rails  Ruby  Rails  app/services  autoload_paths 




1





















10 Rails 


Rack Middleware  Railtie  Rails 

Rack Middleware


Rack  Ruby Python  PSGI Rack 


1 Hash env call 

call  Hasheach 3


Rack Middleware Rack  Rack Middleware 


 Rack 1 initialize 

Rack env  call 


ru use  Rack Middleware  use 

Rails  Rack Middleware 使config/application.rb  config/environments/*.rb  use 
config/application.rb
1
2
3
4
5
module AwesomeEvents
  class Application < Rails::Application
    config.middleware.use AwesomeMiddleware
  end
end

有名な Rack Middleware としては以下のようなものがあります。

名前 内容
Rack::Auth::Digest Digest 認証をかける。Rack gem に標準添付
Rack::Cors Cross-Origin Resource Sharing(ドメインをまたいだ Ajax リクエスト)を実現するために必要なヘッダーを追加してくれる
Rack::Rewrite アクセスされた URL を別のものに変換する
OmniAuth さまざまな外部認証の仕組みとの連携を Rack のレイヤーで済ませてしまう

Railtie


Railtie Rails 3 Railtie 使




 Rake  Rails 

Rails environments 

ActiveSupport::Notifications 


Railtie 3
特徴
Railtie 型 Railtie が提供する基本的な機能のみを利用する
Engine 型 Railtie 型に加えて独自のコントローラやルーティング、モデルなどを提供する DeviseDoorkeeper
Mountable Engine 型 Engine 型よりもさらに独立性の高いアプリケーションをプラグインとして提供する RailsAdminRefinery CMS

Railtie  rails plugin new hoge 

 lib/hoge/railtie.rb  Rails  eager_load_namespaces Rails  initializer ActiveSupport.on_load 使Rails 

Engine  rails plugin new hoge --full  --full 

 lib/hoge/engine.rb OK
lib/hoge/engine.rb
1
2
3
4
module Hoge
  class Engine < ::Rails::Engine
  end
end

Mountable Engine  rails plugin new hoge --mountable  --mountable 

 Engine  Engine Hoge  Rails 
1
2
3
4
5
module Hoge
  class Engine < ::Rails::Engine
    isolate_namespace Hoge
  end
end

コントローラなども app/controllers/hoge/*_controller.rb のようにネームスペース以下に生成されます。

他の Rails アプリで使う場合、config/routes.rb で以下のように mount 宣言をします。

1
mount Hoge::Engine => "/status"

Comments