20190218のRailsに関する記事は20件です。

【Rails】Active Storage環境下でGoogle Cloud Visionのセーフサーチを実装

個人開発のWebアプリまちかどルートv5.42への実装メモです。
cloudvision (1).png

まえがき

Rails 5.2のファイルアップローダーであるActive Storage環境下だとGoogle Cloud Visionのセーフサーチがうまく動かなかったので下記のように工夫しました。

具体的には画像ファイルを /public にいったん配置して、そのパスを指定して画像分析にかけ、終わったら /public から削除するという流れです。

posts_controller.rb
# 画像の選択の有無を判断
if post_params[:image] != nil
 # 画像を uploaded_file に格納
 uploaded_file = post_params[:image]
 # /public へのパスを指定
 output_path = Rails.root.join('public', uploaded_file.original_filename)
 # ファイルを開いて image_file に格納
 image_file = File.open(output_path)

 # Google Cloud VisionのAPIを使う
 image_annotator = Google::Cloud::Vision::ImageAnnotator.new
 response = image_annotator.safe_search_detection image: image_file
 response.responses.each do |res|
  safe_search = res.safe_search_annotation
  # if文では公式ドキュメントと違って .to_s を付ける必要がありました
  if safe_search.adult.to_s == "VERY_LIKELY" || safe_search.adult.to_s == "LIKELY"
    flash[:error] = "不適切な画像と判断されました powered by Google Cloud Vision"
    redirect_to root_path
    return
  elsif safe_search.violence.to_s == 'VERY_LIKELY' || safe_search.violence.to_s == 'LIKELY'
    flash[:error] = "不適切な画像と判断されました powered by Google Cloud Vision"
    redirect_to root_path
    return
  elsif safe_search.medical.to_s == 'VERY_LIKELY' || safe_search.medical.to_s == 'LIKELY'
    flash[:error] = "不適切な画像と判断されました powered by Google Cloud Vision"
    redirect_to root_path
    return
  end
 end
# 最後に /public の画像ファイルを削除します
File.delete(output_path)
end

上記のコードを適所に書けばGoogle Cloud Visionのセーフサーチが機能します。そこでVERY_LIKELYまたはLIKELYと判断されればroot_pathにリダイレクトされるようになります。

あとがき

Active Storage環境下だと公式ドキュメントどおりにやってもうまくいきませんでした。また、事前にgem 'google-cloud-vision', '~> 0.32.2'bundle installしたあとに「protobufが見つかりません」という内容のエラーが出ました。そこでGemfilegem 'google-protobuf', '~> 3.7.0.rc.2'を追記のうえgem update google-protobufを実行することで回避できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails vs PostgreSQL カラムをstringからintegerに変更できない

バージョン情報

  • Ruby: 2.5.0
  • Rails: 5.1.6
  • Postgres: 11.1
  • macOS: 10.14.1

事象

usersテーブルのgenderカラムをstringからintegerに変えるため、以下のコマンドでマイグレーションファイルを作成し、

bin/rails g migration change_gender_datatype_to_integer_in_users

作成されたマイグレーションファイルに以下のように記載した後、

2019mmddhhmmss_change_gender_datatype_to_integer_in_users.rb
class ChangeGendersDatatypeInUsers < ActiveRecord::Migration[5.1]
  def change
    change_column :users, :gender, :integer
  end
end

マイグレーションを実行すると、

bin/rails db:migrate

以下のエラーが表示されました。

rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DatatypeMismatch: ERROR:  default for column "gender" cannot be cast automatically to type integer

ググってみると、同様のエラーに遭遇している先人たちがおりましたので参考にしましたが、いずれの方法でも回避することはできず。。。

https://qiita.com/gam0022/items/a73910de5b8eb44e5b13
https://stackoverflow.com/questions/12603498/rails-migration-error-w-postgres-when-pushing-to-heroku
https://dzone.com/articles/rails-5-change-database-column

解決方法

一度該当カラムをremoveして、再度追加することで対応しました。

一度ドロップして、

remove_column :users, :gender

ドロップできたら新しいマイグレーションファイルを作成して、add_columnしてあげます。

add_column :users, :gender, :integer, after: :phone, default: 0

すると追加できました!が、なぜかafterが機能しておらず、末尾に追加されてしまいました。

schema.rb
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "gender", default: 0

解決したら更新します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

herokuのpush先をサクッと変更してアプリを複製

DBのエラー続きでどうしようもなくなったとき、
『新しいアプリとして公開できたらな〜』
と思っていた時、成功して感動したのでここに記す。

Goal

公開しているアプリを別URLで複製して公開し直す。

*すでにherokuにアプリを公開しているコト前提

How to

まずは、push先がどこになっているのか確認

$ git remote -v

新しい名前でリモートリポジトリを作成

$ heroku create ********

さっき作ったリモートリポジトリにpush先を変更

$ $ git remote set-url heroku ********

さっき見たく、push先を確認。
しっかり変更されていればOK

$ git remote -v

ここからはおなじみ

$ git add .

$ git commit -m"first commit"

$ git push heroku master

$ heroku open
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Cloud9でRailsプロジェクト作成直後にぶつかる問題の対応方法

はじめに

Ruby on Rails初心者がUdemyで入門しようとしています。環境構築で詰まるという初心者が陥りがちな罠を避けるため、AWS Cloud9を利用することにしました。すると・・・早速罠にハマったので備忘録として対応方法をまとめます。

The Complete Ruby on Rails Developer Course
https://www.udemy.com/the-complete-ruby-on-rails-developer-course/

環境

  • AWS Cloud9
  • ruby 2.6.0p0
  • Rails 5.0.7.1

Railsプロジェクト作成

Cloud9を開きます。2019/02/18現在、Cloud9は東京リージョンでは提供されていないので適当なリージョンを選択します。「Create environment」から環境を作成します。Configuration Settingsはデフォルト値で構いません。

IDEが開いたら下部のターミナルで以下のコマンドを実行します。

~/environment $ rails new sample
~/environment $ cd sample/
~/environment/sample $ rails s -b $IP -p $PORT

Previewを実行します。

rails-1.png

罠1 Gem::LoadError

Preview画面に何やらエラーが出ています。

Specified 'sqlite3' for database adapter, but the gem is not loaded. Add gem 'sqlite3' to your Gemfile (and ensure its version is at the minimum required by ActiveRecord).

rails-2.png

sqlite3がロードされていないようですが、インストールはされています。

~/environment/sample $ gem list | grep sqlite3
sqlite3 (1.4.0)

対応方法

検索してもドンピシャなものが見当たりませんでしたが、どうやらsqlite3のバージョンを変えてみるとよいようです。以下のようにGemfileを修正します。

Gemfile
diff --git a/Gemfile b/Gemfile
index 0447f70..03752d1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,7 +4,7 @@ source 'https://rubygems.org'
 # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
 gem 'rails', '~> 5.0.0'
 # Use sqlite3 as the database for Active Record
