20200820のRubyに関する記事は23件です。

Railsチュートリアル 5章最後でHerokuが動かなくなる

Railsチュートリアルをもりもりとやっています。
5章に来て初めて本格的に躓いてしまったので、備忘録として記録します。

herokuが動かない・・・

5章の最後に'git push heroku'をしたところこの画面がでました。
スクリーンショット 2020-08-20 20.25.50.png

Application error
An error occurred in the application and your page could not be served. 
If you are thee application owner, check your logs for details. 
You can do this from the Heroku CLI with the command
heroku logs --tail

対処

ひとまず色々検索して以下のコマンドを打ちました。

heroku logs herokuのログの出力
見るべきポイント:errorと書かれている行

>>$ heroku logs #ログ出力
:
(中略) #errorと書かれている箇所を探す
:
2020-08-20T12:51:51.649616+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=enigmatic-harbor-30759.herokuapp.com request_id=6cf41616-a067-4dc6-8d2c-696895e1b968 fwd="60.76.208.83" dyno= connect= service= status=503 bytes= protocol=https
:

heroku run rails c railsでコンソールを起動する
見るべきポイント:エラーになっているファイル、エラー原因

>>$ heroku run rails c #コンソールの起動
:
(中略) #ファイル名が書かれている箇所を探す
:
4: from /app/app/helpers/application_helper_test.rb:1:in `<top (required)>'
: 
:#最終的なエラーを確認する
/app/vendor/bundle/ruby/2.6.0/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:292:in `require': No such file to load -- test_helper.rb (LoadError)

以上の内容でわかったこと
logを見る限りだと、よくわからない・・・
・console起動でみると、どうやらapplication_helper_test.rbが悪さをしている?
・上のファイルがtest_helper.rbを呼んでるけど、ファイルがないと言われている。

結論

application_helper_test.rbtest_helper.rbを参照できないようフォルダ構成になっていまいました。
本来であれば、testsフォルダ内にあるべきapplication_helper_test.rbファイルが別のフォルダ(app)に入っていたようです。
ひとまず、ファイルを移動させ、再度実行するとエラーがなくなりました。

感想

エラーが出た時の、ログの読み取りがうまく出来なかったのでてこずってしまいました・・・。改めて整理してみて、案外読み取るポイントってシンプルなんだなと改めておもいました。

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

privateメソッドとprotectedメソッド

【概要】

1.結論

2.”private”と”protected”とは何か

3.どのように”private”と”protected”を使うのか

4.ここから学んだこと

1.結論

状況に応じて使い分けるが、ほぼprivateメソッドを使えばいい!

2.”private”と”protected”とは何か

publicは制限がないメソッドですが、private,protectedの違いがわからず悩みました。

private:
クラス外からは呼び出せない。同じインスタンス内で呼び出せる。

protected:
クラス外からは呼び出せない。同じインスタンス内で呼び出せる。しかし、他のインスタンスであれば同じクラスであれば呼び出せる。

ということですが、3.の例を見て
理解していきましょう。

3.どのように”private”と”protected”を使うのか

private
class saturday

 def  day
  holiday
 end

 private

 def holiday
  puts "今日は休日です"
 end
end

human=Human.new
human.holiday
protected
class school

  def class (student)
   @student = student
  end

  def brother(human)
   puts "#{class_brother}#{human.class_brother}は兄弟です。"
 end
end

 protected
  def class_brother
   @student
  end

studentA=Student.new
studentA.school("桜木")
studentB=Student.new
studentB.school("山田")

studentA.brother("山田 ")

もちろんprivateのプログラムでも、
protectedが適用できます!
ただ、protectedの方は、同じクラス内でも
違うメソッド(他のインスタンスであれば同じクラスであれば呼び出せる)のため、protectedのみでしか適用できません。

4.ここから学んだこと

i)privateは使用頻度が高いと思いますが、protectedは使う範囲・頻度が限られそうです。

ii)邪魔されたくないメソッドや、影響されたくないメソッド(インスタンス変数)を使う時にはものすごく便利なのでこれからも使いつつ、プログラムを学んでいきます!

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

RuboCopの導入と基本的な使用方法【ポートフォリオ備忘録①】

はじめに

現在、ポートフォリオとしてオリジナルアプリを作成中ですが、自分一人でコードを書いているとついつい自分が読めれば問題ないコードを書きがちになってしまいます。

変な癖が付かないように、その辺も意識していきたいということで、RuboCopを導入することにしました。導入で学んだことを忘れないようにここに綴ります。

RuboCopとは

RuboCopとは、Ruby向けの静的解析ツールである。Ruby Style Guideのコーディング規約に従って、コードが書かれているかチェックするためのgemである。
参考:RuboCop // Docs

RuboCopをインストール

Gemfileに記入

Gemfile
group :development do
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

インストール

terminal
$ bundle

RuboCopを使ってみる

terminal
$ bundle exec rubocop

(余談ですが、bundle execって何?という時は bundler、bundle execについて ※自分用メモ の記事がオススメ。)

なんだかエラーがたくさん出てしまった・・・
直さなければいけないところが書かれているので親切だけど、全部直すには時間がかかり過ぎてしまう。

ということで以下のコマンドを実行

$ rubocop --auto-gen-config

すると、.rubocop_ymlと.rubocop_todo.ymlの2つのファイルが生成される。

.rubocop_todo.yml

.rubocop_todo.ymlファイルの生成によって、先ほど大量に出た警告が一旦無視されるように設定される。すなわち.rubocop_todo.ymlに書かれているかぎり、RuboCopに怒られなくて済むということである。

試しにもう一度 budle exec rubocop を実行すると、警告が出なくなっている。

とはいえあくまでも今は一時的に警告を回避しているに過ぎないので、このファイルに何も書かれていなくてもRuboCopから警告文が表示されない状況に持っていくことが最終的な目標となる。

警告をなくす

rubocop_todo.yml
Bundler/OrderedGems:
  Exclude:
    - 'Gemfile'

Layout/SpaceInsideArrayLiteralBrackets:
  Exclude:
    - 'config/environments/production.rb'

Layout/TrailingEmptyLines:
  Exclude:
    - 'Gemfile'

例えば、.rubocop_todo.ymlに上記のような記載があったら、まずは

Bundler/OrderedGems:
 Exclude:
  - 'Gemfile'

をコメントアウトして、bundle exec rubocop を実行。すると警告文が出る。

このあとの手順は2パターンある。

①警告に従って、コードを修正する
②コードは変えず、警告のデフォルト変更・もしくは今後警告を出さないように設定する

①は文章通り、該当ファイルを開いて、コードを修正するということ。

.rubocop.yml

②については、先ほど .rubocop_todo.ymlと同時に生成された .rubocop.ymlに設定を書いていく。

  • RuboCopの対象から外す時
rubocop.yml
inherit_from: .rubocop_todo.yml
AllCops:
  # 除外するファイル
  Exclude:
    - "config/environments/*"
    - "vender/**/*"
    - "bin/**"
    - "db/**/*"
    - "tmp/**/*"
    - "lib/**/*"
    - "Gemfile"

自分でExcludeを設定する時は、デフォルトの設定が外れてしまうため、それらも改めて明記する必要がある。

ちなみに、上記のファイルの最上部にinherit_from: .rubocop_todo.yml があるが、この一文がないと、.rubocop_todo_ymlに書かれた内容が反映されないので注意。

  • Copの無効化

Copとは、RuboCopにおける1つのルールのことである。
上から3つ目までのCopは、有効にするか無効にするかを記述しないとエラーが出るので書く。

rubocop.yml
Style/HashEachMethods:   
  Enabled: true
Style/HashTransformKeys:
  Enabled: true
Style/HashTransformValues:
  Enabled: true

# 日本語のコメントを許可する
Style/AsciiComments:
  Enabled: false

警告文に対してどちらかの対処を行ったら、コメントアウトした警告を削除する。
.rubocop_todo.ymlの中身がなくなるまでこの繰り返し。

まとめ

  • .rubocop.yml:コーディングのルールを記載
  • .rubocop_todo.yml:現在の違反部分を記載 (名前の通りToDoリスト的な役割だ!)

.rubocop_todo.ymlの違反を1つずつチェックして、コードを修正もしくは新たなルールを作って.rubocop.ymlに記載するのがRuboCopの基本的な使い方である。

おわりに

とにかく今後はRuboCopを適宜利用して、規約に沿ったコードを書いていきたいと思います。本記事で何かツッコミどころ、おかしな点などあればご教示いただけると幸いです。

尚、本記事は以下を参考にしています。

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

RuboCopの導入と基本的な使用方法【ポートフォリオ①】

はじめに

現在、ポートフォリオとしてオリジナルアプリを作成中ですが、自分一人でコードを書いているとついつい自分が読めれば問題ないコードを書きがちになってしまいます。

変な癖が付かないように、その辺も意識していきたいということで、RuboCopを導入することにしました。導入で学んだことを忘れないようにここに綴ります。

RuboCopとは

RuboCopとは、Ruby向けの静的解析ツールである。Ruby Style Guideのコーディング規約に従って、コードが書かれているかチェックするためのgemである。
参考:RuboCop // Docs

RuboCopをインストール

Gemfileに記入

Gemfile
group :development do
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

インストール

terminal
$ bundle

RuboCopを使ってみる

terminal
$ bundle exec rubocop

(余談ですが、bundle execって何?という時は bundler、bundle execについて ※自分用メモ の記事がオススメ。)

なんだかエラーがたくさん出てしまった・・・
直さなければいけないところが書かれているので親切だけど、全部直すには時間がかかり過ぎてしまう。

ということで以下のコマンドを実行

$ rubocop --auto-gen-config

すると、.rubocop_ymlと.rubocop_todo.ymlの2つのファイルが生成される。

.rubocop_todo.yml

.rubocop_todo.ymlファイルの生成によって、先ほど大量に出た警告が一旦無視されるように設定される。すなわち.rubocop_todo.ymlに書かれているかぎり、RuboCopに怒られなくて済むということである。

