20210610のRubyに関する記事は9件です。

RailsでインポートするCSVにBOMをつけたら列名に一致するカラムの値を取り出せなくなった

気づけば簡単な話なのですが、RailsでCSVインポート機能を作っている時にサンプルCSVがもしエクセルで開いても文字化けしないようにとBOMをつけたら、読み込む時に一致するカラムの値を取り出せなくなったので記録しておこうと思います。 TL;DR BOMがついている時にCSVの値をカラム名で指定してしまうと列名の最初の文字にBOM(ZERO WIDTH NO-BREAK SPACE, 文字コード65279)が入ってしまい、読み込む際に列名を指定しても適切に取得できなくなります。 見えないので最初の文字をString#ordメソッドなどで比較しないと気づきにくいです。 解決方法は追記に書いてあります。 本文 下記がBOMをつけてダウンロードするようにしたサンプルCSVのapplication_controller.rb です。 他のコントローラでこのコントローラを継承して、rowsメソッドを実装する形になります。 controllers/admin/csv/sample/application_controller.rb class Admin::Csv::Sample::ApplicationController < Admin::Csv::ApplicationController def show respond_to do |format| format.csv do send_data create_csv, type: :csv, filename: "sample_#{controller_name}.csv" end end end protected # 子のコントローラでオーバーライドを強制 def rows raise NotImplementedError.new("You must implement #{self.class}##{__method__}") end def create_csv bom = %w[EF BB BF].map { |e| e.hex.chr }.join CSV.generate(bom) do |csv| rows.each do |row| csv << row end end end end 下記は例としてsports_controller.rb controllers/admin/csv/sample/sports_controller.rb class Admin::Csv::Sample::SportsController < Admin::Csv::Sample::ApplicationController private def rows [ ["name"], ["バスケットボール"], ] end end このようにしてダウンロードしたCSVをモデルに記述したメソッドでインポートしようとします。 models/sport.rb class Sport < ApplicationRecord class << self def import(file) CSV.foreach(file.path, headers: true) do |row| record = find_by(id: row["id"]) || new record.attributes = row.to_hash.slice(*updatable_attributes) record.save end end def updatable_attributes ["name"] end end end controllers/admin/csv/sports_controller.rb class Admin::Csv::SportsController < Admin::Csv::ApplicationController def new end def create Sport.import(params[:file]) flash[:notice] = t('action.imported') redirect_to admin_sports_url end end 着目すべきは以下の行です。 record.attributes = row.to_hash.slice(*updatable_attributes) ここではモデルのインスタンスの各フィールドに許可された値を挿入しています。 sport.rbで許可された値はnameのみなので、{'name': 'バスケットボール'}のような値が入るはずですが、BOMがついているとrow.to_hashで返ってくるkeyの部分がBOM付きのnameとなりupdatable_attributesのnameという文字列と一致しません。 これによってCSVインポートしても値が更新されないという現象が発生してしまいます。 しかもぱっと見は同じ文字列に見えるのでなかなか気づきにくいです。 これを解決するにはsampleのCSVにBOMをつけないか、列名ではなく列の順序などで値を取得するなどの方法が考えられるかと思います。 BOM付きのCSVをインポートする場合はお気をつけください。 追記 @mcfishさんのご指摘によりCSVクラスの特異メソッドforeachの引数encodingにBOMを考慮した'BOM|UTF-8'を設定すればBOM付きのCSVでも問題なくインポートできました。 ご指摘頂きありがとうございます。 CSV.foreach(file.path, headers: true, endoging: 'BOM|UTF-8') do |row|
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

index.html.erbに全ての投稿を表示させる方法 Rails

ホロライブの桃鈴ねねさんが大好きです。 なので、Webページに全てのアーカイブを表示したいと思います。 ※あらかじめテーブルに全てのアーカイブ情報を取得してます。 app/controllers/archive_controller.rb class ArchiveController < ApplicationController def index @archive = Archive.all end end これで@archiveにあらかじめarchivesテーブルに登録しておいた情報を格納します。 app/views/archive/index.html.erb <%= render partial: "archive" , collection: @archive %> renderメソッドを使うことで、@archiveというインスタンス変数を_archive.html.erbでも使用できます。 更にオプションでcollectionを使用することで、@archiveに格納されている全ての情報を使用できます。 app/views/archive/_archive.html.erb <div class="card"> <%= link_to "#{archive.archiveurl}" do %> <%= image_tag (archive.archiveimg) %> <% end %> </div> この辺のビューは調整が必要ですが、簡単にやればこんな感じ。 archiveimgにはサムネイルの画像URLが格納されています。 archiveurlには動画のURLが格納されています。 ポイントはrenderメソッドを使用することと、collectionというオプションを使用すること、partialを使用することで、一つのファイルの記述量を減らすことが出来ます。 とりあえずこれで、動画情報を全て表示する、という目標が達成できました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec 基本】 Rails初学者の自分が知らなかった開発環境ごとにDBが違う話

