はじめに
Railsで書いてると、便利なRailsAPIやGemを知らないがために、
早くいってよぉ〜
と思うことが多々ありました。
後続の方の参考になればと、ここに残しておきます ;)
目次
- Enum
ActiveRecord::Enum
- Gem
enum_help
- Decoratorパターン(Gem
active_decorator
) - ValueObject
-
Struct.new
とActiveRecord::Aggregations#composed_of
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
-
ActiveRecord::Enum
をi18nしてくれる - https://github.com/zmbacker/enum_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.new
と ActiveRecord::Aggregations::ClassMethods#composed_of
- n個のフィールドを1つのオブジェクトに格納するパターン
- Usage
- ソース(ActiveRecord::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
- フィールドとオブジェクトが1:1になるパターン
- https://github.com/rails/rails/blob/master/activerecord/lib/active_record/type.rb
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箇所にまとめることができる。 - 継承元には
String
、Binary
、Value
など様々なクラスがあるので、基本なんでもできる。- どんな構造のCSVがくるかわからない場合など、動的な構造を
Hash
で入れてる場合などにも有用だとおもいます。
- どんな構造のCSVがくるかわからない場合など、動的な構造を
- 正直デメリットがあまり見つからない。サービス開始時点で使いたかったw
参考記事
Active Record::Type::Valueを継承した独自タイプの作成
Using the Rails 5 Attributes API Today, in Rails 4.2