20190218のRubyに関する記事は18件です。

9日目。RubyをHerokuで・・・動きません!

今日はProgateでつくったRubyをHerokuで動かすにリトライです。
Heroku公式のRubyの使い方をみて、そのとおりにやってみたのですが、エラー連発で四苦八苦しております。

$ mkdir hello-world
$ cd hello-world/
$ bundle init
  出力されたgemfileを編集
$ bundle install --path vender/bundle
$ subl config.ru
  中身はこれ。`run proc { [ 200,{},["Hello World!"]}`
$ git init
$ git add config.ru Gemfile Gemfile.lock
$ heroku create
$ git push heroku master

 ここでエラー。原因は・・・
-HerokuのRubyは2.4.5以上。
-ローカルのRubyは2.3.7p456。アップデートしてるのに、なぜか出来ない。

ということで、次回はローカルのRubyのUpdateにチャレンジです。
簡単そうにみえて大変だー!

以下、雑多な作業ログです。

スクリーンショット 2019-02-18 22.43.25.png
Ruby アプリをクラウドにデプロイ・運用・スケール | Heroku
https://jp.heroku.com/ruby

①bundle init でRubyプロジェクトを初期化

$ mkdir rubydays
$ cd rubydays
$ bundle init  
-bash: bundle: command not found

こりゃなんだ?
rbenvとbundlerというRubyのプラグインらしい。
なぜここで必要かが分からないけどいれてみる。

bundlerのインストールの参考にさせていただきました!
https://qiita.com/Alex_mht_code/items/d2db2eba17830e36a5f1

$ brew install rbenv ruby-build
Updating Homebrew...

<中略>
$ rbenv --version
rbenv 1.1.1

$ gem install bundler
Fetching: bundler-2.0.1.gem (100%)
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /Library/Ruby/Gems/2.3.0 directory.
$ sudo gem install bundler
Password:

<中略>

$ rbenv rehash  ##
$ bundler -v
Bundler version 2.0.1
$ bundler init
Writing new Gemfile to /Documents/GitHub/rubydays/Gemfile

よーしできたぞ!

②RubyのバージョンとRackの依存関係を特定

$ subl gemfile
-bash: subl: command not found

こりゃなんだ2

Rubyのエディタらしい。いれておこう。

sublについて、こちらを参考にさせていただきました。
https://qiita.com/ashdik/items/aeb50f67e43b910204cb

そしてSubline Textをインストールしようって書いてる。インストールしてみました。
https://www.sublimetext.com/

シンボリックリンクをはる。これ、単なるエディタでしたってこと???

sudo ln -s /Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl /usr/local/bin/subl

$ subl gemfile
ジェムファイルが開いた!
Rubyとrackを指定してっと。

# frozen_string_literal: true

source "https://rubygems.org"
ruby "2.2.3"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem "rack"

$ bundle install --path vender/bundle
The Gemfile specifies no dependencies
Resolving dependencies...
Bundle complete! 0 Gemfile dependencies, 1 gem now installed.
Bundled gems are installed into `./vender/bundle`

③「Hello World」でリクエストに応答

$subl config.ru