プログラミング初学者の私が、RSpecを書いていて 環境ごとにDBが違うことを知らずハマったので、記事に書き留めておきます。 プログラミング初学者の方の参考になれば嬉しいです。 また、間違い等ありましたらご指摘頂けると助かります。 それでは、見ていきましょう!! 【RSpec 基本】 Rails初学者の自分が知らなかった開発環境ごとにDBが違う話 RSpecを書く中で 「テストってそもそもプロダクトコードをブラウザで動かしながらやってるの?」 「RSpecで定義した変数と、プロダクトコードに定義した変数ってどう違う?」 「RSpecで定義したデータって、テストのあとはどうなるの?」 こんな疑問が頭の中にブワーッと入ってきて、「何がわからないのかわからない」状態になりました。笑 そんな初学者あるある?な疑問を解決していきます! Railsの環境には3つの種類がある 結論から言うと、下記の4点を知ることで疑問が解決しました! 「Railsには 本番環境、開発環境、テスト環境の3つの環境が用意されており、それぞれで違うDBを参照している。」 「RSpecは テスト環境で実行されるため、作成されたレコードはテスト環境用のDBに格納され、テスト終了後に削除される。」 テスト時には新しくヘッドレス(見えない)ブラウザを立ち上げて、テスト用DBから取得した内容を表示したページを検証している。 表示ページのHTMLはプロダクトコードを元に生成される。 Railsの「3つの環境」については他の記事でもたくさん解説されています。 【Rails】Railsの3つの環境 【Rails】configのenvironments配下のファイルの意味と記述内容を理解する。(development.rb, production.rb, test.rbとは?開発環境・本番環境・テスト環境の違い) 本番環境 実際にユーザーがシステムを使用する環境のことです。 具体的に言うと、普段私たちが使っている「amazon」や「楽天市場」が本番環境にアップされたサービスです。 例え話をすると、、 プログラミングを初めると必ずといって良いほど 「herokuにデプロイする」 という言葉を見かけます。 これはコーディングしたプログラムをherokuにデプロイする(pushする)ことでURLが作られて、そのURLを友だちに送ったりすると作ったサービスを見られるようになります。 (本番環境にアップしないとそのサービスを第3者がインターネット上で見ることはできません) 誰でも見られるそのサービスは「本番環境で動いている」ということになります。 開発環境 これが普段、私達がプログラミングをして、ブラウザで動きをチェックしている環境です! Railsではデフォルトの設定がこの「開発環境」になっています。 ちなみにそれぞれの環境の設定をしているファイルは config/environmentsの中にある以下の3つです! development.rb(開発環境) production.rb(本番環境) test.rb(テスト環境) テスト環境 そして、残りのひとつがテスト環境です。 プロダクトコードのHTMLをヘッドレス(見えない)ブラウザで表示し、「RSpec」や「Minitest」などによって定義されたテスト用DBのレコードを参照して、テストを実行します。 つまり、localhostでアクセスするページに表示されている内容と、テストで検証している内容には全く関係がありません。 ちなみにRailsでは「Minitest」がデフォルトになっています。 なぜ環境ごとにDBが分かれているのか 環境ごとにDBが分かれていないと、DBには開発用のダミーデータと顧客のデータが混在することになり、間違えて削除してしまう、ということになりかねないからです。 先程も例に挙げた「amazon」を使って説明します。 「amazon」では顧客のクレジットカードや住所などの情報を入力します。 これらの重要な情報をDBに保存し、管理するわけですが、DBがひとつだけの場合、顧客のデータを消してしまうことがあるかもしれません。 そして本番環境では常にサービスが動いています。 本番環境のDBに不具合が発生すると、買い物したくてもできなくなってしまうということです。 amazonくらい大規模になるとその損失は計り知れませんね。。。 おわりに 今回はRSpecで私自身が引っかかったところについて記事を書きました。 まだまだ知識が少なく、間違っている点など見付けた際はぜひご教授頂けると助かります。 それでは、最後まで読んで頂きありがとうございました。 追記 コメントにてご指摘を頂きました。 Railsの環境は名前を付けて増やすことができます。 以下に解説記事のリンクを貼っておきます。 https://qiita.com/yusabana/items/a1f4fe2c37b20db2a3f6 またひとつ勉強になりました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby でグローバルなメソッドが書かれたファイルをグローバル空間に出さずにテストする

何がしたいか Ruby でグローバル空間にメソッドが書かれたファイルを、グローバル空間に出さずにテストしたい。 例 例えば以下のようなディレクトリ構造で、 apps/app1 と apps/app2 は互いに別々のアプリとして起動されるが、specは一箇所でやってしまいたいというとき。(例えば複数の AWS Lambda の lambda_function.rb を一つのプロジェクトとして管理してる時みたいなことを考えてください) . ├── spec │   └── apps_spec.rb └── apps ├── app1 │   └── main.rb └── app2 └── main.rb で、この main.rb はグローバルな空間にメソッドを置いている。 src/app1/main.rb def main 'hello, app1' end src/app2/main.rb def main 'hello, app2' end 普通にテスト書こうとすると何が起きるか このとき spec ファイルでふたつの main.rb を require してしまうと、後で読み込まれた方で main メソッドが上書きされる。 だめなapps_spec.rb require_relative '../apps/app1/main.rb' RSpec.describe 'app1' do describe 'main' do subject { main } # 実行時は app2 の main メソッドが上書きされてるので 'hello, app2' となる it { is_expected.to eq 'hello, app1' } end end require_relative '../apps/app2/main.rb' RSpec.describe 'app2' do describe 'main' do subject { main } it { is_expected.to eq 'hello, app2' } end end 解決策: Module の中にメソッドを展開してモジュールメソッドとして実行する main.rb にグローバル空間を汚染させずに、 main メソッドを実行してあげる必要がある。ここは無理やり Module の中に展開し、モジュールメソッドとして実行してみましょう。 module_eval の第二引数として file_name を渡すと file_name の中で require_relative とかしててもちゃんと解決される。 ただしapp1, app2 で同名のモジュールを require してた場合上書き問題起きる。難あり。 app_executor.rb module AppExecutor def self.execute(file_name, &block) Module.new.yield_self do |m| # file_name を展開して file_name のディレクトリとして eval m.module_eval(open(file_name).read, file_name) m.module_eval { extend self yield(self) } end end end で、テストの方ではモジュールメソッドとして実行させる。 apps_spec.rb RSpec.describe 'app1' do describe 'main' do subject { AppExecutor.execute('src/app1/main.rb') { |context| context.main } } it { is_expected.to eq 'hello, app1' } end end RSpec.describe 'app2' do describe 'main' do subject { AppExecutor.execute('src/app2/main.rb') { |context| context.main } } it { is_expected.to eq 'hello, app2' } end end かなり無理やり感はあるが動いたので良しということにする。 実行環境を取り出せるようにしたらモックとかもできるのでそっちのほうがいいかも。 ソースコードはこちら
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby のプロジェクトから環境変数(ENV)っぽい値を取り出す

