いつか役に立つかもしれないRailsの技3選

k-shogo
63

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。

こんにちは、クリスマスの予定を聞かれてもnilを返すk−shogoです。今回はいつか役に立つかもしれないちょっとニッチなRailsの技を紹介します。

has_manyを拡張する

Railsのhas_manyは自動的にリレーションを構築してくれて便利ですね。実はこのhas_manyのリレーションは拡張することが出来るのです。

さっそくサンプルを作成します。今回はUser,Group,UserGroupingの3つのモデルが存在し、UserGroupUserGroupingを介して多対多の関係を持つことにします。さらにUserGroupingには役割を示すroleカラムも持たせます。マイグレーションで示すと以下のようになります。

class CreateUserGroupings < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name

      t.timestamps null: false
    end

    create_table :groups do |t|
      t.string :name

      t.timestamps null: false
    end

    create_table :user_groupings do |t|
      t.references :user,  index: true, foreign_key: true
      t.references :group, index: true, foreign_key: true
      t.string :role, null: false, default: 'viewer'

      t.timestamps null: false

      t.index [:user_id, :group_id], unique: true
    end
  end
end

続いてモデルの実装です。UserGrouphas_many :user_groupingsを持ち、互いにthroughオプションでuser_groupingsを介して多対多の関連を記述します。UserGroupingのロールは3種類定義してあり、初期値設定にはFooBarWidget/default_value_forを用いています。

class User < ActiveRecord::Base
  has_many :user_groupings
  has_many :groups, through: :user_groupings
end

class Group < ActiveRecord::Base
  has_many :user_groupings
  has_many :users, through: :user_groupings
end

class UserGrouping < ActiveRecord::Base
  enum role: {manager: 'manager', submitter: 'submitter', viewer: 'viewer'}
  default_value_for :role, :viewer

  belongs_to :user
  belongs_to :group

  validates :user,  presence: true
  validates :group, presence: true, uniqueness: {scope: :user}
end

これで多対多の関連であるUser#groupsGroup#usersを使うことが出来るようになりました。ここでGroupからみてmanagerロールを持つユーザーを取得したい場合はどうするでしょうか。スコープブロックを用いて以下のように新たな関連を定義することも出来ます。

class Group < ActiveRecord::Base
  # ...snip...
  has_many :manager_groupings, -> {manager}, class_name: :UserGrouping
  has_many :managers, through: :manager_groupings, class_name: :User, source: :user
end

しかしこの方法では柔軟な拡張が出来ず、仕様追加のたびに関連の定義を増やさなければなりません。このような場合にhas_manyの拡張は有効な手段です。has_manyの拡張はブロックでメソッドを追加するだけです。今回はUsergroups関連にロールで絞り込みが出来るメソッドを追加しています。絞り込みはUserGroupingに追加したスコープをmergeすることで実現しています。

class UserGrouping < ActiveRecord::Base
  # ...snip...
  scope :role_is, -> (*roles) { where(role: roles) }
end

class User < ActiveRecord::Base
  has_many :user_groupings
  has_many :groups, through: :user_groupings do
    def role_is *roles
      merge(UserGrouping.role_is(*roles))
    end
  end
end

これでgroups関連の拡張が出来たので、User.first.groups.role_is(:manager, :submitter)のような絞り込みが可能になりました。しかしロールでの絞り込みはUserGroupのどちらからの関連でも必要になり、ブロックの記述が重複してしまいます。そんな場合にはモジュールにしてextendingオプションを指定しましょう。モジュールでの拡張は複数指定指定することも可能です。

module UserGroupingExtension
  def role_is *roles
    merge(UserGrouping.role_is(*roles))
  end
end

class User < ActiveRecord::Base
  has_many :user_groupings
  has_many :groups, -> {extending UserGroupingExtension}, through: :user_groupings
end

class Group < ActiveRecord::Base
  has_many :user_groupings
  has_many :users, -> {extending UserGroupingExtension}, through: :user_groupings
end

has_manyの拡張をうまく使うと、散らかりがちな関連の定義をすっきりとまとめるとが出来ます。年末のモデル大掃除の時に思い出してみて下さい。

データURIスキームでファイルをアップロードする