試しにもう一度 budle exec rubocop を実行すると、警告が出なくなっている。

とはいえあくまでも今は一時的に警告を回避しているに過ぎないので、このファイルに何も書かれていなくてもRuboCopから警告文が表示されない状況に持っていくことが最終的な目標となる。

警告をなくす

rubocop_todo.yml
Bundler/OrderedGems:
  Exclude:
    - 'Gemfile'

Layout/SpaceInsideArrayLiteralBrackets:
  Exclude:
    - 'config/environments/production.rb'

Layout/TrailingEmptyLines:
  Exclude:
    - 'Gemfile'

例えば、.rubocop_todo.ymlに上記のような記載があったら、まずは

Bundler/OrderedGems:
 Exclude:
  - 'Gemfile'

をコメントアウトして、bundle exec rubocop を実行。すると警告文が出る。

このあとの手順は2パターンある。

①警告に従って、コードを修正する
②コードは変えず、警告のデフォルト変更・もしくは今後警告を出さないように設定する

①は文章通り、該当ファイルを開いて、コードを修正するということ。

.rubocop.yml

②については、先ほど .rubocop_todo.ymlと同時に生成された .rubocop.ymlに設定を書いていく。

  • RuboCopの対象から外す時
rubocop.yml
inherit_from: .rubocop_todo.yml
AllCops:
  # 除外するファイル
  Exclude:
    - "config/environments/*"
    - "vender/**/*"
    - "bin/**"
    - "db/**/*"
    - "tmp/**/*"
    - "lib/**/*"
    - "Gemfile"

自分でExcludeを設定する時は、デフォルトの設定が外れてしまうため、それらも改めて明記する必要がある。

ちなみに、上記のファイルの最上部にinherit_from: .rubocop_todo.yml があるが、この一文がないと、.rubocop_todo_ymlに書かれた内容が反映されないので注意。

  • Copの無効化

Copとは、RuboCopにおける1つのルールのことである。
上から3つ目までのCopは、有効にするか無効にするかを記述しないとエラーが出るので書く。

rubocop.yml
Style/HashEachMethods:   
  Enabled: true
Style/HashTransformKeys:
  Enabled: true
Style/HashTransformValues:
  Enabled: true

# 日本語のコメントを許可する
Style/AsciiComments:
  Enabled: false

警告文に対してどちらかの対処を行ったら、コメントアウトした警告を削除する。
.rubocop_todo.ymlの中身がなくなるまでこの繰り返し。

まとめ

  • .rubocop.yml:コーディングのルールを記載
  • .rubocop_todo.yml:現在の違反部分を記載 (名前の通りToDoリスト的な役割だ!)

.rubocop_todo.ymlの違反を1つずつチェックして、コードを修正もしくは新たなルールを作って.rubocop.ymlに記載するのがRuboCopの基本的な使い方である。

おわりに

とにかく今後はRuboCopを適宜利用して、規約に沿ったコードを書いていきたいと思います。本記事で何かツッコミどころ、おかしな点などあればご教示いただけると幸いです。

尚、本記事は以下を参考にしています。

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

Yardを閲覧するだけのGUIアプリをRubyで作る

はじめに

ここでは1-2年前にGUIの練習のために作ったyardを見るだけのソフトを振り返って、RubyでどうやってGUIソフトを作るんだったかというのを思い出して、自分用テンプレートにまとめたいと思います。この記事は ruby/gtk3 について中級的な内容をまとめています。初心者の方はこちらを見てください。

【Ruby】10分でGladeを使って作るRuby/GTK3 GUIアプリ

このソフトは、ローカルにインストールされたgemを表示するだけのやつなので、かならずしも実用的ではないよ。

Github : https://github.com/kojix2/yardview

image.png

ちょっと透けるようになっています。コードではなくGladeの設定でいじっています。

image.png

一応Gemで試せます。(環境によっては動かないかも)

gem install yardview_gtk3

起動

yardview

元ネタ

Lazarus Lazaridisさんの記事。Todoアプリをつくています。

この元ネタの英語の内容がすらすら読める人はそれで十分なのですが、この記事にもシグナルハンドリングで少し楽をする方法など、元ネタ以上の内容が少し含まれています。

Gemにする

実行ファイルを追加するオプション --exe -b

bundle gem yardview --exe

いつもどおりgemspecファイルを編集

gemspec
  spec.add_dependency 'yard'
  spec.add_dependency 'gtk3'
  spec.add_dependency 'webkit2-gtk'
  spec.add_development_dependency 'bundler'
  spec.add_development_dependency 'rake'

ユーザーはRubyに詳しい → Gemfile.lock は含めない
ユーザーはRuby関係ない → Gemfile.lock 残す

ディレクトリ構成

.
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── exe
│   └── yardview
├── lib
│   ├── yardview
│   │   ├── application.rb
│   │   ├── application_window.rb
│   │   └── version.rb
│   └── yardview.rb
├── resources
│   ├── LICENSE
│   ├── gresources.xml
│   ├── ruby.png
│   ├── yardview.ui
│   └── yardview_screenshot.png
└── yardview.gemspec

面倒なので、exe/yardview に直接いろいろ書いています。

ポイントは、途中で glib-compile-resources を実行しているところです。GResourceというGnomeのリソースフレームワークを利用します。このコマンドにパスが通ってない場合は実行できないと思うので、何らかの対策が必要かも知れません。Windowsだとどうなんだろう…。あと、毎回リソースをコンパイルして、終了時に捨てる方式が本当にいいのかという問題はある気がします。

yardview
#!/usr/bin/env ruby

require 'optparse'
require 'yardview'
require 'fileutils'

port_num ||= 8808

opt = OptionParser.new
opt.program_name = 'Yardview'
opt.version = Yardview::VERSION
opt.on('-p', '--port VAL', Integer) { |v| port_num = v }
opt.parse!(ARGV)

resource_xml = File.expand_path('../resources/gresources.xml', __dir__)
resource_bin = File.expand_path('../gresource.bin', __dir__)

system('glib-compile-resources',
       '--target', resource_bin,
       '--sourcedir', File.dirname(resource_xml),
       resource_xml)

at_exit do
  FileUtils.rm_f(resource_bin)
end

app = Yardview.application

resource = Gio::Resource.load(resource_bin)
Gio::Resources.register(resource)

app.signal_connect :activate do |application|
  window = Yardview::ApplicationWindow.new application, port: port_num
  window.present
end

app.run

今回はウェブページを表示するので webkit2-gtk を追加します。

lib/yardview.rb
require 'yardview/version'
require 'yardview/application'
require 'yardview/application_window'
require 'webkit2-gtk'

module Yardview
end

Glade側でシグナルのハンドラーの名前を指定しておきます。

image.png

使用するファイルを書いておく。

gresource.xml
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/github/kojix2/yardview">
    <file preprocess="xml-stripblanks">yardview.ui</file>
    <file>ruby.png</file>
  </gresource>
</gresources>

ここがミソなのですが、クラス変数にApplicationのインスタンスを詰め込んで参照できるようにしておきます。

lib/yardview/application.rb
require 'gtk3'

module Yardview
  class << self
    def application
      @@application ||= Gtk::Application.new('com.github.kojix2.yardview', :flags_none)
    end
  end
end

type_register, set_template, bind_template_child set_connect_func あたりが重要なおまじないです。
type_register はGlibやGObjectの世界と統合につかわれるそうです。(正直よくわかっていません)
set_templateui ファイルを指定します。ファイルパスではなく、GResourceから指定していることに注意してください。
set_template_child は、widgetにアクセスできるメソッドを提供します。下の方で変数のように登場する box は実はwidgetにアクセスするためのメソッド呼び出しです。
set_connect_func は自動でシグナルをRubyのメソッドにつないでいます。(ここは元ネタではやっていない部分です)

lib/yardview/application_window.rb
require 'gtk3'

module Yardview
  class ApplicationWindow < Gtk::ApplicationWindow
    type_register

    def self.init
      set_template resource: '/com/github/kojix2/yardview/yardview.ui'
      bind_template_child 'box'
      set_connect_func do |handler_name|
        lambda do
          Yardview.application.active_window.__send__(handler_name)
        end
      end
    end

    attr_accessor :port

    def initialize(application, port: port_num)
      super application: application
      set_title 'YardView'
      set_icon GdkPixbuf::Pixbuf.new resource: '/com/github/kojix2/yardview/ruby.png'

      @port = port
      start_yard_server
      create_gui
    end

    def create_gui
      signal_connect('destroy') do
        Process.kill(:INT, @yard)
        @yard = nil
      end
      at_exit { Process.kill(:INT, @yard) unless @yard.nil? }
      @view = WebKit2Gtk::WebView.new
      @view.load_uri("http://localhost:#{port}")
      box.add @view, expand: true, fill: true
      @view.show
    end

    def port_open?(port)
      !system("lsof -i:#{port}", out: '/dev/null')
    end

    def on_home_clicked
      @view.load_uri("http://localhost:#{port}")
    end

    def on_back_clicked
      @view.go_back
    end

    def on_top_clicked
      @view.run_javascript('window.scrollTo(0,0);')
    end

    def on_refresh_clicked
      @view.reload
    end

    def start_yard_server
      if port_open? port
        @yard = spawn("yard server -g -p #{port} --reload")
        sleep 1
      else
        raise "port #{port} is in use!"
      end
    end
  end
end

RubyでGUIアプリを作っていると問題になりやすいのは非同期っぽい処理ですが、コマンドの実行だけでなんとかなってしまうものはspawn がオススメです。比較的プラットフォームを問わずに動作してくれます。

以上、書いている本人もよくわかっていないところが多い記事ですが、だいたいこんな感じ書けば動きます。

これは積極的にGnomeの機能を使っていこうという感じのテンプレートです。いやいやGResourceとかいらないし、GObjectとRubyのオブジェクトを統合する必要もないし、シグナルハンドリングも毎回ベタ書きすればいいじゃん、ということならもっと簡単にできるとは思います。

