20190318のRailsに関する記事は14件です。

PG::ConnectionBad の対処法

PostgreSQLが使えない時

railsでrail s や rails db:migrate
などのコマンドを使用した際にいかのようなエラーが出た時の対処法です。

could not connect to database postgres: could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5432"?

この様な場合は、SQLサーバーが起動していない事が多いそうです。

サーバーのインストールなどの経験は無く、
はじめてのことであったのでかなり苦戦しました。

インストールするだけでは使えない

SQLサーバーはインストールするだけではだめで、起動することで使用可能になります。

自動でSQLサーバー立ち上がる設定や環境に慣れてしまっているせいで、
自分で起動する感覚が僕にはありませんでした。

以下のコマンドを実行することで無事にエラー解決が出来ました!

 $ sudo service postgresql start
    Starting postgresql service:                              [  OK  ]

参考

以下のサイトが参考になりました。

https://www.postgresql.jp/
https://lets.postgresql.jp/

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

fileds_for の使い方

fields_forとは

  • 複数のモデルを扱う form を作りたい時に使用するもの

基本的な使い方

例えばpageモデルと pageモデルとhas_onecategoryモデルを同じformで扱いたいとするとこんな感じになる

<%= form_with model: @page do |f| %>
  <%= f.fields_for :category do |ff| %>
    <%= ff.text_field :name %>
  <% end %>
<% end %>

http://railsdoc.com/references/fields_for

Controller

class PagesController < ApplicationController

  def new
    @page = Page.new
  end
end

Model

class Page < ApplicationRecord
  has_one :category
end

has_manyな場合

こちらもcategorycategoriesに変わっただけで、こんな感じでいける

<%= form_with model: @page do |f| %>
  <%= f.fields_for :categories do |ff| %>
    <%= ff.text_field :name %>
  <% end %>
<% end %>

http://railsdoc.com/references/fields_for

Controller

class PagesController < ApplicationController

  def new
    @page = Page.new
  end
end

Model

class Page < ApplicationRecord
  has_many :categories
end

子の方に最初から複数のformを出したい時

この場合はちょっとcontrollerの方で先に子の方をbuildしてあげれば良い
これでfields_forのブロックの中が3回繰り返される

<%= form_with model: @page do |f| %>
  <%= f.fields_for :categories do |ff| %>
    <%= ff.text_field :name %>
  <% end %>
<% end %>

http://railsdoc.com/references/fields_for

Controller

class PagesController < ApplicationController

  before_action :build_page, only: %i(new)
  before_action :build_categories, only: %i(new)

  def new
  end

  private

  def build_page
    @page = Page.new
  end

  def build_categories
    for i in 1..3 do
      @page.categories.build
    end
  end
end

Model

class Page < ApplicationRecord
  has_many :categories
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング学習記録30〜よりSNSっぽく〜

今日やったこと

  • Progate rails1~3スライド復習
  • Progate rails4,5リセット復習&スライド復習
  • Progate rails6,7

レッスン6は今までのレッスンの総復習という感じでした。

なので、レッスン6は復習なしでいきたいと思います。

一連の動作を繰り返しやってみて、ユーザの表示・登録・編集機能は投稿機能とほぼ同じ作り方で作れる、ということを実感できました。

レッスン7でようやくユーザー情報に画像を登録できる機能を追加し、よりSNSっぽくなってきました。

レッスンの指示通りに動かしているだけとは言え、ちょっとずつ形のあるものが出来上がっていくのは楽しいですね。

このProgateのrailsをやり切るだけでも、簡単なSNSアプリは作れるようになるので、これを月額980円で学べるのは相当お得だと思います。

ということで、明日からも引き続きプログラミング学習頑張ります。

おわり

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

もそ、プログラミングを学ぶ【Ruby on Rails】〜奥義!コントローラの継承〜

土日はしっかりバッチリ休息を取り、学習を再開させた もそ。
心身ともにリフレッシュして、これで勉強もスピードアップ!...なんてウマい話にはならず、頭を抱えながらカリキュラムと睨めっこをしています。

コントローラの継承とは?

さて今日は、この無駄に語感のかっこいい"コントローラの継承"について書いていきます。
なんかゲーム後半の、師匠から主人公への必殺技伝授みたいですよね(?)

コントローラについては以前まとめたMVCについてこちらをご参照ください。

コントローラはルーティングの指示に従って、アクション(処理)した結果をビューに渡す役割があります。
そして継承とは、継承元である他のオブジェクトのメソッドや特徴を引き継ぐことです。

図でもう少し視覚的に説明していきますね。
継承元となるコントローラから、メソッドや特徴を引き継ぐとします。

無題1148.jpeg

そうすると、引き継いだ方は元のコントローラで定義された特徴やメソッドを使うことができます。
要はパワーアップする感じです。

無題1148 2.jpeg

ざっくりとしたイメージですが、これがコントローラの継承です。
コントローラの継承をすることで、たとえばあるAのメソッドと特徴をもった特定のコントローラだけにbefore_actionの処理をすることも可能です。
逆に言えば、すべてのコントローラに共通した処理を行いたいときは継承元のコントローラに書き込むことで機能をもたせます。

つまり上の図で言うところの師匠コントローラにある処理を実行すれば、弟子コントローラたちすべてがそのメソッドなどを継承します。

--
いかがでしょうか?
相変わらずプログラミングには苦戦しているのですが、Qiitaに載せる用のイラストを短時間で描いて文を書いているうちに、若干イラストと文章のクオリティが上がっている気がします...
こういうのを本末転倒というのでしょうか...

イラストの質も上げつつ、プログラミングもしっかり理解をしていきたいです。
もそにもプログラミングを瞬時に理解できる機能を継承できたらいいのになあ。続く。

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

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.が解決できない。

bundle install をするとエラーがでる。

ターミナルの内容はこちら

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

    current directory: /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/msgpack-1.2.9/ext/msgpack