-gem 'sqlite3'
+gem 'sqlite3', '~> 1.3.6'
 # Use Puma as the app server
 gem 'puma', '~> 3.0'
 # Use SCSS for stylesheets

変更を反映して再度試してみます。

~/environment/sample $ bundle update
~/environment/sample $ rails s -b $IP -p $PORT
=> Booting Puma
=> Rails 5.0.7.1 application starting in development on http://127.0.0.1:8080
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.0 (ruby 2.6.0-p0), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:8080
Use Ctrl-C to stop
Started GET "/" for 126.209.207.2 at 2019-02-18 09:16:09 +0000
Cannot render console from 126.209.207.2! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Rails::WelcomeController#index as HTML
  Parameters: {"internal"=>true}
  Rendering /home/ec2-user/.rvm/gems/ruby-2.6.0/gems/railties-5.0.7.1/lib/rails/templates/rails/welcome/index.html.erb
  Rendered /home/ec2-user/.rvm/gems/ruby-2.6.0/gems/railties-5.0.7.1/lib/rails/templates/rails/welcome/index.html.erb (3.1ms)
Completed 200 OK in 17ms (Views: 7.1ms | ActiveRecord: 0.0ms)

Gem::LoadErrorは消えました。

罠2 Cannot render console from 126.209.207.2!

Previewを開くと上記のようなログが出力されます(以下、抜粋)。

Cannot render console from 126.209.207.2! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255

このときの画面は以下のようになっています。接続が拒否されています。

rails-3.png

対応方法

下図の赤枠で示した部分をクリックすると別タブで正常に開くことができます。

rails-4.png

めでたしめでたし。

まとめ

環境構築の罠を避けようとして別の罠にハマる辛さがありましたが、無事Rails入門の準備ができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rspec】APIモードで404 Not Foundをjsonで返す時のテストについて

APIモードで404 Not Foundをjsonで返す時のテストについて

存在しないプロジェクトのページにアクセスしたら、404 not foundのページを返すテストを書いていた。

しかし、今回はAPI仕様なのでhtmlで404を返してはいけない。

そのため、「APIモードでは、存在しないprojectにアクセスしてきたら、status404を返すだけで良い」という結論。

そもそも論だった。

expect(response).to have_http_status :not_found

もしくは、

expect(response.status).to be 404

というテストを書くだけで良い!(この2つは同義)

have_http_statusは、rspecのマッチャー。(eq等と同じ)
https://www.masalog.site/entry/2018/07/26/214211

このhave_http_statusにHTTPステータスを引数(シンボル)で渡せば、引数のステータスが返せる。

HTTPステータスコード一覧
https://blog.toshimaru.net/rails-http-status-symbols/

success

:okは200番

client error

:not_foundは404番

server error

:internal_server_errorは500番

そしてこの2つは同義。

expect(response).to have_http_status :ok:
expect(response.status).to be 200
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

git clone から rails s までの5ステップ

(ほぼ自分用メモです)

新規ではなく、動くものがすでGitHubにあがっているRailsプロジェクトをcloneしてローカルで動かすための5ステップ。
ローカルにRails自体が動く環境ができている=Yay! You're on Rails!が表示できる状態であることが前提。
gitコマンドが使える状態なのも前提。

$ git clone https://github.com/<user_name>/<project_name>.git
$ cd <project_dir>
$ bundle install
$ bin/rails db:migrate
$ bin/rails s
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】自作WebアプリからMastodonのプロフィール画像を変更

個人開発のWebアプリまちかどルートv5.41への実装メモです。

プログラミングに入門して8ヶ月。
今回も SNS「Mastodon」のAPI に挑戦してみました。

profile.png

まえがき

・まちかどルートではMastodonのアカウントでログイン認証するようにしています。

・今回の実装をするまでは、まちかどルートのプロフィール画像を変えるためにわざわざMastodonで変更し、まちかどルートで再ログインする必要がありました。

・画像アップローダーとしてRails 5.2の新機能 Active Storage を使い、ファイルをAmazon S3に保管しているのですがgem 'mastodon-api'(v2.0)を通してアップロードするとき、その環境のせいでとても苦労しました。

model

user.rb
has_one_attached :image

Active Storageを使うのでマニュアルどおりの作法でmodelにこう書きます。バリデーションについては今回、割愛します。

view

edit.html.erb
<%= form_with model: @user, multipart: true, local: true do |f| %>
  <%= f.file_field :image %>
<%= button_tag :type => "submit" do %>変更を保存<% end %>

プロフィール画像のファイルを選択するためのフォームです。

controller

users_controller.rb
def update

   # 画像の選択の有無を確認
   if params[:image] != nil

    # 画像を uploaded_file に格納
    uploaded_file = params[:image]

    # 画像を /public にいったん配置するため output_path にパスを設定
    output_path = Rails.root.join('public', uploaded_file.original_filename)


    # Mastodonには2MB制限があるので画像を縮小

    ## MiniMagickを使います。まずは画像を入力
    img = MiniMagick::Image.read(uploaded_file)

    ## 縮小します
    img.resize "300x300"

    ## 縮小したら /public に書き出します
    img.write output_path


    # MastodonのAPIを通してアップロードします

    ## 配列を用意します。Mastodon指定のパラメーターは avatar です
    user_array = { "avatar": output_path }

    ## APIを叩くためのクライアントを生成します
    domain = '[対象のMastodonインスタンスのドメイン]'
    access_token = '[ユーザーのアクセストークン]'
    client = Mastodon::REST::Client.new(base_url: "https://#{domain}", bearer_token: access_token)

    ## MastodonのAPIを叩きます
    client.update_credentials(user_array)


    # 以上でMastodon側のプロフィール画像が変更されます
    # 続いて、まちかどルートにも同じ画像を反映させます

    ## MastodonのAPIを叩いて変更後のavatarを取得します
    @user.avatar = client.verify_credentials.avatar

    ## ユーザー情報を保存します
    @user.save


    # 最後に、不要となった/publicの画像を削除します
    File.delete(output_path)

   end

   flash[:notice] = "アイコンを変更しました" 
   redirect_to @user
end

controllerが一番苦労しました。
解説はコメントアウトにある通りです。

あとがき

Active Storageはとても簡単に導入できるアップローダーなのですが

user_array = { "avatar": @user.image }

と書ければ、わざわざ/publicに画像を配置する手間がなくて楽なのに url_for(@user.image)とやっても「そんな画像はありません」というエラーが返ってきてしまうんです。

というわけで、いろいろ試行錯誤して上記のようになりました。とくに/publicにいったん配置する方法がわかったので、これから他の画像系APIを使うのに役立ちそうです。今後も学んでいきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Shrineのimage_dataを含むモデルのFactoryの作り方

(編集しようとしたら誤って削除してしまったので再投稿しました)

railsの画像アップロードライブラリshrineを用いた場合の、FactoryBotのFactoryの書き方について悩んだポイントがあったので書いておきます

前提

登場するModel

  • Area
column type memo
name string require
  • Shop