何がしたいか git リポジトリの Ruby のプロジェクトから環境変数(ENV)っぽい値を取り出したくなった。 やり方 これでできる。 割とバグってるので使用は自己責任で。 これでできる.sh git grep -h -o -E "ENV(\[|\.fetch).*" | ruby -ane 'puts $_.match(%r|['\''"](.+?)["'\'']|)[1] rescue nil' | sort | uniq 解説 git grep -h -o -E "ENV(\[|\.fetch).*" ENV['HOGE'] とか ENV.fetch("HOGE") { 'fuga' } とかを抽出する -h ファイル名なし -o マッチした部分だけ -E 正規表現 ruby -ane 'puts $_.match(%r|['\''"](.+?)["'\'']|)[1] rescue nil' ENV.fetch("HOGE") { 'fuga' } から ' " で囲まれた部分を最小で取り出す ENV が同じ行に二つ以上あるとバグります。 sed でも perl でも grep でもできそう。 sort|uniq ソートして重複を排除します 例 mastdon で実行してみるとこんな感じ。 ALLOWED_PRIVATE_ADDRESSES ALLOW_ACCESS_TO_HIDDEN_SERVICE ALTERNATE_DOMAINS AUTHORIZED_FETCH AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY BACKTRACE BIND BRANCH BUNDLE_GEMFILE CACHE_BUSTER_ENABLED CACHE_BUSTER_SECRET CACHE_BUSTER_SECRET_HEADER CACHE_REDIS_URL CAS_CALLBACK_URL CAS_CA_PATH CAS_DISABLE_SSL_VERIFICATION CAS_EMAIL_KEY CAS_ENABLED CAS_FIRST_NAME_KEY CAS_HOST CAS_IMAGE_KEY CAS_LAST_NAME_KEY CAS_LOCATION_KEY CAS_LOGIN_URL CAS_LOGOUT_URL CAS_NAME_KEY CAS_NICKNAME_KEY CAS_PHONE_KEY CAS_PORT CAS_SSL CAS_UID_FIELD CAS_UID_KEY CAS_URL CAS_VALIDATE_URL CDN_HOST DATABASE_URL DB_HOST DB_NAME DB_PASS DB_POOL DB_PORT DB_SSLMODE DB_USER DEFAULT_LOCALE DISABLE_FOLLOWERS_SYNCHRONIZATION DISABLE_SIMPLECOV EMAIL_DOMAIN_ALLOWLIST EMAIL_DOMAIN_DENYLIST ES_ENABLED ES_HOST ES_PORT ES_PREFIX FFMPEG_BINARY GITHUB_API_TOKEN GITHUB_REPOSITORY HEROKU KEYBASE_BASE_URL KEYBASE_DOMAIN LDAP_BASE LDAP_BIND_DN LDAP_ENABLED LDAP_HOST LDAP_MAIL LDAP_METHOD LDAP_PASSWORD LDAP_PORT LDAP_SEARCH_FILTER LDAP_TLS_NO_VERIFY LDAP_UID LDAP_UID_CONVERSION_ENABLED LDAP_UID_CONVERSION_REPLACE LDAP_UID_CONVERSION_SEARCH LIMITED_FEDERATION_MODE LOCAL_DOMAIN LOCAL_HTTPS MAX_FOLLOWS_RATIO MAX_FOLLOWS_THRESHOLD MAX_REQUEST_POOL_SIZE MAX_SESSION_ACTIVATIONS MAX_THREADS NODE_ENV OAUTH_REDIRECT_AT_SIGN_IN OTHER_DATABASE_URL OTP_SECRET PAM_CONTROLLED_SERVICE PAM_DEFAULT_SERVICE PAM_EMAIL_DOMAIN PAM_ENABLED PAPERCLIP_ROOT_PATH PAPERCLIP_ROOT_URL PATH PERSISTENT_TIMEOUT PGHERO_STATS_DATABASE_URL PORT PREPARED_STATEMENTS QUERY_TRACE_ENABLED RAILS_ENV RAILS_LOG_LEVEL RAILS_MASTER_KEY RAILS_SERVE_STATIC_FILES REDIS_DB REDIS_HOST REDIS_NAMESPACE REDIS_PASSWORD REDIS_PORT REDIS_URL REMOTE_DEV REPO S3_ALIAS_HOST S3_BUCKET S3_CLOUDFRONT_HOST S3_ENABLED S3_ENDPOINT S3_HOSTNAME S3_MULTIPART_THRESHOLD S3_OPEN_TIMEOUT S3_OVERRIDE_PATH_STYLE S3_PERMISSION S3_PROTOCOL S3_READ_TIMEOUT S3_REGION S3_SIGNATURE_VERSION SAML_ACS_URL SAML_ALLOWED_CLOCK_DRIFT SAML_ATTRIBUTES_STATEMENTS_EMAIL SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME SAML_ATTRIBUTES_STATEMENTS_FULL_NAME SAML_ATTRIBUTES_STATEMENTS_LAST_NAME SAML_ATTRIBUTES_STATEMENTS_UID SAML_ATTRIBUTES_STATEMENTS_VERIFIED SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL SAML_CERT SAML_ENABLED SAML_IDP_CERT SAML_IDP_CERT_FINGERPRINT SAML_IDP_CERT_FINGERPRINT_VALIDATOR SAML_IDP_SSO_TARGET_PARAMS SAML_IDP_SSO_TARGET_URL SAML_ISSUER SAML_NAME_IDENTIFIER_FORMAT SAML_PRIVATE_KEY SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED SAML_SECURITY_WANT_ASSERTION_ENCRYPTED SAML_SECURITY_WANT_ASSERTION_SIGNED SAML_UID_ATTRIBUTE SECRET_KEY_BASE SIDEKIQ_REDIS_URL SINGLE_USER_MODE SKIP_POST_DEPLOYMENT_MIGRATIONS SMTP_AUTH_METHOD SMTP_CA_FILE SMTP_DELIVERY_METHOD SMTP_DOMAIN SMTP_ENABLE_STARTTLS_AUTO SMTP_FROM_ADDRESS SMTP_LOGIN SMTP_OPENSSL_VERIFY_MODE SMTP_PASSWORD SMTP_PORT SMTP_REPLY_TO SMTP_SERVER SMTP_SSL SMTP_TLS SOCKET SOURCE_BASE_URL SOURCE_TAG STATSD_ADDR STATSD_NAMESPACE STREAMING_API_BASE_URL SWIFT_AUTH_URL SWIFT_CACHE_TTL SWIFT_CONTAINER SWIFT_DOMAIN_NAME SWIFT_ENABLED SWIFT_OBJECT_URL SWIFT_PASSWORD SWIFT_PROJECT_ID SWIFT_REGION SWIFT_TENANT SWIFT_USERNAME TEST_ENV_NUMBER TRUSTED_PROXY_IP USER USER_ACTIVE_DAYS VAPID_PRIVATE_KEY VAPID_PUBLIC_KEY WEB_CONCURRENCY WEB_DOMAIN http_proxy
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

