Rails ActiveRecordで値が変更する前後の値を取得する
ActiveRecordで値が変更する前後の値を取得することがあったのでメモ
Rails 2.1あたりからActiveRecordの変更前後の値を取得する機能が備わっている。
method | 意味 |
---|---|
changed? | 変更されているかどうか |
changed | 変更されているattribute名の配列 |
changeds | 変更されているattribute名と値のハッシュ。値は変更前後の値を配列で。 |
{attr}_changed? | {attr}が変更されているかどうかの判定 |
{attr}_was | {attr}の変更前の値。変更されていない場合は元の値 |
{attr}_change | {attr}の変更前後の値の配列。変更されていなかったらnil |
{attr}_will_change! | {attr}を変更することを明示。 |
attrには属性名が入ります。nameの場合 name_wasで変更前の値を取得することができる
使用例
Railsのコールバック処理で複数の配列データを1つに統一したい場合に利用する
コールバック処理
- before_save
- before_update
- after_update
etc...
before_update処理でmerge_tagメソッドを実行する
merge_tagメソッドでは既に登録されていた既存の値(tag)と、
変更が加えられた新規の値tag_wasを組み合わせる(merge)処理が行われ、
self.tagに代入される
before_update :merge_tag def merge_tag # tag_wasには元々のtagの値が入り、 # mergeの引数のtagは新たなtagの値になる self.tag = tag_was.merge(tag) end
もっと簡単な例で動きを説明すると
tag = Tag.find(1) #=> #<Tag:0x007fc35ee6fc98 id: 1, name: "ドラえもん", created_at: Wed, 11 Apr 2016 20:21:02 JST +09:00> tag.name #=> "ドラえもん" tag.changed? #=> false tag.name = "のび太" #=> "のび太" tag.changed? #=> true tag.name_was #=> "ドラえもん"
のように、tag.name_wasで変更前の値を取得することができています。
ActiveRecordのちょっとした機能ですが、応用的な処理を実装する時には便利に使えるかなと思います。
Singletonパターン Ruby
シングルトンパターンとは
実装
Rubyにはsingletonと呼ばれるモジュールが定義されているためこれをrequireし、 クラスにincludeすることでシングルトンクラスを定義することができる。
require 'singleton' # シングルトン class SingletonObject # instanceメソッドが定義される include Singleton attr_accessor :counter def initialize @counter = 0 end end obj1 = SingletonObject.instance obj1.counter += 1 puts(obj1.counter) # 1 obj2 = SingletonObject.instance obj2.counter += 1 puts(obj2.counter) # 2 # 前回の+1が引き継がれている obj3 = SingletonObject.new # private method `new` called for SingletonObject:Class # newメソッドはプライベートメソッドとして定義される
acts_as_listとjQueryで並べ替えを実装する
jQueryのsortableを用いて並べ替えを実装する Rails
acts_as_list
モデルの並び順を簡単に操作できるようにするGem
昔のRails1系あたりでは標準で搭載されていたらしいが、 途中でGemとして切り離されて現在は独立したGemとして存在する。
引用:acts_as_list: gem か plugin か
本家がこちら。 https://github.com/swanandp/acts_as_list
導入の仕方などはドキュメントにわかりやすく書かれているのでそこを参考すると良さそう。 acts_as_listでは positionカラム がデータのソート基準となるよう決まっている。 なのでacts_as_listを使いたい場合は、対象とするテーブルのカラムにposition絡むを追加しましょう。
# has_manyでtodo_itemsを持つ親モデル class TodoList < ActiveRecord::Base has_many :todo_items, -> { order(position: :asc) } end # belongs_toでtodo_listに紐づく子モデル class TodoItem < ActiveRecord::Base belongs_to :todo_list # scopeで定義する acts_as_list scope: :todo_list end todo_list = TodoList.find(...) # move_to_bottomで値を操作1,2,3とあるならば1が3になり2,3が繰り上がる todo_list.todo_items.first.move_to_bottom # move_higherで1,2,3,とあるならば2,1,3と変更される todo_list.todo_items.last.move_higher
また、最近の紹介記事ですと、Qiitaに上がっているjnchitoさんの記事が素晴らしくわかりやすかったです。 実装の際に参考にさせてもらいました。 Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)
動画は見ていませんが初心者にもわかりやすいように筆者が参考にした記事や、 動画を用いての実装方法まで紹介していてさすがですね。
今回自分は ranked_model を使った実装を行っていないので機会があればそちらも使ってみたいなと思います。 どうやらranked-modleの方がパフォーマンスが良いようです。(検証はしてません)
Railsで順番を管理するgemとして、ranked-model gemを使います。 acts_as_listも有名ですが、ranked-modelの方がパフォーマンス的に優れています。
ドラッグ&ドロップで並べ替えを行う実装
jQueryのsortableを用いて、ドラッグ&ドロップで操作を行う実装を紹介します。 上記でacts_as_listを紹介しましたが、今回の実装では用いません。 acts_as_listではなくjQueryのsortableを用いることでも並べ替えを良い感じにできるんだぜってことを紹介したいと思います。
# Railsで良い感じに実装するためのGem達 gem 'jquery-ui-rails' gem 'jquery-turbolinks'
// application.jsで読み込みます //= require jquery-ui/effect-highlight //= require jquery-ui/sortable
D&Dを行った際の処理 $('.table-sortable').sortable でsortableを適用させる。 D&Dをトリガーにしてajax処理を行いDB側のposition値を変更する使用
// table_sort.js $(function() { return $('.table-sortable').sortable({ axis: 'y', items: '.sentence', update: function() { // ajax処理で並べ替え後の値をサーバー側に渡す $.ajax({ type: 'POST', url: '/sort', dataType: 'json', // $('.table-sortable').sortable('serialize') で変更後の値を取得 data: $('.table-sortable').sortable('serialize') }); }, // ここはおまけで変更完了エフェクトを付け足す stop: function(e, ui) { return ui.item.children('td').effect('highlight'); } }); });
controller側の実装
def sort Content.all.each do |sentence| content.position = params[:content].index(content.position.to_s) + 1 content.save end render nothing: true end
model側での実装 値を追加した際にpositionカラムに値の最大値をセットするために before_createにset_positionメソッドを定義します
before_create :set_position def set_position if max =Content.where(hoge_id: hoge_id).maximum(:position) self.position = max + 1 end end
slimの実装 id="position_#{content.position}"でデフォルト値をセット
table.table.table-striped.table-sortable thead tr th id th name th description tbody - @contents.each do |content| tr.sontent id="position_#{content.position}" td= content.id td= content.name td= content.description
簡単にイケてる実装ができて便利!
参考
Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)
Compositeパターン Ruby
Compositeパターン
コンポジットパターンとは
- あるものが同じような下位のもので作られているという考え方
- 大きなオブジェクトが小さな子オブジェクトから構成されていて、その子オブジェクトもさらに小さな孫オブジェクトでできていたりする
- 階層構造やツリー構造のオブジェクトを作りたい時に利用出来る
構成
- コンポーネント(Component):すべてのオブジェクトの基底となるクラス
- リーフ(Leaf):プロセスの単純な構成要素、再帰しない
- コンポジット(Composite):コンポーネントの1つでサブコンポーネントで構成
「再帰」とは、ある処理の中で再びその手続きを呼び出すこと。
用いるメリット
# FileEntry, DirEntryクラスの共通メソッドを規定(Component) class Entry # ファイル/ディレクトリの名称を返す def get_name; end # ファイル/ディレクトリのパスを返す def ls_entry(prefix) end # ファイル/ディレクトリの削除を行う def remove; end end # Leaf (中身) class FileEntry < Entry def initialize(name) @name = name end # ファイルの名称を返す def get_name @name end # ファイルのパスを返す def ls_entry(prefix) puts(prefix + "/" + get_name) end # ファイルの削除を行う def remove puts @name + "を削除しました" end end # Composite class DirEntry < Entry def initialize(name) @name = name @directory = Array.new end # ディレクトリの名称を返す def get_name @name end # ディレクトリにディレクトリ/ファイルを追加する def add(entry) @directory.push(entry) end # ファイル/ディレクトリのパスを返す def ls_entry(prefix) puts(prefix + "/" + get_name) @directory.each do |e| e.ls_entry(prefix + "/" + @name) end end # ファイル/ディレクトリの削除を行う def remove @directory.each do |i| i.remove end puts @name + "を削除しました" end end root = DirEntry.new("root") tmp = DirEntry.new("tmp") tmp.add(FileEntry.new("conf")) tmp.add(FileEntry.new("data")) root.add(tmp) root.ls_entry("") #/root #/root/tmp #/root/tmp/conf #/root/tmp/data root.remove #confを削除しました #dataを削除しました #tmpを削除しました #rootを削除しました
開発での小ネタ その2
開発での小ネタ その2
普段の開発で得た知見を書き留めておく
YouTubeへのリンクでクエリを付加するロジック
DBにURLが保存してありすでに存在すればそのまま。タグが付いていなければ付加する こちらを参考にしました。
def youtube_url uri = URI.parse("https://www.youtube.com/watch?v=WztpnvW4CHA") query_hash = {} query_hash.merge!(Hash[URI.decode_www_form(uri.query)]) if uri.query.present? query_hash.symbolize_keys!.merge!(tag: youtube.opening_bgm_id) uri.query = URI.encoded_www_form(query_hash.to_a) uri.to_s end
URI.parseでURLをオブジェクトに変換し、クエリが存在するか判定する if query.present?
存在するならば、URI.decode_www_formを用いてクエリをkeyとvalueのhash形式にしquery_hashに格納する。
文字列(“hoge”)とシンボル(:hoge)が同一な存在とするため、symbolize_keys!を用いて文字列をシンボルに変換します。
.merge!(v: youtube.opening_bgm_id)で付加したいidをmergeする。
mergeメソッドはすでに同じkeyが存在する場合に上書きします。
URI.encoded_www_form(query_hash.to_a)でクエリをuriのqueryに格納して完了!
URI周りを厳密に処理を行いたい場合は以上のようなやり方で操作すると良さげです。
Railsでhttpリクエストをhttpsにリダイレクトさせる
gem "rack-ssl-enforcer"
rack-ssl-enforcerを用います。 Gemをbundle installし、config/environments/** に以下を記述します。
config.middleware.use Rack::SslEnforcer
これでSSL通信に限定ます。 ロードバランサーなどを用いておりヘルスチェックのhttpリクエストは通したいとい場合は以下の記述を追加。
# except: ["/ping"]を追加してsslの制限をexceptすることで解決 config.middleware.use Rack::SslEnforcer, except: ["/ping"]
enumerizeでscope: trueにするとscopeとして使える
class User < ApplicationRecord extend Enumerize enumerize :sex, :in => [:male, :female], scope: true enumerize :status, :in => { active: 1, blocked: 2 }, scope: :having_status end User.with_sex(:female) # SELECT "users".* FROM "users" WHERE "users"."sex" IN ('female') User.without_sex(:male) # SELECT "users".* FROM "users" WHERE "users"."sex" NOT IN ('male') User.having_status(:blocked).with_sex(:male, :female) # SELECT "users".* FROM "users" WHERE "users"."status" IN (2) AND "users"."sex" IN ('male', 'female')
slackへのアクション通知はslack-notifierがおすすめ
gem "slack-notifier"
notifier = Slack::Notifier.new(Rails.application.config.slack_webhook_url) notifier.ping(message)
上記で実行するとslack_webhook_urlに対してmessageが送られる。 リッチな通知を実現したい時には以下のようにattachementsを利用すると良い。
message = { title: 'タイトル', title_link: 'タイトルのリンク', text: 'テキスト文', image_url: '表示させるサムネイルのURL', } SlackNotifier.ping(attachments: [message])
こちらを参考にしました。
バッチなどで大量のデータを操作する際にはfind_eachと良い
分割してレコードを取得して処理をする。 デフォルトで1000件ずつ処理をする。
Railsには find_each というメソッドが用意されています。通常の each メソッドを使用すると、全データをまとめてメモリに展開してから処理を開始します。そのため、十分にメモリに載るデータ量であれば何も問題ないですが、数百万、数千万というデータ量になってくるとメモリに載りきらずに溢れてしまって大変なことになります。 find: 全データをメモリに展開してから処理 find_each: 少しずつデータをメモリに展開しつつ処理 そういうときには find_each メソッドを使いましょう。
参考にしました。 Railsで大量のデータをまとめて更新するならfind_each使うよね
Page.where(:category_id => 1).find_each do |page| # hogehoge end
引数の管理にはハッシュとfetchを使おう
クラス設計で依存関係の管理を行う時のテクニックです。 引数が必要なメソッドを定義した時に値を渡す順番が元から決まっておりそれに沿った形で値を渡さなければいけない状況が多々あります。 もしくは、引数が必要がない場合とある場合を想定したメソッドがあったりします。そんな問題を解決する際には引数のHash化とfetchを用いると良い感じになります。
導入前
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end end Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
導入後
class Gear attr_reader :chainring, :cog, :wheel # fetchを使ってデフォルト値を指定している def initialize(args={}) @chainring = args.fetch(:chainring, 40) @cog = args.fetch(:cog, 18) @wheel = args[:wheel] end end Gear.new( chainring: 52, cog: 11, wheel: Wheel.new(26, 1.5)).gear_inches
引数をHashで渡すことにより引数を渡す順番の依存がなくなり、 またfetchメソッドを用いることでデフォルト値を設定することも可能になるのでより柔軟性のあるコードになる。良い!!!
saveメソッドを用いて実行するvalidationを指定する
validatesメソッドのonオプションを用いることで走らせるメソッドを指定できる
validates :title, presence: true, on: :create
こうすれば、createメソッドの時にのみvalidationが走ることになる。 saveメソッドのcontextオプションと一緒に用いることで自由に実行するバリデーションを指定できるようになる。
validates :category, presence: true, on: :publish @obj.attributes = { title: "hoge", content: "こんてんつ" } @obj.save(context: :publish)
すごく良い
複数のvalitetionをかける場合は、
with_options on: :publish do |publish| publish.validates :title, presence: true publish.validates :content, presece: true end
みたいな感じで定義できるっぽい。
こちらの記事を参考にしました。素晴らしい知見に感謝
メソッドキャッシュを用いて少しでも高速化を図る
class Hoge attr_accessor :obj def huga @obj ||= begin sleep 3 "aaa" end end end
一回目にhogeを実行する時よりも2回目の方が実行が早くなる。@objインスタンス変数に値が存在すればそれを使うという書き方をすることで不必要な処理を省く。 すごく細かいところだかこう小さな気遣いが後々効いてくる。なお、使いどころには注意すること
RSpecでスタブを定義しテストをする
スタブ
テストを行うためにメソッドにダミーの振る舞いを定義すること
- allowでクラス名を指定
- receiveでメソッドを指定
- and_returnでメソッド実行の戻り値を指定
describe "" do before do allow_any_instance_of(Twitter).to receive(:post).and_return(true) end end
テストを実行する前に 振る舞いを定義しておく
describe "" do before do allow_any_instance_of(Twitter).to receive(:post).and_raise(ArgumentError) end end
and_raiseで例外エラーを発生させることも可能
コレクションクロージャメソッドのすゝめ
collect (map)、select (filter)、inject (reduce) メソッドを用いて処理を簡潔で分かりやすいものに書き換える
ループを取り除いてコレクションクロージャメソッドを使うと、コードをたどりやすくなる。コレクションクロージャメソッドは、コレクションの中を行き来したり、派生コレクションを作ったりするためのインフラストラクチャコードを隠し、ビジネスロジックに集中できるようにしてくれる。
.select フィルタ処理
# before managers = [] employees.each do |e| managers << e if e.manager? end # after managers = employees.select{ |e| e.manager? }
.collect 収集処理
# before offices = [] employees.each{ |e| offices << e.office } # after offices = employees.collect{ |e| e.office }
.select & .collect 複数の処理をまとめて
# before managerOffices = [] employees.each do |e| managerOffices << e.office if e.manager? end # after managerOffices = employees.select{ |e| e.manager? } .collect{ |e| e.office }
gsubメソッド ブロックを用いる
Rubyのgsubメソッドは置換を行うメソッド
第1引数にマッチした値を第2引数の値に置き換える
values = "apple cherry banana" values.gsub("apple", "pear") #=> "pear cherry banana"
ブロックを使う方法があることを知った。
"<b>ruby</b> <i>python</i>".gsub(/<.>(.+?)<\/.>/) {"<p>#{$1}</p>"} # => "<p>ruby</p> <p>python</p>"
$1から$9が特殊変数として定義されているそうな。
Ruby .mapの返り値は必ず配列が返ってくる
.mapというメソッドがあるがこれをハッシュに対して実行した際に、返り値がハッシュではなく配列が返ってきた。 ハッシュの返り値を期待していたもののmapメソッドは必ず配列が返る仕様になっているようで詰まった。 .mapには別名で.collectメソッドも存在する
each_slice each_with_object()
Rubyの配列操作ではメソッドが豊富に用意されている。
.each_sliceメソッドは引数に渡した数ごとに分割した値を返してくれる
.each_with_objectメソッドはinjectメソッドに似ている配列操作内で使用したいオブジェクトをブロック内で使えるようにする LINK
Observerパターン Ruby
Observerパターン
オブザーバーパターンとは
Observerとは「観測者」 あるオブジェクトの状態が変化した際に、そのオブジェクト自信が「観測者」に「通知」をする仕組み。 オブザーバーは3つのオブジェクトで構成される。
- サブジェクト(subject):変化する側のオブジェクト
- オブザーバ(Observer):変化を関連するオブジェクトに通知するインターフェース
- 具象オブザーバ(ConcreateObserver):状態の変化に関連して具体的な処理を行う
状態とは、クラスが持つ変数の値のこと。
用いるメリット
- オブジェクト間の依存度を下げることができる
- 通知先の管理をオブザーバーが行うことで、サブジェクトは通知側を意識しなくていい
実装
- Employee(サブジェクト):従業員を表す
- Observable(オブザーバ):従業員のニュースを監視する仕組み(observer/Observable)
- Payroll(具体オブザーバ1):給与の小切手の発行を行う
- TaxMan(具体オブザーバ2):税金の請求書の発行を行う
require 'observer' class Employee include Observable # Observerとして働く attr_reader :name, :title, :salary def initialize(name, title, salary) @name = name @title = title @salary = salary # 監視するインスタンスを追加 add_observer(Payroll.new) add_observer(TaxMan.new) end # 給与をセットして、ConcreteObserverに通知する def salary=(new_salary) @salary = new_salary # changedで変化があったかを確認する changed # 変化があった場合に以下実行される notify_observers(self) end end #----------------具体オブザーバ--------------- # 給与の小切手の発行を行う class Payroll # 変更通知用にupdateメソッドを持っている def update(changed_employee) puts "彼の給料は#{changed_employee.salary}になりました!#{changed_employee.title}のために新しい小切手を切ります。" end end # 税金の請求書の発行を行う class TaxMan # 変更通知用にupdateメソッドを持っている def update(changed_employee) puts "#{changed_employee.name}に新しい税金の請求書を送ります" end end john = Employee.new('John', 'Senior Vice President', 5000) john.salary = 6000 #=> 彼の給料は6000になりました!Senior Vice Presidentのために新しい小切手を切ります。 #=> Johnに新しい税金の請求書を送ります john.salary = 7000 #=> 彼の給料は7000になりました!Senior Vice Presidentのために新しい小切手を切ります。 #=> Johnに新しい税金の請求書を送ります
ストラテジーとの違い
- オブザーバー:発生しているオブジェクトに対してイベントを通知している
- ストラテジー:何らかの処理を行うためにオブジェクトを取得している
Strategyパターン Ruby
Strategyパターン
ストラテジーパターンとは
抽象的な処理と具象的な処理を分離することで、 変化に強い構造を実現する。
委譲を用いてアルゴリズムを変更可能にする。
目的の部分としてはテンプレートメソッドパターンと似ているが、
問題に対する解決のアプローチが異なる。
ストラテジは「戦略」という意味。
条件によってアルゴリズムを切り替えるところは、まるで戦略を練っているようだからその名がついたそうな。
ストラテジーパターンの最終的な目的は、 「アルゴリズムの交換」を行うこと。
テンプレートメソッドパターンが抱える問題
テンプレートメソッドパターンは 継承をベースにしているといこと。
これによりよろしくないつながりを生んでしまう。
良い部分を作り出すと同時にそこで生まれたつながりが複雑にしてしまう。
よろしくないつながりを生み出さずに変化しやすい形をとりたい。
そんなときに使うのがStrategyパターン
変化しやすいコードの塊を抽出し、 全く別のクラスに閉じ込める。
Strategyパターンでは、アルゴリズムの部分を他の部分と意識的に分離する。
そしてアルゴリズムとインターフェースを規定し、アルゴリズムの部分は委譲を用いて処理を他クラスの使って解決する。
これによりアルゴリズムは代替可能な状態になるため変更が容易になる。
「アルゴリズムのクラス化」
構成
- コンテキスト(Context):ストラテジの利用者
- 抽象戦略(Strategy):同じ目的をもった一連のオブジェクトを抽象化したもの
- 具象戦略(ConcreateStrategy):具体的なアルゴリズム
用いるメリット
- 使用するアルゴリズムに多様性を持たせることができる
- コンテキストと戦略を分離することでデータも分離することができる
- 継承よりもストラテジを切り替える方が楽
実装
- Report(コンテキスト):レポートを表す
- Formatter(抽象戦略):レポート
- HTMLFormatter(具象戦略):HTMLフォーマットでレポートを出力
- PlaneTextFormatter(具象戦略):PlaneTextフォーマットでレポートを出力
処理をstrategy(戦略)のclassとして定義する。 Formatterクラスを抽象戦略として定義しているが、これはインターフェースを定義するだけのクラスになるので、 Rubyらしくはありません。不要なら定義しなくても問題はない(ダックタイピングの哲学)
# レポートの出力を抽象化したクラス(抽象戦略) class Formatter def output_report(title, text) raise 'Called abstract method !!' end end # HTML形式に整形して出力(具体戦略) class HTMLFormatter < Formatter def output_report(report) puts "<html><head><title>#{report.title}</title></head><body>" report.text.each { |line| puts "<p>#{line}</p>" } puts '</body></html>' end end # PlaneText形式(*****で囲う)に整形して出力(具体戦略) class PlaneTextFormatter < Formatter def output_report(report) puts "***** #{report.title} *****" report.text.each { |line| puts(line) } end end
レポート側の実装
# レポートを表す(コンテキスト) class Report attr_reader :title, :text attr_accessor :formatter # 引数にformatterオブジェクトを渡す(依存オブジェクトの注入) def initialize(formatter) @title = 'report title' @text = %w(text1 text2 text3) @formatter = formatter end # @formatterのoutput_reportに処理を委譲する def output_report @formatter.output_report(self) end end report = Report.new(HTMLFormatter.new) report.output_report #<html><head><title>report title</title></head><body> #<p>text1</p> #<p>text2</p> #<p>text3</p> #</body></html> report.formatter = PlaneTextFormatter.new report.output_report #***** report title ***** #text1 #text2 #text3
注意すべきこと
- コンテキストとストラテジ間のインターフェースがストラテジの種類の増加を妨げないようにすること
- コンテキストの変更がストラテジに影響を与えないようにすること