column type memo
area bigint require
logo bigint require
name string require
  • Logo
column type memo
area bigint require
name string require
image_data text require

Area has_many Shop
Area has_many Logo
Shop belongs_to Logo

各Shopはエリアに登録されているロゴを使用できるイメージです。

悩んだこと

logoのファクトリーのimage_data カラムをどう書くべきなのか悩みました。

spec/factories/logos.rb
FactoryBot.define do
  factory :logo do
    area
    sequence(:name) { |i| "logo#{i}" }
    image { #この部分!! }
  end
end

もともとは↓のように書いていたのですが、これだとlogoのテストデータが作成されるたびにファイルへアクセスするためRSpec全体の実行時間に大きな影響を与えていました。

image { File.open("#{Rails.root}/spec/fixtures/img/example.png") }

RSpec実行時間

Factory追加前→ 27s
追加後 →1min 3s

shopが作られるたびにFactoryBotによってlogoも自動で作られるのだから遅くなるのは当たり前ですね

試したこと

適当な文字列をつっこむ

image_data { 'sample' }

結果
imageを実際に使うテストにて

JSON::ParserError:765: unexpected token at 'sample'

それはそう。。

fixture_file_upload使ってみる

fixture_file_uploadについて

この記事ではfixture_file_upload使ったらFile.openより相当早い結果になっているので期待大!

image { fixture_file_upload("#{Rails.root}/spec/fixtures/img/example.png", 'img/png') }

結果
テストはすべてpass!
ただし

Finished in 1 minute 9.24 seconds (files took 15.67 seconds to load)

File.openのときと変わらないくらい時間かかってる。。
使い方がおかしい?

最終的に

ファイルアクセスではなく架空のcacheを作成してみる

Shrineのuploaded_fileメソッドを用いてキャッシュの状態でファイルが上がっていることにする作戦

factory :logo do
    area
    sequence(:name) { |i| "logo#{i}" }
    image_data { Shrine.uploaded_file(
      'id' => SecureRandom.hex(8),
      'storage' => 'cache',
      'metadata' => { 'mime_type' => 'image/jpeg', 'size' => 1.megabyte }).to_json }
  end

結果

Finished in 31.04 seconds

いいじゃん!!

参考にしたページ
https://stackoverflow.com/questions/44812403/rails-testing-file-upload-validation-shrine-gem-at-model-spec

補足

最終型において image ではなく image_data を使っている理由について書きます。

はじめはFile.openと同じように下記のように書いていたんですが、

image { Shrine.uploaded_file(
      'id' => SecureRandom.hex(8),
      'storage' => 'cache',
      'metadata' => { 'mime_type' => 'image/jpeg', 'size' => 1.megabyte }).to_json}

結果
imageを実際に使うテストにてエラーが起きる

No such file or directory @ rb_sysopen - /usr/src/app/public/uploads/cache/675ce0d849e8a5f8

ファイルをあげようとしてるんだけど、そんなファイルはないよと言われているように見えます。

shrineを用いた実装において、

image_data → 添付ファイルの全情報がjson形式で保存されるためのDBカラム
image      → 添付ファイルをハンドリングするためのvirtualなattribute

ということは

image_data → upload済みのファイル情報を入れる。
image      → uploadしたいファイル情報を入れる。

最終形では、Shrine.uploaded_fileでファイルが上がっていることにしていたのでimage_dataに入れるのが正解ということでした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails mysql2のインストール時のエラー

gemのmysql2をインストールする際にハマったのでメモ

エラーログ

Fetching mysql2 0.3.21
Installing mysql2 0.3.21 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /path/to/vendor/bundle/ruby/2.4.0/gems/mysql2-0.3.21/ext/mysql2
/path/to/.rbenv/versions/2.4.1/bin/ruby -I /path/to/.rbenv/versions/2.4.1/lib/ruby/site_ruby/2.4.0 -r ./siteconf20190218-87873-15epx9f.rb extconf.rb
--with-cppflags\=-I/usr/local/opt/openssl/include
checking for ruby/thread.h... yes
checking for rb_thread_call_without_gvl() in ruby/thread.h... yes
checking for rb_thread_blocking_region()... no
checking for rb_wait_for_single_fd()... yes
checking for rb_hash_dup()... yes
checking for rb_intern3()... yes
-----
Using mysql_config at /usr/local/opt/mysql@5.7/bin/mysql_config
-----
checking for mysql.h... yes
checking for errmsg.h... yes
checking for mysqld_error.h... yes
-----
Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load
-----
-----
Setting libpath to /usr/local/opt/mysql@5.7/lib
-----
creating Makefile

current directory: /path/to/vendor/bundle/ruby/2.4.0/gems/mysql2-0.3.21/ext/mysql2
make "DESTDIR=" clean

current directory: /path/to/vendor/bundle/ruby/2.4.0/gems/mysql2-0.3.21/ext/mysql2
make "DESTDIR="
compiling client.c
compiling infile.c
compiling mysql2_ext.c
compiling result.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

Gem files will remain installed in /path/to/vendor/bundle/ruby/2.4.0/gems/mysql2-0.3.21 for inspection.
Results logged to /path/to/vendor/bundle/ruby/2.4.0/extensions/x86_64-darwin-18/2.4.0-static/mysql2-0.3.21/gem_make.out

An error occurred while installing mysql2 (0.3.21), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.3.21' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2

パスが通っていないのが原因

bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Progate rails道場コースⅣ 攻略メモ中編

今回はいいね機能を実装していきます。

9/14 「いいね!」ボタンを作ろう

最初に流れを説明すると、
①likes_controller内でcreateアクションとdestroyアクションを実装する

②if文を使い、いいね済みとそうでない場合のボタンを実装する
です。

likes_controllerのcreateアクションについて解説

まずは①のcreateアクションから。

likes_controller

def create
  @like = Like.new(user_id: @current_user.id,
                   post_id: params[:post_id])
  @like.save
  redirect_to("/posts/#{params[:post_id]}") 
end

コード解説
いいね!された時に実行されるcreateアクションです。
動きとしては、いいねボタンが押された時のログインユーザーのIDと、投稿データのIDをDBのlikesテーブルに保存するものです。

まずはLikeモデルに対してnewメソッドを用いて、ログイン中のユーザーID(@current_user.id)と、表示している投稿データのID(params[:post_id])の値が入ったインスタンス変数@likeを作成し、DBへ保存します。(2〜4行目)

その後投稿詳細ページへとリダイレクトして完成です。

likes_controllerのdestroyアクションについて解説

続いて、いいねを解除するためのdestroyアクションです。

def destroy
  @like = Like.find_by(user_id: @current_user.id,
                       post_id: params[:post_id])
  @like.destroy
  redirect_to("/posts/#{params[:post_id]}")
end

createアクションと似たコードで動きます。
2行目ではfind_byメソッドを用いて、Likeモデルから作成済みのデータを探してきます。
createアクションのnewメソッドで使ったものと同じ条件で検索してるだけです。
そして見つけたデータをdestroyメソッドで削除します。

ビューにいいねボタンを表示する

if文を使って、

ログイン中のユーザーが表示中の投稿にいいね!済み = いいね!解除ボタンを表示
いいね!してない場合 = いいね!ボタンを表示

という風に実装します。

posts/show.html.erb

<% if Like.find_by(user_id: @current_user.id, post_id: @post.id )%>
  <%= link_to("/likes/#{@post.id}/destroy", {method: "post"}) do %>
    <span class="fa fa-heart like-btn-unlike"></span>
  <% end %>
<% else %>
  <%= link_to("/likes/#{@post.id}/create", {method: "post"}) do %>
    <span class="fa fa-heart like-btn"></span>
  <% end %>
<% end %>

ちょっと複雑そうに見えますが、上から順に解説していきます。
まず1行目。

<% if Like.find_by(user_id: @current_user.id, post_id: @post.id )%>

ログイン中のユーザー(@current_user.id)が、表示している投稿(@post.id)にいいね!済みかどうかを調べるコードです。
likesテーブルのデータはこの2つのIDから成るので、この検索条件にひっかかればいいね!済みということになります。

<%= link_to("/likes/#{@post.id}/destroy", {method: "post"}) do %>
  <span class="fa fa-heart like-btn-unlike"></span>
<% end %>

続いて2〜4行目。link_toメソッドでいいねボタンとdestroyアクションを紐づけます。
もしif文の結果が真の場合(一致するいいね!データが見つかった場合)、いいね解除ボタンが表示されるようにします。

ただ、これまでlink_toメソッドの第一引数には表示したい文字列を渡してきました。(”編集” ”削除”など)しかし、今回のようなfant-awesomeなどのHTMLをそのまま記述しても、文字列として認識されてしまうため上手く表示できません。
これを回避するために、少し記述法が変わります。

<%= link_to("表示する文字列", "URL")%>

通常はこのように記述するものを、

<%= link_to("URL") do %>
  <!-- ここにHTMLのコードを記述 -->
<% end %>

このように変えてあげます。
doが入ることでendも必要になるので、忘れないように!

これに基づき、createアクションに紐付いたfant-awesomeのアイコンを表示しましょう。

<%= link_to("/likes/#{@post.id}/destroy", {method: "post"}) do %>
  <span class="fa fa-heart like-btn-unlike"></span>
<% end %>

これで表示されるはずです。メソッドのpost指定も忘れずに。
else以降も同じように記述しましょう。

10/14 いいね数を表示しよう

まずはビューに渡すためのいいね!の数を数えるインスタンス変数、@likes_countを用意します。

def show
  @post = Post.find_by(id: params[:id])
  @user = @post.user
  @likes_count = Like.where(post_id: @post).count
end

4行目のコード解説。
whereメソッドを使い、likesテーブルのpost_idが投稿のID(@post)と一致するデータを全て集めます。
わかりやすく言うと、全いいね!の中から特定の投稿のものだけ集めてくる感じですね。
そして集めてきたデータに対してcountメソッドを用いることで、要素の数をインスタンス変数に代入します。

これで無事に、特定の投稿にいいね!された数を取得することができました。

「いいね!」した投稿を表示しよう

ユーザーがいいね!した投稿を一覧表示するページを作ります。
指定のURLをペーストした後、新しいページのためのルーティングを作成してあげます。

routes.rb

get "users/:id/likes" => "users#likes"

次はuser_controllerのlikesアクションです。

def likes
  @user = User.find_by(id: params[:id])
  @likes = Like.where(user_id: params[:id])
end

2行目でユーザーデータ表示のためのインスタンス変数@userを用意。
3行目で現在表示中のユーザーをURLのparamsから取得、whereメソッドを用いてlikesテーブルからuser_idが一致するデータ(表示中のユーザーがいいね!した投稿)を全て拾い、@likesに代入します。

これで必要なデータは準備できたので、ビューに表示していきましょう。
ちょっと長くなります。

likes.html.erb

<% @likes.each do |like| %>
  <% post = Post.find_by(id: like.post_id) %>
  <div class="posts-index-item">
    <div class="post-left">
      <img src="<%= "/user_images/#{post.user.image_name}" %>">
    </div>
    <div class="post-right">
      <div class="post-user-name">
        <%= link_to( "#{post.user.name}", "/users/#{post.user.id}" ) %>
      </div>
      <%= link_to(post.content, "/posts/#{post.id}") %>
    </div>
  </div>
<% end %>

まずは1行目。ブロックを使って取得した全いいねのデータを渡す準備です。

<% @likes.each do |like| %>

これでlikeを使うことにより、@likesに入っている、ユーザーがいいね!したデータを一つずつ用いることができます。
しかし今のままだと、@likesから渡されるデータはlikesテーブルから取得したものであるため、中身はuser_idとpost_idしか入っておらず、このままでは投稿内容を表示することができません。
そこで、@likes内のpost_id属性を利用して、紐づくpostsテーブルから投稿内容を取得します!

  <% post = Post.find_by(id: like.post_id) %>

Postモデルにfind_byメソッドを用いて、(Post.find_by)
引数にlikesテーブル内のpost_idを、PostモデルのIDと紐付けて検索します(id: like.post_id)
これでいいね!データから投稿データを引っ張ってくることができました!後は変数postに入れてあげましょう!

これでブロックの処理が始まる際に、変数postに投稿データが入るようになったので、後はこれまでと同じように表示してあげます。

<img src="<%= "/user_images/#{post.user.image_name}" %>">

<%= link_to( "#{post.user.name}", "/users/#{post.user.id}" ) %>

<%= link_to(post.content, "/posts/#{post.id}") %>

これでいいね関連の実装は終了です。
次回で本当のラスト。がんばりましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]複合主キーを持つテーブルのレコードを更新する(*非常用)

