blog.waterlow.work

Ruby, Rails, js, etc...

これだけ抑えればOK!権限管理のDB設計デザインパターン

目的

最近仕事で権限管理の設計をやっていたのだが、設計でかなりはまってしまった。
今後ははまらないように、DB設計や判断基準をまとめておく。

ベースとなるパターン

f:id:waterlow2013:20170127232245p:plain:h300

ユーザとロールは多対1で、ロールとアビリティは多対多に関連している。
権限管理やりましょうという場合にはこれにしておけば大抵の複雑な要求には対応できる。
もしユーザが増えてロールとアビリティの管理が複雑になってきても、DB管理なのである程度自由にやりことができる。
ただし、ちとファーストステップとしてはやりすぎか。

ユーザ-ロールが多対多パターン

f:id:waterlow2013:20170127232544p:plain:h300

前の例に加え、ユーザもロールを多数持つパターン。各権限が互いに素に近い状態、例えばカスタマーサポートとマーケティング、そのどちらも担当する人みたいなケースが有る場合に使える。
アプリケーションが大きくなっていて、権限の数も数十くらいになってきた場合はこれか。

直接アビリティパターン

f:id:waterlow2013:20170127232723p:plain:h300

ユーザ一人ひとりに応じて、できることが完全に異なるパターン。業務が多岐に渡っていて、かつ分業が進んでいる場合に使える。
ただし、ベースパターンでも同様のことはできるため、このパターンが活躍する機会は少ない。

ロールだけパターン

f:id:waterlow2013:20170127232816p:plain:h300 f:id:waterlow2013:20170127232819p:plain:h300 f:id:waterlow2013:20170127232822p:plain:h300

この場合はロールを表すテーブルもしくはカラムを用いてロールの情報のみ保存し、何ができるかはプログラム側で判断する。
アビリティの変化、増減や見直しが少ない場合(社内用管理画面等)に向いている
ユーザ-ロールが多対多、多対1、ユーザにカラム追加など、いろいろあり
このパターンがcancancanと組み合わせるのに良さそう。

admin:booleanパターン

f:id:waterlow2013:20170127232919p:plain:h300

割合的に、管理機能が少なく、管理画面みたいなものが存在しない場合に用いる。
railsチュートリアルとかで出てくる。

no権限パターン

アカウント持っている人は全員がすべてのことをできるパターン。
そもそも使用する人数が少ない場合。
内部向け管理画面はこっちのケースも多い。そもそも必要ない人にはアカウントを発行しないパターン。
できればこれで乗り切りたいので「権限管理したい」という話がきたら、「その権限、本当に必要ですか?」と聞いてあげよう。

まとめ

ベースパターン、ロールだけパターン、admin:booleanパターンだけ覚えておけば、Rails小〜中規模アプリケーションならほぼ対応できそう!

【Railsチュートリアル】慣習的に正しいコードの書き方

目的

Railsチュートリアルで「このコードは慣習的に正しくない」という記述があるが、なぜなのかを説明する。

背景

Railsを仕事で書いていく上で、モデル同士の関連の定義とその使い方をわりと意識しているのだが、いつどこで勉強したのか怪しかった。
出典を明らかにしたかった。

該当の内容

Railsチュートリアルの一説。

https://railstutorial.jp/chapters/user_microposts?version=5.0#code-micropost_validity_test

前提として、以下のような、関連を持つモデルUserMicropostがあるとする。(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の場合はUsermicropostsという関連(インタフェース)に依存させられる。

まとめ

いろいろ理由をつけてみましたが、やはり一番は「慣習的に」なのでしょう。
業務で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のインストールは適宜おこなってください。)

github.com

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)の箇所を読んだのでまとめました。

Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby Series)

Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby Series)

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アトリビュートメソッドのキャッシュの動きで若干はまったため、簡単にまとめる。

github.com

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の紹介」を参考に作ったときのまとめ。

railsapp/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

UsersControllerbefore_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ディレクトリの使い方

目的

railsapp/policiesディレクトリを作るときに、中身には何を入れるのかまとめる。

パーフェクトRuby on Railsの9章を読んでいて、コールバック・バリデーションをモデルから分離して、独立したクラスを作る方法を学びました。

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails

それらはappディレクトリ以下に、callbacksvalidatorsという他にも、app配下に置いておくものとしてpoliciesというディレクトリが有るようです。
ただし、調べてみるとpoliciesディレクトリの使い方は、そこまで一般解があるように思えなかったので、一旦まとめておいて今後検討する材料にしようかと思いました。

policiesディレクトリの使い方概要

  1. punditを使う(わりと一般的?)
  2. cancancanのavility.rbを、modelに入れずにpoliciesに入れる(avilitiesでもいいと思う)
  3. 特にgem等は使わず自分でピュアRubyのオブジェクトを定義して入れる
  4. 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同様、そのロジックが業務上重要で複雑な場合は、早めにクラスをわけてメンテし易いように保ちましょう、という話でした。