この記事は以上です。

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

[Ruby]簡易ポーカー作ってみました。アドバイスください

Ruby で簡易ポーカー(トランプは四枚だけど)を作ってみました。
が、あまりにもわからずに可読性落ちまくりのプログラムになってしまったので
よろしければアドバイスください。

qiita.rb
n = gets.to_i
poker = []
i = 0
check = 0


n.times do
  number = gets.chomp
  poker.push(number.chars)
end


while n > 0 
  check = poker[i].uniq.map{ |e| [e, poker[i].count(e)] }
  case check.size
    when 4 then
      puts "No Pair"
    when 3 then
      puts "One Pair"
    when 2 then
      if check[0][1] == 3
        puts "Three Card"
      else
        puts "Two Pair"
      end
    when 1 then
      puts "Four Card"
    end
    i = i + 1
    n = n - 1
end

ブロック変数の理解が浅いことが悩みです笑

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

ActionControllerエラー集(Ruby on Rails)※随時更新

NoMethodError in [コントローラー名]#[アクション名]→undefined method `[キーワード]' for #<>

→[コントローラー名]の[アクション名]に[キーワード]のメソッドが定義されていない。

Sprockets::Rails::Helper::AssetNotFound in [コントローラー名]#[アクション名]→The asset "" is not present in the asset pipeline.

unknown attribute '[カラム名]' for [テーブル名].

→[テーブル]に[カラム]が見つからない。

【原因】
 引っ張ってきたインスタンス変数やテーブルのカラムにないカラム名を指定してしまっている。

 私の場合編集画面で、

edit.html.haml
  = form_with model: @item, do |f|
    = f.file_field :[カラム名]

 で指定したカラムがテーブルに存在していませんでした。

【対処】
 ①マイグレーションファイルに指定のカラムがあるか
 ②マイグレーションファイルの指定のカラムにスペルミスはないか
 を確認

 ミスがあった場合、
 マイグレーションファイルを修正
 →rails dbコマンド周りはこちら【逆引きrails dbコマンド集】

参考記事

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

rails dbコマンド逆引き

はじめに

Ruby on Railsで開発をしていて、MySQLのDBを修正する機会が何度もあり、その都度エラーやデータの入れ直しが発生するため、コマンドの概要ともう少し掘り下げたところまでそれぞれの意味がおさらいできればと思い、記事にしました。
ご指摘等あればよろしくお願いいたします。

入門編

①モデルファイルを作成したので、DBに反映させたい→migrate

rails g modelでモデルファイル作成後、

$ rails db:migrate

②DBを作ったが、不要になったので削除したい→drop

$ rails db:drop

③1つ前に反映したDBテーブルを修正するために、migrationを戻したい→rollback

$ rails db:rollback

入門編まとめ

・migrateを実行すると、モデルファイルに基づいてマイグレーションファイルが作られる。
・作られたマイグレーションファイルを修正し、DBに反映するためには一度rollbackを実行しDB上から該当のテーブルを下げないといけない。(この際DBに入力したデータは削除される)

初級編

①現在、どのマイグレーションファイルがDB上で有効なのか確認する→migirate:status

$ rails db:migrate:status

適用されている場合はup、適用されていない場合はdownとしてファイルがターミナル上に表示される。

②任意のファイルまでrollbackを戻したい場合→rollback STEP=[ ]

$ rails db:rollback STEP=[戻したいステップ数]

参考記事:【Rails】$rails db:rollbackしたい時の間違えない手順

③作成したDBを一旦削除して再度作成する(その1)→reset

$ rails db:reset

④作成したDBを一旦削除して再度作成する(その2)→migrate:reset

$ rails db:migrate:reset

初級編まとめ

・【resetとmigrate:resetの違い】どちらもDBを一旦dropして再度作成することは同じだが、どのファイルを元に作り直すかが異なるため、migrate:resetではマイグレーションファイルを修正していればその内容も反映して作成されるが、migrate:resetではそうならないようだ。

詳しくは以下(常にmigrate:resetを使っていれば良さそうなものだが、そうもいかない時があるようだ)
参考記事:rake db:reset と rake db:migrate:reset の違い
参考記事:rails db:migrate:resetできなかったのでrails db:resetした

中級編

入力したDBの内容を消さずにカラムを修正したい→g migrationでカラム追加→db:migrate→カラム追加用のmigrationファイル削除

参考記事:Rails カラム名変更方法

まとめ

コマンド       概要 オプションその他
migrate DBを作成
drop    DBを削除
reset     drop + migrate  マイグレーションファイルの修正は反映されない 
migrate:reset  drop + migrate  マイグレーションファイルの修正も反映される
migrate:rollback 1つ前に反映したmigrateまで STEP=[戻したいステップ数]で任意のmigrateまでDBを戻す
migrate:status 現在どこまでmigrateが反映されているか確認する

参考記事

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

逆引きrails dbコマンド集

はじめに

Ruby on Railsで開発をしていて、MySQLのDBを修正する機会が何度もあり、その都度エラーやデータの入れ直しが発生するため、コマンドの概要ともう少し掘り下げたところまでそれぞれの意味がおさらいできればと思い、記事にしました。
ご指摘等あればよろしくお願いいたします。

入門編

①モデルファイルを作成したので、DBに反映させたい→migrate

rails g modelでモデルファイル作成後、

$ rails db:migrate

②DBを作ったが、不要になったので削除したい→drop

$ rails db:drop

③1つ前に反映したDBテーブルを修正するために、migrationを戻したい→rollback

$ rails db:rollback

入門編まとめ

・migrateを実行すると、モデルファイルに基づいてマイグレーションファイルが作られる。
・作られたマイグレーションファイルを修正し、DBに反映するためには一度rollbackを実行しDB上から該当のテーブルを下げないといけない。(この際DBに入力したデータは削除される)

初級編

①現在、どのマイグレーションファイルがDB上で有効なのか確認する→migirate:status

$ rails db:migrate:status

適用されている場合はup、適用されていない場合はdownとしてファイルがターミナル上に表示される。

②任意のファイルまでrollbackを戻したい場合→rollback STEP=[ ]

$ rails db:rollback STEP=[戻したいステップ数]

参考記事:【Rails】$rails db:rollbackしたい時の間違えない手順

③作成したDBを一旦削除して再度作成する(その1)→reset

$ rails db:reset

④作成したDBを一旦削除して再度作成する(その2)→migrate:reset

$ rails db:migrate:reset

初級編まとめ

・【resetとmigrate:resetの違い】どちらもDBを一旦dropして再度作成することは同じだが、どのファイルを元に作り直すかが異なるため、migrate:resetではマイグレーションファイルを修正していればその内容も反映して作成されるが、migrate:resetではそうならないようだ。

詳しくは以下(常にmigrate:resetを使っていれば良さそうなものだが、そうもいかない時があるようだ)
参考記事:rake db:reset と rake db:migrate:reset の違い
参考記事:rails db:migrate:resetできなかったのでrails db:resetした

中級編

入力したDBの内容を消さずにカラムを修正したい→g migrationでカラム追加→db:migrate→カラム追加用のmigrationファイル削除

参考記事:Rails カラム名変更方法

まとめ

コマンド       概要 オプションその他
migrate DBを作成
drop    DBを削除
reset     drop + migrate  マイグレーションファイルの修正は反映されない 
migrate:reset  drop + migrate  マイグレーションファイルの修正も反映される
migrate:rollback 1つ前に反映したmigrateまで STEP=[戻したいステップ数]で任意のmigrateまでDBを戻す
migrate:status 現在どこまでmigrateが反映されているか確認する

参考記事

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

逆引きrailsコマンド集【DB編(入門〜初級)】

はじめに

Ruby on Railsで開発をしていて、MySQLのDBを修正する機会が何度もあり、その都度エラーやデータの入れ直しが発生するため、コマンドの概要ともう少し掘り下げたところまでそれぞれの意味がおさらいできればと思い、記事にしました。
ご指摘等あればよろしくお願いいたします。

入門編

①モデルファイルを作成したので、DBに反映させたい→migrate

rails g modelでモデルファイル作成後、

$ rails db:migrate

②DBを作ったが、不要になったので削除したい→drop

$ rails db:drop

③1つ前に反映したDBテーブルを修正するために、migrationを戻したい→rollback

$ rails db:rollback

入門編まとめ

・migrateを実行すると、モデルファイルに基づいてマイグレーションファイルが作られる。
・作られたマイグレーションファイルを修正し、DBに反映するためには一度rollbackを実行しDB上から該当のテーブルを下げないといけない。(この際DBに入力したデータは削除される)

初級編

①現在、どのマイグレーションファイルがDB上で有効なのか確認する→migirate:status

$ rails db:migrate:status

適用されている場合はup、適用されていない場合はdownとしてファイルがターミナル上に表示される。

②任意のファイルまでrollbackを戻したい場合→rollback STEP=[ ]

$ rails db:rollback STEP=[戻したいステップ数]

参考記事:【Rails】$rails db:rollbackしたい時の間違えない手順

③作成したDBを一旦削除して再度作成する(その1)→reset

$ rails db:reset

④作成したDBを一旦削除して再度作成する(その2)→migrate:reset

$ rails db:migrate:reset

初級編まとめ

・【resetとmigrate:resetの違い】どちらもDBを一旦dropして再度作成することは同じだが、どのファイルを元に作り直すかが異なるため、migrate:resetではマイグレーションファイルを修正していればその内容も反映して作成されるが、migrate:resetではそうならないようだ。

詳しくは以下(常にmigrate:resetを使っていれば良さそうなものだが、そうもいかない時があるようだ)
参考記事:rake db:reset と rake db:migrate:reset の違い
参考記事:rails db:migrate:resetできなかったのでrails db:resetした

中級編

入力したDBの内容を消さずにカラムを修正したい→g migrationでカラム追加→db:migrate→カラム追加用のmigrationファイル削除