30秒かかった画像表示を1.7秒に改善した話(imgix + S3 + CarrierWave)

背景 開発した自作アプリ(レシピ投稿サイト)のトップページで、画像表示があまりに重く遅かったため、初心者なりに改善できないか色々試行錯誤して、何とか改善することができました。今回はその記録と、日本語資料があまりないimgix(外部API)の使い方をまとめておきます! ?ちなみにトップページはこんな感じです  スライダーの画像も入れると20枚強くらいですね まず結果から 最初にトップページをAWSにデプロイした時、画像がなかなか表示されなかったので、GTmetrix(Webサイトのパフォーマンスを評価するツール)にかけてみました。そして、目が点になりました。 32.3秒・・・!? 遅すぎる。さすがにこれは何とかしなければ。 色々と試行錯誤を重ね・・・結果、こうなりました? おぉ、1.7秒・・・! 涙が出るほど嬉しかったです、、 念のため検証ツールのネットワークタブからも確認。 キャッシュ無効で671ms。問題なさそうです。 大まかな流れ CarrierWaveでの画像投稿を可能にする CarrierWaveの投稿画像をS3に保存されるように設定 imgixを登録・設定 大まかな流れはこんな感じです。 技術的に未熟者でだいぶ遠回りしている感もありますが(特に#1)、その背景もこの後ご説明します、、、 なお、imgixだけ知りたい! という方は、飛ばして#3からご覧ください(S3利用が前提です) 環境 ruby 2.6.3 rails 5.2.6 OS:Linux(CentOS) IDE:Cloud9 実装の前に、経緯のお話 上記の流れになった背景を先に整理しておきたいと思います。 ここは私の試行錯誤の履歴ですので、手順だけご覧になりたい方は飛ばしていただいて構いません。 〜imgixにたどり着くまで〜 表示速度を上げるためにはじめに取り組んだのは、投稿画像の圧縮でしたが、ギリギリ許容できる画質まで落とした状態で約30秒かかっていました。しかも、お世辞にも良い画質とは言えなかったため、ユーザー目線で考えるとこれはちょっと、、という課題も新たに発生してしまいました。そもそも、ユーザーはスマホなどで撮影した画像をそのまま投稿するはずで、投稿サイトである以上、対応を考えなければいけません。 「違和感のない画質を維持しつつ、表示速度を改善する」 そんなこと初心者の私にできるのだろうか、、 と不安になりながら方法を探す中で、外部APIであるimgixにたどりつきました。 imgixの公式ページにはサービスについてこう書いてあります: imgix transforms, optimizes, and intelligently caches your entire image library for fast websites and apps using simple and robust URL parameters. 画像の最適化をしてくれ、かつキャッシュによってWebサイトを高速化できる。 画像最適化付きCDNという位置付けのようです。メリットとしては、以下のような点があげられます。 自サーバーの負荷が削減されることでWEBサーバーからのレスポンス速度低下を抑えることができる わざわざリサイズや圧縮せずとも、元画像を入れておき適切なパラメータを付与した画像URLにするだけで適切な画像を返してくれる Japonlineの記事より引用 元々見ていたのはCloudinaryというAPIだったのですが、導入難度がやや高めであるのに対し、imgixは画像URLを使うことで比較的容易に導入でき、公式が紹介しているrails向けのライブラリもあったので、imgixを使ってみることにしました。 〜refileからCarrierWaveに乗り換え〜 そこでまずimgix上で無料登録を済ませ、imxgix-railsの公式ドキュメント(こちら)でRailsアプリへの導入方法を確認しました。 私は元々画像投稿にrefileを使用しており、refileとの併用も可能とは書いてあったのですが、refileはURLやパスをDBに保存しないので、imgix-railsとrefileを繋げるためのヘルパーメソッドの定義が必要と書かれていました。 そのヘルパーメソッドの記述内容も親切にGitHubに書かれているのですが・・・これが、お恥ずかしながら初心者の私には何をしているのかがよく理解できず。理解できていないものをコピペしてうまくいっても、本質的でないと思いました。CarrierWaveなら投稿画像にパスがつくため、接続のための事前設定なしに利用できるとも記載されていたため、思い切ってCarrierWaveに置き換えることにしました(これには賛否両論あるとは思いますが、初心者なりにできることを必死で考えた結果と、温かい目で見ていただければ幸いです、、) 以上の試行錯誤を経て、CarrierWaveで無事画像が表示されることを確認した上で、S3でバケットを作成し、画像がS3にアップロードされることも確認が取れたので、railsアプリケーション上でimgixの設定を行いました(そこに至るまで本当に苦労しました、、) 〜CarrierWave+S3 で既に表示速度は大幅改善していたが、、〜 外部ストレージを設定した時点で、既に速度自体は大幅に改善していたので、もはやimgixを導入しなくてもいいのではないかと思ったのですが、GTmetrixで確認したところGTmetrix Grade(総合評価)が「F」になっていました(元々はD〜Eだったので悪化)。愕然。 表示速度は改善していたのですが、PageSpeedInsightsにもかけてみたところ、「画像サイズが不適切」なことが主な問題なようでした。imgixの画像最適化によってこの部分は修正できそうだったため、導入後あらためて確認したところ、「C」ランクに改善し、圧縮後の粗かった画像も綺麗に表示されるようになりました。 〜ちょっと余談〜 なお、imgixは国内では一休.comや日経新聞で導入されているようです。 導入事例も含めimgixがどういうサービスかはimgix という画像変換サービス(メモ)によくまとまっています。 まだ国内では導入事例が少ないimgixですが、表示速度の改善方法を英語で調べていたことで、このAPIに早い段階で出会えたのはラッキーでした。 ところで、読み方はnginx風なのかと思いきや「 image・icks (イメジックス)」のようです。ずっと読み方わからなかったのですが、公式の解説動画でそう呼ばれていました。 実装手順 CarrierWaveでの画像投稿(STEP1)及びS3バケットの作成(STEP2)については、【Rails】 CarrierWaveチュートリアルを参考にしました。とても丁寧に解説されているので先にご一読をおすすめします。 (STEP1の内容はほぼ上記の記事をもとに書いています。割愛しても良いかと思ったのですが、一部記述内容が異なるのと、アップローダー名やカラム名をSTEP3以降の内容と揃えて記載しておいた方がわかりやすいと考えたため、上記記事の内容をかなり拝借しながら記述しております。ご了承ください) ではそれぞれ見ていきましょう。 STEP1. CarrierWaveでの画像投稿を可能にする ※すでにCarrierWaveを使用している人はSTEP2に飛んでください。 1-1. Gem追加 gemfile gem 'carrierwave', '~> 2.0' 保存したらbundle installします。 1-2. Uploader追加 terminal rails g uploader アップローダー名 例)rails g uploader PostRecipe これで、app/uploaders/post_recipe_uploader.rbが作成されます。 ここには、アップロードするファイルの保存パスやサイズなどを指定することができます。 今回画像ファイルのサイズなどはimgix側で最適化してもらうので特に設定はしませんが、どのストレージにアップロードするかを指定しておきます。 app/uploaders/post_recipe_uploader.rb class PostRecipeUploader < CarrierWave::Uploader::Base # Choose what kind of storage to use for this uploader: if Rails.env.production? storage :fog else storage :file end # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end end 3行目で、本番環境の場合はstorage :fog(今回はS3)を、開発環境の場合はstorage :file(アプリケーションのpublic/uploads/モデル名/画像用カラム名/id配下)を、画像の保存先に指定しています。開発環境で投稿した画像までS3で管理する必要はないので、明示的に分けています。 なお、store_dirでは保存されるディレクトリを設定しています。 ここはデフォルトの表記のままで問題ありません。 開発環境とはいえ、public/uploads/配下に保存される画像がGitHubにあがるのはセキュリティの観点からもあまりよろしくないので、念のため.gitignoreに追加しておきましょう。 .gitignore /public/uploads 1-3. アップロード画像用のカラムを追加 terminal rails g migration add_post_recipe_image_to_post_recipes post_recipe_image:string アップロードする画像の情報を保存するpost_recipe_imageカラムを、PostRecipeモデルに作成します(カラム名やモデル名は適当な内容に修正してください)。なお、このカラムには画像データではなく、画像のファイル名が保存され、ビューで画像を表示する際には、「画像が格納されているパス」と「DBに保存されているファイル名」が使われます。 コマンドを実行したら、マイグレーションファイルが作成されるので、rails db:migrateしましょう。 1-4. コントローラーのストロングパラメーターに追記 post_recipes_controllerのストロングパラメーターに、先ほど作成したpost_recipe_imageカラムを追記します。 app/controllers/post_recipes_controller.rb def post_recipe_params params.require(:post_recipe).permit( :user_id, :title, :introduction, :post_recipe_image, #ここを追加 #以下略 ) end 1-5. Uploaderクラスとカラムを紐づける app/models/post_recipe.rb mount_uploader :post_recipe_image, PostRecipeUploader 先ほど作成した画像用カラムと、Uploaderの紐付けを行います。 紐付けを行うことで、画像アップロード時に、Uploaderに記述した諸設定を利用できます(例えば、アップロード時にどこに画像を保存するか等)。 画像用カラムを作成したモデル(今回の場合はPostRecipeモデル)ファイルに、上記を記述します。 1-6. ビューにファイル選択ボックスを追加 app/views/post_recipes/new.html.erb <%= form_with model: @post_recipe, local:true do |f| %> <div> <div class="from-group"> <h6>写真をアップロード</h6> <%= f.file_field :post_recipe_image %> </div> </div> <% end %> 上記のf.file_filed :カラム名と記述することで、画像投稿が行えます。 もし編集画面もある場合は、同じように追記/修正しましょう。 なお、画像の表示については後ほどimgixのix_image_tagを使用するため、CarrierWaveの画像表示タグにする必要はありませんが、CarrierWaveで投稿した画像を表示するには、<%= image_tag @post_recipe.post_recipe_image.url %>のように書きます(post_recipe_imageの部分は、カラム名です)。 refileから移行する場合 モデル、コントローラー(ストロングパラメーター)、ビューなどのファイルにあるrefile関連の記述は削除し、refileの画像用カラムも削除しておきます。 STEP2. CarrierWaveの投稿画像がS3に保存されるように設定 2-1. S3のバケット作成 S3のバケット作成については他に様々な記事で紹介されているため、割愛します。 私は「実装手順」の冒頭で紹介した記事の「AWS設定」に沿って作成しました。 上記記事の「CarrierWave設定」以降は、記事と少々内容が変わってくるので、次の項でまとめます。 2-2. CarrierWaveにS3の設定を追加 まず、gemfileにfog-awsを追加します。 投稿画像の保存先を外部ストレージ(S3)にするのを助けてくれるgemです。 保存したらbundle installします。 gemfile gem 'fog-aws' ?次に、以下のファイルをコマンドで作成します。 terminal touch config/initializers/carrierwave.rb ?作成したcarrierwave.rbに、S3バケット名とIAMユーザーの情報を記述します。 config/initializers/carrierwave.rb require 'carrierwave/storage/abstract' require 'carrierwave/storage/file' require 'carrierwave/storage/fog' CarrierWave.configure do |config| config.storage :fog config.fog_provider = 'fog/aws' config.fog_directory = '作成したバケット名' config.fog_credentials = { provider: 'AWS', aws_access_key_id: Rails.application.credentials.aws[:access_key_id], aws_secret_access_key: Rails.application.credentials.aws[:secret_access_key], region: 'ap-northeast-1', path_style: true } end IAMユーザーのaws_access_key_idとaws_secret_access_keyの値は、credentials.yml.encに記載します。 credentials.yml.encとmaster.keyは秘密情報を管理する仕組みで、環境変数を使わず秘密情報を管理できます。master.keyは、credentials.yml.encを複合化(暗号化されたデータを元に戻すこと)します。この仕組みについては、【Rails】Rails5.2以降で追加された「credentials.yml.enc」について簡単にまとめてみた!が参考になります。 ?では、credentials.yml.encを編集し、IAMユーザーの情報を保存します。 直接エディタからは編集できないため、vimで記述します。 terminal EDITOR=vim bin/rails credentials:edit credentials.yml.enc # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: yyy aws: access_key_id: xxx #ここを追記 secret_access_key: xxx #ここを追記 xxxの部分に実際の値を記入し、escしてから:wqで保存します。 secret_key_baseはデフォルトの値のままです。 これでCarrierWaveでアップロードした画像が、S3に保存されます。 STEP3. imgixを登録・設定 3-1. 会員登録 公式サイトより会員登録を行います。 3ヶ月間は$10のクレジットが付いてくるため、実質無料です(2021年6月時点)。 無料期間後の料金については、「リアルタイム画像処理機能が充実した CDN、「imgix」 を試してみたらとても簡単で便利だった件」が参考になります。 必須項目を入力してサインアップします。 3-2. Sourceの追加 サインアップ後、ログインするとDashboardが表示され、下図のような画面が表示されます。 ADD A SOURCEをクリックし、Sourceを作成します。 ちなみに、Sourceは複数作成できます。 General Source Type: 「AmazonS3」を選択 AWS Settings Access Key ID: IAMのアクセスキーの値を入力 Secret Access Key: IAMのシークレット・アクセスキーの値を入力 S3 Bucket: S3バケット名を入力 Path Prefix: 「uploads」を入力 Domains imgix Subdomain: imgixのサブドメインとなる値を自由に入力 他の項目は未入力で問題ありません。 入力できたらSAVEをクリックし、デプロイされたらSourceの設定は完了です。 3-3. Railsアプリケーションとimgixを接続 開発環境に戻ります。 ?まずapplication.rbのmodule アプリ名内に、以下を記述します。 config/application.rb Rails.application.configure do config.imgix = { source: ENV['IMGIX_SOURCE'] } end sourceには、先ほどimgix上で設定したsourceのサブドメインを入力します。 念のため、環境変数化しておきます。 .env IMGIX_SOURCE="yyy.imgix.net(sourceのサブドメイン)" ?次に、storage.ymlに以下を追記します。 Active Storageに認識させるS3の情報が、imgixのsourceと同じになるように設定します。 config/storage.yml amazon: service: S3 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> region: ap-northeast-1(AWSリージョン名) bucket: S3バケット名 これでimgixがS3に保存されているマスター画像にアクセスできるようになりました。 3-4. ビューファイルにix_image_tagを追記 imgix-railsの公式ドキュメントにあるように、ix_image_tagは、imgixが画像のリサイズ、クロップなどを行う上で必要なパラメーターを渡してくれるヘルパーメソッドです。 app/views/homes/top.html.erb <%= ix_image_tag(@post_recipe.post_recipe_image.path, url_params: { w: 150, h: 150, fit: 'crop' }, tag_options: {class: 'rounded-circle'}) %> <%= link_to ix_image_tag(@post_recipe.post_recipe_image.path, url_params: { w: 150, h: 150, fit: 'crop' }, tag_options: {class: 'rounded-circle'}), post_recipe_path(@post_recipe.id) %> ?post_recipe_imageの部分はCarrierWaveの設定で追加した画像用カラム名を指定します。 画像にリンクを貼る場合は、link_toを用いればOKです。 classやalt属性を指定する場合はtag_options内に、リサイズの値やクロップの指定はurl_params内に記述します。 ちなみにfallbackの画像を表示したい場合は、assets/images配下にfallback用の画像を格納し、以下のように記述すれば、画像が投稿された場合とそうでない場合とで表示を区別できます。 app/views/homes/top.html.erb <% if @post_recipe.post_recipe_image.blank? %> <%= image_tag('no_recipe_image.jpg', size: '70', class:'rounded-circle') %> <% else %> <%= ix_image_tag(@post_recipe.post_recipe_image.path, url_params: { w: 70, h: 70, fit: 'crop' }, tag_options: {class: 'rounded-circle'}) %> <% end %> これで本番環境にデプロイし、画像がうまく表示されていれば成功です。 最後に、デプロイ時に.envファイルを手動で転送するのを忘れないようにしましょう(これを転送してあげないと、imgixのsourceが読めずにエラーになります)。 終わりに refileで記述していた時は、画像の見せたい部分がうまく表示されないこともありましたが、imgixで最適化したことでうまくクロップされ、画質も格段に良くなりました。 さらに、S3と併用したことで、表示速度もアップすることができました。私が探した限りでは、「Railsアプリ x imgix x S3」の導入方法は日本語の記事が出回っていなかったため、今回整理してみました。何かのお役に立てれば幸いです。お疲れ様でした! 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ransackで孫のモデルの検索をしたいときの書き方

概要 Ransackを使っていると子のモデルのさらに子が持つフィールドで検索したくなることが結構あるのではないかと思います。 公式のREADMEを軽く読んでもそのような方法を実現するコード例が見当たらなかったので、記事にしてみようと思います。 方法 例として特定のタグを持つ記事を書いたユーザーを検索したい場合などを例にしてみようと思います。 モデル構造は下記のような形です。 models/user.rb class User has_many :posts, dependent: :destroy end models/post.rb class Post has_many :post_categories, dependent: :destroy has_many :categories, through: :post_categories end models/category.rb class Category has_many :post_categories, dependent: :destroy has_many :posts, through: :post_categories end models/post_category.rb class PostCategory belongs_to :post belongs_to :category end この時ユーザー一覧ページで特定のカテゴリの記事を書いたユーザーを検索するには下記のように記述します。 なおCategoryはnameというカラムをカテゴリ名として持っているとします。 views/users/index.html.slim form = search_form_for @q, url: users_path do |f| .form-group = f.label :posts_categories_id_eq, "記事カテゴリで検索" = f.collection_select :posts_categories_id_eq, Category.all, :id, :name, {include_blank: true}, class: 'form__field' .form-group = f.submit "検索", class: 'button button--primary mr-2' フィールド名を子テーブル名_孫テーブル名_孫テーブルのフィールド名_検索方法(eq, contなど)とすれば検索できるかと思うので、試してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

閲覧数を管理する足跡モデルの設計 - その1 DB設計とその実装

閲覧数を管理するモデルの設計にあたって 目的:SQLの学習 ポートフォリオサイトの機能拡張とSQLの学習を目的に「この作品を閲覧しているユーザーはこんな作品に興味があります」機能を追加をします。 この機能の追加に向け、どのユーザーがどの作品を閲覧したかを管理する、足跡モデルを作成します。 利用イメージ ユーザーがログイン(current_user) ユーザーが作品を閲覧 → リクエスト (works_controllerのshowメソッド) 新規足跡オブジェクトを追加、または既存の足跡オブジェクトのカウントを+1する 作品viewを表示 DB設計(関連付けと足跡モデルについて) テーブル設計は以下のように設定する。 各モデルの相関 class User < ApplicationRecord has_many :works has_many :footprints, dependent: :destroy end class Work < ApplicationRecord belongs_to :user has_many :footprints end class Footprint < ApplicationRecord belongs_to :user belongs_to :work validates :user_id, presence: true, uniqueness: { scope: :work_id } validates :work_id, presence: true validates :counts, presence: true end Footprintモデルの各カラム create_table "footprints", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.integer "counts", default: 1, null: false t.bigint "user_id", null: false t.bigint "work_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id", "work_id"], name: "index_footprints_on_user_id_and_work_id", unique: true t.index ["user_id"], name: "index_footprints_on_user_id" t.index ["work_id"], name: "index_footprints_on_work_id" end こだわりポイント user_idとwork_idの組み合わせは一意 あるユーザーによるある作品の閲覧数はcountsで管理する 足跡オブジェクトが作成された時点でcountsのデフォルト値は1 閲覧される度にcountsに+1する 足跡作成メソッド models/work.rb class Work < ApplicationRecord : def create_footprint_by(user) if Footprint.find_by(user_id: user.id, work_id: id).present? footprint = Footprint.find_by(user_id: user.id, work_id: id) counts = footprint.counts footprint.update_attribute(:counts, counts + 1) else Footprint.create(user_id: user.id, work_id: id) end : end 足跡作成メソッドのこだわり点 ユーザーと作品の組み合わせが存在するかどうかで条件分岐 存在する場合にはcounts+1 存在しない場合には足跡オブジェクト作成 実際の動作に基づき足跡を追加する controller/works_controller.rb class WorksController < ApplicationController : def show @work = Work.includes(:user).find(params[:id]) @work.create_footprint_by(current_user) @footprints = Footprint.select("SUM(footprints.counts) as total").find_by(work_id: @work.id) end : end views/works/show.html.erb <div class="work__footprints"> <i class="far fa-eye"></i><%= @footprints.total %> </div> 課題点 今回の変更で可能になった点 足跡モデルの作成・関連付け・デフォルト値の設定 足跡オブジェクトの自動作成(create_footprint_by) 足跡の表示  Footprint.select("SUM(footprints.counts) as total").find_by(work_id: @work.id) これらの設定により、実際の流れに基づき足跡を作成する事ができた。 一方で課題点 1. ModelへのSQLの発行が乱立している。 controller/works_controller.rb def show @work = Work.includes(:user).find(params[:id]) # 1回目 SELECT @work.create_footprint_by(current_user) # 2回目 UPDATE @footprints = Footprint.select("SUM(footprints.counts) as total").find_by(work_id: @work.id) # 3回目 SELECT end コントローラーにて、個別のSQLへのリクエストが3度も行われてしまっている。3回目のSQLに関しては、1回目と同一にする事ができると考えられる事から、以下のように変更する。 @work = Work.select("works.*, SUM(footprints.counts) as total").joins(:footprints).includes(:user).find(params[:id]) # 1回目 SELECT @work.create_footprint_by(current_user) # 2回目 UPDATE しかしこの場合には、2つの問題が発生する。 workの呼び出し後に、足跡が作成または追加されている。(create_footprint_byメソッドの位置) joinsにおける内部結合の特徴 = 結合相手がいない行は結合結果から消滅する これらの解決や原因などについては次の記事でまとめるものとする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Activemodel::modelをincludeした際のinitializeの挙動について