Railsでファイルアップロードを実装したいとき、様々なGemが存在していて簡単に実現することができます。今回はcarrierwaveuploader/carrierwaveを使ってUserモデルにアバター画像を設定できるようにしてみます。Getting Startedに習ってAvatarUploaderを作成し、Userモデルにmount_uploaderするだけです。

class User < ActiveRecord::Base
  mount_uploader :avatar, AvatarUploader
end

ここでアップロード時にユーザーがクロッピングできるようにしたいという要望が来たとしましょう。単純なリサイズの場合carrierwaveには resize_to_fitなどのリサイズ用メソッドが用意されています。しかし任意の座標でのクロッピングは自分で作らなければなりません。RailsCastsでpaperclipJcropを用いたクロッピングのサンプル(#182 Cropping Images)を発見しましたが、この手法は切り抜き開始座標と縦横サイズを表すパラメータ( :crop_x, :crop_y, :crop_w, :crop_h)を扱わなければならず面倒ですね。そもそもサーバーサイドに画像を加工する為に必要なImageMagickを準備するのが面倒な場合もあります。

そんなときにはクライアントで加工してデータURIスキームでサーバーに送信する手法を検討してみてはいかがでしょうか。データURIスキームはwebページにデータを埋め込む際に用いられますが、逆にサーバーに送信することも可能です。クライアントでのクロッピングにはこれも沢山の選択肢がありますが、今回はcropitを用いました。データURIスキームでの出力をサポートしていればクライアントのライブラリは何でも構いません。

実装に入りましょう。まずはデータURIスキームを処理するためのモジュールを用意します。data_uri_to_fileメソッドはデータURIスキーム、つまり文字列をRailsにおいてアップロードされたファイルを扱うActionDispatch::Http::UploadedFileに変換します。メソッドは、データURIスキームを正規表現で分割、Tempfileへの書き出し、ActionDispatch::Http::UploadedFileの生成の3つのパートで構成されています。

module DataUriParseable
  extend ActiveSupport::Concern

  module ClassMethods
    def data_uri_to_file data_uri
      data = data_uri.try do |uri|
        uri.match(%r{\Adata:(?<type>.*?);(?<encoder>.*?),(?<data>.*)\z}) do |md|
          {
            type:      md[:type],
            encoder:   md[:encoder],
            data:      Base64.decode64(md[:data]),
            extension: md[:type].split('/')[1]
          }
        end
      end
      return nil unless data

      temp_file = Tempfile.new('uploaded-data_uri').tap do |file|
        file.binmode
        file << data[:data]
        file.rewind
      end

      ActionDispatch::Http::UploadedFile.new(
        filename: "data_uri.#{data[:extension]}",
        type:     data[:type],
        tempfile: temp_file
      )
    end
  end
end

続いては作成したモジュールを使用する側のUserクラスです。モジュールをincludeしてdata_uri_to_fileメソッドを使えるようにしておきます。そしてデータURIスキームをフォームから受け取るためにattr_accessoravatar_data_uriを用意しました。データURIスキームからActionDispatch::Http::UploadedFileへの変換については今回はbefore_validationコールバックで行っていますが、これは要件次第でしょう。

class User < ActiveRecord::Base
  include DataUriParseable
  mount_uploader :avatar, AvatarUploader

  attr_accessor :avatar_data_uri

  before_validation :set_avatar_from_data_uri, if: -> { self.avatar_data_uri.present? }

  def set_avatar_from_data_uri
    self.avatar = self.class.data_uri_to_file(avatar_data_uri)
  end
end

フォームも準備します。サンプルではslim記法で、かつsimple_formも使っていますが、重要なのはhidden_fieldでデータURIスキームを格納するavatar_data_uriを準備することです。CSSは画像のプレビュー要素の為に用意しました。

= simple_form_for @user, html: {id: :user_form}  do |f|
  = f.error_notification

  .form-inputs#image-cropper
    .cropit-image-preview
    input.cropit-image-zoom-input type="range"

    = f.label :avatar
    = f.file_field :avatar, class: 'cropit-image-input'
    = f.hidden_field :avatar_data_uri
  .form-actions
    = f.button :submit
.cropit-image-preview
  width: 300px
  height: 300px
  background-color: #f0f0f0

最後ににちょっとしたCoffeeScriptを追加します。最初はcropitを有効化するためのもので、2行目以降はサブミット時に画面に表示されている画像をデータURIスキームでavatar_data_uriに書き込むためのコードです。

$('#image-cropper').cropit()
$('#user_form').on 'submit', ->
  imageData = $('#image-cropper').cropit('export')
  $('#user_avatar_data_uri').val(imageData)
  $('#user_avatar').replaceWith($('#user_avatar').clone())

これでクライアントで画像のクロッピングを行い、サーバー側はクロッピング済み画像をデータURIスキームで受け取ることが出来ました。クライアントはデータURIスキームで書き出せさえすれば良いので、cropit以外のライブラリに変更してもサーバー側の実装は変更する必要はありません。CanvasJavaScriptistを用いてクライアントで画像にフィルターをかけてから加工済み画像をサーバーに送る、なんて使い方も出来ますね。

絵文字の対応

絵文字の対応していますか?モバイルからの入力を受け付ける場合、簡単に絵文字が入力できるので対応する場面は増えていますね。データベースにMySQLを使用している場合、文字コード指定をutf8mb4として4バイト文字を扱えるようにしておかなければなりません(MySQL 5.5.3 以降)。

データベースに突っ込むだけなら文字コード指定だけで良いのですが、DB以外に検索用途でElasticsearchやRedisにもデータを格納する場合や、独自の絵文字に対応したいなんて時には絵文字を特定フォーマットの文字列に変換して格納する方法も検討してみてはいかがでしょうか。今回は絵文字を画像として扱うためのgithub/gemoji、gemojiを扱いやすくするgmac/gemoji-parser、そして入力のノーマライズを行うdimko/normalizrを用いてサンプルを作成します。

まず、gemojiの絵文字表示用の画像をアセットのパスに追加します。config/initializers/assets.rbにgemojiの設定を追記します。

Rails.application.config.assets.paths << Emoji.images_path
Rails.application.config.assets.precompile << "emoji/**/*.png"

続いて、入力時に使用する絵文字用のノーマライザを用意します。config/initializers/normalizers.rbを用意し、以下の記述を追加します。EmojiParser.tokenizeによって絵文字が:smile:のようなコロンで囲まれた文字列に変換されます。例えば熱帯魚の絵文字🐠は:tropical_fish:に変換されます。

Normalizr.configure do
  add :emoji do |value|
    String === value ? EmojiParser.tokenize(value) : value
  end
end

ユーザーモデルは絵文字用のノーマライザを用いるカラム(ここではname)を指定するだけです。
これでフォームから絵文字を入力された場合、絵文字はコロンで囲まれた文字列に変換された後にデータベースに格納されます。

class User < ActiveRecord::Base
  normalize :name, with: :emoji
end

用意されている絵文字だけでは面白くないので、独自の絵文字も追加してみましょう。app/assets/images以下にemojiディレクトリを用意してファイルを配置します。今回はruby.svgというファイルを配置しました。そしてconfig/initializers/emoji.rbを作成し、独自の絵文字を登録します。ここではrubyとして登録を行いました。

Emoji.create('ruby') do |char|
  char.image_filename = 'emoji/ruby.svg'
end

最後に表示用のヘルパーを作成します。app/helpers/emoji_helper.rbを用意し、emojifyメソッドを定義します。EmojiParser.parse_tokensはコロンで囲まれた文字列が登録されている絵文字の場合にgemojiのEmoji::Characterへと変換してくれます。独自に登録した絵文字か否かでパスの調整を行い、image_tagヘルパーに渡しています。

module EmojiHelper
  def emojify content
    EmojiParser.parse_tokens(content.to_s) do |emoji|
      path = [(emoji.custom? ? nil : 'emoji'), emoji.image_filename].compact.join('/')
      image_tag(path, class: 'emoji', alt: emoji.name)
    end.html_safe if content.present?
  end
end

あとは絵文字用のCSSを用意し、表示時に絵文字用のヘルパーを使うだけです。

img.emoji
  vertical-align: middle
  width: 20px
  height: 20px
p
  strong Name:
  = emojify @user.name

これでI love :ruby:と入力してみると自分で追加した絵文字も表示されます。

emoji

今回のサンプルでは絵文字を変換して格納し、表示時は画像で置き換える手法をとりました。この手法ではPCでも絵文字を入力しやすく出来るメリットもあります。本格的にするならJavaScriptなどでサジェストしてあげると良いですね。自分のサービス独自の絵文字を用意するのも楽しいです。