参考記事:Rails カラム名変更方法
参考記事:マイグレーションを使ったカラムの追加、削除、データ型の変更 [ 自分用メモ ]

まとめ

コマンド       概要 オプションその他
migrate DBを作成
drop    DBを削除
reset     drop + migrate  マイグレーションファイルの修正は反映されない 
migrate:reset  drop + migrate  マイグレーションファイルの修正も反映される
migrate:rollback 1つ前に反映したmigrateまで STEP=[戻したいステップ数]で任意のmigrateまでDBを戻す
migrate:status 現在どこまでmigrateが反映されているか確認する

参考記事

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

【個人アプリ開発メモ】ログアウトしようとして生じたエラーの解決

ログアウトしようとしたらエラーが出てしまった。

エラー文

undefined method `id' for nil:NilClass
posts_controller.rb
def index
  @posts = Post.all.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  @post = Post.new

  #エラーが出たのはこの部分↓↓↓
  @calorie_sum = Post.where(user_id: current_user.id, created_at: Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:calorie)

  gon.today_sum = @calorie_sum
  @standard = Standard.find_by(user_id: current_user.id)
  @calorie_standard = @standard.calorie
  gon.standard = @calorie_standard
  if @calorie_sum >= @calorie_standard
    @difference = @calorie_sum - @calorie_standard
  else
    @difference = @calorie_standard - @calorie_sum
  end
end

なぜこのエラーが生じたのか?

posts_controller.rb
@calorie_sum = Post.where(user_id: current_user.id, created_at: Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:calorie)

エラーが出たこのコードはどういう意味かというと、

Postモデル(投稿データが入ってる)から、
今日1日のcurrent_userの投稿を検索して抽出して、calorieカラムの数値を合計した結果をcalorie_sumというインスタンス変数に代入してる。

なんでエラーになるかというと、
ログアウトするとuser_id: current_user.idのidのところがnilになってしまうから。

解決するにはどうすればいいのか?

current_user.idがnilでもエラーにならないようにするためには、
posts_controller.rbを条件分岐で書けばいい気がする。

こんな感じでどうだろうか?

posts_controller.rb
  def index
    @posts = Post.all.includes(:user).order("created_at DESC").page(params[:page]).per(5)
    @post = Post.new

    # サインインしてる時だけ今日の摂取カロリーを表示するグラフを作るためのデータを取得する下記の関数が動く
    if user_signed_in?
      @calorie_sum = Post.where(user_id: current_user.id, created_at: Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:calorie)
      gon.today_sum = @calorie_sum
      @standard = Standard.find_by(user_id: current_user.id)
      @calorie_standard = @standard.calorie
      gon.standard = @calorie_standard
      if @calorie_sum >= @calorie_standard
        @difference = @calorie_sum - @calorie_standard
      else
        @difference = @calorie_standard - @calorie_sum
      end
    else
      # サインインしてない時は何も動かない
    end
  end

これにあとviewのグラフの部分を- if user_signed_in?で条件分岐して、サインインしてない時はグラフが出てこないようにしたら、無事解決しました。

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

【個人アプリ開発メモ】ArgumentError (wrong number of arguments (given 0, expected 2)):の解決

直面している問題

投稿しようとしたらできない。
なぜ?

ターミナルに以下のエラー文が表示された。

エラー文

ターミナル
ArgumentError (wrong number of arguments (given 0, expected 2)):
app/controllers/posts_controller.rb:21:in `create'

argumentとは引数のことらしい。
2つの引数がなければいけないのに、それが0になってるからダメだという。

引数が2つ必要?何のことだろう?

posts_controller.rb
def create
  @post = Post.new(post_params)  ここで止まってしまってる
  if @post.save
  redirect_back(fallback_location: root_path) # なぜredirect_to root_pathじゃダメなのかわかってない
else
  @posts = Post.includes(:user)
  flash.now[:alert] = '必須項目をしてください。' # フラッシュメッセージが出るか確認する
    redirect_back(fallback_location: root_path)
  end
end

private
  def post_params
    params.require(:post).permit(:food, :calorie, :protein, :fat, :carbo, :text, :image).merge(user_id: current_user.id)
  end

試してみたこと

binding.pryでpost_paramsの中身を確認してみよう。

