Ruby
Rails

Rails早くいってよなアレコレ

はじめに

Railsで書いてると、便利なRailsAPIやGemを知らないがために、

早くいってよぉ〜

と思うことが多々ありました。

後続の方の参考になればと、ここに残しておきます ;)

目次

  1. Enum
    1. ActiveRecord::Enum
    2. Gemenum_help
  2. Decoratorパターン(Gemactive_decorator)
  3. ValueObject
    1. Struct.newActiveRecord::Aggregations#composed_of
    2. ActiveRecord::Type

Enum

ActiveRecord::Enum

Before

#  status :integer not null, default 0
class Conversation < ActiveRecord::Base
  STATUS_ACTIVE = 1
  STATUS_ARCHIVED = 0
  def toggle_status
    if status == STATUS_ACTIVE
       status = STATUS_ARCHIVED
    else
       status = STATUS_ACTIVE
    end
    save!
  end    
end

After

#  status :integer not null, default 0
class Conversation < ActiveRecord::Base
  enum status: { active: 1, archived: 0 }
  def toggle_status
    if active?
       archived!
    else
       active!
    end
  end    
end

Other Usages

# conversation.update! status: 0
conversation.active!
conversation.active? # => true
conversation.status  # => "active"

# conversation.update! status: 1
conversation.archived!
conversation.archived? # => true
conversation.status    # => "archived"

# conversation.status = 1
conversation.status = "archived"

conversation.status = nil
conversation.status.nil? # => true
conversation.status      # => nil

Conversation.statuses[:active]    # => 0
Conversation.statuses["archived"] # => 1

Gemenum_help

setup

Gemfile

gem 'enum_help'

config/locales/ja.yml

ja:
  enums:
    conversation:
      status:
        active: アクティブ
        archived: アーカイブ済み

usage

conversation.status    # => "active"
conversation.status_i18n # => "アクティブ"

conversation.archived!
conversation.status    # => "archived"
conversation.status_i18n # => "アーカイブ済み"

Conversation.statuses_i18n.invert
# => {"アクティブ"=>"active", "アーカイブ済み"=>"archived"}

Drawing Checkboxes

conversations/_form.slim

= form_for @conversation do |f|
  .form-group
    = f.label :status, class: 'control-label required'
    .
      - Conversation.statuses_i18n.invert.each do |state|
        label.radio-inline
          = f.radio_button :status, state[1], class: 'optradio'
          = state[0]

good/bad points

  • マジックナンバーを意味あるメソッドでラップできる。
  • 慣れないとFormでいじる時などハマるポイントはあるっちゃーある。
  • デメリットは少ないので基本的に使うべきだと思う

参考記事

ActiveRecordのenumで気をつけたい3つのポイント

Decorator

active_decorator
https://github.com/amatsuda/active_decorator

Before

index.slim

- @remittances.each do |remittance|
  .row
    .col-md-3
      - if remittance.approved?
        smal.label.label-info
          = fa_icon('check-square-o', text: status_i18n)
      - else
        smal.label.label-warning
          = fa_icon('exclamation-triangle', text: status_i18n)
      h4 = remittance.mansion_name

After

remittance_decorator.rb

module RemittanceDecorator
  def status_label
    if approved?
      content_tag(:small, class: 'label label-info') do
        concat fa_icon('check-square-o', text: status_i18n)
      end
    elsif unapproved?
      content_tag(:small, class: 'label label-warning') do
        concat fa_icon('exclamation-triangle', text: status_i18n)
      end
    end
  end
end

index.slim

- @remittances.each do |remittance|
  .row
    .col-md-3
      = remittance.status_label
      h4 = remittance.mansion_name

good/bad points

  • view側では「ステータスのラベルを出したい」という要求のみを書くだけで良い。
    • slimテンプレートからロジックを排除できる
  • ぶっちゃけReactなりVueなりを入れればもっと綺麗にかける
    • 止むを得ずrubyのテンプレートエンジンを使うときの逃げ道

参考記事

http://tech.gmo-media.jp/post/91900069058/rails-presenter-decorator

