Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby:ShrineでUploader個別にアップロード先を設定するメモ

最近、ファイルアップロード用のgemであるShrineを使用する機会があったんですが、Uploader別にアップロード先を設定する方法が全然見つからず、だいぶハマってしまったので、やり方をφ(..)メモメモ

github.com

とりあえず公式のQuick startを見てみる

Shrine.storageに各Uploaderが共通で使用するストレージを情報を設定できる。

config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"), # permanent
}

Shrine.plugin :sequel # or :activerecord
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
Shrine.plugin :rack_file # for non-Rails apps

共通のアップロード先にアップロードするだけだったら、簡単で使いやすい😃

アップロード先を個別に設定する

Quick startを見てみるとShrine.stragesにアップロード先の情報を設定されているため、この値を変更する必要があるっぽい。 でも単純にShrine.strageを変更すると、画像アップロード処理全体に影響してしまう...😭

各クラスにShrine.stragesがなぜ反映されているか?

Shrine.staragesグローバル変数みたいな感じで定義されていて、それを参照しているのかと思ったけど、 どうやらサブクラスのクラスインスタンス変数としてdupでコピーされた値が反映されているっぽい😳

https://github.com/shrinerb/shrine/blob/master/lib/shrine.rb#L101

class Shrine
  @storages = {}
  module Base
    # 省略
    subclass.instance_variable_set(:@storages, storages.dup)

各Uploaderクラスに個別でアップロードパスを設定するには?

サブクラスのクラスインスタンス変数として設定されているということは、こんな感じでUploader別に設定出来るのでは?? 👀

class HogeImageUploader < Shrine
  @storages = {
    cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/hoge/cache"), # temporary
    store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/hoge/store"), # permanent
  }

image_urlも変わってる、できたっぽい🙌

# 個別の設定が使用されている
[1] pry(main)> HogeImage.first.image_url
=> "/uploads/store/hoge/hogehoge.png"
# 共通設定が使用されている
[2] pry(main)> FugaImage.first.image_url
=> "/uploads/store/fugafuga.png"

S3のバケットを使う場合でも同じように個別に設定してあげればOKみたいです😃

class HogeImageUploader < Shrine
  s3_options = {
    bucket:            "my-bucket", # required
    access_key_id:     "abc",
    secret_access_key: "xyz",
    region:            "my-region",
  }

  @storages = {
    cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
    store: Shrine::Storage::S3.new(**s3_options),
  }

2018/06/03追記

default_stragepluginをつかっても下記のような感じで出来るみたいですφ(..)

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"), # permanent
  special_cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
  special_store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"), # permanent
}

Shrine.plugin :sequel # or :activerecord
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
Shrine.plugin :rack_file # for non-Rails apps
class HogeImageUploader < Shrine
  plugin :default_storage, cache: :special_cache, store: :special_store
end

Shrine::Plugins::DefaultStorage

GithubMarketplaceで自動テスト、コード解析環境が簡単に出来て最高かよってなった話

GithubMarketPlaceからCircleciを導入してみたら、初めてだったたけど意外と簡単に、しかも無料で自動テスト・コード解析環境が作れて「最高かよ」という気持ちになった💪
ちなみに構築した環境は下記のような感じで、普通のrailsアプリケーションです🚋

key val
Langage ruby 2.5
Framework rails 5.2
Database posgresql
Testing Framework rspec
Code analyze rubocop

やり方メモ📝

GithubMarketPlaceでCircleciを導入する

まずはここから、Circleciを導入しましょう! github.com

こんな感じでcircleciを検索して、、、 f:id:madogiwa0124:20180524231227p:plain

私はもう導入済なので、「Edit your plan」となってしまってますが、「Setup a plan」になっていると思います🙌 f:id:madogiwa0124:20180524231558p:plain

クリックしたらcircleciの導入は完了です!

circleciにプロジェクトを追加する

circleciの導入が完了したら、次はリポジトリを紐付けます! Add Projectから自分のリポジトリを追加しましょう! f:id:madogiwa0124:20180524231739p:plain

circleci/config.ymlを作成する

リポジトリを追加したら、下記コマンドを実行してcircleciの設定ファイルを作成します。

$ mkdir circleci
$ touch circleci/config.yml

config.ymlの中身は私は下記のように設定しました。