挨拶 こんにちは、プレイライフの熊崎です! 最近はあったかくなって、お出かけ日和ですね! コロナでなければ、どこかお出かけにでも行きたい気分なのですが、こんなご時世なので粛々と学習のアウトプットをしていきたいと思います笑 背景 今回、実務でフォームオブジェクトを作ることになり、その際にinitializeメソッドを使いました。 自分でフォームオブジェクトを作るにあたって、既存のフォームオブジェクトのソースリーディングを行っていました。その際に既存のコードが以下のような感じだったが、どのような挙動をしているかわからなかったので、まとめました。 コード例(一部コードを省略しております。) hogehoge.rb Class hogehoge include ActiveModel::Model def initialize(name, attributes = {}) @name = name super(attributes) end end superとは スーパークラス(親クラス)のメソッドを呼び出す。 引数がついている場合は、引数つきでそのメソッドを実行する。 今回の場合は、、、 Activemodel::Modelのinitializeメソッドを呼び出す。 Activemodel::Modelのinitializeはどうなってるか? 以下、公式のgithubリポジトリから引用 model.rb def initialize(attributes = {}) assign_attributes(attributes) if attributes super()   # ここのsuper()は正直理解できておりません、、、 どなたかご存知の方がいらっしゃれば、コメントなどを頂けますと幸いです。 end # assign_attributesメソッドは、Activemodel::Modelがインクルードしている、ActiveModel::AttributeAssignmentに入っています。 ActiveModel::modelのintializeメソッドに、assign_attributesメソッドがあって、attributes内に入っている属性を変更してくれている。ということでした1 assign_attributesメソッド 複数の属性を変更したいときに使用される https://qiita.com/tyamagu2/items/8abd93bb7ab0424cf084 最後に まだまだ未熟ゆえに、間違っていることを書いていることもあると思います。(なるべくそのようなことのないよう、気をつけてはおりますが、、、) もし、間違い等ございましたらコメントにてご指摘いただけますと幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む