run proc { [ 200,{},["Hello World!"]}

$ git init
Reinitialized existing Git repository in /Documents/GitHub/hello-world/.git/

これでいいのかな???
Githubを見に行ったけどファイルはなし。うーん。

$ git add config.ru Gemfile Gemfile.lock

Gemfile.lockってなんだろう。いま手元にはないし。
調べてみたら、なんだか大事そうなファイルです。
もういちど bundle install

$ bundle install --path vender/bundle
Your Ruby version is 2.3.7, but your Gemfile specified 2.2.3

Gemfileのバージョンが違うって?だめもとで書き換えてみる。

$ bundle install --path vender/bundle
Fetching gem metadata from https://rubygems.org/..............
Resolving dependencies...
Using bundler 2.0.1
Fetching rack 2.0.6
Installing rack 2.0.6
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Bundled gems are installed into `./vender/bundle`
$
$ ls
Gemfile     Gemfile.lock    config.ru   vender

Gemfile.lockができた!

$ git add config.ru Gemfile Gemfile.lock
$ git commit -m “init”
[master (root-commit) 3a043e6] “init”
 3 files changed, 24 insertions(+)
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
 create mode 100644 config.ru

これでいいのかな?

④heroku createを使ってアプリをプロビジョン

$ heroku create
Creating app... done, ⬢ pure-garden-42094
https://pure-garden-42094.herokuapp.com/ | https://git.heroku.com/pure-garden-42094.git

$ git push heroku master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 586 bytes | 293.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Ruby app detected
remote: 
remote:  !
remote:  !     You must use Bundler 2 or greater with this lockfile.
remote:  !
<中略>
To https://git.heroku.com/pure-garden-42094.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/pure-garden-42094.git'
$

エラーメッセージで検索。2019年からのバージョンエラーって!
こちらを参考にさせていただきました。
https://qiita.com/yoshijbbsk1121/items/87250501b32c6433943e

やってみる。

$ rm Gemfile.lock
$ gem install bundler -v 1.17.3
$ bundler _1.17.3_ install
$ git push heroku master
Enumerating objects: 5, done.
<中略>
remote:  !
remote:  !     An error occurred while installing ruby-2.3.7
remote:  !     
remote:  !     This version of Ruby is not available on Heroku-18. The minimum supported version
remote:  !     of Ruby on the Heroku-18 stack can found at:
remote:  !     
remote:  !     https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
remote:  !

だめかい!
同じくHeroku-18でひっかかっていた記事。
Ruby2.4.5以降が必要らしい。
https://qiita.com/newburu/items/bb31ef2b429b5557b45a

$ rbenv install 2.4.5

バージョンアップしてgit push

$ git push heroku master
Enumerating objects: 5, done.
<中略>
remote:  !     An error occurred while installing ruby-2.3.7

そういえば、Gemfileに2.3.7って書いたっけ。2.4.5に修正してみよう。・・・しかし同じくエラー。

もう一回。

$ rbenv install 2.4.5
<中略>
$ rbenv local 2.4.5
$ ruby --version
ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]
$ 

うーん、なんでだ!?
どうやったらローカルのRubyをアップデートできるんだろう。
続きはまたこんど。

(所要時間 3時間半)

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

Rails Herokuにpushで詰まった話

git push heroku masterで詰まった

今回は,Herokuにアップロードしようとした所で詰まったので記録しておきます。

$ git push heroku master
・
・
・
remote:  !     Push rejected, failed to compile Ruby app.
remote: 
remote:  !     Push failed
remote: Verifying deploy...
remote: 
remote: !       Push rejected to tranquil-wave-48446.
remote: 
To https://git.heroku.com/tranquil-wave-48446.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to・・・以下略
remote: -----> Ruby app detected
remote: 
remote:  !
remote:  !     You must use Bundler 2 or greater with this lockfile.
remote:  !

実行した所複数のエラーが発生し、何が原因化を模索・・・

pgの指定がおかしいのか、gemfile.lock、bundlerのバージョンが問題なのか調べても中々解決策が見つからない、わからない

bundler バージョンが問題?

自分の環境とHeroku上の環境でbundlerのバージョンが異なることによるエラーが原因(?)の様でした。

https://github.com/bundler/bundler/issues/6784

以上の記事を参考にさせていただきました。

$ heroku buildpacks:set https://github.com/bundler/heroku-buildpack-bundler2

を実行の後、再びHerokuへPushした所無事にアップロードができました!!!

感想

問題解決後にエラーを再検索してみると、同じ様なエラーになった人の記事がチラホラとありました。

もっと早い段階で気がつくことはできなかったのだろうか?
調べ方に改善の余地があるのかもしれない

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

Rubyのメソッド定義の入れ子

動作確認

Ruby 2.5

記事を書いたきっかけ

予想が外れたので.

はじめに

標準出力に何が表示されるだろうか

def f
    def g
        p self
        p "g"
    end
    p self
    p "f"
end
p f.g

結果

出力結果はあえて後回しにして,先に解説から.
実行順に追ってみる.

流れ

1行目.
def fブロックで,トップレベルにメソッド f を定義している.
ブロックの中は実行されない.よって,def gの定義も行われない.
ブロックをスルーして9行目へ.

9行目.
p f.g
まず.メソッド f を呼ぶ.その次に,f の返り値を ret とすると,ret.g を呼ぶ.
f が呼び出されるので2行目へ.

2行目.
def g によって,トップレベルにメソッド g が定義される.
6行目へ.

6行目.
トップレベルでの self を inspect すると main になるらしい.