環境

  • Ruby 2.5.1
  • Rails 5.2.1
  • Mysql2 0.5.1

前提

そもそも、Rails において複合主キーの Model を扱うことはサポート外になります。複合主キーを使うと下記のような警告も出力されます。

WARNING: Active Record does not support composite primary key.
Composite primary key is ignored.

また、 find, update, save, delete メソッドの使用が不可能になります。これらのメソッドを呼び出すと下記のようなエラーを出力します。

Model

  • MultiPkeyModel という複合主キーの Model があったとします
  • pkey_1, pkey_2 カラムが主キーとします

find

MultiPkeyModel.find(1) # 数値は適当

# ActiveRecord::UnknownPrimaryKey: 
# Unknown primary key for table multi_pkey_models in model MultiPkeyModel.

そもそも id カラムを持たないので find の引数に何も指定しようがないですが、引数有無問わず上記のエラーが発生することを確認しました。

update, save, delete

record = MultiPkeyModel.find_by(pkey_1: somevalue1, pkey_2: somevalue2)

# update
record.update(foo: 'bar')

# ActiveRecord::StatementInvalid: Mysql2::Error:
# Unknown column 'multi_pkey_models.' in 'where clause': 
#   UPDATE `multi_pkey_models` SET `foo` = 'bar' WHERE `multi_pkey_models`.`` IS NULL