ValueObject

Struct.newActiveRecord::Aggregations::ClassMethods#composed_of

Before

# first_name:string, middle_name:string, last_name:string
class Person < ApplicationRecord
  def full_name
    if middle_name
      "#{first_name} #{middle_name} #{last_name}"
    else
      simple
    end
  end
  def simple_name
    "#{first_name} #{last_name}"
  end
end

After

class Name < Struct.new(:first, :middle, :last)
  def full
    to_s
  end
  def simple
    "#{first} #{last}"
  end
  def to_s
    if middle
      "#{first} #{middle} #{last}"
    else
      simple
    end
  end
end

# first_name:string, middle_name:string, last_name:string
class Person < ApplicationRecord
  composed_of :name, mapping: [%w[first_name first],
                               %w[middle_name middle],
                               %w[last_name last]]
end

Usage

p = Person.create(name: Name.new('Emma','Charlotte', 'Watson'))
p.attributes # => {"id"=>4, first_name: "Emma", middle_name: "Charlotte", last_name: "Watson"}
p.name.simple # => "Emma Watson"
p.name.full # => "Emma Charlotte Watson"
Person.find_by(name: Name.new('emma', 'charlotte', 'watson')) # => #<Person:0x007fd9c4204f90 id: 5, first_name: "Emma", middle_name: "Charlotte", last_name: "Watson"}

good/bad points

  • Good
    • 複数フィールドを跨いだ値そのものに振る舞いを持たせる事ができる
      • 名前をどう出すか?はPersonが持つというより、名前そのものが関心を持っている。
    • Fat Modelにならずに多様なふるまいを定義できる
  • Bad
    • Struct#newを理解していないと、めちゃつまづく
    • find_byに渡すと全てのフィールドを条件にするので、使い勝手が悪い
    • Struct間を跨いだバリデーションとかで冗長的になる

ActiveRecord::Type

Before

# target_year_month:integer
class Salary < ApplicationRecord
  def target_time
    Time.mktime(target_year_month.to_s[0, 4], target_year_month.to_s[4, 2]) if value
  end
end
  • DBからすると、年月までしか情報としてはいらないので、Date型、DateTime型でもつには冗長的な情報
  • でもモデルではTime型として扱いたい情報

After

# config/initializers/active_record/year_month.rb
class YearMonth < ActiveRecord::Type::Integer
  def cast(value)
    if value.kind_of? Time
      str = format('%04d', value.year) + format('%02d', value.month)
      super str.to_i
    else
      super
    end
  end
  # DBから値を取り出す時のキャスト処理
  def deserialize(value)
    Time.mktime(value.to_s[0, 4], value.to_s[4, 2]) if value
  end
end

ActiveRecord::Type.register(:year_month, YearMonth)

# app/models/salary.rb
# target_year_month:integer
class Salary < ApplicationRecord
  attribute :target_year_month, :year_month
end

Usage

Salary.create(target_year_month: Time.current)
# => INSERT INTO `salaries` (`target_year_month`) VALUES (201709)
Salary.find_by(target_year_month: 201709)
# => SELECT  `salaries`.* FROM `salaries` WHERE `salaries`.`target_year_month` = 201709 LIMIT 1
s = Salary.find_by(target_year_month: Time.current)
# =>  SELECT  `salaries`.* FROM `salaries` WHERE `salaries`.`target_year_month` = 201709 LIMIT 1
s.target_year_month.class # => Time

Good/Bad Points

  • YearMonth型という型を自作することで、共通処理を1箇所にまとめることができる。
  • 継承元にはStringBinaryValueなど様々なクラスがあるので、基本なんでもできる。
    • どんな構造のCSVがくるかわからない場合など、動的な構造をHashで入れてる場合などにも有用だとおもいます。
  • 正直デメリットがあまり見つからない。サービス開始時点で使いたかったw

参考記事

Active Record::Type::Valueを継承した独自タイプの作成
Using the Rails 5 Attributes API Today, in Rails 4.2

以上!!!