Edited at

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


以上!!!