blog

紫陽花

Rails, Carrierwave, MiniMagick, Piet, 画像アップロード, 画像圧縮

Railsで画像をアップロードし、かつ画像圧縮も行いたい時の実装をメモで残す。

環境

Macbook

OSX 10.11.6

Rails 5.0.0

Ruby 2.3

Carrierwave

Piet

MiniMagick

Gem

実装に必要なGemをインストール

gem "carrierwave"
gem "piet"
gem "mini_magick"

bundle install --path vendor/bundle

Piet

Carrierwaveのextensionとして画像圧縮を行えるようにするGem

圧縮を行う類似のGemがいくつか存在するがPietを採用

トータルダウンロード数 比較

piet : 242,713
carrierwave-imageoptimizer : 97,947

jpegoptim

.jpeg画像の圧縮で使用するパッケージ

brew install jpegiotim

pngquant

.png画像の圧縮で使用するパッケージ

画像圧縮を行うPietではpngquantを用いて圧縮を行えるように他のGemを利用している。

png_quantizator

Pietのドキュメントに導入方法が記載

pngquantが圧縮効率が良いのでこちらを採用する。

圧縮効率の比較資料

brew install pngquant

アップローダーの作成

bundle exec rails g uploader image
#=> app/uploaders/image_uploader.rb

上記のコマンドを実行しアップローダーを作成します。