# save
record.foo = 'bar'
record.save # => update と同様のエラーのため省略

# delete
record.delete

# ActiveRecord::StatementInvalid: Mysql2::Error:
# Unknown column 'multi_pkey_models.' in 'where clause':
#   DELETE FROM `multi_pkey_models` WHERE `multi_pkey_models`.`` IS NULL

いづれの場合も、 SQL 文の WHERE 句が WHERE `multi_pkey_models`.`` IS NULL というおかしな状態になっているため失敗していると思われます。 Model クラスが id カラムを持たないため、本来 id カラムが指定されうる箇所が空になってしまっています。

本題

では、どうするべきか。そもそも複合主キーによるテーブルを使った運用自体をよしとするかどうかという話もありますが、本項ではそこまで壮大な話は扱わず、 複合主キーテーブルを使わざるをえない状況 という前提で、苦肉の策を共有してみたいと思います。

find の代わり

これは find_by ですみます。レコードの更新を伴わない場合はすぐには困ることもなさそうです。

MultiPkeyModel.find_by(pkey_1: somevalue1, pkey_2: somevalue2)

update, delete の代わり

身も蓋もないですが、SQL文を直接発行したほうが手っ取り早いと思います。
下記は update を行う場合のコードの記述例です。

sql = <<-SQL
  UPDATE `multi_pkey_models` SET `foo` = :foo
  WHERE `pkey_1` = :pkey_1 AND `pkey_2` = :pkey_2
SQL

ActiveRecord::Base.connection.execute(ActiveRecord::Base.send(
  :sanitize_sql_array,
  [
    sql,
    foo: 'bar',
    pkey_1: someValue1,
    pkey_2: someValue2
  ]
))

どうしても Active Record を使いたい場合は update_all という手もあります。

MultiPkeyModel.where(pkey_1: someValue1, pkey_2: someValue2).update_all(foo: 'bar')

ただ、update_all は本来は複数のレコードをまとめて更新するためのメソッドです。 pkey_1, pkey_2 の組み合わせを持つレコードは必ず1つであることがテーブルの制約上保証されてはいますが、そういった背景を知らない人がコードを読んだ際に、コード上は複数のレコードを更新しにいっているように見えるので、あまり読み手に優しくないと思います。

ただ、「なんでここSQL文直接書いてるんだろう?」と思う人もいるかもしれないのでSQL文生書きにしていることの理由などをコメントで補足しておくといいとおもいます。あれ、update_all 呼んでコメント書くのも一緒か とも思いましたが、そこはエンジニア各位のご判断にお任せしたいとおもいます。

まとめ

複合主キーを持つテーブルを扱う際には、各種 CRUD メソッドが想定どおり機能しないケースがあるのでご注意ください。

ちなみに

gem でなんとかしている例もけっこうあるようです。実際問題、筆者も含め複合主キーってけっこう使いますよね。。。

識者からのご指摘、ご感想ありましたらぜひコメントいただけるとうれしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_with は remote (Ajax) が既定値なのに scaffold で生成されるコードは local: true オプションが付いていてどうにかしたい

やりたいこと

  • Rails の scaffold をジェネレートすると form_withlocal: true のオプションが付いています。
  • local オプションのデフォルトは false なわけで、 Ajax にしていこうという流れの中で scaffold のコードが Ajax じゃないのに違和感を覚えました。
  • というわけで、 scaffold で生成されたコードに対し、現実的なコード改修量で Ajax リクエストによる CRUD を実現してみます。

前提

  • webpacker を使っている
    • rails-ujs を使っている
    • turbolinks を使っている
$ yarn add rails-ujs turbolinks
app/javascript/packs/application.js
import Rails from 'rails-ujs'
Rails.start()

import Turbolinks from 'turbolinks'
Turbolinks.start()
  • 下記のようなユーザモデルを scaffold で作成した直後の状態を想定
    • name および age という属性を持つ。前者は string で後者は integer
    • name および age ともに入力必須 (バリデーションエラー時の挙動確認で必要なので)
$ ./bin/rails g scaffold User name age:integer
app/models/user.rb
 class User < ApplicationRecord
+  validates :name, :age, presence: true
 end
  • Rails のバージョンは 5.2.2
$ rails -v
5.2.2

実施

ざっくり言うと以下の流れに。

  • save 成功時、リダイレクトが Ajax の場合でも動くようにする
  • save 失敗時、 form 要素だけを html で返し、その結果により既存 form を入れ替える

_form パーシャルファイル、 local: true を外す

このポストの内容の前提なので。

app/views/users/_form.html.erb
-<%= form_with(model: user, local: true) do |form| %>
+<%= form_with(model: user) do |form| %>
   <% if user.errors.any? %>
     <div id="error_explanation">
       <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

_form パーシャルファイルが <form> タグだけを描画する、という前提を変えない

通常、 scaffold ジェネレータを実施すると _form パーシャルファイルは <form> タグに該当する html を描画するわけですが、それ以外の要素を _form パーシャルファイル内で描画しないようにします。
たとえば <form> タグの直下に <a> タグでリンクを置きたいからといって _form パーシャルファイルの中にそれを記述してはいけません。同じことを実現するには _form パーシャルファイルを呼び出す側に記述すればことが済むはずです。

例えば以下のようにするのではなく。。。

app/views/users/edit.html.erb
<%= render 'form', user: @user %>
app/views/users/_form.html.erb
<%= form_with(model: user) do |form| %>
  ...省略
<% end %>
<%= link_to 'Other Site', 'http://example.com' %>

_form パーシャルでは <form> だけ描画します。

app/views/users/edit.html.erb
<%= render 'form', user: @user %>
<%= link_to 'Other Site', 'http://example.com' %>
app/views/users/_form.html.erb
<%= form_with(model: user) do |form| %>
  ...省略
<% end %>

gem turbolinks を入れる

単に turbolinks を動かすだけなら JS の npm 管理下にある turbolinks を入れるだけで良いけど、 gem の turbolinks は別の理由で必要です。
gem turbolinks は Rails コントローラの redirect_to の挙動をさしかえ、非Ajax の場合のリダイレクトと同じような動きをするようにします。
参考: https://github.com/turbolinks/turbolinks-classic/blob/master/lib/turbolinks/redirection.rb

create/update アクションのバリデーションエラー時に html 全体でなく _form パーシャルの内容だけ返す

  • render で _form パーシャルだけを描画して返すようにします。このときに locals を指定するのを忘れずに
  • 更に、ステータスコードは 200 でなく 422 Unprocessable Entity などのクライアントエラーを返すようにします
app/controllers/users_controller.rb
   def create
     @user = User.new(user_params)

     if @user.save
       redirect_to @user, notice: 'User was successfully created.'
     else