version: 2
jobs:
  build:
    docker:
       - image: circleci/ruby:2.5.1-node-browsers
         environment:
           RAILS_ENV: test
           PGHOST: 127.0.0.1
       - image: circleci/postgres
         environment:
           POSTGRES_USER: circleci

    working_directory: ~/repo

    steps:
      - checkout

      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "Gemfile.lock" }}
          - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
        
      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

      # run tests!
      - run:
          name: run tests
          command: |
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"            
            bundle exec rspec $TEST_FILES
      - run:
          name: run code analyze
          command: bundle exec rubocop

結果を確認する

circleci/config.ymlを作成したら、いよいよリポジトリにpushします(・∀・) そうすると、、、 f:id:madogiwa0124:20180524232316p:plain

おおー、無事にコミット時にrspecとrubocopが実行されてますね🌟 f:id:madogiwa0124:20180524232443p:plain

おわりに

どうですかね?私はあまりこういう環境とか作ったことなかったんですが、結構簡単に作れてびっくりしました👀
circleciで自動テスト+コード解析して、本番環境にHerokuを使えばGithubのブランチと同期させて自動デプロイとかいう環境が無料で作れるとか、個人開発がはかどりますね😇!

RubyonRails:collection_selectにデフォルト値を設定する

Railsには、データベースの値からselectタグを作るcollection_tagという非常に便利なメソッドがあるんですが、デフォルト値の設定方法で少しハマったので、そのやり方をメモしておきますφ(..)

やり方

今回は、タスクにユーザーを割り当てる際に使用するセレクトボックスをイメージしてます。 ポイントは、selectedオプションに値を渡すことです(・∀・)

%div.form-group
  = label_tag :user, Task.human_attribute_name(:user)
  = collection_select :task, :user_id, User.all, :id, :name, { include_blank: true, selected: search_attr[:user_id] }, { class: 'form-control' }

selectedオプションに値を設定すれば、collection_selectにデフォルト値を設定することが出来ます!!

参考

collection_select - リファレンス - - Railsドキュメント

RubyonRails:検索条件を引き継いで検索結果のソート順を変更する

検索結果を指定したプロパティで並び変える際に検索条件を引き継いで、ソートする際に少しハマったのでやり方をメモφ(..)

イメージは下記みたいな、検索後のタスクの一覧を期限の降順で並び替えるような機能を想定してます(・∀・)

f:id:madogiwa0124:20180506224144g:plain

検索条件を引き継いでソートする方法

そもそもなぜ検索条件を引き継ぐ必要があるか

何も考えずにソート処理を実装すると下記のような形になるんじゃないかなぁと思うんですが、、、

  • 画面上のソートボタンを押下した際にgetパラメータとして項目とソート(降順・昇順)を設定
  • controllerでパラメーターを取得し、ソート順を設定しタスクの再取得
  • 一覧画面を再描画

上記実装では、少し問題があります。それは、入力された検索条件をcontollerに渡せていないので、ソートボタンを押すと検索条件が未設定の状態で検索処理が行われてしまう恐れがあり下記のように使いにくいものとなってしまいます。。。

f:id:madogiwa0124:20180506225012g:plain

検索条件を引き継いでソートを変更する

先程でも書きましたが、問題はソートを行う際にcontrollerに対して既存の検索条件を設定出来ていないために問題が発生してしまっているので、ソート時にも検索条件をパラメーターとして送信する対応を行いましたφ(..)

_order_form.html.hamlでソートする際にhidden_tagを使って検索条件も合わせてgetパラメーターとして送信するようにしています。

index.html.haml

= render partial: 'search_form', locals: { search_attr: @search_attr }
%table{ border: 1 }
  %thead
    %tr
      %th= Task.human_attribute_name(:title)
      %th= Task.human_attribute_name(:status)
      %th
        = Task.human_attribute_name(:priority)
        = render partial: 'order_form', locals: { target: 'priority', search_attr: @search_attr, order: 'asc' }
        = render partial: 'order_form', locals: { target: 'priority', search_attr: @search_attr, order: 'desc' }

_search_form.html.haml

%div
  = form_tag(search_tasks_path, method: :get) do
    = label_tag :task_title, Task.human_attribute_name(:title)
    = text_field_tag 'task[title]', search_attr[:title]
    = label_tag :task_status, Task.human_attribute_name(:status)
    - statuses = Task.statuses_i18n.invert
    = select_tag 'task[status]', options_for_select(statuses, search_attr[:status]), prompt: '-'
    = submit_tag t('common.search')

_order_form.html.haml

= form_tag(search_tasks_path, method: :get, style: 'display: inline') do
  = hidden_field_tag 'task[title]', search_attr[:title]
  = hidden_field_tag 'task[status]', search_attr[:status]
  = hidden_field_tag "order[#{target}]", order
  = submit_tag order_icon(order) # asc: ▲、desc:▼