class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  include Piet::CarrierWaveExtension
  
  process quality: 80
  
  # Large size
  process resize_to_limit: [640, 640]
  process :custom_optimize
  
  # Midium size
  version :midium do
    process resize_to_limit: [300, 300]
    process :custom_optimize
  end
  
  # Small size
  version :small do
    process resize_to_limit: [200, 200]
    process :custom_optimize
  end
  
 
  
  def extension_white_list
    %w(jpg jpeg gif png)
  end
  
  def mimetype
    IO.popen(["file", "--brief", "--mime-type", path], in: :close, err: :close) { |io| io.read.chomp.sub(/image\//, "") }
  end
  
  
  def custom_optimize
    case mimetype
    when "png" then pngquant
    when "jpeg", "gif" then optimize(quality: 90)
  end
end

custom_optimize メソッド独自で定義し、画像の拡張子によって実行する圧縮を分岐させるような形に。

initialize以下のcarrierwave.rbにも定義

CarrierWave.configure do |config|
  # hogehoge
end
module CarrierWave
  module MiniMagick
    def quality(percentage)
      manipulate! do |img|
        img.quality(percentage.to_s)
        img = yield(img) if block_given?
        img
      end
    end
  end
end

流れで画像圧縮のGemを作ったので興味があればこちらも。

Rails CarrierWave アップロード画像を良い感じに圧縮するGem作った

リファクタリング:Rubyエディション 読んだ

リファクタリング:Rubyエディション」 読みました。

リファクタリング:Rubyエディション

リファクタリング:Rubyエディション

最近は「コードの質を高め、生産性の高いコードを書く」ことを1つの目標にしていて、綺麗なコードの書き方や設計周りの本を読んでいます。

リファクタリング:Rubyエディション」は前々から読んでみたいと思っていたのですが、1冊4800円もするので買うのに少しビビってました。(学生財布)

しかしいざ、買って読んでみるとなんでもっと早く買わなかったんだと。4800円をケチってた自分を叱りたくなりました。

内容はもう目から鱗で。今まで自分が思っていた、考えていたことをうまく言語化してくれていて本当に素晴らしかったです(特にカタログ周り)

メタプログラミングRubyも素晴らしい本でしたが、リファクタリング:Rubyも負けないぐらいいい本です。 Rubyエンジニアの人たちの必読書になると思います。

感想

内容は主に4つの構成

1.リファクタリング -最初のサンプル-

リファクタリングの重要性を理解するためにも最初に大きくサンプルコードが載ってあり非常にわかりやすかったです。 学び始めのとっかかりとしては最高。

1つ1つのリファクタリングの積み重ねでここまで綺麗な構成に変わるものだと、before afterで見せてくれます。

また、UMLでも構成を表現してくれるので処理のフローが理解しやすいです。 実務でも構成だったり、処理のフローを整理したい場合の模範にできるなと思いました。

2.リファクタリングの原則

リファクタリングとはこういうものだよ。という概念的な部分の説明

これらをわかりやすく説明しています。

リファクタリングを導入すると、強調点が変わる。事前設計をすることに変わりはないが、完璧な解を探そうとはしなくなる。まずまず妥当な解であれば良い。」

3.コードの臭い

コードの臭いは、リファクタリングをするべき箇所を臭いという表現で説明しています。 3があるおかげで4のカタログの内容理解がグッと高まります。(バイキルト的な)

4.リファクタリングのカタログ

リファクタリングのテクニックをカタログ形式で示してくれています。

カタログを読むことでテクニックが言語化されてまとめられてとても良かったです。

全て頭に入っているわけではないので、今後リファクタリングを進める中で全てマスターしていきたい。

一覧は覚えたテクニックリスト

メソッドの構成方法

  • メソッドの抽出
  • メソッドのインライン化
  • 一時変数のインライン化
  • 一時変数から問い合わせメソッドへ
  • 一時変数からチェインへ
  • 説明用変数の導入
  • 一時変数の分割
  • 引数への代入の除去
  • メソッドからメソッドオブジェクトへ
  • アルゴリズム変更
  • ループからコレクションクロージャメソッドへ
  • 名前付き引数の導入
  • 名前付き引数の除去
  • 使われていないデフォルト引数の除去
  • 動的メソッド定義

オブジェクト間でのメンバの移動

  • メソッドの移動
  • フィールドの移動
  • クラスの抽出
  • クラスのインライン化
  • 委譲の隠蔽
  • 横流しブローカーの除去

データの構成

  • 自己カプセル化フィールド
  • データ値からオブジェクトへ
  • 値から参照へ
  • 参照から値へ
  • 配列からオブジェクトへ
  • マジックナンバーからシンボル定数へ
  • タイプコードからポリモーフィズム
  • タイプコードからモジュールのextendへ
  • タイプコードからState/Strategyへ
  • 属性初期化の遅延実行
  • 属性初期化の先行実行

条件式の単純化

  • 条件文の分解
  • 条件分岐の組み替え
  • 条件式の統合
  • 重複する条件分岐の断片の統合
  • 制御フラグの除去
  • 条件分岐のネストからガード節へ
  • 条件分岐からポリモーフィズム

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

シングルトンパターンとは

  • 作成したクラスのインスタンスが唯一であることが保証される
  • 唯一であるがゆえに、生成した1つだけのインスタンスにアクセスすることができる。

実装

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として存在する。

Rails 1.x の時代には、Rails 本体に組み込まれていたけれど、その後プラグインとして分離されました。

引用: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

簡単にイケてる実装ができて便利!

参考

acts_as_list: gem か plugin か

Rails 4で作るドラッグアンドドロップで表示順を変更できるサンプルアプリ(スクリーンキャスト付き)

Ruby/Ruby on Rails/acts_as_list

[Rails]acts_as_listでモデルオブジェクトの並び替え

Compositeパターン Ruby

Compositeパターン

コンポジットパターンとは

  • あるものが同じような下位のもので作られているという考え方
  • 大きなオブジェクトが小さな子オブジェクトから構成されていて、その子オブジェクトもさらに小さな孫オブジェクトでできていたりする
  • 階層構造やツリー構造のオブジェクトを作りたい時に利用出来る

構成

再帰」とは、ある処理の中で再びその手続きを呼び出すこと。

用いるメリット

  • ファイルシステムなどの木構造を伴う再起的なデータ構造を表現できる
  • 階層構造で表現されるオブジェクトの取扱いを楽にする
# 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を削除しました