-      render :new
+      if request.xhr?
+        render partial: 'form', status: :unprocessable_entity, locals: { user: @user }
+      else
+        render :new
+      end
     end
   end
app/controllers/users_controller.rb
   def update
     if @user.update(user_params)
       redirect_to @user, notice: 'User was successfully updated.'
     else
-      render :edit
+      if request.xhr?
+        render partial: 'form', status: :unprocessable_entity, locals: { user: @user }
+      else
+        render :edit
+      end
     end
   end

フォームで ajax:error イベントを拾ったときに自身の内容を書き換える

app/javascript/packs/application.js
import Rails from 'rails-ujs'
Rails.start()

import Turbolinks from 'turbolinks'
Turbolinks.start()

// ここから
document.addEventListener('turbolinks:load', (event) => {
  const forms = document.querySelectorAll('form[data-remote="true"]') // remote フォームについて
  forms.forEach((form) => {
    form.addEventListener('ajax:error', (event) => { // 先のコントローラの処理で 200 を返しているとここは発火しないので注意
      const detail = event.detail
      const xhr = detail[2]
      const contentType = xhr.getResponseHeader('content-type')

      if (contentType === 'text/html; charset=utf-8') { // html が返ってきている場合
        const target = event.currentTarget
        const tmp = document.createElement('div')
        tmp.innerHTML = xhr.responseText
        const element = tmp.firstElementChild
        target.innerHTML = element.innerHTML // <form> タグの innerHTML の中身を入れ替える
      } // TODO: form タグ自体の属性についても厳密に丸々入れ替えるべきかもしれないが、ここではそこまでしていません
    })
  })
})

ここまで実施すると、 scaffold コードをベースにした Ajax CRUD が実現できているはずです。

良し悪し

  • Pros.
    • _form パーシャルに <form> を書くというルールを前提とした場合に非常にシンプル
    • Javascript の記述がほとんど要らない
    • rake app:templates:copy でコピーした controller と view のテンプレートファイルに少し手を入れる程度で、他にほとんど気にすることがない
  • Cons.
    • この実装は、バリデーションエラー時に <form> タグ以外の場所を更新できない
      • 実施する場合、たとえば <form ... data-remote-placeholder="#form-wrapper"> のような属性があれば form でなく document.querySelector(form.dataset.remotePlaceholder) が示す参照先をベースに書き換えする。みたいな拡張は可能。
      • あるいは SJR でやるとか

まとめ

  • scaffold はまだ Ajax 化されていない (今後されるの?)
  • scaffold のコードをベースに Ajax 化してみた
    • gem turbolinks を使うと Ajax/非Ajax を意識せずに redirect_to を使える
    • save 失敗時に form の html 要素を返し、それにより自身の form の中身を差し替える(と決めておくと簡単)

補足

https://github.com/hamajyotan/scaffold_ajaxify
この記事での内容を実施したリポジトリの共有です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでログイン機能実装

Goal

deviseでログイン機能を実装

How to

step1.Gemをインストールしてサーバーを立ち上げ直す
step2.コマンドを利用してdeviseの設定ファイルを作成する
step3.コマンドを利用してUsersモデルを作成する
step4.未ログイン時にはログインと新規登録ボタンを表示する
step5.コントローラにリダイレクトを設定する

step1.Gemをインストールしてサーバーを立ち上げ直す

gtmfileの一番最後に記述

  gem 'devise'

step2.コマンドを利用してdeviseの設定ファイルを作成する

先ほど記述したgemを実行


  $ bundle install

step3.コマンドを利用してUsersモデルを作成する

一気に必要な機能をインストール


  $ rails g devise:install

モデルを生成

  $ rails g devise user
  $ rake db:migrate

user.rbを開き、5行目の「:trackable」を削除

app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

step4.未ログイン時にはログインと新規登録ボタンを表示する

test.rb
<% if user_signed_in? %>
  <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to "ログイン", new_user_session_path, class: 'post' %>
  <%= link_to "新規登録", new_user_registration_path, class: 'post' %>        
<% end %>

step5.コントローラにリダイレクトを設定する