ちなみにdeadline DESC等の文字列は、order_stringメソッドを使って生成するようにしてみました(・∀・)

task_controller.rb

class TasksController < ApplicationController
  def index
    prepare_search_attr
    @tasks = Task.all.order(order_string)
  end

  def search
    prepare_search_attr
    @tasks = Task.search(@search_attr).order(order_string)
    render :index
  end

  private

  def order_string
    return 'created_at DESC' unless params.key?(:order)
    order_params.to_h.map { |key, val| "#{key} #{val.upcase}" }.join(',')
  end

  def prepare_search_attr
    @search_attr = { title: '' }
    @search_attr = task_params.delete_if { |_key, val| val.blank? } if params.key?(:task)
  end

  def task_params
    params.require(:task).permit(:title, :description, :status, :priority, :deadline)
  end

  def order_params
    params.require(:order).permit(:deadline, :priority)
  end
end

おわりに

今回はhidden_tagを使って検索条件を引き継いでソートする方法で今回は対応してみました。やりたいことは出来ましたが、もっとRailsっぽい良い方法があるような気がしますね・・・!
なにかまたいい方法があればまとめてみようと思いますφ(..)

参考

hidden_field - リファレンス - - Railsドキュメント

Webエンジニア転職1ヶ月目まとめ、 コーディング量とか数字で整理してみた

みなさん、こんにちは!(・∀・) 4月からSIerからWeb系の企業に転職して、あっという間に1ヶ月が経ちました。

この1ヶ月間につくったPRのSTEP数と作成するまでに掛かった工数を整理してみましたφ(..)
Web系企業に転職すると、1ヶ月でどれくらいコードを書くのか参考になれば幸いです!

つくったPRの一覧

PR 増加STEP数 削減STEP数 開発工数(人日)
1 177 234 5
2 92 6 3
3 1 1 0.5
4 35 2 2
5 3 3 0.25
6 89 5 2
7 3 3 0.5
8 37 17 1
9 648 75 8

合計STEP数(増加+削減)は1431でした!稼働日(20日)で割ると1日に71.55STEPぐらいは1日にコード書いて過ごしてます(・∀・) ちなみに1番小さいPR#5はGihubのPRテンプレートの修正、1番大きいPR#6は大きめのUI改修でした。その他は、管理画面の検索条件増やしたり、ランキングの集計期間を変更したりといった機能修正を行っている感じですねφ(..)

SIer時代は、ほとんどコード書いてなくてなかったので、すでにSIer時代に書いたコードの総量を超えた気がするので、そういう意味では、転職してよかったなぁと思いますね(・∀・)

所感

まずは転職して1ヶ月無事に過ごせてホッとしているのが正直な感想です。

プログラミングスクールに通ったり、個人で勉強したり、Webサービス作ったりしましたが、いかんせん業務では未経験だったので、本当にやっていけるか不安で仕方がなかったんですが、会社のエンジニアの方々に色々教えて頂いてなんとか1ヶ月無事に過ごすことが出来ました。感謝。(..)

しかし、もうちょっとオブジェクト指向を意識したコーディングとかRails機能への理解とか、RubyのHashとかArrayの効率的な扱い方とか挙げるときりが無いですが、早くコストを脱却してチームに貢献出来るように頑張りたいです・・・!

最近、勉強のために万葉さんの新人研修を最近始めてみました!
各機能毎にPRを作っているので、アドバイス等頂けると嬉しいです、また誰か同じように始めた人がいれば私もコメントしますm( )m

github.com

RubyonRails:form_tagを使って入力値がリセットされない検索フォームを作る

Railsでフォームを作る際にform_forを使うとページの再描画が発生しても入力値がリセットされませんが、form_tagを使うとリセットされてしまいます。。。

f:id:madogiwa0124:20180504220013g:plain

検索フォーム等では、form_tagを使用することが多いような気がするのですが、検索ボタンを押してもリセットされずに残ってほしいなー!と思い対応してみたので、その方法をメモしておきますφ(..)

入力値がリセットされない検索フォームの作り方メモ

今回は、indexアクションでは全件取得、searchアクションでは検索条件に合致したレコードを取得して、最終的に共通なViewを描画するようなケースを想定してます。

Controller側

controller側のポイントは、prepare_search_attrです。
indexの場合はパラメーターが無いケースがあるので、検索条件を保持するインスタンス変数@search_attrに初期値を入れておき、検索条件が設定されていた場合は、それを上書きするようにしています。

