【Railsチュートリアル】慣習的に正しいコードの書き方
目的
Railsチュートリアルで「このコードは慣習的に正しくない」という記述があるが、なぜなのかを説明する。
背景
Railsを仕事で書いていく上で、モデル同士の関連の定義とその使い方をわりと意識しているのだが、いつどこで勉強したのか怪しかった。
出典を明らかにしたかった。
該当の内容
https://railstutorial.jp/chapters/user_microposts?version=5.0#code-micropost_validity_test
前提として、以下のような、関連を持つモデルUser
とMicropost
があるとする。(DBのカラム等は自然に定義されているものとする)
# app/models/user.rb class User has_many :microposts end # app/models/micropost.rb class Micropost < ActiveRecord::Base belongs_to :user end
このときuserに紐づくmicropostを作りたいときは、まず思いつく方法としては以下のようなもの。
user = User.first Micropost.new(content: "test", user_id: user.id)
しかしこのやりかたは、このコードは慣習的に正しくないとRailsチュートリアルで言及されている。
実際は以下のようにする。
user = User.first user.microposts.build(content: "test")
習慣的に良いコードは何がいいのか
関連が明確になる
まずuser.microposts
というコードはUser
からMicropost
への関連を定義していないと動かない。user.microposts
というコード
を見るだけで関連が定義されていることが明確になる。
引数を減らせる
user_idを渡さなくて良い
主語をuserにすることができる
最初のレシーバがuser
になるため、「ユーザがマイクロポストを作る」という見方ができる
依存が少なくなる
Micropost.new
する場合はMicropost
というクラス、user_idという属性に依存しているが、user.microposts
の場合はUser
のmicroposts
という関連(インタフェース)に依存させられる。
まとめ
いろいろ理由をつけてみましたが、やはり一番は「慣習的に」なのでしょう。
業務でRailsを書いていると、先人の書いたコードに引っ張られついついこのあたりの「自然な書き方」を忘れがち。
誰に見せても恥ずかしくないようなRailsのコードを書けるよう、日々心がけたいものです。
その他
この辺はRubyMine使うと指摘してくれるのかな?
sendgrid-rubyを使ったSendGridでのメール送信
はじめに
この記事は「Sansan Advent Calendar 2016」7日目の記事です。
昨日はerikoobeさんによる「エンジニア未経験者が Ruby を学んでみた件」でした。
特に「2. 未経験者から見た、エンジニアの世界」は共感できたのと同時に、やる気のある人へのサポートはやっぱり大切だなと感じました。
この記事の概要
SendGridが公開しているgemを使って、webapi経由でメール送信する方法をざっくり紹介する。
背景
SendGridとは?
SendGridはクラウドベースのメール配信サービスです。smtpサーバを自前で運用することなくメール送信を行うことが出来ます。
この点ではAmazon Simple Email ServiceやMailChimpも同等の機能を持っています。
また、配信だけでなくバウンス後の処理や開封、クリック数取得等のさまざまことをwebapi経由で行えることも特徴の一つです。
なぜこの記事を書いたか
最近までたくさんメールを送る仕事をしていたのですが、smtp→webapiにかえることで配信時間をかなり圧縮出来たので、ぜひ布教していきたいと思いまとめてみました。
前提
SendGridのアカウント取得、apikeyの発行ができている。
サンプル
以下サンプルコードです。(gemのインストールは適宜おこなってください。)
require 'sendgrid-ruby' require 'dotenv' Dotenv.load API_KEY = ENV['API_KEY'] USERS = [ { email: 'to1@example.com', name: 'to1', fullname: 'to_user1' }, { email: 'to2@example.com', name: 'to2', fullname: 'to_user2' } ] mail = SendGrid::Mail.new mail.from = SendGrid::Email.new(email: 'from@example.com') mail.contents = SendGrid::Content.new(type: 'text/plain', value: "%name%さん\nあなたのフルネームは%fullname%です") USERS.each do |u| email, name, fullname = u.values_at(:email, :name, :fullname) sp = SendGrid::Personalization.new sp.to = SendGrid::Email.new(email: email) sp.subject = "#{name}さんこんにちは" sp.substitutions = SendGrid::Substitution.new(key: '%name%', value: name) sp.substitutions = SendGrid::Substitution.new(key: '%fullname%', value: fullname) mail.personalizations = sp end sg = SendGrid::API.new(api_key: API_KEY) response = sg.client.mail._('send').post(request_body: mail.to_json)
登場するclass
SendGrid::Mail
SendGrid::Mail#to_json
を呼ぶことで、現在持っているインスタンス変数からHash形式でrequest_bodyを作ります。
SendGrid::Mail#personalizations=
は、内部ではArray#push
を使っている。インスタンス変数@personalizations
を書き換えるわけではない。
SendGrid::Email
送信先、受信先のメールアドレスや表示名を扱うのに用いる。
SendGrid::Content
メールの本文、type(text, html)を指定するためのクラス。
SendGrid::Personalization
送信先、件名、substitutions等を扱うクラス。どんな項目が指定できるできるかは以下を参考に。
personalizations
気になる点
インタフェースがいけてない
SendGrid::Mail#personalizations=
とかSendGrid::Mail#to_json
とか、パット見と違う挙動なものが多いのでは??
ソースが読みにくい
gemのプロダクトコードはsendgrid/helpers/mail/mail.rbという1つのファイルに集約されている。
その他
本家のreadmeにはsendgrid/helpers/mail/mail.rbを使わない方法も記載されています。
without-mail-helper-class
参考までに上に書いたサンプルコードのhelper使わない版も書いてみました。
mail_info = { from: { email: 'from@example.com' }, content: [ { type: 'text/plain', value: "%name%さん\nあなたのフルネームは%fullname%です" } ] } mail_info[:personalizations] = USERS.map do |u| email, name, fullname = u.values_at(:email, :name, :fullname) { to: [{ email: email }], subject: "#{name}さんこんにちは", substitutions: { '%name%': name, '%fullname%': fullname } } end sg = SendGrid::API.new(api_key: API_KEY) response = sg.client.mail._('send').post(request_body: mail_info)
SendGrid::API
もだいたい40行くらいのコードなので、sendgrid-rubyを使わない方法もありかなとは思いまいた。
その際はsendgrid-ruby
が内部的に使っているruby_http_client
を使うもよし、メールを送信するだけなら全て自分で書いてしまってもいいかなと思います。(webapiにリクエスト送るだけだし。)
まとめ
以上、SendGridが公開しているgemを使って、webapi経由でメール送信する方法でした。 apiクライアントの設計に興味が湧いてきたので、他のgemのソースも読んでみたいと思います!
さいごに
明日はTakeruTakahashiさんの記事です!引き続き「Sansan Advent Calendar 2016」をお楽しみに!
【Ruby】【Rails】RAILS ANTIPATTERNS、chapter3 viewのまとめ
目的
RAILS ANTIPATTERNSをのchapter3(view)の箇所を読んだのでまとめました。
- 作者: Chad Pytel,Tammer Saleh
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2010/11/09
- メディア: Kindle版
- この商品を含むブログを見る
PHPitis(PHPっぽくなりがち)
ドメインロジックとか複雑な表示ロジックとか、全部ビュー(app/views
の下にあるファイル)に書かれがちだよねという話です。
解決策:View Helper勉強しよ!
詳しくは書かないけど、form_for
, render
, content_for
, yield(:symbol) || 'default'
, content_for?
について書かれていました。
form_for
は本当にいろいろ使えてすごいのだけど、まだまだ使い切れていない感がある。
content_for
の並びで言うと、provide
もぜひ使いたいですね。
http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-provide
解決策:いい感じにモデルにメソッド追加しよ!
以下のようなコードがあるとする。
<% if current_user && (current_user == @post.user || @post.editors.include?(current_user)) && @post.editable? && @post.user.active? %> <%= link_to 'Edit this post', edit_post_url(@post) %> <% end %>
これはモデルにメソッドを定義してあげて、view側は以下のような形にする。
<% if @post.editable_by?(current_user) %> <%= link_to 'Edit this post', edit_post_url(@post) %> <% end %>
すっきり!
メソッドを定義する場所としては、まあまあ当たり前だけどコントローラで使う場合はモデル、そうじゃなければhelperに置くと書いてあった。
そのif文、helperに移動しよ!
app/views/alerts/index.html.erb
が以下のようになっているとする。
<div class="feed"> <% if @project %> <%= link_to "Subscribe to #{@project.name} alerts.", project_alerts_url(@project, :format => :rss), :class => "feed_link" %> <% else %> <%= link_to "Subscribe to these alerts.", alerts_url(format => :rss), :class => "feed_link" %> <% end %> </div>
この場合はapp/helpers/alerts_helper.rb
を作って以下のようにメソッドを追加する。(若干仕様変わってる?)
module AlertsHelper def rss_link(project = nil) link_to("Subscribe to these #{project.name if project} alerts.", alerts_rss_url(project), class: 'feed_link') end def alerts_rss_url(project = nil) if project project_alerts_url(project, format: :rss) else alerts_url(:rss) end end end
view側のapp/views/alerts/index.html.erb
は以下。
<div class="feed"> <%= rss_link(@project) %> </div>
いつも<div class="feed">
が必要な場合は、content_tagを使ってhelperにうつしてしまってもいいとありました。
その場合はlink_to
じゃなくてcontent_tag :a
をあえて使ってもいいと書いてました。
ちょっとした感想
最後の、「helperに移動しよ!」的な話はいつも悩むところなのと、helperメソッドでcontent_tagをもりもり書くのなら、パーシャルでいいのでは?と思ったりします。
でも今回の= rss_link(@project)
が、= render rss_link, project: @project
になるのはイマイチ感がありますね…なんでだろ。
viewにかかわらず、Railsはやりたいことを色んな方法で実装することができるので、一人で作っているときでさえ統一感がなくなることが有ります。rubocopで潰せるところはいいけど、自分が統一感を持って実装出来ているかどうかたまに振り返りながらすすめていきたいですね。
【Rails】【RSpec】【shoulda-matchers】validate_uniqueness_ofのscoped_toではまったときのまとめ
目的
shoulda-matchersでモデルのバリデーションのテストを書いていたときに、Railsのアトリビュートメソッドのキャッシュの動きで若干はまったため、簡単にまとめる。
shoulda-matchersの簡単な導入
shoulda-matchersはgemで、RSpecやMinitestにvalidate_presence_of
などのワンライナーのテストのシンタックスを追加するもの。
詳しい話、やその他のテストメソッドに関しては、本家のreadme参照。(知らない機能がたくさんある!)
Railsのアトリビュートメソッドのキャッシュ機能
メタプログラミングRubyを読んでいただいた方はわかるかもしれないが、Rails4系・5系のコードはメソッドの定義をキャッシュすることでパフォーマンスの向上を図っている。
詳しい実装には触れないが、具体的には以下のような動作になる(Rails 5.0.0.1, Ruby 2.3.1)
2.3.1 (main):0 > User.method_defined?(:name) => false 2.3.1 (main):0 > User.new => #<User id: nil, name: nil, role_id: nil, created_at: nil, updated_at: nil, email: ""> 2.3.1 (main):0 > User.method_defined?(:name) => true
shoulda-matchersのvalidate_uniqueness_ofメソッドのscoped_toチェーン
モデルでのバリデーションを以下のように書く
class Phone < ActiveRecord::Base validates :phone, uniqueness: true end
shoulda-matchersでテストを書くと以下のように書ける
require 'rails_helper' describe Phone do it { is_expected.to validate_uniqueness_of(:phone) } end
さらに、複数カラムでユニークになるようにバリデーションをすることもでき
class Phone < ActiveRecord::Base validates :phone, uniqueness: { scope: :contact_id } end
shoulda-matchersで書くとこうなる
require 'rails_helper' describe Phone do it { is_expected.to validate_uniqueness_of(:phone).scoped_to(:contact_id) } end
しかし、上記のテストは(他のテストにもよるが、)コケる。
なぜテストが失敗するのか
以下のようなメッセージが出る
:contact_id does not seem to be an attribute on Phone.
ん?カラムあるよ?
これでしばらく進まなかった。
実際は、scoped_toで指定されたカラムは、以下のようにshoulda-matchersのなかでmethod_defined?
を使ってチェックしている。
def attribute_present_on_model? model.method_defined?("#{attribute}=") || model.columns_hash.key?(attribute.to_s) end
試しにbinding.pry
でとめ、contact_id=
を実行してみてからテストを流すとうまくいった。
メソッドのキャッシュのタイミングはバージョンによって異なるので、最新版であれば大丈夫かもしれません。
まとめ
感想
- gemのソースを読むのはなかなか楽しい。
- 今回のがバグなのかわからないが、最新版を使ってバグを踏んで、PR送ろう!
【Rails】権限管理のgem「pundit」を使ってみたときのまとめ
目的
punditというgemを使う際に「Rubyist Magazine - 権限管理のgem、Punditの紹介」を参考に作ったときのまとめ。
railsにapp/policies
ディレクトリって何を入れるのかというところを前回調べたのですが、代表的なものとしてpundit
というgemを使うと出て来るというのが代表的な例なようなので、使ってみました。
【Rails】policiesディレクトリの使い方 - ITの勉強をいろいろやってみたブログ
るびまを参考にして作ったときに考えていたこと等をまとめてみます。
まとめ
ApplicationPolicyのサブクラスの初期化時には、recordではなくclassを渡す
rails g pundit:install
を実行すると、app/policies/application_policy.rb
が作られるのだが、初期実装は以下のようになっている。
class ApplicationPolicy # ... def initialize(user, record) @user = user @record = record end # ... end
これだと、レコードを渡すことで「自分のレコードは更新できる。他の人のレコードは閲覧だけ。」みたいなレコード単位の権限をつけられる。
それはそれで便利なのだけど、社内用の管理画面等はレコード単位ではなくクラス・モデル単位での権限がついていれば十分。なので、以下のようにクラスを受け付ける想定にしておいて、その他も適宜書き換える。(モンキーパッチを当てる必要はなし)
class ApplicationPolicy # ... def initialize(user, klass) @user = user @klass = klass end # ... end
ApplicationController
には以下のようなメソッドを準備しておく。
class ApplicationController < ActionController::Base # ... def pundit_auth authorize controller_name.classify.constantize end end
UsersController
でbefore_action
を呼ぶ
class UsersController < ApplicationController before_action :pundit_auth # ... end
すっきり。
cancancan vs punditはRSpec vs minitestと似ている気がする
cancancanはAbility
というクラスを定義して、その中にDSLをわーっと書いて権限設定を定義します。
それに対しpunditはピュアなObjectを継承したpureなRubyのクラスで定義し、コントローラで呼ぶためのメソッドを提供しているだけです。
このような観点の比較はRSpec vs minitestのときの議論と似ているかなと思っています。
MinitestとRSpec、FixturesとFactoryGirlの良いところ悪いところをコードを書いて比較してみた - give IT a try
RSpec vs minitestのときはテストコードの話なのですが、権限は実際のアプリケーションの設計に関わってくる所なので、妥協したりプラグインに引っ張られた設計になるのは避けたいところです。
複雑な仕様がある場合にはcancancanにモンッキーパッチをあてる等して使うより、自由に書けるpunditを使っておくのも悪くない選択かなと思いました。
TODO
- cancancanで同じことをやる
- 認証はdeviceを使ってやったが、自前で実装してみる
【Rails】policiesディレクトリの使い方
目的
railsにapp/policies
ディレクトリを作るときに、中身には何を入れるのかまとめる。
パーフェクトRuby on Railsの9章を読んでいて、コールバック・バリデーションをモデルから分離して、独立したクラスを作る方法を学びました。
- 作者: すがわらまさのり,前島真一,近藤宇智朗,橋立友宏
- 出版社/メーカー: 技術評論社
- 発売日: 2014/06/06
- メディア: 大型本
- この商品を含むブログ (8件) を見る
callbacks
、validators
という他にも、app配下に置いておくものとしてpolicies
というディレクトリが有るようです。ただし、調べてみると
policies
ディレクトリの使い方は、そこまで一般解があるように思えなかったので、一旦まとめておいて今後検討する材料にしようかと思いました。
policiesディレクトリの使い方概要
- punditを使う(わりと一般的?)
- cancancanのavility.rbを、modelに入れずにpoliciesに入れる(avilitiesでもいいと思う)
- 特にgem等は使わず自分でピュアRubyのオブジェクトを定義して入れる
- policyっていうgemを使う(3をちょっと便利にした感じのもの。メンテされてる??)
punditを使う
app/policies
は以下のようなクラスが入ります
# app/policies/post_policy.rb class PostPolicy attr_reader :user, :post def initialize(user, post) @user = user @post = post end def update? user.admin? or not post.published? end end
punditの使い方に関しては以下を
るびま
参考
Railsを責務に応じてリファクタリングするための9つのパターン | Simplie Post
gemを自然に使うとこうなるため、サンプルには事欠かなさそう。
cancancanのavility.rbを、modelに入れずにpoliciesに入れる
app/models/avility.rb
が肥大化してイヤだ!というときにapp/policies
に入れるという選択肢があるらしい。
中規模Web開発のためのMVC分割とレイヤアーキテクチャ - Qiita
http://blog.thefrontiergroup.com.au/2015/05/separating-abilities-in-cancan/
サンプルがあまり多くないので、上記ふたつをよく読んでから
特にgem等は使わず自分でピュアRubyのオブジェクトを定義して入れる
ちょっと古めの記事ですが、以下の記事で出て来るPolicy Objectのことです。
7 Patterns to Refactor Fat ActiveRecord Models
典型例は以下でしょうか
Policy Object – Steve Robinson – Medium
punditとpolicy objectがおなじpolicyでややこしいから、名前を変えたよという話?
https://sethvargo.com/authorizers-extractors-and-policy-objects/
policyというgemを使う
メンテされてなさそうですが、自分で作ったpolicy objectとcontrollerの橋渡しをしてくれるようなgemのようです
http://eng.joingrouper.com/blog/2014/03/20/rails-the-missing-parts-policies/
まとめ
今後の方針としては以下のような形で使い分けていきたい
- アクションベースのシンプルな権限管理にはpundit
- ロールベースの権限管理はcancancanを使う。
app/policies
は必要になったら作る - 条件が複雑なロジック(アドやレコメンドを出すかどうかみたいな?)がたくさんある場合にはpolicy objectを作る。たくさん無いときはserviceとかmodelに入れれば良さそう
gemの使い方はgemに合わせるわけですが、そうでない場合はcallback、validators同様、そのロジックが業務上重要で複雑な場合は、早めにクラスをわけてメンテし易いように保ちましょう、という話でした。
【パーフェクトRails】バリデーションをクラスに分離する
目的
パーフェクトRuby on Railsの9章の9−2「複雑なバリデーションとコールバックを整理する」を読んでいて 「バリデーションをクラスに分離する」について、知らないことがいくつかあったのでまとめます。
- 作者: すがわらまさのり,前島真一,近藤宇智朗,橋立友宏
- 出版社/メーカー: 技術評論社
- 発売日: 2014/06/06
- メディア: 大型本
- この商品を含むブログ (8件) を見る
バリデーションをクラスに分離するのは必要?
メールとか氏名とか、プロジェクトで要件が共通になると思うので積極的にやって良いと思う。
Open Source Rails でもapp/validators
は結構見かける。
どうやってやるのか?
例によってサンプルを見るのがわかりやすいと思います。
EachValidatorを使う
# app/validators/email_validator.rb class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors.add(attribute, 'is not an email') unless value =~ /.+@.+/ end end
上記の通りActiveModel::EachValidator
を継承してvalidate_each
を定義すればいい。
呼ぶときは以下のように呼ぶ。
class Person include ActiveModel::Validations attr_accessor :email, :name validates :email, email: true end
validates :email, email: true
は「emailというフィールドに対して、EmailValidator#validate_eachでチェックしてください。」というような解釈で良いと思う。EmailValidator
を見ると引数をがどのように渡ってくるかがわかると思う。
Validatorを使う
# app/validators/multi_presence_validator.rb class MultiPresenceValidator < ActiveModel::Validator def validate(record) record.errors.add(:base, 'bust be presence') if options[:attributes].all? { |c| record.send(c).blank? } end end
ActiveModel::Validator
を継承してvalidate
メソッドを定義する。
呼び出し方は以下。
class Person include ActiveModel::Validations attr_accessor :email, :name validates_with MultiPresenceValidator, attributes: [:email, :name] end
validates_with MultiPresenceValidator
は見たとおりMultiPresenceValidator#validateでバリデーションをする。
attributes: [:email, :name]
という引数に関しては、ActiveModel::Validator
のインスタンスからoptions[:attributes]
とすればさわれるようになっている。
validates_with Validator1, Validator2
と書くと両方のバリデーションを付け加えることができる
http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates_with
EachValidatorとValidatorの違い
EachValidator
は、上のサンプルの呼び出し方にある通り、1つのフィールドをチェックするやりかた(もちろん複数のフィールドをチェックするように書くことも出来なくはない)になる。
Validator
は複数のフィールドをチェックするのに使う。(こちらも1つのフィールドだけをチェックチェックするのは問題なく出来る。)
EachValidator
はValidator
のサブクラスになっている。
その他
これらの自作バリデーションや標準のバリデーションに関しては、モデルクラスごとにオブジェクトを持つ。インスタンス単位ではない。したがって、状態を持たないようにすること。
まとめ
app/valiadtors
ディレクトリはわりと一般的になっているので、プロジェクトたちあげのときに追加してバリデーションのクラス化を促すのが良いと思う。1フィールドのときは、
ActiveModel::EachValidator
を継承してvalidate_each
メソッド定義複数フィールドのときは、
ActiveModel::Validator
を継承してvalidate
メソッド定義PresenceValidatorsの実装とか読むといいと思う