7行目.
"f" を出力.
メソッド f の末尾.最後の評価値が "f" なので,メソッド f"f" を返す.
9行目に戻る.

9行目.
f の返り値が "f" だったので, "f".g を呼ぶ.
g という名前のメソッドは String には定義されていない…
が,先程トップレベルに g という名前のメソッドを定義したので,先程のメソッド g が呼び出される.
3行目へ.

3行目.
"f" に対して g を呼び出したので,self は "f"

4行目.
"g" を出力.
メソッド g の末尾.最後の評価値が "g" なので,メソッド g"g" を返す.
9行目に戻る.

9行目.
g の返り値が "g" だったので, p "g" する.

EOF.

出力結果

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

【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で続きを読む

Sidekiqのデフォルト再試行スケジュール表(目安)

It will perform 25 retries over approximately 21 days.

Sidekiqの再試行が デフォルト設定の場合にどのくらいの間隔で行われるか が知りたかったのだけれども、公式 を読んでも 約21日で25回行う くらいしか書かれていなかったので表にしてみた。

追記(2019-02-19)

と思ったら、公式 に書いてあった。プルダウン見てなかった。。。

リトライの計算式

公式を参照。

(retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))

結果

前提

  1. あくまで目安
  2. 最短、平均、最長の各値は、最初のジョブが失敗してから実行されるまでの期間を表す
  3. ジョブ自体の処理時間等は加味していない(仮に処理が10秒かかるものだとすれば、2回目が実行されるタイミングは最短でも41秒後)
  4. 再試行回数は、何回目の再試行かを表す
  5. 最短は、上記計算式のrand(30) が毎回0を返した場合としている
  6. 平均は、上記計算式のrand(30) が毎回15を返した場合としている
  7. 最長は、上記計算式のrand(30) が毎回30を返した場合としている

再試行回数 最短 平均 最長
1回目 0日00時間00分15秒後 0日00時間00分30秒後 0日00時間00分45秒後
2回目 0日00時間00分31秒後 0日00時間01分16秒後 0日00時間02分01秒後
3回目 0日00時間01分02秒後 0日00時間02分32秒後 0日00時間04分02秒後
4回目 0日00時間02分38秒後 0日00時間05分08秒後 0日00時間07分38秒後
5回目 0日00時間07分09秒後 0日00時間10分54秒後 0日00時間14分39秒後
6回目 0日00時間17分49秒後 0日00時間23分04秒後 0日00時間28分19秒後
7回目 0日00時間39分40秒後 0日00時間46分40秒後 0日00時間53分40秒後
8回目 0日01時間19分56秒後 0日01時間28分56秒後 0日01時間37分56秒後
9回目 0日02時間28分27秒後 0日02時間39分42秒後 0日02時間50分57秒後
10回目 0日04時間18分03秒後 0日04時間31分48秒後 0日04時間45分33秒後
11回目 0日07時間04分58秒後 0日07時間21分28秒後 0日07時間37分58秒後
12回目 0日11時間09分14秒後 0日11時間28分44秒後 0日11時間48分14秒後
13回目 0日16時間55分05秒後 0日17時間17分50秒後 0日17時間40分35秒後
14回目 1日00時間51分21秒後 1日01時間17分36秒後 1日01時間43分51秒後
15回目 1日11時間31分52秒後 1日12時間01分52秒後 1日12時間31分52秒後
16回目 2日01時間35分52秒後 2日02時間09分52秒後 2日02時間43分52秒後
17回目 2日19時間48分23秒後 2日20時間26分38秒後 2日21時間04分53秒後
18回目 3日19時間00分39秒後 3日19時間43分24秒後 3日20時間26分09秒後
19回目 5日00時間10分30秒後 5日00時間58分00秒後 5日01時間45分30秒後
20回目 6日12時間22分46秒後 6日13時間15分16秒後 6日14時間07分46秒後
21回目 8日08時間49分41秒後 8日09時間47分26秒後 8日10時間45分11秒後
22回目 10日14時間51分17秒後 10日15時間54分32秒後 10日16時間57分47秒後
23回目 13日07時間55分48秒後 13日09時間04分48秒後 13日10時間13分48秒後
24回目 16日13時間40分04秒後 16日14時間55分04秒後 16日16時間10分04秒後
25回目 20日09時間49分55秒後 20日11時間11分10秒後 20日12時間32分25秒後

