Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

以上!!!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away