app/controllers/*****_controller.rb
class *****Controller < ApplicationController

    before_action :move_to_index, except: :index

    def index
    end

    def new
    end

    def create
    end

    private
    def *****_params
      params.permit(:*****, :*****, :*****)
    end

    def move_to_index
      redirect_to action: :index unless user_signed_in?
    end
  end
```
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでLogin機能実装

Goal

deviseでログイン機能を実装

How to

step1.Gemをインストールしてサーバーを立ち上げ直す
step2.コマンドを利用してdeviseの設定ファイルを作成する
step3.コマンドを利用してUsersモデルを作成する
step4.未ログイン時にはログインと新規登録ボタンを表示する
step5.コントローラにリダイレクトを設定する

step1.Gemをインストールしてサーバーを立ち上げ直す

gtmfileの一番最後に記述

  gem 'devise'

step2.コマンドを利用してdeviseの設定ファイルを作成する

先ほど記述したgemを実行


  $ bundle install

step3.コマンドを利用してUsersモデルを作成する

一気に必要な機能をインストール


  $ rails g devise:install

モデルを生成

  $ rails g devise user
  $ rake db:migrate

user.rbを開き、5行目の「:trackable」を削除

user.rb
  $ rails g devise user
  $ rake db:migrate

step4.未ログイン時にはログインと新規登録ボタンを表示する

test.rb
<% if user_signed_in? %>
  <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to "ログイン", new_user_session_path, class: 'post' %>
  <%= link_to "新規登録", new_user_registration_path, class: 'post' %>        
<% end %>

step5.コントローラにリダイレクトを設定する

app/controllers/*****_controller.rb
class *****Controller < ApplicationController

    before_action :move_to_index, except: :index

    def index
    end

    def new
    end

    def create
    end

    private
    def *****_params
      params.permit(:*****, :*****, :*****)
    end

    def move_to_index
      redirect_to action: :index unless user_signed_in?
    end
  end
```
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails gem carrierwave imageがnull

rails

画像を投稿機能を付ける段階でネットの情報を見ながらgem で carrierwaveを使う方もいると思います。

エラーがないはずなのにデータベースのimageの値がnull になるケースがある方もいるかもしれませんのでここに書いておきます。

form_tag, のなかに → multipart: :true

を書き忘れているかもしれませんので確認をしてみては、

低レベルの記事ではありますが参考になればいいと思います。

なぜ必要かと言うと
multipartオプションがないと

例えば

cat1.jpeg という画像の場合、

cattt1.jpeg というファイル名だけをstringとして受け取ってしまい、画像情報を受け取れないのです。
ファイルを取り込むときは

:multipart => true

をform_tagの第二引数に指定すると
StringIO(stringを拡張したもの)でクエリーがやってきて

画像が取り込めるようになるそうです。

以上です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Shrineのimage_dataを含むモデルのFactoryの作り方

railsの画像アップロードライブラリshrineを用いた場合の、FactoryBotのFactoryの書き方について悩んだポイントがあったので書いておきます

前提

登場するModel

  • Area
column type memo
name string require
  • Shop
column type memo
area bigint require
logo bigint require
name string require
  • Logo
column type memo
area bigint require
name string require
image_data text require

Area has_many Shop
Area has_many Logo
Shop belongs_to Logo

各Shopはエリアに登録されているロゴを使用できるイメージです。

悩んだこと

logoのファクトリーのimage_data カラムをどう書くべきなのか悩みました。

spec/factories/logos.rb
FactoryBot.define do
  factory :logo do
    area
    sequence(:name) { |i| "logo#{i}" }
    image { #この部分!! }
  end
end

もともとは↓のように書いていたのですが、これだとlogoのテストデータが作成されるたびにファイルへアクセスするためRSpec全体の実行時間に大きな影響を与えていました。

image { File.open("#{Rails.root}/spec/fixtures/img/example.png") }

RSpec実行時間

Factory追加前→ 27s
追加後 →1min 3s

shopが作られるたびにFactoryBotによってlogoも自動で作られるのだから遅くなるのは当たり前ですね

試したこと

適当な文字列をつっこむ

image_data { 'sample' }

結果
imageを実際に使うテストにて

JSON::ParserError:765: unexpected token at 'sample'

それはそう。。

fixture_file_upload使ってみる

fixture_file_uploadについて

この記事ではfixture_file_upload使ったらFile.openより相当早い結果になっているので期待大!

image { fixture_file_upload("#{Rails.root}/spec/fixtures/img/example.png", 'img/png') }

結果
テストはすべてpass!
ただし

Finished in 1 minute 9.24 seconds (files took 15.67 seconds to load)

File.openのときと変わらないくらい時間かかってる。。
使い方がおかしい?

最終的に

ファイルアクセスではなく架空のcacheを作成してみる

Shrineのuploaded_fileメソッドを用いてキャッシュの状態でファイルが上がっていることにする作戦

factory :logo do

  transient do
    cache_image Shrine.uploaded_file(
      'id' => SecureRandom.hex(8),
      'storage' => 'cache',
      'metadata' => { 'mime_type' => 'image/jpeg', 'size' => 1.megabyte }).to_json
  end

  area
  sequence(:name) { |i| "logo#{i}" }
  image { cache_image }
  image_data { cache_image }
end

結果

Finished in 31.04 seconds

いいじゃん!!

参考にしたページ
https://stackoverflow.com/questions/44812403/rails-testing-file-upload-validation-shrine-gem-at-model-spec

補足

この場合、image/image_dataともに明示的にcache_imageを入れてあげないとうまくいきませんでした。

はじめは下記のように書いていたんですが、

image { Shrine.uploaded_file(
      'id' => SecureRandom.hex(8),
      'storage' => 'cache',
      'metadata' => { 'mime_type' => 'image/jpeg', 'size' => 1.megabyte }).to_json}

結果
imageを実際に使うテストにて

No such file or directory @ rb_sysopen - /usr/src/app/public/uploads/cache/675ce0d849e8a5f8

ちゃんとimageが上がっていない?とってこれていない?
shrineの実装を見ないとわからなそうなので今回はここまでで

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

駆け出しrailsエンジニアがはじめの一ヶ月でくらったレビューをさらす

はじめに

早いもので、現職について1ヶ月が過ぎました。
初めてrailsを業務で扱っている自分ですが、バリバリの先輩エンジニアにメタメタのギッタギタにレビューをしていただいています。
とてもありがたい。。

備忘録の意味も込めて整理しておこうと思います。
指摘事項の雰囲気ごとにまとめました。

  • 開発の常識だよ系
  • 知っておこう系
  • かっこよく書こう系

設計だったり実装方針だったりのレビュー事項は一般化しづらいので含めていません。
そんなこと言われなくてもわかるでしょという恥ずかしい内容も多々ありますが、駆け出しなんだし恥ずかしがらずに晒していきます。

いってみよう。

開発常識系

rubyやrailsに限らず、開発者としてやっておこう/意識しようという内容のもの

ファイル末尾に空行を入れよう

POSIXという偉い規格が定めているテキストファイルの定義に反するから

テキストファイルとは「1 つ以上の行」行は「0 個以上の改行以外の文字と末尾の改行」

引用元

不要なファイルはコミットしない

例えば、rails gで自動生成されたものでも不要なファイルは消そう
modelsのspecとかは絶対作るわけではないので

rails gはいろいろ作ってくれがちなのでmigrationファイルを作成するときくらいしか使わないかもとのこと。

不要なトランザクションは貼らない

当たり前なのだけれど

example_update.rb
def update
  @hoge = Hoge.find(params[:id])
  if Hoge.transaction_with {update_with_huga} 
    redirect_to ..
  else
    ...
  end
end

private

def update_with_huga
  #呼び出し元でトランザクション貼ってるからこっちでは貼る必要なし
end

※transaction_withはswitch_pointのメソッド

カラムの追加位置を意識しよう

add_column時はbeforeオプションを使って適切な位置に追加する

知っておこう系

「知らないとやばいよ」〜「知っておくと便利だよ」まで含めて

外部キー貼るとき

外部キーを表すときは、t.bigintではなく t.references で設定する
勝手にindexを貼ってくれる

.first / .last

.first だと、 全てのデータをSELECT文で取得して最初のカラムを返す
レコードの数が増えていくであろうモデルに対しては使わないように気をつける

clients = Client.first
#SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1

Active Record クエリインターフェイスはどんなSQLが発行されるのかを確認した上で使うこと

関連付けの使用

不要なSQLを発行させていないか気をつける

post.user.id
#postに紐づくuserを取得するselect文が発行される

post.user_id
#sqlは発行されない

sanitize_sql_like使おう

そもそもsanitizeってなんだよと最初は思いました。
ものすごく簡単に言うとユーザーからの不正なSQL実行を防ぐための入力値のエスケープ処理というイメージであっているはず

#一般的なlike検索 -> 文字列にsql入れられたら実行されて困っちゃう
User.where('name LIKE ?', `%#{args[:name]}%`)

#sanitize_sql_like -> 文字列にエスケープかかるから安心
User.where('name LIKE ?', "%#{sanitize_sql_like(args[:name])}%")

validationはいろいろ準備されてるよ

例:入力値として整数だけを受け付けたいカラムが存在するとき

数字のみ受け付けたい
# もともとの自分の実装
VALID_NUMBER_INPUT_REGEX = /\A[0-9]+\z/.freeze
validates :hoge, presence: true, format: { with: VALID_NUMBER_INPUT_REGEX }

# レビュー後
validates :hoge, numericality: { only_integer: true }

numericality以外にもたくさんある
validationに限らずRailsGuideは一通り目を通すべきと痛感しました

かっこよく書こう系

こうするとrubyぽいよ、railsぽいよ、逆にそう書くとかっこわるいよという指摘

()いらない

java出身だと最初は違和感が抜けないです

@post = current_user.post.build()
#()は不要だよ
@post = current_user.post.build

不要な返り値いらない

場合によりけりですが、例えばなにかのbefore_actionで認証して失敗したらリダイレクトさせるときなんかは返り値不要

return true if @account&.authenticated? #このtrueいらない
redirect_to hoge_path

冗長なifはださい

これはrubyだからというわけでもない気がしますが。
シンプルな分岐はifをつかわなくても書ける場合が大半

booleanを返すメソッド
if account.admin?
  true
else
  account.shop.id == record.id
end

#こう書ける
account.admin? || account.shop_id == record.id

scopeを使おう

ItemはShopに所属している前提

もともとの自分の実装
#コントローラ
@items = Item.search(current_user.shop_id, params[:search_word]).page(params[:page]).per(USER_PER_PAGE)

#Userモデル searchメソッド
def self.search(shop_id, name)
  Item.where(shop: shop_id).where('name LIKE ?', "%#{name}%")
end
レビュー後
#コントローラ
@items = Item.where(shop: current_user.shop).keyword_by(params[:search_word]).page(...
# keyword_byは、model側にscopeを作る

#Userモデル scope
scope :keyword_by, ->(search_word) do
  if search_word.present?
    where('name LIKE ?', "%#{name}%")
  end
end

scopeを用いたほうがsearch の中に処理を内包するより、Controllerでどのようなフィルタリングをするかがわかりやすくなります。

関連があるときは明示的に使おう

#もともとの自分の実装
Item.where(shop: current_user.shop).find(params[:id])

#レビュー後
current_user.shop.items.find(params[:id])

このように書くことで、 current_user に紐づく何かを処理しないといけないということを明確にすることが多い

partial collection

なんらかの配列があってそれぞれになにかを表示したいというケース

haml
-# もともとの自分の実装
- hoge_array.each do |hoge|
  = hoge.name
  =  link_to ..
  ...

-# レビュー後
= render partial: 'hoge', collection: hoge_array, as: 'hoge'

-# この上で下のような_hoge.html.hamlを準備する
= hoge.name
=  link_to ..
...


最後に

本当は「要件と照らし合わせたときの実装方針」とか「責務の分担に関する考え方」みたいな自分が感動した部分に関して紹介したかったのですが言語化が難しかったです。。

この部分に熟練のエンジニア方の凄さがつまっているはずなので、またの機会にアウトプットできればと思います。

駆け出しエンジニアのみなさま
おれはこんなレビューされて痺れたぜという話があれば教えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

db:migrateでalready existだった時の話

modelが気に入らなくて作り直そうとした

そういうこと、ありません?
開発中に「あれ?この変数忘れてたわ」とmodelを作り直したいなーって思って、rails destroy => rais generate modelしたんですよ。
んでdb:migrateしようと思ったらご覧のありさま。

ec2-user:~/environment/circle-list (master) $ rails db:migrate
== 20190216164559 CreateCircles: migrating ====================================
-- create_table(:circles)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

SQLite3::SQLException: table "circles" already exists: CREATE TABLE "circles" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "space" varchar, "memo" varchar, "url" varchar, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL)
  (以下略)

SQLite上に作りたいテーブルは既にいるよ!と怒られました。
うん知ってる。ゴリ押しでどうにかしたいんだよこっちは。

つよくてにゅーげーむ

テーブルの中身をふっとばしていいなら「db:migrate」の後ろに「reset」をつけよう

$ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Database 'db/test.sqlite3' does not exist
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20190216164559 CreateCircles: migrating ====================================
-- create_table(:circles)
   -> 0.0020s
== 20190216164559 CreateCircles: migrated (0.0027s) ===========================
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsを本番環境で作動させる

Railsを本番環境で稼働するのに必要なコマンド

作ったプログラムを本番環境で落としてきたときにそのままでは動かなかったので必要だった作業を(覚えている範囲で)まとめておこうと思います。

Assetファイルのプリコンパイル

静的ファイルについては、サーバを起動する際にhtml,cssなどの必要なファイルを一つにまとめるようになっています。そのため、ある場所のファイルは自動的にコンパイルされます。具体的には、public/assets中にあるファイル群については自動的にコンパイルされ、そのコピーが静的なassetとしてwebサーバに保存されます。しかし、いくつかの場所にあるファイルはproduction環境においては自動的にはコンパイルされないので、手動でCSSファイルなどをコンパイルしておく必要があリます。これをプリコンパイルと呼び、次のコマンドで実行することができます。
bundle exec rake assets:precompile RAILS_ENV=production
プリコンパイルの際に実際にコンパイルする対象のファイルは、設定ファイルをいじることで指定することができます。

Secret Keyの設定

重要なファイルなどを暗号化する際に用いるSecret Keyというのがあって、これは手元のPCで作業している際に自動的に生成され、それを元に暗号化・復号化を行なっているようです
このファイルは他の人に知られてはいけない情報のためGitの管理対象から外れており、従ってGit経由でサーバーに落としてきたときにはこの自動生成されたファイルをコピーすることができず、サーバを起動しようとすると自動的に新たな(間違った)Keyが生成されるため、復号化がうまくいかずエラーが発生します。 ので、手元の環境でSecret Keyをコピーして本番環境にうつしてやる必要があります。どのデータを移行する必要があるかは、エラーメッセージを読めばある程度わかるようになっている(はずです)。
具体的にSecret_key_baseが用いられる対象として、次のものがあげられます。
1. 暗号化cookie
2. 署名済みcookie
3. アプリのmessage_verifier


参考にしたWebサイト
Rails Guid
Tech Racho

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsを本番環境で作動させるために必要な作業

Railsを本番環境で稼働するのに必要なコマンド

作ったプログラムを本番環境で落としてきたときにそのままでは動かなかったので必要だった作業を(覚えている範囲で)まとめておこうと思います。

CSSのプリコンパイル

詳しい仕様についてはよくわかってないのですが、Railsでは本番環境で稼働させる前に手動でCSSファイルをコンパイルしておく必要があるようです。これをプリコンパイルと呼び、次のコマンドで実行することができます。
bundle exec rake assets:precompile RAILS_ENV=production
プリコンパイルの際に実際にコンパイルする対象のファイルは、設定ファイルをいじることで指定することができます(いつかまとめます)

Secret Keyの設定

重要なファイルなどを暗号化する際に用いるSecret Keyというのがあって、これは手元のPCで作業している際に自動的に生成され、それを元に暗号化・復号化を行なっているようです
このファイルは他の人に知られてはいけない情報のためGitの管理対象から外れており、従ってGit経由でサーバーに落としてきたときにはこの自動生成されたファイルをコピーすることができず、サーバを起動しようとすると自動的に新たな(間違った)Keyが生成されるため、復号化がうまくいかずエラーが発生します。 ので、手元の環境でSecret Keyをコピーして本番環境にうつしてやる必要があります。どのデータを移行する必要があるかは、エラーメッセージを読めばある程度わかるようになっている(はずです)。

また詰まったときにでも更新していこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む