controller

class TasksController < ApplicationController
  def index
    prepare_search_attr
    @tasks = Task.all
  end

  def search
    prepare_search_attr
    @tasks = Task.search(@search_attr)
    render :index
  end

  private

  def prepare_search_attr
    @search_attr = { title: '', status: '' }
    @search_attr = task_params.delete_if { |_key, val| val.blank? } if params.key?(:task)
  end

  def task_params
    params.require(:task).permit(:title, :description, :status, :priority, :deadline)
  end
end

View側

view側のポイントは_search_form.html.hamltext_field_tagselect_tagを記載しているところです。
text_field_tagは第二引数に渡された値が初期値として設定され、select_tagoptions_for_selectの第二引数に渡された値が初期値として設定されます。

そのため、controller側で定義しておいた@search_attrを、渡して初期値を設定することが出来ます。

index.html.haml

%h1= t '.title'
= render partial: 'search_form', locals: { search_attr: @search_attr }
%br
%table{ border: 1 }
  %thead
    %tr
      %th= Task.human_attribute_name(:title)
      %th= Task.human_attribute_name(:status)
      %th= Task.human_attribute_name(:priority)
      %th= Task.human_attribute_name(:deadline)
      %th Actions

_search_form.html.haml

%div
  = form_tag(search_tasks_path, method: :get) do
    = label_tag :task_title, Task.human_attribute_name(:title)
    = text_field_tag 'task[title]', search_attr[:title]
    = label_tag :task_status, Task.human_attribute_name(:status)
    - statuses = Task.statuses_i18n.invert
    = select_tag 'task[status]', options_for_select(statuses, search_attr[:status]), prompt: '-'
    = submit_tag t('common.search')

おわりに

Railsの検索フォームの作り方は、毎度悩んでこれで良いのかなぁ感が大きいですね・・・(._.)もっと良い作り方等があれば、教えてください!!🙌

またコードの全文は万葉さんの公開している新人研修をやっている私のリポジトリにあるので見てアドバイスもらえるとありがたいですm( )m

github.com

参考

qiita.com

text_field - リファレンス - - Railsドキュメント

select_tag - リファレンス - - Railsドキュメント

RubyonRails:acts-as-taggable-onのN+1問題を解決する

自分で運営しているサービスのタグ付け機能にacts-as-taggable-onを使っていたのですが、N+1が発生しまくってしまったので、その解決方法をφ(..)メモメモ

acts-as-taggable-onのバージョンは下記の通りです。

$ gem list
acts-as-taggable-on (5.0.0)

解決法

修正前のコード

まず修正前のコードですが、普通にPageの全件を取得してtag_listでタグの名称のリストを取得して、それを表示するようなコードですね(・∀・)

controller

  def index
     @pages = Page.all
  end

view

<% @pages.each do |page| %>
  <% if page.tag_list.present? %>
    タグ:<%= page.tag_list.join(', ') %>
  <% end %>
<% end %>

しかし、以下のようなコードだと下記のようにタグの検索SQLがレコード単位に発生してパフォーマンスが良くないです。。。(N+1問題が発生してます、、、。)

log

  ActsAsTaggableOn::Tagging Load (0.5ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2  [["taggable_id", 23], ["taggable_type", "Page"]]
  ActsAsTaggableOn::Tag Load (0.7ms)  SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" ="taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL)  [["taggable_id", 23], ["taggable_type", "Page"]]
  ActsAsTaggableOn::Tagging Load (0.6ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2  [["taggable_id", 16], ["taggable_type", "Page"]]
  ActsAsTaggableOn::Tag Load (1.0ms)  SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" ="taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL)  [["taggable_id", 16], ["taggable_type", "Page"]]

修正後のコード

修正のポイントは、下記の2つですφ(..)

  • tagsincludesし、キャッシュすること
  • タグの名称取得にがtag_listを使用しないこと

controller側で、Pageを取得する際に.includes(:tags)を追記し、キャッシュするようにしています。

controller

  def index
     @pages = Page.all.includes(:tags)
  end

また、viewで表示するときにはtagsを使用し、名前だけ取得するときにはpluckを使うようにします(・∀・)

view

<% @pages.each do |page| %>
  <% if page.tags.present? %>
    タグ:<%= page.tags.pluck(:name).join(', ') %>
  <% end %>
<% end %>

これにより、毎回タグ関連のTBLを取得するSQLが発生を防ぐことが出来ました!\(^o^)/

参考

qiita.com