/Users/tech-camp/.rbenv/versions/2.5.1/bin/ruby -r ./siteconf20190318-3361-1d5xqm2.rb extconf.rb
checking for ruby/st.h... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/Users/tech-camp/.rbenv/versions/2.5.1/bin/$(RUBY_BASE_NAME)
/Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:456:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:590:in `try_cpp'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:1097:in `block in have_header'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:947:in `block in checking_for'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:350:in `block (2 levels) in postpone'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:320:in `open'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:350:in `block in postpone'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:320:in `open'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:346:in `postpone'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:946:in `checking_for'
    from /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:1096:in `have_header'
    from extconf.rb:3:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-15/2.5.0-static/msgpack-1.2.9/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/msgpack-1.2.9 for inspection.
Results logged to /Users/tech-camp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-15/2.5.0-static/msgpack-1.2.9/gem_make.out

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

In Gemfile:
  bootsnap was resolved to 1.4.1, which depends on
    msgpack

解決方法

エラーの起こっているディテクトリで以下のコマンドを実行

xcode-select --install

これを実行するとインストール画面がでるので、インストールし再度,bundle installすると無事インストール完了

疑問点

  • xcodeって?
  • msgpackではなく、bootsnapが原因だった?
  • そもそも上記2つってなに?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsのキホンvol.8(ログイン機能)

ログイン機能

ログインページを作る

まず、ルーティングとコントローラを設定する。
usersコントローラのlogin_formアクションを定義する。

routes.rb
get "login" => "users/login_form"
users_controller.rb
def login_form
end

ビューでログインフォームを表示する。

login_form.html.erb
<p>メールアドレス</p>
<input name="email" value="<%= @email %>">
<p>パスワード</p>
<input type="password" name="password" value="<%= @password %>">
<input type="submit" value="ログイン">

フィームの送信

次に、フォームのあたいを送信できるようにloginアクションを作る。

routes.rb
post "login" => "users/login"
users_controller.rb
def login
end

ログインフォームの方もform_tagを追加しておく。

login_form.html.erb
#/loginへフォームの値を送信
<%= form_tag("/login") do %>
<p>メールアドレス</p>
<input name="email" value="<%= @email %>">
<p>パスワード</p>
<input type="password" name="password" value="<%= @password %>">
<input type="submit" value="ログイン">
<% end %>

また/loginで被っているが、post,getで異なったルーティングと認識されるので問題ない。

ユーザーを特定

loginアクションで、ユーザーを特定する処理をする。
情報が一致した場合はフラッシュメッセージとともに投稿一覧へ。
しなかった場合はエラーメッセージとともにログインフォームへ、初期値として入力済みのもの。

users_controller.rb
def login
    #ログインフォームで入力されたものが、データベースの情報とあっているか確認する
    @user = User.find_by(email: params[:email], password: params[:password])
    if @user
      flash[:notice] = "ログインしました"
      redirect_to("/posts/index")
    else
      @error_message = "メールアドレスまたはパスワードが間違っています"
      @email = params[:email]
      @password = params[:password]
      render("users/login_form")
    end
end

session変数

ページを移動してもユーザー情報を保持するためにsession関数を使う。
session[:キー] = 値
と記述をする。
今回は、ログインに成功した時にユーザー情報を保持しts保持したいので、loginアクションを追加修正する。

users_controller.rb
def login
    #ログインフォームで入力されたものが、データベースの情報とあっているか確認する
    @user = User.find_by(email: params[:email], password: params[:password])
    if @user
      session[:user_id] = @user.id
      ...
end

また新規登録時にそのままログイン状態にして、セッション変数で情報を保持する。

user_controller.rb
def create
    @user = User.new(
      name: params[:name],
      email: params[:email],
      image_name: "default_user.jpg",
      password: params[:password]
    )
    if @user.save
      #新規登録に成功すれば、ログイン状態にする
      session[:user_id] = @user.id
      flash[:notice] = "ユーザー登録が完了しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/new")
    end
  end

ログアウト機能

ログアウトする場合はセッションの値をからにする。
セッションの値を変更するときはpostとする。

routes.rb
post "logout" => "users#logout"
users_controller.rb
def logout
  session[:user_id] = nil
  flash[:notice] = "ログアウトしました。"
  redirect_to("/login")
end

ログイン中のユーザーを取得する

before_action

各コントローラの全アクションで共通の処理がある場合に使う。

ユーザー情報は全アクションで使うためapplication_controller.rb で処理を書いておく。
ここでは、set_crrent_userメソッドを定義する。

controllers/application_controller.rb
before_action :set_current_user
def set_current_user
  @current_user = User.find_by(id: session[:user_id])
end

アクセス制限

ログインしていない場合

ログインが必要なページを表示しようとした場合は、フラッシュメッセージとともにログインページに移動する、authenticate_userメソッドを作る。

controllers/application_controller.rb
def authenticate_user
    #ユーザー情報がない場合
    if @current_user == nil
      flash[:notice] = "ログインが必要です"
      redirect_to("/login")
    end
end

onlyを用いて、指定したアクションのみでメソッドを実行する。

users_controller.rb
before_action :authenticate_user, {only: [:index, :show, :edit, :update]}
posts_controller.rb
before_action :authenticate_user

ログインしている場合

ログインしている場合も同様に、アクセス制限のメソッドを作っておく。

controllers/application_controller.rb
def forbid_login_user
    if @current_user
      flash[:notice] = "すでにログインしています"
      redirect_to("/posts/index")
    end
end

対応するアクションに対してメソッドを適応させる。

home_controller.rb
before_action :forbid_login_user,{only: [:top]}
users_controller.rb
before_action :forbid_login_user, {only: [:new, :create, :login_form, :login]}

ユーザー編集の制限

自分以外のユーザーの編集をできないように制限をかける。

views/users/show.html.erb
#詳細ページのユーザーidとログインユーザーidが一致している場合は編集ok
<% if @user.id == @current_user.id %>
  <%! linl_to("編集","users/#{@user.id}/edit") %>
<% end %>

次にアクションの方でも制限しておく。
ensure_correct_userアクションを用いる。
ログインしているユーザーと編集したいユーザーが等しくないとき、投稿一覧ページへとリダイレクトさせる。

controllers/users_controller.rb
before_action :ensure_correct_user, {only: [:edit,:update]}
def ensure_correct_user
  if @current_user.id != params[:id].to_i
    flash[:notice] = "権限がありません。"
    redirect_to("/posts/index")
  end
end

to_iメソッド

params[:id]で帰ってくる数値は文字列になっているため、to_iメソッドを使うことによって、数値としてidを取得できる。

続き
https://qiita.com/jonnyjonnyj1397/items/dbc91ac83aea40e868ef

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

rbenvで指定したい[バージョンがない]ときの対応

rbenv

機能としてはrailsそんなできない私からする感覚だと、とりあえずrbenvを使ってrubyのバージョンを合わせるような切り替えスイッチ的な感覚

本題の指定したいバージョンがないときの対応

今回はいつものように git clone xxx をして、そのあとrails sをすると
当然のごとくversion 2.4.5 is not installed(set xxxxx) とか出てくる

rbenv versions →現在インストールされているものが見える
rbenv local 2.xxx →指定したversionに切り替え
rbenv install 2.x → ない場合はこれで一旦インストール

今回はこの時になかったので

brew upgrade ruby-buildをしてアップグレード
rbenv install -l ※今までこっちでしてましたけど左のやつでいけることをさっきしったrbenv install -list

これでみると、指定したいversionが表示されており、インストールできる

記事のまとめ

最初このrbenvがどうとか
versionがどうとか
ほんと意味プーすぎて教えてもらったときにこういってくれたらなぁ〜って感じで思ってましたが
とりあえず、コマンドぽちぽち入力して対応すれば感覚つかめますね!(圧倒的無駄なやり方笑)

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

herokuでアプリが起動しない

スクリーンショット 2019-03-16 19.29.09.png
いつも見るこの画面。
いい加減、この表示からしっかり抜け出せるようになるためメモします。

各目次、参考文献のタイトルです。
中には今回の原因と関係ないものもありますが、気にしないでください。

参考記事一覧

①Ruby on RailsのファイルをHerokuのデプロイする方法

https://akihiko-s.com/heroku-deploy-rubyonrails/

Config/database.ymlを

production:
  <<: *default
  database: db/production.sqlite3

production:
  <<: *default
  adapter: postgresql
  encoding: unicode
  pool: 5

に変更。変化なし。

②heroku run rails db:migrateのエラー発生時の解決法

https://qiita.com/wann/items/6966f1022ce823a56cc5

node.jsのversionをアップデート。
$ nvm install v8.0.0

変化なし。

③Rails tutorial 1章でheroku loginできない時[Cloud9]

https://qiita.com/hama1/items/a86fde879326e5feb9ef

CLIのpluginが何も無いためアップデート。
$ heroku plugins:install heroku-repo

変化なし。

④heroku へデプロイでエラー「The page you were looking for doesn’t exist.」

http://stuby.hatenablog.com/entry/2014/02/02/163544

assetsをプリコンパイルしてなかったのが原因かも?
$ rake assets:precompile

変化なし。

⑤HerokuにWebアプリを公開する方法

http://0gravity000.sunnyday.jp/ProgramingNote/2016/08/24/tip_07_01_001/

パイプラインを作成し、Connect to GitHubを実施。

変化なし。

⑥Heroku + rails4.2 + Deviseで本番環境,ログアウト画面に遷移する際にエラー

https://ja.stackoverflow.com/questions/10980/heroku-rails4-2-deviseで本番環境-ログアウト画面に遷移する際にエラー

JavaScriptが正常に読み込まれていないため、ローカルにpublic/assetsディレクトリがあるなら消してcommit→pushで良いとのこと。

ーーーー途中

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

ActiveRecord 単体でCRUD

はじめに

この記事はあまりRailsを使ったことが無い自分の備忘録と共に、
ささっと使い安くまとめます
Railsは構築済みなことを想定しています

環境

# フォルダ構成
RailsRoot
    - config
        - database.yml
    - script
        - crud.rb
DB
articles
+-------------+------------+------+-----+---------+----------------+
| Field       | Type       | Null | Key | Default | Extra          |
+-------------+------------+------+-----+---------+----------------+
| id          | bigint(20) | NO   | PRI | NULL    | auto_increment |
| title       | text       | YES  |     | NULL    |                |
| created_at  | datetime   | NO   |     | NULL    |                |
| updated_at  | datetime   | NO   |     | NULL    |                |
+-------------+------------+------+-----+---------+----------------+

ActiveRecordをrequire

require 'active_record'

DBの情報を取得

db_config_path =  File.expand_path('./../config/database.yml', __dir__)

db_config = YAML.load_file(db_config_path)
db_config = db_config["development"]

ActiveRecord::Base.establish_connection(db_config)

Select

# select * from articles;
articles = Articles.all

# where id = 1;
articles_by_id = Articles.where(id: 1)

# order by id desc
 articles_order_id = Articles.all.order(id: "desc")


# データ取得
disp_datas = []
articles.each do |article|
    p "id:#{article.id}"
    p "title:#{article.title}"

    # カラムを全てハッシュにする
    data = article.attributes
    disp_datas.push(data)
end
p disp_datas

Insert

# 新規データのハッシュを取得
new_article = Articles.new

# データを新規ハッシュに入れる
new_data.each_pair { |key, val| new_article[key] = val }

# データをセーブ
new_article.save

Update

# データの更新(Update)
first_data = Articles.first
first_data[:title] = "update_title"
first_data.save

Delete

# データ削除(Delete)
first_data = Articles.first
# 単体削除
first_data.destroy

# ALLを使うとアソシエーション先も実行してくれる
first_data.destroy_all

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

ActiveRecord単体でCRUD

はじめに

この記事はあまりRailsを使ったことが無い自分の備忘録と共に、
ささっと使い安くまとめます
Railsは構築済みなことを想定しています

環境

# フォルダ構成
RailsRoot
    - config
        - database.yml
    - script
        - crud.rb
DB
articles
+-------------+------------+------+-----+---------+----------------+
| Field       | Type       | Null | Key | Default | Extra          |
+-------------+------------+------+-----+---------+----------------+
| id          | bigint(20) | NO   | PRI | NULL    | auto_increment |
| title       | text       | YES  |     | NULL    |                |
| created_at  | datetime   | NO   |     | NULL    |                |
| updated_at  | datetime   | NO   |     | NULL    |                |
+-------------+------------+------+-----+---------+----------------+

DBへ接続

require 'active_record'

db_config_path =  File.expand_path('./../config/database.yml', __dir__)

db_config = YAML.load_file(db_config_path)
db_config = db_config["development"]

ActiveRecord::Base.establish_connection(db_config)

Select

select * from articles;

articles = Articles.all

where id = 1;

articles_by_id = Articles.where(id: 1)

order by id desc

articles_order_id = Articles.all.order(id: "desc")

取得したデータを扱いたい

articles_by_title = Articles.where(title: "title").order(id: "desc")
disp_datas = []
articles_by_title.each do |article|
    p "id:#{article.id}"
    p "title:#{article.title}"

    # カラムを全てハッシュにする
    data = article.attributes
    disp_datas.push(data)
end
p disp_datas

Insert

# 新規データのハッシュを取得
new_article = Articles.new

# データを新規ハッシュに入れる
new_data.each_pair { |key, val| new_article[key] = val }

# データをセーブ
new_article.save

Update

# データの更新(Update)
first_data = Articles.first
first_data[:title] = "update_title"
first_data.save

Delete

# データ削除(Delete)
first_data = Articles.first
# 単体削除
first_data.destroy

# ALLを使うとアソシエーション先も実行してくれる
first_data.destroy_all

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

ツイッターのエラーメッセージで学ぶ英語

英語でエラーメッセージを書く時、英語ネイティブでない私は下記のような点に悩むことが以前はよくありました。

  • 先頭の文字を大文字にすべきか否か
  • 文章で書くべきか、短いフレーズにすべきか
    • (文章の場合)ピリオドを付けるべきか否か
  • 冠詞を付けるべきか否か
  • 形容詞を前置にすべきか後置にすべきか(例:'Not found user' それとも 'User not found')
  • エラーメッセージは文章でよいとして、エラークラス名はどうすべきか
    • エラークラス名にbe動詞は入れるべきか否か

『設計というのは名前が決まればだいたい終わったようなもの』教の私としては、名前付けには何よりもこだわりたい。

そうしていたところ、ツイッターのエラーメッセージがなかなかバリエーションに富んでいることに気付いたので、今後の参考になればと思い、エラーメッセージの特徴をまとめてみた。

紹介するエラーメッセージは Twitter API および Twitter gem から引用しています

引用しているエラーメッセージは Twitter API のレスポンスに含まれるものです。引用しているエラークラス名は Twitter gem で定義されているものです。

紹介しているエラーメッセージとエラークラス名は、一例としてRubyで下記のように使用されます。

begin
  api_client.request_something
rescue Twitter::Error::Unauthorized => e
  puts "##{e.class}, #{e.message}"
end

# => Twitter::Error::Unauthorized, Invalid or expired token.

該当するRubyコードやドキュメントのURLは下記の通りです。

認証に失敗している系のエラー

クラス名はTwitter::Error::Unauthorizedとなる。

Invalid or expired token.

トークンの有効期限が切れている場合。先頭は大文字、フレーズのみ、ピリオドで終わるというパターン。

You have been blocked from viewing this user's profile.

ブロックされているユーザーのプロフィールを見ようとした場合。がっつり文章パターン。

Could not authenticate you.

何らかの理由でOAuthに失敗している場合。文章で主語が省略されている。

文法としては正しくないが、gitのcommitメッセージも主語が省略されることが多く、少なくともエンジニア関係の文章に限るとこの書き方でもよいらしい。

Not authorized.

理由は分からないが極稀にでる認証のエラー。こんなに短くてもピリオドがあった方がよいらしい。

アカウントが凍結されている系のエラー

クラス名はTwitter::Error::Forbiddenとなる。

User has been suspended.

ユーザーが凍結されている場合。文章かつ現在完了(継続)用法を使っている。

非ネイティブが書くには「長すぎるかな?」と躊躇しそうな文章だが、これでもよいらしい。(今は慣れたので自分でもこう書く)

Your account is suspended and is not permitted to access this feature.

凍結されているユーザーの認証情報を使って何かしらのAPIにアクセスした場合。andで文章をつないでいる。

これ私にも長過ぎるように思えるが、理由が2つあるのでこれでもよいらしい。(これも慣れた)

You are unable to follow more people at this time. Learn more <a href='http://support.twitter.com/articles/66885-i-can-t-follow-people-follow-limits'\>here\.

単位時間あたりにフォローできる数の制限を超えた場合。文章かつHTMLタグ付き。

かなり前衛的だが、このエラーについての質問が多いと想定される場合はこれでもよいのかもしれない。

To protect our users from spam and other malicious activity, this account is temporarily locked. Please log in to https://twitter.com to unlock your account.

理由が不明だがたまにでるエラー。かなり長めの文章かつURL付き。

こっちはHTMLタグにはなっていない。ツイッター社の中でもエンジニアによってエラーメッセージの書き方に幅があるんだろうか。

You can't follow yourself.

自分のIDをフォローしようとした場合。ここまで読み進めている人にはもう見慣れたエラーメッセージになっているはず。

Could not determine source user.

ユーザー間の相互関係を変更するAPIでソースユーザーが見付からなかった場合。これももはやよくある形。

You cannot send messages to users you have blocked.

自分がブロックしているユーザーにDMを送ろうとした場合。can'tではなくcannotを使っている。省略形にするかどうかはどちらでもよいらしい。

You cannot send messages to users who are not following you.

自分のフォローしていないユーザーにDMを送ろうとした場合。関係代名詞を使うパターン。

これも非ネイティブの私が書く時は「長すぎるかな?」と躊躇してしまうが、へたに省略して対象が誰なのか分からなくなるよりはこの方がよいのだろう。

You are sending a Direct Message to users that do not follow you.

自分をフォローしていないユーザーにDMを送ろうとした場合。

直前のエラーメッセージとこのエラーメッセージを見比べた後なら、確かに関係代名詞以降の文章まで正確に書いた方がよいかも、と思わせられる。(もし書かなかったら紛らわし過ぎるので)

ユーザーが見付からなかった系のエラー

クラス名はTwitter::Error::NotFoundとなる。

User not found.

ユーザーをlookupしようとして見付からなかった場合。これに限らず、not foundは後置のパターンをよく見かける気がする。

余談だが、似たような例としてtoo many requestsは前置、rate limit exceededは後置という違いがある。単語によるものの、過去分詞で名詞を修飾する場合のエラーメッセージは後置になるらしい。(ただし例外も後述される)

No user matches for specified terms.

複数ユーザーのlookupを一括でしようとしたが1人もユーザーが見付からなかった場合。

termには様々な意味(後述)があるので非ネイティブとしてはこの単語は選びづらいが、少なくともこういう場合にも使える単語らしい。

termの意味の例
期間、期限、用語、条件(terms of use == 利用規約)

Sorry, that page does not exist.

何かしらのリソースが見付からなかった場合。ひとまず一言謝っておく時は先頭にSorry,と付けておけばよいらしい。

サーバーのキャパシティを超えてしまった系のエラー

クラス名はTwitter::Error::ServiceUnavailableとなる。

Over capacity

理由はよく分からないがたまにでるエラー。シンプルなフレーズかつピリオドがないパターン。

(エラーメッセージなし)

同じエラークラスで、エラーメッセージが空の場合がたまにある。エラークラス名で理由が十分に伝わる場合はこれでもよいらしい。

その他のエラークラス名

Twitter gemには他にもたくさんのエラーが定義されている。エラークラス名として参考になるのでそれらも引用しておく。code

RequestEntityTooLarge

「too + 形容詞」が後置になっているパターン。TooManyRequestsというエラークラス名もある。この2つから察する限り、どっちでもよいんだろうか?

AlreadyFavorited

「既に何かしら処理が終わっている」というエラーの場合、クラス名には「have + 過去分詞」は付けないらしい。

AlreadyRetweeted

これも上記の例と同様だが、エラーメッセージが少し特殊で、

sharing is not permissible for this status (Share validations failed)

となっている。先頭が大文字じゃない、かつ、カッコ書きでより具体的な理由が書かれている。このエラーメッセージだけ違う人が書いたんだろうか。

「カッコ書きで理由を書くことの是非」について、これまでの例を見る限りカッコ書きはあまり一般的ではないらしい。ひとまず私は滅多に使わないようにしている。

DuplicateStatus

この場合のエラーメッセージは、

Status is a duplicate.

となっている。duplicateが形容詞に思えるが、この場合は名詞の用法もあるらしい。重複した処理を許さない場合のエラーメッセージとして使えるかもしれない。

その他のエラー定数名

Twitter gemに定義されているエラー定数名から参考になりそうなものをいくつかピックアップ。code

UNABLE_TO_VERIFY_CREDENTIALS

何かができない場合は「unable to 動詞」でよいらしい。ちょっと長いようにも思えるのでご参考まで。

CANNOT_MUTE

何かができない場合のもっとシンプルな書き方はこちら。

OAUTH_TIMESTAMP_OUT_OF_RANGE

何かが範囲外であるという状況はよく見かけるが、そういう場合はこの書き方でよいらしい。

まとめ

質問何でもどうぞ!

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

Ruby on RailsでGitHubからクローンしてアプリを立ち上げるために気をつけること

Railsのアプリを1から制作するための環境設定は、どの参考書にも載っていますが、GitHubからクローンしてアプリを立ち上げるための情報を、網羅している情報源は見当たりませんでした。

私は、プログラミング未経験から独学で勉強していまして、GitHubからクローン(Clone)してアプリを立ちあげるのに、かなりつまづいてしまいましたので、初学者の方に参考にしていただければ幸いです。

ちなみに、現在はRSpecについて学ぼうとしているところでして、『Everyday Rails』の環境構築で迷った経験をもとにしています。(一般化して、汎用性の高い考え方にすることを目的としています。)

Ruby on RailsでGitHubからクローンしてアプリを立ち上げるために気をつけること

順番が重要でして、下記の通り進める必要があります。

1.Ruby/Ruby on Railsのバージョン確認
2.Gem関連のバージョン確認
3.データベースの設定

1.Ruby/Ruby on Railsの確認(インストール/バージョンアップ/バージョンダウン)

  • Ruby
  • Rails

RubyやRailsのバージョンは、GitHubの「README.md」というプログラムの仕様を記述するファイルに記載されているはずです。

2.Gem関連のバージョン確認(インストール/バージョンアップ/バージョンダウン)

  • Bundler
    • "gemfile"と"gemfile.lock"に従って、gemを管理をするライブラリ
  • Ruby-gems
    • gemを管理する仕組みで、Rubyで書かれたプログラムを管理するためのパッケージツール(ライブラリ)が簡単に使えるように配布されている場所

上記のバージョンを全てクリアした上で、下記コマンドでGemのバージョンを合わせます。
bundle install

RubyやRailsだけでなく、GemやBundler、Ruby-gemsのバージョンも確認することがポイントですね。

私の場合は、「Bundlerのバージョンをあげようとした時に、Ruby-gemsのバージョンが低いからだめ」というエラーが出たので、「先にRuby-gemsのバージョンを上げてから、Bundlerのバージョンを上げる」ことで解決できました。

3.データベース接続

  • データベース作成(bin/rails db:create)
  • マイグレーションファイル実行(bin/rails db:migrate)

データベースの設定は、クライアント側(自分のPC)で行う必要があります。

ここまでやれば、あとは「bin/rails server」でサーバーを起動して、アプリを立ち上げることができるはずです。

最後に

GitHubからクローンしてアプリを立ちあげるために、留意すべきことを列挙しました。

あとは、エラー文をググって解決の糸口を見つけるしかないですが、その際はUNIXコマンド(ターミナルでの操作)の知識をしっかりつける必要があるな、と感じている次第です。

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

Ruby on Rails の Active Job と SideKiq でバックグラウンドジョブをキューイングして実行する

はじめに

本記事は Active Job とアダプタとして Sidekiq を使ってバックグラウンドジョブを実行する環境を整えた時の備忘録です。

Active Job とは

Active Job は Rails におけるバックグラウンドジョブを動かすための共通インタフェースです。
バックグラウンドジョブを動かす Sidekiq、Resque、Delayed Job をアダプタとして利用できます。

Rails で提供されるのはジョブをメモリに保持するインプロセスのキューイングシステムだけなので Rails を再起動するとジョブは全て失われます。(アダプタを指定しなかった場合のデフォルト動作)(参考)

Rails4.2 から Active Job が利用できるようになりました。(参考)

ジョブを作成する

Rails コマンドの generate コマンドを使って作成できます。Rails ガイドにも作り方と Active Job の開設があるので参考にしてみて下さい。(参考)

ActiveJobのジョブを作成する
$ bin/rails g job generate_ticket
Running via Spring preloader in process 13038
      invoke  test_unit
      create    test/jobs/generate_ticket_job_test.rb
      create  app/jobs/generate_ticket_job.rb

すると app/job/ 配下に application_job.rb と generate でオプション指定された名前の末尾に _job が付いたファイルが作成されます。(例では generate_ticket_job.rb )

ジョブが実行すべきタイミングになると perform メソッドが呼び出されるので、このメソッドにジョブで実行したい処理を記述することになります。

作成されたファイルには何もすることが書かれておらずコメント「# Do something later」があるのみなので、ひとまず Hello Active Job. と stdout に出力することにします。

app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
end
app/jobs/generate_ticket_job.rb
class GenerateTicketJob < ApplicationJob
  queue_as :default

  def perform(*args)
    p "Hello Active Job."
  end
end

以上で Active Job を呼び出す準備は終わりです。(まだアダプタは利用しない)

次のようにジョブクラスを使って perform_later メソッドを呼び出すだけです。
呼び出す際に引数が必要であれば指定できます。(perform メソッドの仮引数 *args で受け取れる。

# キューが空き次第実行する(実行時引数は無し)
GenerateTicketJob.perform_later
# 実行時引数 some_arg を渡してキューが空き次第実行する
GenerateTicketJob.perform_later some_arg

また、5秒後に実行したいといった時間指定は set メソッドをメソッドチェーンでつなぐことで実現できます。

# 5秒後に実行する
GenerateTicketJob.set(wait: 5.second).perform_later

ここまでで Rails でジョブを使うことは出来ます。
しかし、全てメモリ内で管理されているため Rails が停止すると実行する前のジョブは全て消え去り、Rails を起動しても実行されることはありません。

次に、Sidekiq を Active Job のアダプタとして使うことで、Rails が停止してもジョブが失われないようにしていきます。

Active Job のアダプタとして Sidekiq を使う

Active Job アダプタとして Sidekiq を使うことが出来ます。

Sidekiq はバックグラウンドジョブを動作させるためのフレームワークです。
Ruby が動作する環境であれば AcitveJob(Rails) を使わずとも Sidekiq は使えます。

Sidekiq を使うためには Client, Redis, Server の 3 つが必要です。

Rails から Sidekiq を使う場合、Client は Rails アプリケーション自身を指します。
GenerateTicketJob.perform_later 等を実行する主体が Client です。

Server は Rails と別プロセスとして起動します。
bundle exec sidekiq で実行します。

Redis は Job をキューイングするために使います。
Client からアクセスできるようにする必要があります。

本記事では Client, Server, Redis は同一ホストにインストールすることにします。

Sidekiq と Redis をインストールして起動する (Ubuntu用)

Redisのインストール
$ sudo apt-get -y update && sudo apt-get install -y redis-server
Redisを起動する(必要に応じて自動設定を有効にする)
# Redis を起動する
$ sudo systemctl start redis-server

# (必要に応じて) 自動起動設定を確認する
$ sudo systemctl status redis-server

# (必要に応じて) 自動起動設定を有効にする
$ sudo systemctl enable redis-server
Sidekiqをインストールする(Gemfileに記載してbundleを使ってinstallする場合)
# Sidekiq をインストールする
$ cat app/Gemfile
  : <snip>
# use sidekiq (https://github.com/mperham/sidekiq)
gem 'sidekiq'
$ bundle install

# Sidekiq を起動する
$ bundle exec sidekiq

Rails アプリケーションを設定する

config.active_job.queue_adapter にて :sidekiq を指定します。(参考)

config/application.rb
  : <snip>
module Rails52SampleApp
  class Application < Rails::Application
      : <snip>
    # use sidekiq
    config.active_job.queue_adapter = :sidekiq
  end
end

Sidekiq が動作することを確認する

以上で Rails を Client として Sidekiq を動作させるための設定は完了です。

Sidekiq が動作していること、Redis が動作していることを確認して Rails server を再起動しましょう。

Hello Active Job. が表示されたら成功です。

Sidekiq で動作したことを確認するためにも以下のように実行したジョブが増えていることを確認するとよいでしょう。

$ rails c --sandbox
irb(main):014:0> require 'sidekiq/api'
=> false
irb(main):014:0> Sidekiq::Stats.new.processed
=> 400
irb(main):014:0> Sidekiq::Stats.new.processed # 実行が完了すると数が増える
=> 401

Active Job に処理を記述する

perform メソッドに記述する時に出来る事・出来ないことなどを記述します。

ジョブ内でモデルを使う方法

Active Job 内では特別なことを行うことなく ActiveRecord が利用できます。(参考)

Each Sidekiq server process pulls jobs from the queue in Redis and processes them. Like your web processes, Sidekiq boots Rails so your jobs and workers have the full Rails API, including Active Record, available for use. The server will instantiate the worker and call perform with the given arguments. Everything else is up to your code.

例えば Ticket モデルがあり、そのモデルを作成したり特定の属性を表示させようとしたら次のように記述出来ます。

app/jobs/generate_ticket_job.rb
class GenerateTicketJob < ApplicationJob
  queue_as :default

  def perform
    ticket = Ticket.create!(name: '550e8400-e29b-41d4-a716-446655440000')
    p "Generating ticket #{ticket.name} ..."
  end
end

ジョブの引数に Rails のモデルを指定する

Sidekiq は perform_sync メソッドの引数に渡された値を JSON へ変換して Redis に保存します。
しかし複雑なオブジェクトの場合は JSON に変換されないことや、仮にキューがバックアップされて引用したオブジェクト側を変更した場合にどうなるか等を考えると、引数としてはインスタンスを渡さずに ID を渡す方がベストプラクティスのようです。

参考

Active Job の perform メソッドにモデルを引数として渡す場合はクラスと ID を指定する必要はなく、簡潔に書くことが出来るとのことです。参考

class TrashableCleanupJob  < ApplicationJob
  def perform(trashable, depth)
    trashable.cleanup(depth)
  end
end

Active Job を制御する

Rails の Controller 等から Active Job を制御する時の方法を記述します。

ジョブを同期的に実行する

ジョブをインスタンス化して、perform メソッドを直接呼び出せば同期的に実行できます。

GenerateTicketJob.new.perform

ジョブが終了するまでロックする

with_advisory_lock 等を利用してジョブを実行する前にロックをかけて、ジョブが終了したらロックを解除するとよいでしょう。

但し、ジョブが終了されずに強制削除される場合等を考慮して、ロックをいつでも解除できる仕組みは用意しておいた方がよいと思われます。

ジョブを Rails console で操作する

以下、共通で Rails console にて require 'sidekiq/api' を実行していることが前提となります。

'sidekiq/api' を読み込むことで Sidekiq::Queue, Sidekiq::RetrySet が実行できるようになります。

ジョブ実行結果の統計情報を表示する

irb> Sidekiq::Stats.new.to_json
irb(main):065:0> p Sidekiq::Stats.new.to_json
"{\"stats\":{\"processed\":401,\"failed\":334,\"scheduled_size\":0,\"retry_size\":0,\"dead_size\":0,\"processes_size\":1,\"default_queue_latency\":0,\"workers_size\":0,\"enqueued\":0}}"
項目 説明
processed 実行完了数
failed 実行失敗数
scheduled_size 予定キュー内ジョブ数
retry_size リトライキュー内ジョブ数
dead_size デッド状態(※1)のジョブ数
processes_size 実行中のジョブ数
default_queue_latency デフォルトキューの遅延時間(※2)
workers_size Worker の数
enqueued 全キュー内のジョブ数(リトライキューと予定キューは除く)

※1: ジョブ実行時に例外が発生するとリトライ数(デフォルトでは25回)だけリトライした後にデッド状態となる
※2: キュー内の最初のジョブがキューに入るまでの時間 (参考)

参考: https://github.com/mperham/sidekiq/wiki/API#stats, https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/api.rb

リトライキューにあるジョブを確認・削除する

リトライキューは Sidekiq::RetrySet インスタンスをつかって操作できます。

ジョブを確認する

リトライキューにある全てのジョブを確認する
irb> Sidekiq::RetrySet.new.each {|job| puts "#{job.jid} #{job.klass} #{job.args}"}
リトライキューにある特定のジョブを確認する(実行例※見やすいよう改行してます)
irb(main):024:0> Sidekiq::RetrySet.new.find_job('59318b9a94f9da40e973a951')
=> #<Sidekiq::SortedEntry:0x00007f9e1c2cb250 @args=nil,
@value="{\"class\":\"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper\",
\"wrapped\":\"GenerateTicketJob\",\"queue\":\"default\",
\"args\":[{\"job_class\":\"GenerateTicketJob\",
\"job_id\":\"dabb26ff-87fd-438c-b93f-f6964e624fec\",\"provider_job_id\":null,
\"queue_name\":\"default\",\"priority\":null,
\"arguments\":[{\"_aj_globalid\":\"gid://rails52-sample-app/Ticket/4\"}],
\"executions\":0,\"locale\":\"en\"}],\"retry\":true,
\"jid\":\"59318b9a94f9da40e973a951\",\"created_at\":1552203698.4819345,\"enqueued_at\":1552203762.003836,
\"error_message\":\"Validation failed: Diff summary can't be blank\",
\"error_class\":\"ActiveRecord::RecordInvalid\",\"failed_at\":1552203698.826664,
\"retry_count\":2,\"retried_at\":1552203762.1639638}", 
@item={"class"=>"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", 
"wrapped"=>"GenerateTicketJob", "queue"=>"default", 
"args"=>[{"job_class"=>"GenerateTicketJob", "job_id"=>"dabb26ff-87fd-438c-b93f-f6964e624fec", 
"provider_job_id"=>nil, "queue_name"=>"default", "priority"=>nil, 
"arguments"=>[{"_aj_globalid"=>"gid://rails52-sample-app/Ticket/4"}], 
"executions"=>0, "locale"=>"en"}], "retry"=>true, "jid"=>"59318b9a94f9da40e973a951", 
"created_at"=>1552203698.4819345, "enqueued_at"=>1552203762.003836, 
"error_message"=>"Validation failed: Diff summary can't be blank", 
"error_class"=>"ActiveRecord::RecordInvalid", "failed_at"=>1552203698.826664, 
"retry_count"=>2, "retried_at"=>1552203762.1639638}, @queue="default", 
@score=1552203835.1639743, @parent=#<Sidekiq::RetrySet:0x00007f9e1c2e9660 @name="retry", 
@_size=1>>
リトライキューを確認する(実行例※見やすいよう改行してます)
irb(main):007:0> Sidekiq::RetrySet.new.each {|job| p job }
#<Sidekiq::SortedEntry:0x00007f9e1c2963c0 @args=nil, 
@value="{\"class\":\"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper\",
\"wrapped\":\"GenerateTicketJob\",\"queue\":\"default\",
\"args\":[{\"job_class\":\"GenerateTicketJob\",\"job_id\":\"674c8510-02fe-4183-864c-9ce038ead984\",
\"provider_job_id\":null,\"queue_name\":\"default\",\"priority\":null,
\"arguments\":[{\"_aj_globalid\":\"gid://rails52-sample-app/Ticket/7\"}],\
"executions\":0,\"locale\":\"en\"}],\"retry\":true,\"jid\":\"bb447a9176f436a1343043ac\",
\"created_at\":1552206799.7265983,\"enqueued_at\":1552206799.7266936,
\"error_message\":\"Validation failed: Diff summary can't be blank\",
\"error_class\":\"ActiveRecord::RecordInvalid\",\"failed_at\":1552206800.230454,\"retry_count\":0}", 
@item={"class"=>"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", 
"wrapped"=>"GenerateTicketJob", "queue"=>"default", 
"args"=>[{
"job_class"=>"GenerateTicketJob", "job_id"=>"674c8510-02fe-4183-864c-9ce038ead984", 
"provider_job_id"=>nil, "queue_name"=>"default", "priority"=>nil, 
"arguments"=>[{
"_aj_globalid"=>"gid://rails52-sample-app/Ticket/7"}], "executions"=>0, "locale"=>"en"}], 
"retry"=>true, "jid"=>"bb447a9176f436a1343043ac", "created_at"=>1552206799.7265983, 
"enqueued_at"=>1552206799.7266936, "error_message"=>"Validation failed: Diff summary can't be blank", 
"error_class"=>"ActiveRecord::RecordInvalid", "failed_at"=>1552206800.230454, 
"retry_count"=>0}, @queue="default", @score=1552206832.2304695, 
@parent=#<Sidekiq::RetrySet:0x00007f9e1c297b30 @name="retry", @_size=1>>
=> nil

ジョブを削除する

リトライキューを1つ削除する
irb> Sidekiq::RetrySet.new.find_job(<JID>).delete
リトライキューを1つ削除する(実行例)
irb(main):026:0> Sidekiq::RetrySet.new.find_job('59318b9a94f9da40e973a951').delete
=> true
リトライキューを全て削除する
irb> Sidekiq::RetrySet.new.clear
リトライキューを全て削除する(実行例)
irb(main):008:0> Sidekiq::RetrySet.new.clear
=> 1

Sidekiq ダッシュボードを使ってジョブを確認・削除する

ダッシュボード用のルートをマウントすれば http://localhost:3000/sidekiq/ にアクセスすることで表示できます。

routes.rb
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'

image.png

実行中のジョブを確認・停止(Sidekiqプロセスの停止)する

image.png

  • 実行中のジョブを確認する
    • 「実行中」タブを選択する
  • 「すべて処理終了」ボタン
    • 全プロセスを、新規ジョブ実行を受け付けない(Quit)状態にする
  • 「すべて停止」ボタン
    • 全プロセスを停止させる(bundle exec sidekiq プロセスが停止する)
    • 実行中のジョブは Redis に戻される。Sidekiq を起動すると再度 Redis からジョブが読みだされて実行される)
「すべて停止」ボタン実行時のSidekiqServerログ例(実行中のジョブが残っていた場合)
2019-03-10T16:51:36.753Z 13783 TID-gq5pri3aj INFO: Shutting down
2019-03-10T16:51:36.753Z 13783 TID-gq5pri3aj INFO: Terminating quiet workers
2019-03-10T16:51:36.753Z 13783 TID-gq5qinqn7 INFO: Scheduler exiting...
2019-03-10T16:51:36.859Z 13783 TID-gq5pri3aj INFO: Pausing to allow workers to finish...
2019-03-10T16:51:44.754Z 13783 TID-gq5pri3aj WARN: Terminating 1 busy worker threads
2019-03-10T16:51:44.754Z 13783 TID-gq5pri3aj WARN: Work still in progress [#<struct Sidekiq::BasicFetch::UnitOfWork queue="queue:default", job="{\"class\":\"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper\",\"wrapped\":\"GenerateTicketJob\",\"queue\":\"default\",\"args\":[{\"job_class\":\"GenerateTicketJob\",\"job_id\":\"5e6c6b86-3f62-4604-ab86-5431eabbfbaf\",\"provider_job_id\":null,\"queue_name\":\"default\",\"priority\":null,\"arguments\":[{\"_aj_globalid\":\"gid://rails52-sample-app/Ticket/13\"}],\"executions\":0,\"locale\":\"en\"}],\"retry\":true,\"jid\":\"7871b44f67cf4d9d995e7cd0\",\"created_at\":1552236631.6320188,\"enqueued_at\":1552236656.0809422}">]
2019-03-10T16:51:44.755Z 13783 TID-gq5pri3aj INFO: Pushed 1 jobs back to Redis
2019-03-10T16:51:44.810Z 13783 TID-gq5pri3aj INFO: Bye!
2019-03-10T16:51:44.812Z 13783 TID-gq5qinppv GenerateTicketJob JID-7871b44f67cf4d9d995e7cd0 INFO: fail: 48.731 sec

キューにあるジョブを確認・削除する

image.png

  • キューにあるジョブを確認する
    • 「予定」タブを選択する
  • キューにあるジョブを1つ削除する
    • 「予定」タブから削除したいジョブを選択して「削除」ボタンを押す

リトライキューにあるジョブを確認・削除する

image.png

  • リトライキューにあるジョブを確認する
    • 「再試行」タブを選択する
  • リトライキューにあるジョブを1つ削除する
    • 「再試行」タブから削除したいジョブを選択して「削除」ボタンを押す
  • リトライキューにあるジョブを全て削除する
    • 「再試行」タブから「全て削除」ボタンを押す
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nginx,Redis,MySQLを使ってほんの少し実践的なRails ActionCableと、iOS/Androidのサンプルアプリを作って全体像を学ぶ〜バックエンド編〜

前置き

「Rails ActionCableで双方向通信してみたい」「モバイルアプリでリアルタイム通信アプリ作りたい」と思いサンプルアプリを作ってみました。個々の詳細については既に解説してくださっている記事はありますので、大まかに環境構築やソースコードをご紹介します。以下の3部構成になっています。

お遊びサンプルの紹介

以下のアニメーションGIFをご覧ください。各ユーザーのアクティブ状況を表示して、メッセージをやりとります。もっと砕けた表現をするならば、筆者の愛犬たちが寝起きして、鳴いたり、遠吠えしたり、唸ったりします。
ios_demo.gif

このアプリでは大きく2つのActionCableの使い方があります。

  • 同じルーム内の全ユーザーにブロードキャスト
    • ルームに入る
      現在ルーム内にいるユーザー(以下、アクティブユーザー)にルームに入ったことを通知し、アクティブユーザーを取得します。そして、各ユーザーのアクティブ状況を表示します。
    • 「ワンワン」ボタンと「ワオーン」ボタン
      文字入力で任意の文字が送れないだけで、チャットでいうところの「メッセージ」とほぼ同義です。ボタンに対応したメッセージを送信します。
    • ルームから出る
      iOSなら「キャンセル」、Androidなら「←」をタップしたり、アプリを閉じたりするとルームから出たことにします。ルームに入るときと同様にアクティブユーザーを取得して、各ユーザーのアクティブ状況を表示します。
  • 自分にブロードキャスト
    • 「独り言」
      「独り言」ということで自分のみメッセージを受信します。

構成

以前作成した開発環境をベースにDockerで構築しています。この構成もGitHubに公開していますのでDockerfileやDocker Composeと併せてご覧ください。ここではサービス毎に主な設定をピックアップします。

MySQL

ActionCableへ接続する際にユーザー情報を取得する処理のために利用します。特筆することはありませんが、強いて言えばデフォルトの文字コードをutf8mb4にしています。

Nginx

  • ポート (nginx.conf / docker-compose.yml)
    自己署名証明書(所謂オレオレ証明書)ではSSLハンドシェイクの関係でうまく通信出来ませんでしたので、この環境では平文のWebSocket通信(ws://)を行うため80番ポートを許可します。なお、筆者環境では独自ドメインとLet' EncryptのSSL証明書でも動作確認しています。その場合は443番ポードで想定通り暗号化されたWebSocketの通信(wss://)ができることを確認しています。
  • WebSocket (nginx.conf)
    ActionCableのエンドポイントである「location /cable」はhttp(https)ではなくWebSocket(ws/wss)として通信できるようにします。

Rails

  • WebAPIモード
    WebのViewやフロントエンドは不要なのでWebAPIとしてプロジェクトを構築します。
  • MySQLのデフォルト文字コード: utf8mb4
    このサンプルアプリでは動作上の意味はありません。折角なので導入しただけです。

Railsは本記事の主題なので後ほど別途説明します。

Redis

  • サブスクリプションアダプター
    Railsのサブスクリプションアダプターには「Async」ではなく「Redis」を使います。
  • キャッシュ
    簡易的なデータストアに使います。

RailsでWebAPIを構築する

前述のとおり本環境はGitHubに公開していますので、この記事では要点のみ紹介します。

プロジェクトを作成する

Docker Composeを初期起動した時点で以下が実行されます。

docker/containers/rails/docker-entrypoint.sh
bundle exec rails new . -d mysql -f -T --api --skip-bundle

ActionCableを設定する

  1. originを許可
    Nginxの設定でも触れましたとおり自己署名証明書ではSSLハンドシェイクの関係でうまく通信出来ませんでした。この環境では平文のWebSocket通信(ws://)も許可します。正規のSSL証明書がインポートされていればwss://のみ指定すれば問題ありません。
    Androidエミュレーターで動作させるためにdisable_request_forgery_protectiontrueに設定して送信元の制限を緩めます。この設定をしないと以下のエラーが発生します。
    筆者はiOS版を開発してからAndroid版を開発しています。iOSシミュレーターでは発生しなかったのでRails側のログ調査を怠っており調査に時間が掛かってしまいました。

    log/development.log
    Request origin not allowed: 
    Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
    
    volumes/app/config/environments/development.rb
    config.action_cable.allowed_request_origins = [ /wss?:\/\/.*/, /ws?:\/\/.*/ ]
    config.action_cable.disable_request_forgery_protection = true
    
  2. サブスクリプションアダプターにRedisを指定
    開発環境のアダプターはデフォルトでasyncです。プロダクション環境ではRedisが推奨(async非推奨)されています。ここではRedisを利用することにします。

    volumes/app/config/cable.yml
    default: &default
      adapter: redis
      url: <%= ENV.fetch("REDIS_URL") { "redis://cache:6379/0" } %>
      channel_prefix: app_production
    
    development:
      <<: *default
    

キャッシュストアを設定する

  1. Redis Gemをインストール
    Gemfileのコメントアウトを外します。

    volumes/app/Gemfile
    gem 'redis', '~> 4.0'
    
  2. キャッシュストアにRedisを指定
    デフォルトのconfig.cache_store = :memory_storeを書き換えます。
    ActionCableでRedisはDB番号0を指定しているので、キャッシュストアはDB番号1を指定しています。

    volumes/app/config/environments/development.rb
    config.cache_store = :redis_cache_store, { url: "redis://cache:6379/1", namespace: 'cache' }
    
  3. 開発環境でキャッシュストアを有効化
    app/tmpディレクトリにファイル「caching-dev.txt」を配置します。
    ファイルさえ配置すればよいのでtouch caching-dev.txtで作成してください。

モデルとマイグレーション

ほんの少し実践的にユーザーの情報がRDBに格納されていることを想定してモデルを作成します。

コンソール
$ bundle exec rails g model user account:string name:string
$ bundle exec rails db:migrate

その他、いくつか制約を加えてマイグレーションした結果以下のようになりました。ただし、追加した制約が無くても動作に影響はありません。

volumes/app/db/schema.rb
ActiveRecord::Schema.define(version: 2019_02_23_052441) do
  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC", force: :cascade do |t|
    t.string "account", null: false
    t.string "name", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["account"], name: "index_users_on_account", unique: true
  end
end

サンプルデータ(初期データ)を投入する

ActionCable以外の部分の説明が増えることを防ぐため、アプリ側のソースコードを簡略化しました。そのため、以下の3ユーザー以外は動作させることができません。
すみません:bow::bow::bow:

volumes/app/db/seeds.rb
User.create(account: 'chiyo', name: '千代')
User.create(account: 'eru', name: 'エル')
User.create(account: 'otome', name: '乙女')
コンソール
$ bundle exec rails db:seed

コネクションを設定する

公式とほとんど変わりません。アプリとWebAPIはステートレスにしたいのでユーザーの情報はCookieからではなくパラメーターから取得します。実際はOpenID Connectなどのアクセストークンの妥当性をチェックしてコネクションの可否を判断すると思います。

ユーザー情報のやりとり
request.params[:account]
volumes/app/app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    def disconnect
    end

    private
      def find_verified_user
        if verified_user = User.find_by(account: request.params[:account])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

チャンネルを作成する

コンソール
$ bundle exec rails g channel room
volumes/app/app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
    :
    :
end

デフォルトで作成されているメソッドと、サンプルアプリ向けに作成したメソッドを紹介します。(パブリックメソッドのみ)

subscribed

アプリ側からリクエストされたroomのパラメーターを取得します。サンプルアプリでは無条件にサブスクライブしますが、ユーザーとroomの検証を行って不正なアクセスだったら遮断するなどの処理があっても良いと思います。続いてstream_forを指定します。この表現が正しいかわかりませんが、送信先のグルーピングを指定するイメージです。

  • room全体
    @room(params[:room])を指定します。同じroomを指定(以下、同じルーム)しているアクティブユーザーにブロードキャストすることができます。
  • 自身のみ
    ユーザーのアカウントとroomを連結して自身のみを指定します。一意になれば何でも良いと思います。ブロードキャストではあるものの実質的に自身のみが宛先になります。

room_inは簡易的にルーム毎のアクティブユーザーを追加するメソッドです。このメソッドは説明程度に動作すれば良いので作りは甘いです:bow:

def subscribed
  @room = params[:room]
  @user = self.current_user.id.to_s + @room
  stream_for @room
  stream_for @user

  room_in(key: @room, account: self.current_user.account)
end

unsubscribed

暗黙的、明示的に関わらずサブスクライブを解除したとき、room_inとは反対にルーム毎のアクティブユーザーを削除します。このメソッドも同様に説明程度に動作すれば良いので作りは甘いです:bow:
また、ルームを出たことをアクティブユーザーにブロードキャストします。

def unsubscribed
  room_out(key: @room, account: self.current_user.account)

  # 全員に送ります。
  RoomChannel.broadcast_to(@room, account: self.current_user.account, type: :out)
end

greeting

各ユーザーはサブスクライブしたあと、アクティブユーザーに「ルームに入った」ことを通知します。このとき、roommate(※)を利用してアクティブユーザーのリストを送信します。このリストでアプリ側で表示している各ユーザーのアクティブ状況を更新しています。
※ roommateはルーム毎にアクティブユーザーを取得するメソッドです。前述のroom_in/room_outで管理しています。

def greeting
  # 全員に送ります。
  RoomChannel.broadcast_to(@room, roommate: roommate(key: @room), account: self.current_user.account, type: :in)
end

mumbling

「独り言」ボタンで自分自身だけにブロードキャストします。
「自身だけに通知した処理をつけたい」という理由ありきで実装しました:sweat:

def mumbling
  # 独り言です。
  RoomChannel.broadcast_to(@user, content: '(゚Д゚;)', account: self.current_user.account, type: :mumbling)
end

bark

「ワンワン」「ワオーン」ボタンで、その鳴き声を同じルームのアクティブユーザーに送ります。
多くのサンプルが公開されているチャットアプリだと、このメソッドが肝ですよね。

def bark(data)
  # 全員に送ります。
  RoomChannel.broadcast_to( \
      @room, content: data["content"], account: self.current_user.account, type: :bark)
end

起動する

  1. Docker Composeで起動

    $ cd docker
    $ docker-compose up
    :
    cache_1  | 1:M 09 Mar 2019 09:05:57.692 * Ready to accept connections
    :
    db_1     | Version: '5.7.25'  socket: '/var/run/mysqld/mysqld.sock'  port:   :
    3306  MySQL Community Server (GPL)
    :
    app_1    | Use Ctrl-C to stop
    
  2. ブラウザでトップページのにアクセス
    この環境では正規のドメインを取得していないので、とりあえずhostsに設定してください。
    image

動作を確認したいけれど・・・

本記事ではアプリからのアクセスを前提としてAPIモードでプロジェクトを作成しましたのでWeb向けのCoffeeScriptなどがありません。ここのご紹介はiOS/Android編に譲りたいと思います。

終わりに

ActionCableのドキュメントは分かりやすいと思います。しかし、事例や情報量が少ない上にBeta版の頃の情報も混在しており何が正しいか分かりづらい印象でした。筆者は構築した経験はありませんが、この点は「Socket.IO強し」と言ったところでしょうね。今回の構成が少しでもご参考になれば幸いです。

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