[1] pry(#<PostsController>)> post_params
=> <ActionController::Parameters {"food"=>"うどん", "calorie"=>"10", "protein"=>"10", "fat"=>"10", "carbo"=>"10", "text"=>"うどんを食べました", "image"=>#<ActionDispatch::Http::UploadedFile:0x00007fc7e7b4eae0 @tempfile=#<Tempfile:/var/folders/sy/26p55v5j47s2zjd8h7vr8f6h0000gn/T/RackMultipart20200820-34304-1e1sjww.png>, @original_filename="image4.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"post[image]\"; filename=\"image4.png\"\r\nContent-Type: image/png\r\n">, "user_id"=>17} permitted: true>

ここが原因ではないようだ。

https://teratail.com/questions/164134
によれば、

ターミナル
ArgumentError (wrong number of arguments (given 0, expected 2)):

これは呼び出し側の引数の数 = 0
メソッド側の引数の数 = 2
という意味らしい。

呼び出し側の引数の数と、メソッド側と引数の数はそれぞれどこで設定してるのだろうか?

試したこと

画像を選択せずに、画像以外を投稿してみた。
その結果、これは問題なく投稿できた。
この結果からわかったのは、やはり問題は画像関連だということ。

画像を選択して、画像も投稿しようとするとエラーが生じる。
画像を投稿しようとすると、メソッド側の引数が2になる?

解決した。原因は「え?そこ?」という感じ

image_uploader.rb
#[300, 200][100, 100]という感じで2つあるとダメ
process resize_to_fit: [300, 200][100, 100]

#1つに戻したら解決した
process resize_to_fit: [300, 200]

前に投稿画像サイズの設定で、2通りのサイズを設定したいと思って上記のように設定していたが、これが邪魔だったみたい。
https://qiita.com/naota7118/items/33943b8887a4990645a9

余計なことしなければよかった。

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

【Rails】Enumを使う

どんな時に使う?

カラムstatusの値が
0だったら未処理
1だったら進行中
2だったら処理済み
みたいな時に、いい感じに書くことができます。

定義

migrationファイル
def change
  add_column :orders, :status, :integer, default: 0 # デフォルト値は0に設定する
end
app/models/order.rb
class Order < ApplicationRecord
  enum status: { waiting: 0, working: 1, completed: 2 }
end

できること

インスタンスを作成

order = Order.new(
          item: 'ボールペン',
          price: 150,
          status: 'working' # 今まで「1」と数値でセットしていた部分を文字列でセットできる。DBには「1」が保存される
        )

インスタンスの中身を確認

pry(main)> order.status
=> "working"
pry(main)> order.working?
=> true 
pry(main)> order.completed?
=> false

インスタンスを更新

pry(main)> order.status
=> "working"
pry(main)> order.completed!
=> "completed"
pry(main)> order.status
=> "completed"

DBを検索

pry(main)> completed_orders = Order.completed
=> SELECT `orders`.* FROM `orders` WHERE `orders`.`status` = 2 LIMIT 11

複数カラムで同じ名前を使いたい場合

こういう場合
class Order < ApplicationRecord
  enum hoge_status: { waiting: 0, working: 1, completed: 2 }
  enum fuga_status: { waiting: 0, working: 1, completed: 2 }
end

Order.waiting?としても、hoge_statusfuga_statusのプログラムはどちらを見れば良いかわかりません。
こういった場合、_prefix: trueを使って解決します。

prefixで解決
class Order < ApplicationRecord
  enum hoge_status: { waiting: 0, working: 1, completed: 2 }, _prefix: true
  enum fuga_status: { waiting: 0, working: 1, completed: 2 }, _prefix: true
end
pry(main)> Order.hoge_status_completed?
=> SELECT `orders`.* FROM `orders` WHERE `orders`.`hoge_status` = 2 LIMIT 11
`
pry(main)> Order.fuga_status_completed?
=> SELECT `orders`.* FROM `orders` WHERE `orders`.`fuga_status` = 2 LIMIT 11

参考

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

SNS認証 facebookだけ通らなかった原因

前提

  • 今回、sns認証(google, facebook)を実装していました。
  • そこで、googleは既存のusersテーブルと紐づくのに、facebookだけ認証が通らないエラーが発生しましたので、原因を記録します。(そのため、sns認証の細かい実装方法に関しては割愛します。)
  • 原因は判明しましたが、根本的な解決には至っていませんので、あくまで原因を探す参考程度にご覧ください。

実行環境

rails 6.0.0

mysql 5.6.47

エラーの内容

 今回、snsの認証とともに、userの情報を紐付けて保存する仕様にしていました。
ところがログインしようとすると、、、
image.png
おかしい、、、
Googleでは確かにログインができるのに、Facebookでは新規登録画面へ飛ばされてしまいました。

原因

原因は、user.rbにありました。apiから送られてくるデータの検索方法がまずかったようです。
【user.rb】

  def self.from_omniauth(auth)
    sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
    # sns認証したことがあればアソシエーションで取得
    # 無ければemailでユーザー検索して取得orビルド(保存はしない)
    user = sns.user || User.where(email: auth.info.email).first_or_initialize(
      nickname: auth.info.name,
        email: auth.info.email
    )
    # userが登録済みの場合はそのままログインの処理へ行くので、ここでsnsのuser_idを更新しておく
    if user.persisted?
      sns.user = user
      sns.save
    end
    { user: user, sns: sns }
  end

 この記述の中だと、auth(apiから送られてくるユーザーデータ)の中から、:emailを参照してしまいます。
 しかし、私自信、facebookのアカウントにemailを登録していなかったため、この検索に引っかからず、snsの情報をDBに保存していないよねと判断され、弾かれていたようです。
 その後、Facebookのアカウント設定でemailを登録したところ、無事にログインできるようになりました。

今後の課題

このままだと、emailをもたないfacebookユーザーは原因がわからないまま永遠にログインできない状態になってしまいます。ですので、データ参照の仕様を工夫する必要がありそうです、、、

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

Rails × Herokuアプリ高速化のためにやった/やりたいこと

実装が比較的楽で、インパクトが大きいものをピックアップ。

Cloudflare (CDN)

JSやCSSはCDNに乗せる。他のCDNでも良いが、Cloudflareは無料だから最高。

Cloudinary (画像配信)

画像はCloudinaryで最適化して配信。画像の圧縮、適切なサイズでの配信、Webp対応等全部やってくれる。

クエリ最適化

N+1をなくす。pluckを使う。

partialを使いすぎない

partialは、保守性可読性とパフォーマンスのトレードオフ。
collectionオプションが使える場合には活用する。

ループの中の処理に気を使う

インスタンスを無駄に生成しない、など。

HTMLをgzip圧縮

Herokuだとデフォルトでgzip圧縮してくれないので、gemを使って対応。

ブラウザにキャッシュさせる期間を伸ばす

Cloudflare等の設定をいじって対応

重い処理はPumaの外に流す

例えば画像アップロードはLambda等に捌かせる

金を積む

最後はこれ

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

【個人アプリ開発メモ】画像が粗くなってしまう問題の解決

直面している問題

スクリーンショット 2020-08-20 午後1.51.56.png
こんな感じで投稿画像が粗くなってしまう

ヒントはつかめてる

画像のデフォルトのサイズは
app/uploaders/image_upload.rbのこの部分で設定できる。

image_upload.rb
process resize_to_fit: [300, 200]

これと同じ大きさで画像を表示すれば、キレイに表示できる。
逆にこれと違う大きさで画像を表示しようとすると、粗くなってしまう。

そのことがわかったら、
スクリーンショット 2020-08-20 午後2.06.26.png
こんな感じでキレイに表示できた。

しかしこれで問題が完全にクリアになったわけではない。
スクリーンショット 2020-08-20 午後2.09.35.png
こちらはプレビュー画像なのだが、こちらが少し粗くなってしまった。

複数の画像の大きさの設定したいときはどうすればいいのだろうか?

今気づいたのだが、選択する画像によってキレイになったり粗くなったりする。
これがなぜそうなるのかがまだわからない。

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

【個人アプリ開発メモ】画像のプレビュー表示

やりたいこと

アップロードしたい画像を選択したら、プレビュー表示できるようにしたい。

なぜ画像プレビュー機能が必要なのか?

フォームを作っただけでは、画像を選択してもどの画像が選択されたかアップロード前に確認することができない。

アップロードされてから違う画像をアップロードしてしまったと気づくようなことを防ぐために、この機能は必要。

どう進めるか

まず画像プレビュー用のブランチを立ち上げる。
次に「画像 rails プレビュー」で検索して参考記事を探す。

https://qiita.com/Masanori_N/items/71dbf648737f32dd8588
この記事を参考に進めていこう。

作業内容

jQueryで実装するため、jsファイルを作る。
javasciptディレクトリ直下に「image-preview.js」ファイルを設置した。

_main.html.haml
.new-post
      = form_with model: @post, id: 'new_post' do |f|
        .input-box
    (省略)
          .icon
            .image-upload
              %i.fa.fa-camera.fa-2x
              = f.file_field :image, class: 'image_upload'
            .btn-square
              = f.submit '投稿する', class: 'post-btn'
    .posts
      = render @posts

画像ファイル選択時にイベントを発火させる

まずはファイルを選択した時にイベントが発火するようにする。

image-preview.js
$(function() {
  $(function() {
    $(document).on('change', 'image_upload', function() {
      console.log('hoge');
    })
  });
});

これだとイベントは発火しなかった。

image-preview.js
$(document).on('change', '.image_upload', function() {
  console.log('hoge');
})

クラス名の指定で「.」が抜けているというミスが原因だった。
'image_upload''.image_upload'と修正したら無事イベントは発火した。

画像データの読みこみ

いよいよここから難しくなる。
よくわからない時はconsole.logで中身を確認するに限る。

image-preview.js
$(function() {
  $(function() {
    $(document).on('change', '.image_upload', function() {
      console.log('hoge');
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      console.log(file);
    })
  });
});

console.log(file);で選択したfileを取得できているか確認する。

検証画面で確認すると、

File {name: "image4.png", lastModified: 1596487703899, lastModifiedDate: Tue Aug 04 2020 05:48:23 GMT+0900 (日本標準時), webkitRelativePath: "", size: 110024, …}

と出てきたので、おそらくちゃんと画像fileを取得できている。

次にFileReaderを使ってFileReaderオブジェクトを生成する。
FileReaderとは何なのか?

https://developer.mozilla.org/ja/docs/Web/API/FileReader
によると、

FileReader オブジェクトを使うと、ユーザーのコンピューター内にあるファイル (もしくはバッファ上の生データ) をウェブアプリケーションから非同期的に読み込むことが出来ます。読み込むファイルやデータは File ないし Blob オブジェクトとして指定します。

とある。

なるほど、ローカルにある画像ファイルを非同期で読み込むことができるのか。

image-preview.js
//FileReaderオブジェクトの生成
var reader = new FileReader(); 

これもコンソール画面で確認してみよう。

FileReader {readyState: 0, result: null, error: null, onloadstart: null, onprogress: null, …}

次は

image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file); 

これもよくわからないのでそのままググってみよう。

https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL
ここでもMDNが参考記事として出てきた。

これもFileオブジェクトを読み込むために必要らしい。

image-preview.js
var hoge = reader.readAsDataURL(file); 
console.log(hoge);

こんな感じで変数に代入して中身を見てみた。
すると、undefinedとなった。

image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
var hoge = reader.readAsDataURL(file); 
console.log(hoge[0]);

これだとどうか?
これもダメだ。
これは中身を見ることはできないみたい。

image-preview.js
//読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
reader.onload = function() {
  ここに画像の読みこみ完了後の処理を書いていく
}

これもわからないので調べた。
https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload

FileReader.onload プロパティは、readAsArrayBuffer や readAsBinaryString、 readAsDataURL、readAsText でのコンテンツ読み込みが完了して、利用可能になると発火する load イベント時に実行されるイベントハンドラを含みます

3回読んでも完全にはわからない。
readAsDataURLで画像ファイルの読みこみが完了して、利用可能になるとloadイベントが発火する。
というところまではわかった。

image-preview.js
//loadイベントが発火するかを調べる
reader.onload = function() {
  console.log('hoge');
}

コンソール画面でちゃんと「hoge」と出てきたので、loadイベントは問題なく発火してるとわかった。

その次にloadイベントの結果を取得する。

image-preview.js
//直前に実行したイベントが返した値を取得する
var image = this.result; 

プレビュー用のHTMLを設置します。
ここが結構時間かかるかな?と思っていたが、一発でいけました!!

スクリーンショット 2020-08-20 午前11.49.47.png

ここまでのコードはこんな感じです。

image-preview.js
$(function() {
  $(function() {

    // プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

    $(document).on('change', '.image_upload', function() {
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      //FileReaderオブジェクトの生成
      var reader = new FileReader(); 
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file); 
      //読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
      reader.onload = function() {
        console.log('hoge');
        //直前に実行したイベントが返した値を取得する
        var image = this.result;
        // プレビュー用のhtmlを追加
        var html = buildHTML();
        $('.text').append(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
      }
    })
  });
});

プレビュー画像のサイズ変更

image-preview.js
// プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image" height="100px" width="100px">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

<img src="" alt="preview" class="upload-image" height="100px" width="100px">
imgタグのところにheightとwidthを追加すればOK?

どのクラスに設定するかが大事で、親クラスに設定してもサイズは変わらないので注意です

プレビュー画像の削除

「削除」を押したらプレビュー画像が消えるようにしたい。

参考記事:https://qiita.com/gakinchoy7/items/f52577d0c5f6b2edff89#2-%E3%83%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%81%AE%E5%89%8A%E9%99%A4

まず「削除」を押した時にイベントが発火するようにする。

image-preview.js
$(document).on("click", '.delete-box', function(){
  console.log('hogehoge');
})

これで問題なく発火した。
クラス名の指定'.delete-box'が合っていればこれで発火する。

削除はremoveメソッドを使えばOK。
久しぶりだから( )をつけるのを忘れていた?

image-preview.js
$(document).on("click", '.delete-box', function(){
  $('.preview-box').remove();
})

これだけだと、プレビュー画像は消えたが、まださっきの画像ファイルを選択したままになってる。

image-preview.js
    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })

inputタグに入ってる画像ファイルも削除しておこう。
これでプレビュー画像を削除すると、ファイルの中身が空っぽになる。

2回連続ファイルを選択するとプレビュー画像が2つ表示されてしまう問題の解決

こんな感じで表示されてしまう。
これを選択するたびに入れ替わるように修正したい。
スクリーンショット 2020-08-20 午後0.53.32.png
どうすればこの問題を解決できるか?

考えたのは、

  • すでにプレビュー画像がある場合
  • プレビュー画像がない場合

に条件分岐すればいいのでは?

  • プレビュー画像あり → プレビュー画像を一度削除する必要あり
  • プレビュー画像なし → ここまで書いてきたコードでOK

どうやってプレビュー画像がある場合とない場合の条件分岐をするか?

image-preview.js
if($('.preview-box').length == 0){
  プレビュー画像がない場合の処理
}else{
  プレビュー画像がある場合の処理
}

こんな感じでlengthプロパティを使って条件分岐しました。

image-preview.js
        // プレビュー画像がまだ場合
        if($('.preview-box').length == 0){
          // プレビュー用のhtmlを追加
          var html = buildHTML();      
          $('.icon').before(html);

          //画像を追加
          $(`.upper-box img`).attr('src', `${image}`);

        // すでにプレビュー画像が存在する場合
        }else{

          // プレビュー画像を削除 ⇦ 違うのはここです
        $('.preview-box').remove();

        // プレビュー用のhtmlを追加
        var html = buildHTML();      
        $('.icon').before(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
        }

参考記事:https://www.sejuku.net/blog/34465

最終的に出来上がったコードはこちら

image-preview.js
$(function() {
  $(function() {

    // プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image" height="100px" width="100px">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

    $(document).on('change', '.image_upload', function() {
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      //FileReaderオブジェクトの生成
      var reader = new FileReader(); 
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file); 
      //読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
      reader.onload = function() {
        //直前に実行したイベントが返した値を取得する
        var image = this.result;

        // プレビュー画像がまだ場合
        if($('.preview-box').length == 0){
          // プレビュー用のhtmlを追加
          var html = buildHTML();      
          $('.icon').before(html);

          //画像を追加
          $(`.upper-box img`).attr('src', `${image}`);

        // すでにプレビュー画像が存在する場合
        }else{
          // プレビュー画像を削除
        $('.preview-box').remove();

        // プレビュー用のhtmlを追加
        var html = buildHTML();      
        $('.icon').before(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
        }

      }
    })

    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })

  });
});

結局何をしたのか?

  • プレビュー画像の表示ができた。
  • プレビュー画像のサイズを小さくすることができた。
  • プレビュー画像を削除できるようにした。
  • 連続でファイルを選択したら、直前に選択したファイルのプレビューが消えて、新たにプレビューが表示されるようにした。

おまけ

各投稿の画像を大きくしたい。

before

スクリーンショット 2020-08-20 午後1.41.34.png

after

スクリーンショット 2020-08-20 午後1.51.56.png

_post.html.haml
- if post.image.present?
  = image_tag post.image.url, class: 'post-image', width: '300px', height: '200px'

各投稿は部分テンプレートで上記のHTMLで設定しており、image_tagのところにwidthとheightを追加して設定しました。

ただここで問題が。。。
見てわかるとおり、画像が粗くなってしまいました。

これについては解決したらまた別の記事で書きたいと思います。

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

RailsでGraphQL APIからデータを取得・表示する

はじめに

初めまして、hinokageと申します。
Qiitaへの記事投稿初めて且つ、
プログラミングスクールを卒業したての知識で本記事を書きましたので
お手柔らかにお願いいたします。

経緯

「Annict」という、有志が開発しているアニメ関連の情報が網羅してあるDBのGraphQLAPIが公開されています。
Railsでアプリ制作をしている際、制作していたアプリ内でこれを利用するためRailsからGraphQLAPIを叩く方法を調べたところ、「Railsで制作したアプリをAPIにする方法としてGraphQLを使用する」という内容の記事は結構な数ヒットするものの、「Railsアプリ内から公開されているGraphQLAPIを叩いてデータ取得する」という内容の記事が全くヒットせず、実装に非常に苦しみました。
私と同じように「有志が制作したGraphQLAPIをRailsで利用したい!」という方に向けて少しでも役に立てるよう本記事を投稿いたします。

開発環境

  • Ruby(2.6.5)
  • Ruby on Rails(5.2.3)

導入

まず最初にGraphQLをrailsで使えるように
Ruby製のGraphQL Client 「graphql-client」を導入します。
(公式リファレンス:https://github.com/github/graphql-client

Gemfile
gem 'graphql-client'(追記)

gemfileに追記後、bundle install

bundle install

次に、使用したいAPIのアクセストークンの作成を行います。
ここでは例としてGithubのGraphQL APIを使用します。

GitHub APIのアクセストークンは
GitHubの自分のアイコンをクリック
Settings > Developer settings > Personal access tokens >Generate new token
から作成できます。
(ここでは詳しい作成方法は省略させていただきます。)

httpアダプタ、Schema、クライアント設定のためapplication.rbに以下を追記します。
(今回はgql_testというアプリ名で制作しています。)

config/application.rb
module GqlTest(ご自身のアプリ名)
  ...

  AUTH_HEADER = "Bearer xxx"
 # xxx=作成した際に表示されたアクセストークン
  HTTP = GraphQL::Client::HTTP.new("https://api.github.com/graphql") do 
  #上記には、接続したいAPIのエンドポイントURLを記入
    def headers(context)
      { "Authorization": AUTH_HEADER }
    end
  end
  Schema = GraphQL::Client.load_schema(HTTP)
  # 上記を使って API サーバーから GraphQL Schema 情報を取得
  Client = GraphQL::Client.new(schema: Schema, execute: HTTP)
  # 上記を使ってクライアントを作成
end

これで大まかな設定は完了です。

クエリ作成

次に取得したいデータのクエリを書きます。
ちなみに

クエリとは
データベース管理システムに対する問合せ(処理要求)のこと。

簡単に書くと
「欲しいデータを取得するためのコード」
です。

GraphQLのクエリの書き方は
https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000
上記のサイトが参考になると思います。もし自分でクエリを書きたい場合はご活用ください。

今回はGitHubAPIを使用するのでクエリは以下のようなものを使用しました。
自分のリポジトリの新しいものから5つ分取得するクエリとなっています。

query {
    viewer {
      repositories(last:5) {
        edges {
          node {
            name
          }
        }
      }
    }
  }

また、自分でクエリを書きたい場合はAPIに応じたSchemaを知る必要があります。
ちなみに

Schemaとは
GraphQL APIの仕様を表現するものです。スキーマ定義言語(SDL(Schema Definition Language))を使って表現します。

先程のクエリを例にすると「viewer」「repositories」「node」「name」など全てSchemaです。
API作成時、作成者がデータを取り出す方法をSchemaとして文字に定義しており、
それをAPI使用者がSchemaを用いてデータを取得するために書くコードがクエリだと考えてもらえば
わかりやすいかもしれません。

「クエリがどういうものなのか」を分かりやすく伝えるための自分の中で辿り着いた例えが
辞典でいう目次みたいなものがSchemaです。

例えば、辞典でとある動物を調べたい、
しかしその動物の〇〇類〇〇科〇〇目などは分かるが名前はわからないみたいな時に
目次のページを見ると思います。この目次のページがSchemaです。
目次を見て〇〇類のページを開きます。
またその辞典には〇〇類の最初のページに「〇〇科一覧」みたいな目次があるかもしれません。
これもSchemaです。

つまりはSchemaは
目的のデータに辿り着きたい時の検索手段
みたいなものだと考えれば分かりやすいかもしれません。
その検索手段をAPI作成者側が文字に定義しているのが
schemaだと私は解釈して使用しています。
(間違っていたら申し訳ないので、ぜひ一度GraphQL Schemaとググって調べてください)

さて、Schemaの種類や使い方については
各APIの公式リファレンスを読めば書いてあると思いますが
私は「GraphiQL」というアプリケーションを使用して調べることが多いです。
スクリーンショット 2020-08-17 18.27.53.png

GraphiQLはSchemaの確認や検索ができるほか、
APIへの接続やクエリ、返ってくるデータの確認ができるため非常に便利なツールとなっています。

このアプリはブラウザ上で使用することもできますが
オススメはスタンドアローンで使用できるダウンロード版です。
https://electronjs.org/apps/graphiql

少し話が逸れてしまいました本題に戻ります。

実装

先ほどのクエリをcontorollerに追加します。
その際、下記のように記述します。

controllers/blogs_controller.rb
class BlogsController < ApplicationController
  Query = GqlTest::Client.parse <<-GRAPHQL
  query {
    viewer {
      repositories(last:5) {
        edges {
          node {
            name
          }
        }
      }
    }
  }
    GRAPHQL

  def index
    @works = result
  end

  private
  def result
    response = GqlTest::Client.query(Query)
  end
end

privateメソッドの下層に
resultというメソッドを用意して
クエリの結果データをrailsで使用できるように加工しています。
そして、使用したいアクション内でresultを用いることで、
結果データを使用することができます。

@worksというインスタンス変数にAPIからの結果データを格納したので
これをviewで使用してみます。最新5件のリポジトリ名が表示されればOKです。

index.html.erb
<h2>私が最近作成したリポジトリは以下の5つです</h2>
<%= @works%>

スクリーンショット 2020-08-19 22.43.01.png

あれ?オブジェクトしか表示されない…?

実はこの時点では取り出したいデータを指定していなかったので
オブジェクトが表示されるだけになっています。
これは以下のようにすると表示することができます。

controllers/blogs_controller.rb
  def index
    @works = result.data.viewer.repositories.edges
  end

private
  def result
    response = GqlTest::Client.query(Query)
  end

これだけだと、変数内に配列のようにリポジトリ名が複数入っている状態なので、
下記のように表示されてしまいます。
スクリーンショット 2020-08-19 22.42.21.png

そのため、viewファイルの方でeachメソッドを使い1つずつ表示するようにします。

index.html.erb
<% @works.each do |work|%>
  <p><%= work.node.name%></p>
<% end %>

こうすることで表示することができました。
スクリーンショット 2020-08-19 22.51.53.png

…しかしこれだと、追記したコードの意味が分からないですよね。

コードの解説

実は取得したデータだけでは目的の
「自分の最新リポジトリ5件のタイトル」というデータ以外のものも含まれるので、
クエリで書いたように何の情報が欲しいのかを記述する必要があります。

つまり、「自分の最新リポジトリ5件のタイトルを取得する」というクエリを書いても、
結果として返ってくるデータは「自分の最新リポジトリ5件のタイトル」だけでは無いということです。

GraphiQLを使って、先程と同じクエリを送った時にどのようなデータが返ってくるのか見ると非常にわかりやすいです。

スクリーンショット 2020-08-19 20.01.33.png

画面左側のコードが先程と同じ「最新リポジトリ5件のタイトルを取得する」クエリです
画面右側はそのクエリをAPIに送った時に返ってきた結果データです。
スクリーンショット 2020-08-19 23.59.14.png

結果データを見るとクエリで書いたSchemaに値が入って返ってきてるのが分かると思います。
また、コードの書かれ方がハッシュと同じような形でkeyとvalueが含まれているのが分かると思います。

この結果データがハッシュであった場合、リポジトリのタイトルであるnameの値を取り出そうとすると
resultという変数に結果データが格納されていた時

result[:data][:viewer][:repositories][:edges].each do |work|
 work[:node][:name]
end

このようなコードを書くと思います。
結果データはハッシュでは無いため、このような取り出し方はできないのですが
(ちなみに結果データはJSON形式で返ってきているようです。)
公式リファレンスを読むとハッシュでkeyになっていたものをメソッドのように扱うことで
上記で書いたハッシュの展開と似たような方法で取り出すことができるようです。
つまり、コードは

controllers/blogs_controller.rb
 def index
   @works = result.data.viewer.repositories.edges
 end

private
 def result
   response = GqlTest::Client.query(Query)
 end
index.html.erb
<% @works.each do |work|%>
  <p><%= work.node.name%></p>
<% end %>

と書くことで目的のデータが表示できるようになります。

(夜中に殴り書きしたので色々誤字脱字あるかもしれません…)

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

【Rails 6.0】Action Cable を解説しながらプログレスバーを実装する

TL;DR

  • Action Cable を使用してプッシュ通信のプログレスバーを実装した
  • Action Cable について解説する
  • ProgressChannel.broadcast_to(current_user, percent: (index+1) * 100 / pictures.length)

背景

旅行の思い出投稿サイトを作ったときに写真アップロードが遅くて、UX 的にアップロードの進捗状況を表すプログレスバーがあったほうが良いのではないかとなり、Action Cable の勉強も兼ねて実装しました

モノ

ezgif-2-3851e5ddc0e8.gif

Action Cable 解説

Rails に Action Cable という WebSocket と Rails のその他の部分をシームレスに統合するための機能が存在します
Rails 公式なので Rails ガイドを見るのが良いと思ったのですが、Rails ガイドの解説がとても分かりづらかったので自分でまとめ直してみました
参考になれば幸いです

図が一番わかり易い

Rails ガイドがわかりづらかった理由の一つが図がなかったことだと思ったので図を作りました
どこか間違ってたら教えてください
スクリーンショット 2020-08-20 0.18.22.png

用語解説

スクリーンショット 2020-08-20 0.21.08.png

Rails ガイドと順番が違うかもしれませんが、一番わかり易いと思う順番にしました

究極的にはパブリッシャからサブスクライバへ通信が行われる pub/sub リンクです
Rails アプリケーションの controller でブロードキャストを行い、チャネルを通して各タブ(サブスクライバ)に配信します

プログレスバーの実装

プログレスバーを主題に実際の実装を見ていきます

実行環境

  • ruby:2.6.5 (Docker image)
  • Rails 6.0.2

まずは rails g channel <チャネル名> でひな形を作成します

$ rails g channel progress

サーバー側

チャネル

サーバー側のプログレスチャネルです

app/channels/progress_channel.rb
class ProgressChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

subscribedunsubscribedのひな形は用意されていて、それぞれサブスクリプションができた/なくなったときの挙動を書きます
一般的には、stream_from "progress_#{params[:progress_id]}" のような形で受け取るブロードキャストを指定します
しかし、ここでは stream_for を使用し、ユーザーごとに変わるブロードキャストを指定しています
これで、progress:Z2lkOi8vbWVtb3BpYy9Vc2VyLzE (ブロードキャスト名) のようなブロードキャストを受信することができます

コネクション

サーバー側ではこのコネクションクラスをもとに、コネクションインスタンスを作成します

app/channels/application_cable/progress_channel.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        User.find(session['warden.user.user.key'][0][0])
      rescue
        reject_unauthorized_connection
      end

      def session
        @session ||= cookies.encrypted[Rails.application.config.session_options[:key]]
      end
  end
end

identified_by に指定したものを参考に、サーバーはコネクションを識別できるようになります
このコードでは、current_user に入る User オブジェクトをもとに識別しています
アプリケーションで Devise を使用しているので、ここの実装は
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91
を参考にしました

パブリッシャ

今回は model のところにブロードキャストするコードを書きます

app/models/album_form.rb
pictures.each_with_index do |picture, index|
   album.pictures.new(picture_name: picture)
   ProgressChannel.broadcast_to(
      user,
      percent: (index+1) * 100 / pictures.length
   )
end

ここでの usercurrent_user に当たります(実装上、変数名が違います)
ここでクライアントにわたす data (percent: (index+1) * 100 / pictures.length) を指定してプログレスバーの進めるわけです
単純に枚数で進捗の%を計算しています
このブロードキャストがチャネルの stream_for current_user につながり、current_user がいるクライアントにデータが送信されます

以上で、サーバー側の実装は終わりです

クライアント側

好きな場所にプログレスバーを置きます

app/views/albums/index.html.erb
<div class="progress-percentage text-xs-center">0%</div>
<progress class="progress progress-striped" max="100" value="0"></progress>

クライアント側でもコネクションを作成しないといけません

app/javascript/channles/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
app/javascript/channels/progress_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("ProgressChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    $('.progress-percentage').text(`${data.percent}%`)
    $('progress').prop('value', data.percent)
  }
});

consumer.jsでコンシューマを作成し、progress_channel.js でコネクション(サブスクリプション)を作成しています
今回は ProgressChannel につながるサブスクリプションです
connected()にコネクションができたときの動作、disconnected()にコネクションがなくなったときの動作を記述します
received()にはブロードキャストされたデータを受け取ったときの挙動を書きます
プログレスバーの進捗状況がサブスクライバにブロードキャストされて受け取るので、ここでプログレスバーの各要素を jQuery で書き換えます

完成!

まとめ

以上のコードを実装することでユーザーごとにプッシュ通信にするプログレスバーが実装できます
Qiita 初投稿で ActionCable も学びたてなので間違っているところもあるかもしれません、その部分はご指摘・ご教授いただけるとありがたいです
また、記事のこの部分がわかりづらいというコメントもお待ちしています!

参考記事

https://railsguides.jp/action_cable_overview.html
https://qiita.com/QUANON/items/0ba35cd0eb5d720f4237
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91

P.S.

memopic』 という旅行の思い出・アルバム・写真共有サービスを作っています。是非使ってみてください!
ご意見・ご要望などあれば @memopic_bambooまで DM お願いします!

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

【Rails 6.0】Action Cable を解説しながらプログレスバーを実装する

TL;DR

  • Action Cable を使用してプッシュ通信のプログレスバーを実装した
  • Action Cable について解説する
  • ProgressChannel.broadcast_to(current_user, percent: (index+1) * 100 / pictures.length)

背景

旅行の思い出投稿サイトを作ったときに写真アップロードが遅くて、UX 的にアップロードの進捗状況を表すプログレスバーがあったほうが良いのではないかとなり、Action Cable の勉強も兼ねて実装しました

モノ

ezgif-2-3851e5ddc0e8.gif

Action Cable 解説

Rails に Action Cable という WebSocket と Rails のその他の部分をシームレスに統合するための機能が存在します
Rails 公式なので Rails ガイドを見るのが良いと思ったのですが、Rails ガイドの解説がとても分かりづらかったので自分でまとめ直してみました
参考になれば幸いです

図が一番わかり易い

Rails ガイドがわかりづらかった理由の一つが図がなかったことだと思ったので図を作りました
どこか間違ってたら教えてください
スクリーンショット 2020-08-20 0.18.22.png

用語解説

スクリーンショット 2020-08-20 0.21.08.png

Rails ガイドと順番が違うかもしれませんが、一番わかり易いと思う順番にしました

究極的にはパブリッシャからサブスクライバへ通信が行われる pub/sub リンクです
Rails アプリケーションの controller でブロードキャストを行い、チャネルを通して各タブ(サブスクライバ)に配信します

プログレスバーの実装

プログレスバーを主題に実際の実装を見ていきます

実行環境

  • ruby:2.6.5 (Docker image)
  • Rails 6.0.2

まずは rails g channel <チャネル名> でひな形を作成します

$ rails g channel progress

サーバー側

チャネル

サーバー側のプログレスチャネルです

app/channels/progress_channel.rb
class ProgressChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

subscribedunsubscribedのひな形は用意されていて、それぞれサブスクリプションができた/なくなったときの挙動を書きます
一般的には、stream_from "progress_#{params[:progress_id]}" のような形で受け取るブロードキャストを指定します
しかし、ここでは stream_for を使用し、ユーザーごとに変わるブロードキャストを指定しています
これで、progress:Z2lkOi8vbWVtb3BpYy9Vc2VyLzE (ブロードキャスト名) のようなブロードキャストを受信することができます

コネクション

サーバー側ではこのコネクションクラスをもとに、コネクションインスタンスを作成します

app/channels/application_cable/progress_channel.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        User.find(session['warden.user.user.key'][0][0])
      rescue
        reject_unauthorized_connection
      end

      def session
        @session ||= cookies.encrypted[Rails.application.config.session_options[:key]]
      end
  end
end

identified_by に指定したものを参考に、サーバーはコネクションを識別できるようになります
このコードでは、current_user に入る User オブジェクトをもとに識別しています
アプリケーションで Devise を使用しているので、ここの実装は
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91
を参考にしました

パブリッシャ

今回は model のところにブロードキャストするコードを書きます

app/models/album_form.rb
pictures.each_with_index do |picture, index|
   album.pictures.new(picture_name: picture)
   ProgressChannel.broadcast_to(
      user,
      percent: (index+1) * 100 / pictures.length
   )
end

ここでの usercurrent_user に当たります(実装上、変数名が違います)
ここでクライアントにわたす data (percent: (index+1) * 100 / pictures.length) を指定してプログレスバーの進めるわけです
単純に枚数で進捗の%を計算しています
このブロードキャストがチャネルの stream_for current_user につながり、current_user がいるクライアントにデータが送信されます

以上で、サーバー側の実装は終わりです

クライアント側

好きな場所にプログレスバーを置きます

app/views/albums/index.html.erb
<div class="progress-percentage text-xs-center">0%</div>
<progress class="progress progress-striped" max="100" value="0"></progress>

クライアント側でもコネクションを作成しないといけません

app/javascript/channles/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
app/javascript/channels/progress_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("ProgressChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    $('.progress-percentage').text(`${data.percent}%`)
    $('progress').prop('value', data.percent)
  }
});

consumer.jsでコンシューマを作成し、progress_channel.js でコネクション(サブスクリプション)を作成しています
今回は ProgressChannel につながるサブスクリプションです
connected()にコネクションができたときの動作、disconnected()にコネクションがなくなったときの動作を記述します
received()にはブロードキャストされたデータを受け取ったときの挙動を書きます
プログレスバーの進捗状況がサブスクライバにブロードキャストされて受け取るので、ここでプログレスバーの各要素を jQuery で書き換えます

完成!

まとめ

以上のコードを実装することでユーザーごとにプッシュ通信にするプログレスバーが実装できます
学びたてなので間違っているところもあるかもしれません、その部分はご指摘・ご教授いただけるとありがたいです
また、記事のこの部分がわかりづらいというコメントもお待ちしています!

参考記事

https://railsguides.jp/action_cable_overview.html
https://qiita.com/QUANON/items/0ba35cd0eb5d720f4237
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91

P.S.

『memopic』 という旅行の思い出・アルバム・写真共有サービスを作っています。是非使ってみてください!
ご意見・ご要望などあれば @memopic_bambooまで DM お願いします!

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

C++とRubyで処理速度比較してみた 第一回

はじめに

C++の処理速度が速い、Ruby生産性が高いと巷でよく聞くので、
実際に処理速度を比較してみたいと思いました。
それぞれで1000万までの素数生成プログラムを作り比べてみようと思います。

どちらもまだまだチューニングや測定方法などの改善の余地があると思うので、
こうやってみたら速くなるのでは:point_up_tone2::point_up_tone2::point_up_tone2:
と気づいた方はコメントいただけるとすごく嬉しいです:stuck_out_tongue_closed_eyes:

そんな感じで改善していければ面白いかもと思い「第一回」としてます。

アルゴリズムは「エラトステネスのふるい」を使用してます。
自然数を何回かふるいにかけていって素数でない数を消していくと言うやり方です。
詳細で正確な解説は下記参照下さい。
https://ja.wikipedia.org/wiki/エラトステネスの篩

環境は下記になります。
macOS Catalina 10.15.6 [メモリ:8GB/CPU:Intel Core i5]
zsh
Apple clang version 12.0.0
ruby 2.6.3p62

Ruby編:コードと測定結果

prime_number.rb
def create_prime_number(max_number)
    terms = Array.new
    puts(Time.now.strftime("%H:%M:%S.%N hurui start"))
    max_number.times {|x| terms.push x + 1} # natural number
    terms.shift # reject 1
    last_number = Math.sqrt(max_number).floor # end of hurui

    puts(Time.now.strftime("%H:%M:%S.%N hurui start"))
    i = 0
    current_number = terms[i]
    while current_number <= last_number do
        ary = terms.reject {|x| x % current_number == 0 && x != current_number}
        terms = ary
        i += 1
        current_number = terms[i]
    end
    puts(Time.now.strftime("%H:%M:%S.%N hurui end"))

# result
#    p terms
end

create_prime_number(10000000)

実行結果は下記になります。

% ruby prime_number.rb
23:57:23.813912000 hurui start
23:57:24.487299000 hurui start
23:57:49.403797000 hurui end

Ruby編:気づいたこと

・破壊的メソッドでふるいにかけようとするとすごく遅くなりました。

prime_number.rb
terms.reject! {|x| x % current_number == 0 && x != current_number}

C++編:コードと測定結果

eratosthenes.cpp
#include<iostream>
#include <math.h>
#include <list>
#include <sys/time.h>

#define MAX_NUMBER 10000000

using namespace std;

int main()
{
    unsigned long last_num = (unsigned long)sqrt(MAX_NUMBER);
    unsigned long cur_num;
    struct timeval tv;
    struct tm *time_fmt;

    list<unsigned long> *num_ls = new list<unsigned long>;
    list<unsigned long>::iterator num_ls_itr;
    unsigned long itr_skip;
    unsigned long i;

    /* ======== measurement ======== */
    gettimeofday(&tv, NULL);
    time_fmt = localtime(&tv.tv_sec);
    cout << time_fmt->tm_hour << ":" << time_fmt->tm_min << ":" << time_fmt->tm_sec << "." << tv.tv_usec << endl;
    /* ======== measurement ======== */

    /* create natural number */
    for (unsigned long ntrl_num = 2; ntrl_num <= MAX_NUMBER; ntrl_num++) {
        num_ls->push_back(ntrl_num);
    }

    /* ======== measurement ======== */
    gettimeofday(&tv, NULL);
    time_fmt = localtime(&tv.tv_sec);
    cout << time_fmt->tm_hour << ":" << time_fmt->tm_min << ":" << time_fmt->tm_sec << "." << tv.tv_usec << endl;
    /* ======== measurement ======== */

    /* hurui */
    itr_skip = 0;
    num_ls_itr = num_ls->begin();
    cur_num = *num_ls_itr;
    while (cur_num <= last_num) {
        for (; num_ls_itr != num_ls->end();) {
            if (
                (*num_ls_itr % cur_num == 0) &&
                (*num_ls_itr != cur_num)
            ) {
                num_ls_itr = num_ls->erase(num_ls_itr);
            } else {
                num_ls_itr++;
            }
        }
        num_ls_itr = num_ls->begin();
        itr_skip++;
        for (i = 0; i < itr_skip; i++) {
            num_ls_itr++;
        }
        cur_num = *num_ls_itr;
    }

    /* ======== measurement ======== */
    gettimeofday(&tv, NULL);
    time_fmt = localtime(&tv.tv_sec);
    cout << time_fmt->tm_hour << ":" << time_fmt->tm_min << ":" << time_fmt->tm_sec << "." << tv.tv_usec << endl;
    /* ======== measurement ======== */

    /* result */
#if (0)
    cout << "[";
    for (num_ls_itr = num_ls->begin(); num_ls_itr != num_ls->end(); num_ls_itr++) {
        cout << *num_ls_itr << ",";
    }
    cout << "]" << endl;
#endif

    delete num_ls;

    return 0;
}

コンパイルと実行結果は下記になります。

% clang++ eratosthenes.cpp -o eratosthenes -O3
% ./eratosthenes
23:56:12.820103
23:56:13.458872
23:56:37.406918

C++編:気づいたこと

・最適化オプション(-O3)を付けずにコンパイルすると10秒ちょっと遅くなりました。

% clang++ eratosthenes.cpp -o eratosthenes

・listをヒープ領域からスタック領域に変更してもそんなに変わらなかった。

eratosthenes.cpp
list<unsigned long> num_ls;

終わりに

今回はC++の方がRubyよりもおよそ1秒ほど早かった形になります。
C++の方はiteratorループ内のeraseのやり方を誤ったせいで「segmentation fault」でしばらくハマりましたし、時間計測に使ったgettimeofday関数を後で調べてみると非推奨になっていたりしました(chronoを使うと良いらしい)。
やはり生産性はRubyの方が高かった印象です。

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

rails csv ファイル読み込み トライ

【Ruby on Rails】CSVインポート
Rails上で、CSVファイルを読み込んでみた
【Rails】CSVファイルからデータをインポート

これを参考にトライ

結果その通りにやったらできました。
(投稿者様、ありがとうございます)

だが、rubyの文法でつまづく。
だれかわかる方いらっしゃたら助けていただけるとありがたいです。

app/controllers/users_controller.rb
def index
    @users = User.all
  end

  def import
    # fileはtmpに自動で一時保存される
    User.import(params[:file])
    redirect_to users_url
  end

このimportコントローラーUser.importのimportメソッドは、user Modelを操作するために、下記で、クラスメソッドとして定義されている、importメソッドであることはわかった。

app/model/user.rb
class User < ApplicationRecord
  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|
      # IDが見つかれば、レコードを呼び出し、見つかれなければ、新しく作成
      user = find_by(id: row["id"]) || new
      # CSVからデータを取得し、設定する
      user.attributes = row.to_hash.slice(*updatable_attributes)
      # 保存する
      user.save
    end
  end

  # 更新を許可するカラムを定義
  def self.updatable_attributes
    ["id", "name", "age"]
  end
end

まず、

user = find_by(id: row["id"]) || new

でつまづいた。
調べると、
https://www.sejuku.net/blog/19044
nil判定を行っていることは理解
find_by(id: row["id"])がnilなら newをuserに代入している。

でも "new"とはなんだろう、インスタンスか?
公式document
https://docs.ruby-lang.org/ja/latest/class/CSV=3a=3aRow.html
をみると、
CSV::Rowクラスの、newメソッドなのではないかと予想
であれば、
row.newを省略して書いている??

もう一個

row.to_hash.slice(*updatable_attributes)

がわからない。

rowは、csvの特定の行
それをto_hashでハッシュにしている
sliceで ["id", "name", "age"]のみ取扱たいが、
to_hashメソッドではを使うと、["id", "name", "age"]がハッシュのキー(あるいは値)として使われるのだろうか?
https://docs.ruby-lang.org/ja/latest/class/CSV=3a=3aRow.htm

謎は深まる。

あと、*updatable_attributes
引数にアスタリスク付きのメソッドを指定している記法がわからない。

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

Rubyで段落を作る

Rubyで段落を作るには、文字の後に\nをつける必要があります。

バックスラッシュ\の出し方はオプションキーと¥を押せば簡単に出すことが出来ます:ok_hand:

おはよう\nこんにちは\nこんばんは\n

と記載すると...

下のように簡単に縦に文字を並べることができました。

おはよう
こんにちは
こんばんは

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