表を作った時に書いたプログラム

秒から日に変換する処理は、秒を時間表示へ変換するを使用させていただいた。

require "time"

def retry_seconds(retry_count, additional_value)
  (retry_count ** 4) + 15 + (additional_value * (retry_count + 1))
end

def print_retry_schedule(retries, execution_time)
  sec_list = (0..retries).reduce([0, 0, 0]) do |sum, retry_count|
    sum[0] += (retry_seconds(retry_count, 0) + execution_time)
    sum[1] += (retry_seconds(retry_count, 15) + execution_time)
    sum[2] += (retry_seconds(retry_count, 30) + execution_time)
    sum
  end

  str = "| #{retries + 1}回目 |"
  sec_list.each do |sec|
    day, sec_r = sec.divmod(86400)
    str += (Time.parse("1/1") + sec_r).strftime(" #{day}日%H時間%M分%S秒後 |")
  end

  p str
end

(0...25).each { |n| print_retry_schedule(n, 0) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング学習記録2〜railsチュートリアルにかかる時間〜

今日やったこと

  • railsチュートリアルはどれくらいの時間をかけるとクリアできるのか?を調べた
  • ProgateのRuby1 リセット復習
  • ProgateのRuby2 リセット復習
  • ProgateのRuby3 リセット復習&そのまま復習

railsチュートリアルにかかる時間【40〜130時間】

Progateのrailsが終わったらrailsのチュートリアルをやろうと思っています。

そこで「他の人は大体どれくらいの時間をかけてやっているのだろう?」と思い、気になったので調べてみました。

結論から言うと当たり前ですが「人による」みたいですね。

早い人だと40時間で終わってたりもしますが、130時間くらいはかかるといった意見もありました。

私はそんなに飲み込みが早い方でもないですし、復習に時間をかけがちなタイプなので最低でも130時間以上はかかると思って、railsチュートリアルに臨みたいと思います。

Progate Ruby1,2,3

今日Progate Rubyの復習をしてみた結果、Rubyの1,2,3はもうリセット復習はしなくていいなと思いました。

復習するにしても、そのまま復習(コードを打たず、見るだけの復習)しかしないことにします。

私は9割理解してても、残りの1割ちょっとでも理解できないところがあるとできるまで復習してしまうタイプです。

ただ、プログラミングは基礎学習に完璧を求めてもスキルは向上しないみたいなので、名残惜しいですが割り切って前に進みます。

100DaysOfCode Day2感想

100DaysOfCodeが1日坊主にならずに済んでよかったです。

Qiitaに投稿することを考えると、ちょっとカッコつけたくなるというか、ちゃんとした記録を載せなきゃダメだなと思えるので、学習過程を記録していくことは今のところ自分にとってプラスだと思いました。

このまま100Days完走できるように頑張ります。

おわり

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

メソッド

メソッドの定義

Rubyのメソッドは「def」を使って定義する。

def メソッド名(引数1、引数2)
 #必要な処理
end

exp,

#2つの数字を加算するメソッド
def add(a, b)
 puts a + b
end
add(1, 2)

=>3 
#と出力される。

メソッド名の付け方

メソッドの名前の付け方も変数と基本的に一緒。

#スネークケースでかく
def hello_world
 puts 'hello, world'
end

#キャメルケースは使わない
def helloworld
 puts 'hello,world'
end

#_(アンダースコア)でメソッド名をかく(あんま使わないみたい)
def _hello_world
 puts 'hello, world'
end

#メソッド名に数字を入れる
def hello_world_2
 puts 'hello, world'
end

#数字で始まるメソッド名は使わない
def 2_hello_world
 puts 'hello, world'
end

#メソッド名はひらがなにしない
def あいうえお
 puts 'かきくけこ'
end

メソッドの戻り値

Rubyは最後の式が戻り値になるのが特徴。
なのでreturnはいらない。

def greeting(country)
  if country == 'japan'
    puts 'こんにちは'
  else 
    puts 'hello'
  end
end
greeting('japan')

=>こんにちは
#と出力される。

参考URL

Programming from 30 - わからないまま進んだ、戻り値とnil。

・・・やっぱり戻り値は難しい。。。
何かいい解釈の仕方はないかなー、そして多くの人が戻り値苦労してるのか笑
ちょっと安心( ^ω^ )

以上

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

真偽値と条件分岐

Rubyの真偽値

Rubyの真偽値は以下のルールがある。

・false or nil であれば偽。
・それ以外は真。
※nilも偽として扱われるので気をつける

真として扱われるもの

#trueそのもの
true

#全ての数値
1
0
-1 とか

#全ての文字列
'true'
'false' とか

論理演算子(&&と||)

&&や||のような論理演算子を使うと、複数の条件を1つにまとめることができる。

&&(かつ)
「かつ」は「条件1も条件2も真であれば真、それ以外は偽」となる

条件1 && 条件2

a1 = true
a2 = true
a3 = false

a1 && a2 => true
a1 && a3 => false

||(または)
「または」は「条件1か条件2のどちらかが真だと真になり、両方偽であれば偽」となる。

条件1 || 条件2

a1 = true
a2 = false
a3 = false

a1 || a2 => true
a1 || a3 => false

組み合わせ
&& と || は組み合わせて使うこともできる。
&& は ||より優先順位が高いため()を使った方がわかりやすい。

「条件1かつ条件2が真、または条件3かつ条件4が真なら真」
・条件1 && 条件2 || 条件3 && 条件4
・(条件1 && 条件2) || (条件3 && 条件4)

a1 = true
a2 = true
a3 = false
a4 = false

a1 && a2 || a3 && a4 => true
(a1 && a2) || (a3 && a4) => true

if式

Rubyのif式は下記のように書く。

if 条件A
#条件Aが真だった場合の処理
elsif 条件B
#条件Bが真だった場合の処理
elsif 条件C
#条件Cが真だった場合の処理
else
#それ以外の条件の処理
end

※elseif ではなく elsifと書く!

#値が10より大きいかどうかで処理が変わるif式

n = 11

if n > 10
 puts '10より大きい'
else
 puts '10以下'
end

=>10より大きい と出力される。

#国名で処理が変わるif式

country = 'italy'
if country == 'japan'
 puts 'こんにちは'
elsif country == 'u.s'
 puts 'Hello'
elsif country == 'italy'
 puts 'ciao'
else
 puts '???'
end

=> ciao と出力される。

・if修飾子(後置if)
Rubyのif式は修飾子として文の後ろに置くこともできる。

そもそも修飾子って何よってなったので一応調べました。。。
「名詞または動詞の意味を修飾する内容語」
・・・修飾語みたいな感じかな?多分。

#普通の毎月1日はポイント5倍にするif式
point = 7
day = 1

if day == 1
 point *= 5
end

#if修飾子を使った毎月1日はポイント5倍にするif式
point = 7
day = 1

point *= 5 if day == 1
point => 35 と出力される。

参考URL

勉強に使った参考URL。

Let's プログラミング - 式修飾子(if修飾子、unless修飾子)

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

(Ruby)セッターにはselfを付けてレシーバを明示的にしよう。

はじめに

・なんでRubyのセッターにはselfを付けないといけないの? って人のための記事。
 (自分が1時間ハマったため)
・Ruby初心者向けです。

前置き

attr_accessor :name

  # これがゲッター
  def name
    @name 
  end

  # これがセッター
  def name=(argument)
    @name = argument
  end

の略です。
(ちなみにattr_accessorは「属性へのアクセス(アトリビュートアクセサー)」という意味)

本題:ユーザークラスを用意する。

userclass.rb
class User
  attr_accessor :name

  def set_default_name
    self.name = "デフォルト山田" if name.nil?
  end

end

このクラスには、@nameがnilだったとき、@nameにデフォルトネームを代入してくれるというメソッド(set_default_name)が定義されています。

[1] pry(main)>user = User.new
[2] pry(main)>user.name
=> nil
[3] pry(main)>user.set_default_name
=> "デフォルト山田"
[4] pry(main)>user.name
=> "デフォルト山田"



しかし、このメソッド、どこか違和感ありませんか?

  def set_default_name
    self.name = "デフォルト山田" if name.nil?
  end

なんで左側の「self.name=」はname=メソッドのレシーバを明示的にして(selfがレシーバであることを明記して)、右側の「name.nil?」のレシーバは暗黙的なんでしょうか?(self.name.nil?となっていない)

「納得行かない、セッター側である「self.name=」のselfをとりあえず消してみよう!」と、実際に消してみると、インスタンス変数の値を書き換えることができなくなります。(エラーは出ないが、セッターとして@nameの値を書き換えられない。)

だめだ…わからん…。

結論

  def set_default_name
    name = "デフォルト山田" if name.nil?
  end

これみたくself.name=にしてない定義だとname=メソッドを呼び出せずに、nameというローカル変数に"デフォルト山田"を代入するという挙動になるからでした。
これがわからずselfを書いていない状態で、「なんでname=メソッド呼び出せないんや…」と一生詰まってたのでこの記事を書きました。

同じ壁にぶつかる方はいないかもしれませんが、駆け出しの方は二の舞を踏まないようご注意ください。

…ていうかname=メソッドって何?

こちらの記事がわかりやすかったのでどうぞ。

【Ruby】「ゲッター」と「セッター」を理解する
https://qiita.com/k-penguin-sato/items/5b75be386be4c55e3abf

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

数値

数値について

正の整数 → 10とか
浮動小数点数 → 1.5とか

負の整数 → -3とか
負の浮動小数点数 → -4.75とか

※数値には(アンダースコア)を含めることができる。
 
は無視されるので数の区切りとして使うこと便利。

1_000_000_000
=>1000000000
と出力される。

四則演算

数値を使って足し算・引き算・掛け算・割り算もできる。
演算子の優先度は数学と同じで「*」と「/」が「+」と「-」よりたかい。

10 + 20 => 30
100 - 25 => 75
12 * 5 => 60
20 /5 => 4

正と負の逆転

変数の手前に-をつけると、数値の正と負を逆転できる。

n = 1
puts -n
=> -1 と出力される。

整数同士の割り算

整数同士の割り算は整数になってしまう。(小数点以下は切り捨てられる)

1 / 2 => 0
と出力される。

#0.5ではない

浮動小数点数を出力したいときは、どっちかの数値に小数点の.0をつける。

1.0 / 2 => 0.5
1 / 2.0 => 0.5
と出力される。

※変数に整数が入っているときは「to_f」メソッドで整数から浮動小数点数に変更できる。

n = 1
n.to_f => 1.0
n.to_f / 2 => 0.5
と出力される。

変数に格納された数値の増減

Rubyの変数の値を増減させる演算子は「+=」と「-=」、「=」、「/=」、「*=」。

#nの値を1増やす
n = 1
n += 1
=> 2 と出力される。

#nの値を1減らす
n = 3
n -= 1
=> 2 と出力される。

#nの値に3をかける
n = 2
n *= 3
=> 6 と出力される。

#nの値を2でわる
n = 6
n /= 2
=> 3 と出力される。

#nの値を2乗する
n **= 2
=> 9 と出力される。

参考URL

勉強するときに参考にしたURL。
Ruby入門 - 演算子
Let'sプログラミング - 自己代入

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

文字列の作成

文字列

Rubyでは文字列を作成する方法がいくつかあるが、一般的なのは
「シングルクォート(’)」と「ダブルクォート(”)」で囲む方法。

'これは文字列です'
"これは文字列です"

シングルクォートとダブルクォートでは共同が異なることがある。
例えば、文字列中に改行文字(/n)を入れる場合は、ダブルクォート(”)で囲まなくてはいけない。

ダブルクォート(")

puts "おはようございます\nこんにちは"

=>おはようございます
=>こんにちは
#と出力される。

他にもダブルクォートを使うと式展開が使える。
式展開を使うときは変数や式を#{}の中に書く。

name = sato
puts "Hello, #{name}!"
=>Hello, sato!
#と出力される。

i = 10
puts "#{i}は16進数にすると#{i.to_s(16)です}"
=>10は16進数にするとaです
#と出力される。

シングルクォート(')

puts 'おはようございます\nこんにちは'

=>おはようございます\nこんにちは
#と出力される。

シングルクォートの場合は式展開はされない。
「#{}」を使ってもただの文字列とみなされる。

name = sato
puts 'Hello, #{name}!'
=>Hello, {name}!
#と出力される。

バックスラッシュ(/)

ダブルクォートを使う文字列で、改行文字や式展開の機能を打ち消したいときは、
手前にバックスラッシュをつける。

puts "おはようございます\\nこんにちは" 
=>おはようございます\nこんにちは
#と出力される。

name = sato
puts "Hello,\#{name}!"
=>Hello, #{name}!
#と出力される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む