20191126のRubyに関する記事は19件です。

eager_load/preload/includes/joinsの違いメモ

動機

日頃Goしか書いてないGopherがRails書いたときにつまったActiveRecord、特にjoins周りの挙動が直感的によくわからなかったので実際にデータ用意して調べてみた
(メソッドの具体的な説明は他にたくさん記事があるのでそちらを参照してください。)

実行環境

Rails公式Docのサンプルブログアプリ
の article.rb と comment.rb のモデルを使用。
今回は、それぞれ約25万行を用意した。

article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  validates :title, presence: true,
                    length: { minimum: 5 }
end
comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

Controllerから preload/eager_load/joins/includes を用い、それぞれの吐くSQLやパフォーマンスを比較してみる

0. 素

Article.limit(100)

SELECT `articles`.* FROM `articles` LIMIT 100
-- Completed 200 OK in 154ms (Views: 127.1ms | ActiveRecord: 15.7ms | Allocations: 20418)

まずは検証用にそのままで...

1. eager_load

Article.eager_load(:comments).limit(100)

SQL (1.9ms)  SELECT DISTINCT `articles`.`id` FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
SQL (575.4ms)  SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`text` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`commenter` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`article_id` AS t1_r3, `comments`.`created_at` AS t1_r4, `comments`.`updated_at` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4869ms (Views: 4280.1ms | ActiveRecord: 585.9ms | Allocations: 2103879)

万能感ある。駆動表のキャッシュをしながら、絞り込みもできる。IN長すぎワロタ。

2. preload

Article.preload(:comments).limit(100)

Article Load (1.9ms)    SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (405.8ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4304ms (Views: 3883.6ms | ActiveRecord: 416.9ms | Allocations: 1658640)

eager_loadより省エネな感じ。ただ、最後のSELECT文がcommentsテーブルしか参照してないので絞り込みができない

3. includes

Article.includes(:comments).limit(100)

Article Load (1.5ms)  SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (384.1ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 3737ms (Views: 3339.1ms | ActiveRecord: 395.3ms | Allocations: 1652329)

今回はpreloadと同じ挙動
(includes は場合によって挙動が変わる. 最後のSELECT文にないテーブルのwhereとかはできないので、そういう時はeager_loadになる)

def eager_loading?
  @should_eager_load ||=
    eager_load_values.any? ||
      includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end

4. joins

Article.joins(:comments).limit(100)

Article Load (2.1ms)  SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
-- Completed 200 OK in 67ms (Views: 56.1ms | ActiveRecord: 7.0ms | Allocations: 21703)

いつものアレ。安心感ある。

まとめ

そもそもActiveRecordは LazyLoad、すなわち 必要になったときにSQLが実行される 。
for文の中でSQLが複数回発行(N+1)されないために、こういう機構が必要になってくる。
(生SQLerの私にはこの感覚があんまりない)

後は、where使いたいなら eager_load 何もしないなら preload って感じ。
頭使いたくない人は 「とりあえず includes」 しとけばなんとかなる。
ただ、複数紐づくテーブル扱い出すと最終的に吐き出されるSQLが予測困難になるので、ちゃんと使い分けたいところ。
ちゃんと使い分けたい人はこの記事とか読みましょう。実装追ってて勉強になります。

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

Herokuでデプロイした際にApplication error(エラーコードH10 (App crashed))が出た時の対処法

環境

Ruby 2.6.3
Rails 5.2.3

現象

Railsアプリケーションを作成後、Herokuへデプロイした際に以下画像のエラーが発生。
その際の対処法を備忘録として残しておきます。

https___qiita-image-store.s3.amazonaws.com_0_110452_526e9c21-ec79-5305-c7e5-b536f689a3c4.jpeg

 【対処方法】エラーの原因を把握する

以下コードで状況を把握します。

$ heroku logs --tail

エラーコードが確認できました。

2019-11-26T05:06:44.357974+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=agile-meadow-66722.herokuapp.com request_id=e2c36b19-f3d1-4544-9f84-5ddcb7027cd2 fwd="219.113.146.130" dyno= connect= service= status=503 bytes= protocol=https
2019-11-26T05:06:44.823379+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/favicon.ico" host=agile-meadow-66722.herokuapp.com request_id=c5d323a0-b4be-497c-847b-2d3ebf4942d0 fwd="219.113.146.130" dyno= connect= service= status=503 bytes= protocol=https

エラーコードの中身を読んでみると、code=H10でApp crashedとのこと。
ネットで調べてみたところ、以下のようにリスタートすれば直ることもあると情報があったため試しました。

$ heroku restart -app

もしくは

$ heroku restart -app application_name

しかし、こちらを試してもエラーのままでした。

コンソールを開いて詳細エラー情報を確認する。

エラーの詳細内容を確かめるために以下コードでHeroku上でコンソールを開きました。

$ heroku run rails c

すると、以下エラーコードが出てきました。

Error loading the 'sqlite3' Active Record adapter. Missing a gem it depends on? sqlite3 is not part of the bundle. Add it to your Gemfile. (LoadError)

sqlite3がバンドルされていない?Gemfileに加えろとの指示。
しかし、sqlite3は本番環境に対応していません。

どうやらrails newの時点でpostgresqlの指定を忘れていてsqlite3がデータベース作成時に指定されていたみたいです・・・

アプリ作成初期段階の凡ミスでエラーが発生することになりました。

SQLite3からPostgreSQLに変換

※参照
https://qiita.com/isotai/items/67bafc37a16ea5de5e3b

上記記事を参考にし、
datebase.ymlのsqlite3時点の記載内容を全て消し、
postgresql指定でrails newした場合の記載に変更しました。

gemfileに以下を追加しました。

gem 'pg', '>= 0.18', '< 2.0'

以上。

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

RubyとAWS LambdaでサクッとSlackへポストしてみる

とりあえずAWS Lambdaを動かしてみたかった。
試しなのでブラウザ上で完結するようにしたが、割と時間がかかった。

関数の作成

(AWSアカウントを作成してAWSコンソールにサインインするところまでは省略)

サービス一覧から「Lambda」 → 「関数」 → 「関数の作成」を選択。

関数の作成画面では「一から作成」を選択。関数名は任意。
ランタイムはRuby2.5 (2019/11/26現在) を選択。
ロールはデフォルトのまま。

image.png

「関数を作成」ボタンを選択すれば関数が作成され、編集画面へ遷移する。

image.png

関数の実行

とりあえずサンプルを動かしてみる。
右上の「テスト」を選択し、「イベント名」を適当に入力して作成する。

image.png

作成したイベント名が表示されていることを確認したら、「テスト」を押せば関数が動く。

image.png

Slackのincoming-webhookの設定

Slackにポストするためにwebhookを設定する。
(このあたりは初回設定時とそれ以外で表示が異なるかもしれない)

通知したいチャンネルの設定アイコン → 「アプリを追加する」を選択して、「incoming-webhook」で検索。
ブラウザ側でAppディレクトリへ遷移するはずなので、「Slackに追加」を選択。

image.png

webhookを追加するチャンネルを選択して、「Incoming Webhookインテグレーションの追加」を選択。
webhook URLが作成されるので、これをコピーしておく。
(このときに簡単な使い方なども表示されるので読むといいかも)

LambdaからSlackへポストする

作成したLambda関数からポストしてみる。
試すだけなので、net/httpを利用。
user_ssl = trueを設定しないとBad Requestとなる。

require 'json'
require "net/http"

URL = "https://hooks.slack.com/services/*******"

def lambda_handler(event:, context:)
    uri = URI.parse(URL)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.post(uri.path, { text: "hello from lambda." }.to_json)
end

修正後、「保存」 → 「テスト」 と選択するとリクエストが実行される。
正常に処理された場合、Slackに通知が飛ぶ。

image.png

その他

Lambda自体は無料枠に収まったが、ログを保存するS3の料金が僅かながら発生していた。
回避方法などあるかもしれないが、気にならない程度だったので今回は無視。

次は何らかのトリガーで実行する部分と変数を渡す部分を調べる。

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

kaminariのlast_page?の動作に気をつけて!

主旨と結論

連番で管理するのではなく、「前へ」「次へ」ボタンを設置しての運用です。
記事のポイントは、データが入っていない状態でページネーション機能を使う際に気をつけること
常に何らかのデータが存在する場合は気にする必要はありません。

下記を実現したい場合に参考になります。

  • 最初のページは「前へ」ボタンを消す
  • 最後のページは「次へ」ボタンを消す
  • 最初で最後のページは両方とも消す←ここで次へ問題が発生します。

なので

当初問題のlast_page?の条件分岐は、

<% unless pagenates.last_page? %>
 <a href="<%= path_to_next_page(pagenates) %>">次のページ</a>
<% end %>

のようにしていました。
これだと、データがない状態では、最初で最後のページに次へボタンが表示されてしまうのです。

最終的には下記のようにすればOK

<% unless pagenates.last_page? || paenates.out_of_range %>
 <a href="<%= path_to_next_page(pagenates) %>">次のページ</a>
<% end %>

まとめ

  • last_page?は、現在ページと総ページ数が同じ時にtrueを返し、違う時にfalseを返す。
  • データが存在しない際は、表示されるページ数(current_page)は、1となるが、total_pagesは、0となる。
  • そのため、データが存在しない場合、表示されたページが最初で最後のページとなるものの条件分岐にはout_of_rangeを併用する必要がある。

最初で最後のページの戻り値は下記の通り

メソッド データあり データなし
current_page 1 1
total_pages 1 0
last_page? true false
first_page? true true
out_of_range false true

解説

結論に書きましたが、last_page?は、最後のページの場合trueを返す、ではありません。
last_page?は、現在ページと総ページ数が同じ時にtrueを返し、違う時にfalseを返すです。

kaminariのソースを確認します。

def last_page?
  current_page == total_page
end

これだけではよくわからないので、

  • current_pageメソッド
  • total_pageメソッド

を確認します。

current_pageメソッド

    def current_page
      offset_without_padding = offset_value
      offset_without_padding -= @_padding if defined?(@_padding) && @_padding
      offset_without_padding = 0 if offset_without_padding < 0

      (offset_without_padding / limit_value) + 1
    rescue ZeroDivisionError
      raise ZeroPerPageOperation, "Current page was incalculable. Perhaps you called .per(0)?"
    end

これは、offset_without_paddingの値が0になるのでしょうか。
データがないのでpaddingなど出ないはずです。
最後には、

(offset_without_padding / limit_value) + 1

として、0をlimit_valueで割って+1しているので、基本的に戻り値は1以上であることが想像できます。

そして、下記の実行結果を確認すると、1とでていましたのでよしとしましょう。
お分かりの方コメント頂けると幸いです、、、、!!!

<%= @users.current_page %>

total_pagesメソッド

    def total_pages
      count_without_padding = total_count
      count_without_padding -= @_padding if defined?(@_padding) && @_padding
      count_without_padding = 0 if count_without_padding < 0

      total_pages_count = (count_without_padding.to_f / limit_value).ceil
      max_pages && (max_pages < total_pages_count) ? max_pages : total_pages_count
    rescue FloatDomainError
      raise ZeroPerPageOperation, "The number of total pages was incalculable. Perhaps you called .per(0)?"
    end

これも
count_without_paddingが0になるのはわかります。
あとは単純な計算とif文ですね。
戻り値がtotal_pages_countになるのはわかりますので、答えは0。(お分かりの方コメンry...)

そして、下記の結果は0でした。

<%= @users.total_pages %>

まとめ、last_page?メソッドとout_of_rangeメソッド

def last_page?
  current_page == total_page
end

データがない状態では、

  • current_pageは1
  • total_pageは0

となりますので、最初で最後のページで、last_page?を使って条件分岐してしまうと、falseが返ってきます。

そこで登場するのが

  • out_of_rangeメソッド

これは

    def out_of_range?
      current_page > total_pages
    end

なので、データがない状態ではtrueが返ってきます。
現在ページの方が合計ページを上回ることって普通ないと思うのですが、今回のようなケースを想定していたのでしょうか。

git_hub、紹介したメソッドが定義されているページ

Git_Hub/kaminari
https://github.com/kaminari/kaminari/blob/c5186f5d9b7f23299d115408e62047447fd3189d/kaminari-core/lib/kaminari/models/page_scope_methods.rb

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

libvipsを使ってRails6でActiveStorageする【Windows,Ubuntu環境】

この記事でつくるもの

RailsのActiveStorageを使って簡単なサムネイル表示するCRUDを作成します。
tmp.gif

手順

  • Ubuntuにlibvipsの最新ソースを持ってきて、ビルドとインストール
  • UbuntuからWindow環境をマウントしてrails new
  • ActiveSrorageに必要なRailsコマンドと設定
  • 必要なgemのインストール
  • Ubuntuからrails s

Ubuntuにlibvipsの最新ソースを持ってきて、ビルドとインストール

RailsのActiveStorageはlibvipsまたはImageMagickが必要です。
この記事では実行速度の速いlibvipsを使用します。

まずは必要なパッケージをインストールします。

Ubuntu
$ sudo apt-get install git build-essential libxml2-dev libfftw3-dev \
    libmagickwand-dev libopenexr-dev liborc-0.4-0 \
    gobject-introspection libgsf-1-dev \
    libglib2.0-dev liborc-0.4-dev


gitソースから取得したソースをビルドするのに必要なパッケージ

Ubuntu
$ sudo apt-get install automake libtool swig gtk-doc-tools 


libvips取得し、ビルドとインストール

Ubuntu
$ git clone https://github.com/libvips/libvips.git
$ cd libvips
libvips$ ./autogen.sh
libvips$ make
libvips$ sudo make install


PATHの設定

Ubuntu
$ export VIPSHOME=/usr/local
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$VIPSHOME/lib
$ export PATH=$PATH:$VIPSHOME/bin
$ export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$VIPSHOME/lib/pkgconfig
$ export MANPATH=$MANPATH:$VIPSHOME/man

UbuntuからWindow環境をマウントしてrails new

UbuntuではWindow環境をマウントして参照できます。
Windowsからの方がソース編集しやすいので、マウントしてrails newします。

Ubuntu
$ cd /mnt/c/tmp_rails
/mnt/c/tmp_rails$ rails new sample
/mnt/c/tmp_rails$ cd sample

ActiveSrorageに必要なRailsコマンドと設定

ActiveSrorageを使うためのマイグレーションファイルを作成

Ubuntu
/mnt/c/tmp_rails/sample$ rails active_storage:install


Userモデルをscaffoldで作成

Ubuntu
/mnt/c/tmp_rails/sample$ rails g user name image:attachment


Userモデルにはimage:attachmentによりhas_one_attachedが追加される

app/models/user.rb
class User < ApplicationRecord
  has_one_attached :image
end


モデルをマイグレーションする

Ubuntu
/mnt/c/tmp_rails/sample$ rails db:migrate


showページでサムネイル表示するようにする

app/views/users/show.html.erb
<p>
  <strong>Image:</strong>
  <%= image_tag @user.image.variant(resize_to_limit: [100, 100]) %>
</p>


libvipsを使うように指定

app/config/application.rb
module Sample
  class Application < Rails::Application
    config.load_defaults 6.0
    config.active_storage.variant_processor = :vips #追記
  end
end

必要なgemのインストール

image_processingはコメントアウトを外し、ruby-vipsは追記します

Gemfile
gem 'image_processing', '~> 1.2'
gem 'ruby-vips'

Ubuntuからrails s

Ubuntu
/mnt/c/tmp_rails/sample$ rails s

これで http://127.0.0.1:3000/users をブラウザから開けばデモのように動きます。

参考

https://github.com/libvips/libvips/wiki/Build-for-Ubuntu

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

RubyMineをさらに便利にするプラグインたち:後編

はじめに

Ateam cyma Adevent Calendar 2019 の 3日目です。
本日の担当は昨日に引き続き、エイチームのEC事業本部でWebアプリケーションエンジニアをしている@hibiheionです。

この記事はRubyMineをより便利にするプラグインたち:前編の続きです。
後編では、あると便利なプラグインを紹介します。

紹介内容について

前編と同じ内容ですが、紹介方法について改めて記載しておきます。

  • 各プラグインの基本機能のみ説明しています。詳細は各プラグインのWebページをご確認ください。
  • 特定の言語やサービスを使えるようにするものは対象外としています。
  • 個人的な評価を三段階でつけています。
    • 優:ないと困る
    • 良:使用頻度が多め
    • 可:使用頻度が少なめ
  • ショートカットはキーマップ次第で変わるのでメニューからの操作で説明しています。なお、メニューから操作するとショートカットキーを教えてくれるプラグインもあります。
  • プラグインの情報は記事を公開した2019年12月時点のものです。

プラグインの紹介

一覧

全部で20個あります。
順序は評価と名称の順です。
後編では「AceJump」から「SQL Formatter」の11個を説明します。
残りのプラグインは前編で説明しています。

名称 概要 評価
Grep Console tail -fで出力したログをGrep検索で絞り込める 優
highlightbracketpair カーソルがある箇所を囲んでいるカッコをハイライト表示する 優
IdeaVim Vimと同じような操作で使用できるようにする 優
Key Promoter X ショートカットキーが存在する操作をマウス等から実行した際に操作内容をショートカットキーとともに記録する 優
Quick File Preview ファイル一覧で選択したファイルのプレビューを表示する 優
Railways Railsのルーティングの一覧を表示する 優
Rainbow Brackets 対になるカッコを色分けして表示する 優
Scroll From Source Projectツールウィンドウ(ファイル一覧)の開いているファイルの場所に移動する 優
Tabdir タブ一覧で同じ名前のファイルにはディレクトリ名を表示する 優
AceJump ページ内の指定した文字にカーソルを移動する 良
Git Scope ファイルの一覧からGitの差分を確認できる 良
GitToolBox Git連携の機能を強化する 良
MultiHighlight 複数の検索結果を異なった色でハイライト表示する 良
String Manipulation 色々な方法で文字列を操作する 良
BrowseWordAtCaret カーソルがあたっている単語をファイル内検索する 可
JSON Viewer JSONを見やすい形に整形する 可
Open in splitted tab 参照先を分割したウィンドウで表示する 可
Pipe Table Formatter パイプ(|)を使ったテーブルを揃える 可
Realigner 指定した文字で複数行の連結、複数行への分割、文字列の囲い込みができる 可
SQL Formatter SQLを見やすい形に整形する 可

AceJump

https://plugins.jetbrains.com/plugin/7086-acejump/

機能

  • ページ内の指定した文字にカーソルを移動する
    • 例えば、ページ内の「A」という文字がある場所に少ないタイプ数で移動できる
  • 効率よくカーソルを移動できる
  • 使用例
    「d」をタイプすると下記のように「d」の位置にナビゲーションが表示され、この状態で「j」をタイプすると「def」にカーソルを移動する ace_jump.png

使い方

説明のために機能の検索から実行していますが、ショートカットキーを割り当てて使うのが現実的です。

  • 指定した文字に移動
    1. shiftキーを2回押して表示される検索ウィンドウで「acejump」を検索する
    2. 検索結果から「Activate AceJump mode」を選択する
    3. 移動したい文字(1文字)をキーボードからタイプする
    4. ページ内のタイプした文字の左にナビゲーションが出るので、移動したい先のアルファベットをキーボードからタイプする
  • 指定した行に移動
    1. shiftキーを2回押して表示される検索ウィンドウで「acejump」を検索する
    2. 検索結果から「Display Line Markers」を選択する
    3. ページ内の各行にナビゲーションが出るので、移動したい先のアルファベットをキーボードからタイプする

補足

  • 機能を限定したAceJump-Liteというプラグインもある
    • 個人的にはAceJump-Liteのほうがナビゲーションが見やすいのでこちらを使っている

Git Scope

https://plugins.jetbrains.com/plugin/10083-git-scope/

機能

  • ファイルの一覧からGitの差分を確認できる
    • 内容はトップメニューの「VCS→Git→Commit file…」で出てくる内容と同じ
  • Git Scopeはサイドメニューに出せるため確認しやすい

使い方

  • ツールウィンドウの表示
    • サイドメニューから「Git Scope」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→Git Scope」から表示できる
  • ツールウィンドウの操作
    • ツールウィンドウには差分のあるファイルの一覧が表示される
    • ファイル名をクリックすると差分を確認できる

GitToolBox

https://plugins.jetbrains.com/plugin/7499-gittoolbox/

機能

  • Git連携の機能を拡張する
  • 次のような機能が増える
    • ステータスバーに出す情報を増やす
      • ステータスの表示
      • 今いる行の最終更新者の表示(git blameの情報)
    • blame(更新情報)が見れるようになる
    • ブランチにタグをつけられる
    • Gitのリモートリポジトリを定期的に自動でフェッチし、変更があれば通知する

使い方

  • プラグインの設定の変更
    • 設定画面で「Other Settings→GitToolBox Global」を開く
      • 自動フェッチ以外の表示項目などの設定
    • 設定画面で「Other Settings→GitToolBox Project」を開く
      • プロジェクト別の自動フェッチの設定
  • git blameの確認
    • 右クリックメニューの「Git→show inline blame」にチェックを入れると、カーソルが当たっている行のblameをつねに表示する(意外と邪魔にならない)
    • 右クリックメニューの「Git→show blame details」でカーソルのある行の詳細なblameをポップアップで表示する
      • ポップアップからコミット履歴の確認やリビジョンのコピーなどができる
  • ブランチへのタグの追加
    • 右クリックメニューの「Git→repository→Push Tags on Branch」を選択する

MultiHighlight

https://plugins.jetbrains.com/plugin/9511-multihighlight/

機能

  • 複数の検索結果を異なった色でハイライト表示する
  • 込み入ったコードを読まないといけないときにあると便利
  • 使用例 multihighlight.png

使い方

説明のために機能の検索から実行していますが、ショートカットキーを割り当てて使うのが現実的です。

  • ハイライト表示の切り替え
    1. 対象の文字列を選択する
    2. shiftキーを2回押して表示される検索ウィンドウで「multihighlight」を検索する
    3. 検索結果から「MultiHighlight: toggle highlight」を選択する
  • ハイライト表示の全クリア
    1. shiftキーを2回押して表示される検索ウィンドウで「multihighlight」を検索する
    2. 検索結果から「MultiHighlight: clear highlights in current editor」を選択する

String Manipulation

https://plugins.jetbrains.com/plugin/2162-string-manipulation/

機能

  • 色々な方法で文字列を操作する
  • 操作内容は盛りだくさん
    • フォーマット(JSON等)ごとに文字列をエスケープする
    • エンコードする(URLエンコード等)
    • スネークケース・キャメルケースといった記述方式を変換する
    • 数値に対して値の加減算などをの計算を行う
    • 複数の行を並び替える
    • 条件を指定して不要な行を削除する
    • 余分な半角スペースを取り除く(トリム)
    • 複数の行の左端や右端を揃える

使い方

  • 文字列の操作方法
    1. 操作対象の文字列を選択する
    2. 右クリックメニューの「String Manupilation」を選択する
  • プラグインの設定の変更
    • 設定画面で「Other Settings→String Manipulation」を開く

BrowseWordAtCaret

https://plugins.jetbrains.com/plugin/201-browsewordatcaret/

機能

  • カーソルがあたっている単語をファイル内検索する
    • 通常のファイル内検索でwordsにチェックを入れて検索したときの検索方法を使う
  • IdeaVimを使っていると、「*」の検索で同じことができる
  • 通常のファイル内検索より少ない手数で使用できる

使い方

説明のために機能の検索から実行していますが、ショートカットキーを割り当てて使うのが現実的です。

  • 下方向に検索
    1. 検索したい単語にカーソルを合わせる
    2. shiftキーを2回押して表示される検索ウィンドウで「browse to」を検索する
    3. 検索結果から「browse to next word」を選択する
  • 上方向に検索
    1. 検索したい単語にカーソルを合わせる
    2. shiftキーを2回押して表示される検索ウィンドウで「browse to」を検索する
    3. 検索結果から「browse to previous word」を選択する

JSON Viewer

https://plugins.jetbrains.com/plugin/9679-json-viewer/

機能

  • JSONを見やすい形に整形する
  • レスポンスの解析などで使う

使い方

  • ツールウィンドウの表示
    • サイドメニューから「JSON Viewer」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→JSON Viewer」から表示できる
  • JSONの整形
    1. JSONをコピーし、ツールウィンドウにペーストする
    2. ツールウィンドウの「Format」ボタンをクリックする

Open in splitted tab

https://plugins.jetbrains.com/plugin/7407-open-in-splitted-tab/

機能

  • 参照先を分割したウィンドウで表示する

使い方

  1. ジャンプしたいメソッド等にカーソルを合わせる
  2. 右クリックメニューの「Go To→Open in split tab」を選択する

Pipe Table Formatter

https://plugins.jetbrains.com/plugin/7550-pipe-table-formatter/

機能

  • パイプ(|)を使ったテーブルを揃える
    • RubyではCucumberやTurnipのテストフィーチャ(Gherkin)のテーブルで使う
  • ただ、日本語などのマルチバイト文字が含まれていると揃って見えない
  • 操作例
    • 操作前
      pipe_before.png
    • 操作後
      pipe_after.png

使い方

  • カーソルが当たっているテーブルを揃える
    1. 対象のテーブルにカーソルを合わせる
    2. 右クリックメニューの「Pipe Table→Format」を選択する
  • ファイル内のすべてのテーブルを揃える
    1. 対象ファイルを開く
    2. 右クリックメニューの「Pipe Table→Format All」を選択する
  • テーブルに列を追加する
    1. 列を追加したい位置の後ろにカーソルを移動する
    2. 右クリックメニューの「Pipe Table→Add Column Before」を選択する
  • テーブルを選択する
    1. 対象のテーブルにカーソルを合わせる
    2. 右クリックメニューの「Pipe Table→Select Table」を選択する

Realigner

https://plugins.jetbrains.com/plugin/7082-realigner/

機能

  • 指定した文字で複数行の連結、複数行への分割、文字列の囲い込みができる
    • 例えば、ハッシュの項目が増えたときに「,」で複数行に分けられる
    • 分割や結合に使う文字は決められる

使い方

  • 文字列の分割
    1. 分割する文字列を選択する
    2. トップメニューの「Edit→Split into Lines」を選択する
    3. 分割に使う文字などを選択する
  • 文字列の結合
    1. 結合する文字列を選択する
    2. トップメニューの「Edit→Join Lines With Glue」を選択する
    3. 結合に使う文字などを選択する
  • 文字列の囲い込み
    1. 囲む文字列を選択する
    2. トップメニューの「Edit→Wrap」を選択する
    3. 囲む文字などを選択する
      • 開始文字と終了文字で別の文字を指定できる

SQL Formatter

https://plugins.jetbrains.com/plugin/10858-sql-formatter/

機能

  • SQLを見やすい形に整形する
  • ログに出ているSQLをコピペしてSQLを確認できる

使い方

  • ツールウィンドウの表示
    • サイドメニューから「SQL Formatter」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→SQL Formatter」から表示できる
  • SQLの整形
    1. SQLをコピーし、ツールウィンドウにペーストする
    2. ツールウィンドウの「Format」ボタンをクリックする

まとめ

RubyMineのプラグインは以下のサイトで検索できます。
https://plugins.jetbrains.com/search?products=ruby

メジャーなプラグインで出てきていないものもありますが、それは私の使い方に合わなかったためです。
例えば、前編で紹介したIdeaVimはVimmer以外には不要なものですよね。
時間があるときにでも探してみれば、自分にあったプラグインがきっと見つかると思います。

最後にプラグインの開発者の皆様に対して感謝を述べて締めたいと思います。
プラグインを作ってくださってありがとうございます!

次回予告

Ateam cyma Adevent Calendar 2019 の 3日目の記事は以上です。
2回にわたりお付き合いいただきありがとうございました。

4日目はエンジニアの@bayasistさんが担当します。
Railsのフォームオブジェクトに関する記事です。

さいごに

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

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

RubyMineをさらに便利にするプラグインたち:前編

はじめに

Ateam cyma Adevent Calendar 2019 の 2日目です。
本日の担当はエイチームのEC事業本部でWebアプリケーションエンジニアをしている@hibiheionです。
業務では主に自転車ECサイトcymaのバックエンドの機能をRailsで書いています。

本題

みなさんはエディタは何を使ってますか?
私は業務ではRailsを触っているのですが、エディタはRubyMineを使っています。
RubyMineは有料なだけあってRailsでの開発をとても効率よくできるようになっています。
RubyMineは他のエディタと同じようにプラグインで機能を拡張することも可能です。
ただ、RubyMineは他のエディタと比べると日本語でプラグインの情報が見つかりにくい気がします。

そんなわけで、RubyMineのプラグインの情報をまとめれば役に立つのではないかと思ったので、自分が使ってみて良かったと感じたプラグインのざっくりとした使い方を紹介します。
この記事でRubyMineを使った開発がより便利になれば幸いです。

なお、紹介するプラグインの多くはPHPStorm等の他のJetBrains製品でも利用できます。

紹介内容について

  • 各プラグインの基本機能のみ説明しています。詳細は各プラグインのWebページをご確認ください。
  • 特定の言語やサービスを使えるようにするものは対象外としています。
  • 個人的な評価を三段階でつけています。
    • 優:ないと困る
    • 良:使用頻度が多め
    • 可:使用頻度が少なめ
  • ショートカットはキーマップ次第で変わるのでメニューからの操作で説明しています。なお、メニューから操作するとショートカットキーを教えてくれるプラグインもあります。
  • プラグインの情報は記事を公開した2019年12月時点のものです。

プラグインのインストール方法

プラグインの紹介の前に念のためプラグインのインストール方法を紹介しておきます。

  1. 設定画面を開く
    • Macではトップメニューの「RubyMine→Preferrences」を選択する
    • Windowsではトップメニューの「File→Settings」を選択する
  2. 「Plugins」を開く
  3. 「Marketplace」から検索してインストールする

プラグインの紹介

一覧

全部で20個あります。
順序は評価と名称の順です。
記事が長くなったため、前後編の2回に分割しました。
今回は個人的には手放せない「Grep Console」から「Tabdir」の9個を紹介します。

名称 概要 評価
Grep Console tail -fで出力したログをGrep検索で絞り込める 優
HighlightBracketPair カーソルがある箇所を囲んでいるカッコをハイライト表示する 優
IdeaVim Vimと同じような操作で使用できるようにする 優
Key Promoter X ショートカットキーが存在する操作をマウス等から実行した際に操作内容をショートカットキーとともに記録する 優
Quick File Preview ファイル一覧で選択したファイルのプレビューを表示する 優
Railways Railsのルーティングの一覧を表示する 優
Rainbow Brackets 対になるカッコを色分けして表示する 優
Scroll From Source ファイル一覧で開いているファイルの場所に移動する 優
Tabdir タブ一覧で同じ名前のファイルにはディレクトリ名を表示する 優
AceJump ページ内の指定した文字にカーソルを移動する 良
Git Scope ファイルの一覧からGitの差分を確認できる 良
GitToolBox Git連携の機能を強化する 良
MultiHighlight 複数の検索結果をハイライト表示する 良
String Manipulation 色々な方法で文字列を操作する 良
BrowseWordAtCaret カーソルがあたっている単語をファイル内検索する 可
JSON Viewer JSONを見やすい形に整形する 可
Open in splitted tab 参照先を分割したウィンドウで表示する 可
Pipe Table Formatter パイプ(|)を使ったテーブルを揃える 可
Realigner 指定した文字で複数行の連結、複数行への分割、文字列の囲い込みができる 可
SQL Formatter SQLを見やすい形に整形する 可

GrepConsole

https://plugins.jetbrains.com/plugin/7125-grep-console/

機能

  • tail -fで出力したログをツールウィンドウに表示する
    • tail -fで出力するログをGrep検索で絞り込める
    • 設定に基づいてログをハイライト表示する
      • Rails用、SQL用など用途に応じた設定が可能
      • 例えば、Rails用ではFATALを赤くする、SQL用ではINSERTを赤くする、といったことができる
      • 設定内容は慣れないとわかりにくい
  • 設定に基づいてファイルをハイライト表示する
    • 基本的にログファイルを対象に使う
  • 確認したいログを見つけやすくなる

使い方

  • tailログに関する機能
    • tailログの開始
      • トップメニューの「Tools→Tail Fail in Console」もしくは「Tools→Tail Current File in Console」を選択する
        • エディタで開いているファイルのログを出力するときに後者を使う
    • tailログの絞り込み
      1. ログ出力のツールウィンドウで右クリックする
      2. 右クリックメニューの「Grep」を選択する
      3. Grep用の表示に切り替わるので、「Expression」に条件を入力する
        • この状態でログが増えると、条件に該当するログだけが表示される
        • 「RELOAD」ボタンをクリックすると、出力済みのログから条件に該当するものだけを絞り込む
    • tailログのハイライト表示
      1. ログ出力のツールウィンドウで右クリックする
      2. 右クリックメニューの「Show Grep Console Statistics in Console」を選択する
      3. 設定用のウィンドウが表示されるので設定を行い、「OK」をクリックする
  • ファイルに関する機能
    • ファイルのハイライト表示
      1. 対象のファイルを開いている状態で、トップメニューの「Tools→Highlight Editor According to Grep Console Settings」を選択する
      2. 設定用のウィンドウが表示されるので設定を行い、「OK」をクリックする
    • ファイルのハイライト表示の解除
      • ハイライト表示した状態で、トップメニューの「Tools→Clear Grep Highlight in Editor」を選択する

HighlightBracketPair

https://plugins.jetbrains.com/plugin/10465-highlightbracketpair/

機能

  • カーソルがある箇所を囲んでいるカッコをハイライト表示する
  • ソースコードが読みやすくなる
  • 表示例
    • このプラグインを入れていない場合
      highlight_bracket_pair_before.png
    • このプラグインを入れている場合
      highlight_bracket_pair_after.png

使い方

  • プラグインを有効にするだけ

IdeaVim

https://plugins.jetbrains.com/plugin/164-ideavim/

機能

  • Vimと同じような操作で使用できるようにする
    • 私はVimからRubyMineに乗り換えたのですが、さほど違和感なく使用できています
  • RubyMineの機能も同時に使用できる
    • IdeaVimの状態がノーマルモードでも挿入モードでも動作する
  • IdeaVimがCtrlキーを使うため、RubyMineのキーマップの調整が必要
    • Macはまだしも、Windowsだと影響が大きい

使い方

  • ON・OFFの切り替え
    • トップメニューの「Tools→Vim Emulator」でON・OFFを切り替えられる
  • プラグインの設定の変更
    • 本家Vimの「.vimrc」と同じように、「.ideavimrc」というファイルで設定を変更できる
  • RubyMineと重複しているキーマップの確認
    • 設定画面の「Editor→Vim Emulation」からRubyMineと重複しているキーマップを確認できる
    • Vim Emulatorが有効な状態で確認する必要がある

Key Promoter X

https://plugins.jetbrains.com/plugin/9792-key-promoter-x/

機能

  • ショートカットキーが存在する操作をマウス等から実行した際に操作内容をショートカットキーとともに記録する
  • ショートカットキーの存在の気づくことができるため、ショートカットキーを覚えやすくなる

使い方

  • 操作の記録
    • プラグインを有効にするだけ
  • 操作履歴の確認
    • サイドメニューから「Key Promoter X」のツールウィンドウを開く
    • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→Key Promoter X」から表示できる

Quick File Preview

https://plugins.jetbrains.com/plugin/12778-quick-file-preview/

機能

  • Projectツールウィンドウ(ファイル一覧)で選択したファイルのプレビューを表示する
    • プレビューといっても普通にファイルを開いたときとの違いはほとんどなく、プレビューの状態でも内容を変更できる
    • 普通にファイルを開いたときと違い、Projectツールウィンドウで他のファイルに移動したときにプレビューは自動的に閉じられる
  • 複数のファイルを辿っていくときに便利

使い方

  • 他の設定の影響を受けるため、以下の事前準備が必要
    1. Projectツールウィンドウの設定を開く
    2. 「AutoScroll to Source」を無効にする
  • プレビューの表示
    • Projectツールウィンドウで対象ファイルをクリックする
      • ダブルクリックしたときに正式にファイルを開く
    • プラグインの設定によって変わるため、これは初期設定の場合の動作
  • プラグインの設定の変更
    • 設定画面で「Editor→General→Quick File Preview」を開く

Railways

https://plugins.jetbrains.com/plugin/7110-railways/

機能

  • Railsのルーティングの一覧を表示する
    • ルーティングを検索できる
    • 選択したルーティングのアクションに移動できる
  • ルーティングが把握しやすくなる

使い方

  • サイドメニューから「Routes」のツールウィンドウを開く
  • サイドメニューに出ていない場合、トップメニューの「View→Tool Windows→Routes」から表示できる

Rainbow Brackets

https://plugins.jetbrains.com/plugin/10080-rainbow-brackets/

機能

  • 対になるカッコを色分けして表示する
    • Rubyでは()、[]、{}が対象
    • HTMLではペアになるタグの<>を同じ色で表示する
  • ソースコードが読みやすくなる
  • 表示例
    • このプラグインを入れていない場合
      rainbow_brackets_before.png
    • このプラグインを入れている場合
      rainbow_brackets_after.png

使い方

  • カッコの色分け
    • プラグインを有効にするだけ
  • プラグインの設定の変更
    • 設定画面で「Other Settings→Rainbow Brackets」を開く

Scroll From Source

https://plugins.jetbrains.com/plugin/7606-scroll-from-source/

機能

  • Projectツールウィンドウ(ファイル一覧)で開いているファイルの場所に移動する
  • 他の方法でも開いているファイルに移動できるが、そちらの方法よりショートカットが割り当てられる・任意のタイミングで実行できるという利点がある
    • ナビゲーションバー(パンくずリスト)から「Jump To Source」したときと違い、ショートカットキーが割り当てられる
    • Projectツールウィンドウの設定で「Autoscroll from Source」を有効にした場合はファイルを開いたときに自動で移動するが、このプラグインでは必要なときにだけ移動できる

使い方

  1. ファイルを開いている状態で右クリックする
  2. 右クリックメニューの「Scroll From Source」を選択する

Tabdir

https://plugins.jetbrains.com/plugin/5045-tabdir/

機能

  • タブ一覧で同じ名前のものが存在するファイルにはディレクトリ名を表示する
    • RubyMineの標準機能だと同じ名前のファイルがタブ一覧にないとディレクトリ名が出ないが、このプラグインでは同じ名前のファイルがタブ一覧になくても良い
  • Railsではどうしても同じ名前のファイルが増えるので、このプラグインがあればファイルを見分けやすくなる
  • 表示例
    • このプラグインを入れていない場合
      tabdir_before.png
    • このプラグインを入れている場合
      tabdir_after.png

使い方

  • 他の設定の影響を受けるため、以下の事前準備が必要
    1. 設定画面で「Editor→General→Editor Tabs」を開く
    2. 「Show directory for non-unique file names」のチェックを外す
  • ディレクトリ名の表示
    • プラグインを有効にするだけ
  • プラグインの設定の変更
    • 設定画面で「Other Settings→Tabdir」を開く

次回予告

Ateam cyma Adevent Calendar 2019 の 2日目の記事は以上です。

3日目は引き続き私@hibiheionが担当します。
内容はもちろん「RubyMineをより便利にするプラグインたち:後編」です。
今回紹介しきれなかった残りの11個のプラグインを紹介します。

さいごに

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

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

Ruby記事まとめ

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

※Ruby記事まとめ

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

RSpecのlet(let!)とは

letとは

  • RSpecのメソッドのひとつ
  • letは呼ばれた時に初めてデータを読み込むという遅延読み込みをするメソッド。letはboforeブロックの外部で呼ばれるため、セットアップのコードを減らすことが可能。
  • before ブロックの場合では各dscribeやcontextの前に毎回呼び出される。これはテストに予期しない影響を及ぼす恐れがある。また、不要なデータ作成をしてテスト実行を遅くする原因になることもある。

beforeブロックとの違い

  • letは遅延読み込み
  • letはインスタンス変数に格納しない

let!

  • let!は遅延読み込みされないlet
  • すべてのテストが余分なデータを持つ可能性がある

letとlet!,beforeブロックの使い分け

  • 決まった定義はないがテストはDRYを意識するあまり読みにくくなってしまったら本末転倒。
  • よって、読みやすいと思った方を使用していく。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIキー・環境変数を設定したのにうまく反映されない時には.....

概要

メルカリのクローンサイトを作成する際にインフラ、デプロイ関係の担当をした者が沼にハマった部分を共有し、同じような状態から抜け出すための一助になればと思ったことが投稿するきっかけになります。
今回は、API関連のキーや環境変数を設定したのに本番環境では上手く挙動しない際に対処した方法をいくつか紹介します!
基本的なことから盲点な部分まで、同じような状況で困っている方の一助に少しでもなれたら幸いです。

環境

  • Rails v5.2.3
  • Ruby v2.5.1
  • Unicorn
  • capistrano
  • nginx
  • mysql
  • AWS, EC2(本番環境)

①まずはスペルミス、スペース(空白)の確認

最初から言われなくても分かるよっていう内容ですいません。
しかし、スペルミスは皆さん気をつけていることだとは思いますが、意外と足を引っ張るのはスペース(空白)の確認です。

(例)
AWS_SECRET_ACCESS_KEY = '************************'
=> スペースあり
AWS_SECRET_ACCESS_KEY='************************'
=> スペースなし

上記の微妙な違いで、内容は正確だがキーや環境変数が反映されない状態に陥ってしまいます。
まずは初心に戻った気持ちでスペース(空白)のチェックをしてみるのも良いかもしれません。

②設定を反映させるためには...

スペルミスやスペースもなく設定が完了。以上終わり。 ではないのです。
設定した内容を反映させるためにはAWS(EC2)であれば、一度EC2上からログアウトし再度ログインすることによって設定が反映されます。
ただし、 .bash_profile などに設定をし直ぐに反映をさせたい場合は以下のコマンドを打つことによって反映されます。

source ~/.bash_profile

設定ファイル名は任意のもので大丈夫です。
また、設定ファイルの違いって案外知らないと思うので(自分は笑)、気になる方は下記にまとめたので見てみてください!

読み込み順 設定ファイル 概要
1 /etc/profile ログインする全ユーザー共通設定
2 ~/.bash_profile ※下記に概要記載
3 ~/.bashrc bash起動時に読み込ませたい設定
4 /etc/bashrc システム全体の関数等の設定

※ ~/.bash_profileに関しては、~/.bash_login, ~/.profileと順に、上のファイルが無ければ下のファイルが読み込まれる流れになります。

③capistrano(自動デプロイ)導入後に設定が反映されない時

今回の投稿で一番伝えたかった内容になります。なぜかと言いますと...自身が沼にハマったからです。笑
スペルミス、余計なスペースも無い、EC2からログアウト・再ログインして設定の反映もバッチリ。さあ、後はcapistranoを動かして挙動を見に行きますか.....? んん? あれ、反映されていない。(白目)
この状況で沼にハマり、かれこれ2~3時間。 やっと見つけた救世主のコマンドを紹介します。(※今回はcapistranoを使用しています。)

bundle exec cap -t production unicorn:stop
=> unicornの停止。これを打たないとコードが反映されていない状況に。
bundle exec cap production deploy
=>再度capistranoで自動デプロイの実行。これで反映。

この2つのコマンドを実行することによって、本番環境でAPIを利用したり、しっかりと環境変数も用いることができていました。

終わりに

インフラ関連に対する苦手意識を持っていましたが、何事も根拠があって、それを理解することによって物事の流れを掴むことができ、やりがいに繋がっていくと感じます。僕もまだまだこれからですが、また沼ったことを共有できればと考えています。長々と見て頂きありがとうございました。

参照

https://qiita.com/yunzeroin/items/480a3a677f78a57ac52f

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

rails 自動更新

ajaxを用いて自動更新機能を実装する。
今回は前回、作成した非同期通信のコードに書き加える形で自動更新を実装する。

完成品

Image from Gyazo

環境

ruby:2.5.1
rails:2.5.3
DB:mysql(Sequel Pro)
ブラウザ:Google
OS:Mac(10.14.6)

Github

https://github.com/tana1818/auto_update

作成する前に

なぜ必要か

日々使ってるLINEを考えてみましょう。
LINEのように、相手側が入力すると自分側が、LINEを開いたままでも、
リアルタイムで更新されます。あれも自動更新機能が実装されています。
自動更新がないままLINEのチャット機能を使用するとなると
表示されている画面をいちいちリロードしないと画面が更新されません。
これを一人ならまだしも、グループでチャットをするとなると面倒です。

自動更新の大きなメリットはリロードしなくてもリアルタイムでデータが更新されるという点です。

通信の流れ

実装に移る前に通信の流れを把握しておきましょう。
①ブラウザからindexアクションのリクエストが来たのでサーバーはレスポンスを返す
②ブラウザはサーバーからのレスポンスを解析
③解析して必要な全てのアセット (JavaScriptファイル、スタイルシート、画像) をサーバーから取得する
④JavaScriptを読み込む(最新のデータがあったら取得、なかったら取得しない)
⑤json形式で返ってきた値をcontrollerで判別
⑥最後にviewとjbuilderの情報を返す

同じ画面に居続けたら③から⑥を繰り返し自動更新が行われる

作成手順

ここでは自動更新機能を実装するだけの人用で書いていくが
一応データベースの情報はいじってないので自分の非同期通信を実装した方がいれば
「コード編集(ビュー)」の手順からファイルに追記していけば同じデータベースで自動更新機能が実装できる。

結論、データベースの名前が一律"ajax"になる。
もし「非同期通信」と「自動更新」のファイルを比較化をしたいって人は
データベースの名前を決めるファイル(database.yml)を編集してrails db:createしてください

 

Githubからコードをダウンロード

下記のURLからコードをダウンロード
https://github.com/tana1818/ajax
1.png

ダウンロードが完了したらファイル名を変更(なんでも良い)

任意のディレクトリにファイルうを配置したらファイル名変更
自分は自動更新なので’auto_update’って名前にしました。

 

gemのインストール、DBの作成、マイグレーション実行、サーバー起動

auto_update
bundle install #gemのインストール
rake db:create #DB作成
rake db:migrate  #マイグレーションファイルを実行する
rails s  #サーバーの起動

http://localhost:3000/
を検索

下図のように表示されたらOK
2.png

コード編集(ビュー)

①タイトルバーの名前変更

views/fruits/application.html.haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    //下記記述変更
    %title Auto_update
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

 
②indexにdata-id付与

views/fruits/index.html.haml
.container 
  .row
    %h1 Listing fruit
    - if flash[:notice]
      %p= flash[:notice]

  .row
    %table.table.table-bordered.table-sortable
      %thead
        %tr
          %th Name
          %th
          %th
          %th
      %tbody.fruit_item
        - @fruits.each do |fruit|
          //下記追加('item'クラスにデータのid情報追加)
          %tr.item{"data-id": "#{fruit.id}"}
            %td
              = fruit.name
            %td
              = link_to 'Show', fruit
            %td
              = link_to 'Edit', edit_fruit_path(fruit)
            %td
              = link_to 'Destroy', fruit, data: {confirm: 'Are you sure?'}, method: :delete

  .row
    %h1 New fruit
    = render 'form'

 

jsファイル編集

assets/javascripts/fruits.js
$(function(){
  //非同期通信
  function buildHTML(fruit){ //通信が成功するとdoneメソッドの引数にデータが入るようになっているため、これを利用してHTMLを組み立てる
    //5行目にdata-id = ${fruit.id}を追加
    var html = `<tr class = "item ui-sortable-handle" data-id = ${fruit.id}>
                  <td>
                    ${fruit.name}
                  </td>
                  <td>
                    <a href = /fruits/${fruit.id}>Show</a>
                  </td>
                  <td>
                    <a href = /fruits/${fruit.id}/edit>Edit</a>
                  </td>
                  <td>
                  <a data-confirm = "Are you sure?", rel = "nofollow", href = /fruits/${fruit.id}, data-method = "DELETE">Destroy</a>
                  </td>
                </tr>`
    return html;
  }
  $('#new_fruit').on('submit', function(e){ //'#new_fruit'の'submit'が押された時に発火
    e.preventDefault(); //これを書いてるせいで'submit'を押した際に要素にdisabledが付与される→最後の.alwasが書いてある行を足すことでそのdisabled要素を削除してる
    var formData = new FormData(this); //FormDataオブジェクトの引数はthisとなってる。イベントで設定したfunction内でthisを利用した場合はイベントが発生したDOM要素を指す。今回であればnew_commentというIDがついたフォームの情報を取得している
    $.ajax({
      url: "/fruits/", //ここはアクションのURLなのでrails routesで確認
      type: "POST", //ここはアクション名
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){ //非同期通信の結果として返ってくるデータは、done(function(data) { 処理 })の関数の引数で受け取る
    var html = buildHTML(data);
      $('.fruit_item').append(html) //htmlに追加
      // $('.b').val('') //なんかエラー出るなーと思ったらvarと書き間違えていた(ここでは'submit'を押した後テキストボックスに空の要素を付与してる)
      $('.new_fruit')[0].reset() //初期値があれば初期値にリセットされる
    })
    .fail(function(){ //エラーが置きた際
      alert('投稿できませんでした')
    });

    // .always(function(){ //この記述を書いてないと連続で投稿できない
    //   $(".c").removeAttr("disabled")
    // })
    return false;
    //要素の効果を無効化する、ちょっとわかんないけどとりま親要素のクリックをなかったことにするっぽいw
    //上記のコメント化記述は'disabled'を消しにいってる
    //こっちの方が記述量が少なくて良い
  })


  //自動更新
  if (location.pathname.match()){ //もし現在のURLパスがindexアクションだったら(http://localhost:3000/fruitsもしくはhttp://localhost:3000)
    setInterval(update, 5000);//5000ミリ秒ごとにupdateという関数を実行する
  }

  function update(){
    if($('.item')[0]){ //もし'.item'というクラスがあったら
      var fruit_id = $('.item:last').data('id'); //一番最後にある'.item'クラスの'id'というデータ属性を取得し、'fruit_id'という変数に代入
      $.ajax({
        url: location.href, //urlは現在のページを指定
        type: 'GET', //アクション名指定(データを表示させる)
        data: { id: fruit_id }, //rails に引き渡すデータ(これがparams[:id]になる)
        dataType: 'json'
      })
      .done(function(data){
        if (data.length){ //もしdataに値があったら
          $.each(data, function(i, data){ //'data'を'data'に代入してeachで回す
            var html = buildHTML(data);
            $('.fruit_item').append(html);
          })
        }
      })
      .fail(function(){ //そんなにこの記述はいらない気がするけど、異常系のエラー(途中で通信が中断されたり)が起きた時用
        alert('自動更新に失敗しました')
      });
    }
    else {
      clearInterval(); //値がなかったらsetIntervalを止める(この記述はなくても動く、別画面に遷移した際は、ブラウザを閉じた時同様、遷移した時点で遷移前のJavaScript実行は勝手に打ち切られる模様)
    }
  }
});

 

コントローラー編集(indexのみ)

controllers/fruites.controller.rb
  def index
    @fruits = Fruit.all
    @fruit = Fruit.new

    respond_to do |format| #記述追加
      format.html
      format.json { @new_fruit = Fruit.where('id > ?', params[:id]) } #jsのdataの情報がparams[:id]に入る
    end
  end

index.json.jbuilderのファイル編集

views/fruits/index.json.jbuilder
json.array! @new_fruit.each do |fruit|
  json.name fruit.name
  json.id fruit.id # 配列かつjson形式で@new_fruitを返す
end

 

以上で実装は終了
再度サーバーを立ち上げ直して

auto_update
rails s

うまく実装してできていれば上記のGIFのように自動で最新のデータが更新される

 
うまく実装できました?
実装できて自分の理解ができたら無駄なコメントは削除して読みやすいコードにしましょう。
(なんだか自分のコードにも_form.html.hamlに無駄なコメントアウトがあったりしたので、、)

今回でqiita2記事目なんですが、めちゃしんどいっすw
書いてみるとやってる事全然簡単に見えるのに、これあげるのにどれだけかかったか、、、
qiita人民の凄味を改めて実感しました。
ちょいまだまだ補足が書けていないのですが、一旦あげようと思います。
指摘事項あれば教えて頂けると嬉しいです。
それでは

はてなブログもやっているので是非!
https://tanagram18.hatenablog.com/

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

RailsでAPIを作ってJson形式のレスポンスを返すまで

はじめに

まとめておくと後で楽だよねってことで

環境

Ruby 2.6.5p114
Rails 6.0.1

環境自体はDockerコンテナに押し込めているので、まずはDockerの設定周りからやっていきます。

Docker関連

docker-compose.yml

こちらの記事を参考にしつつ作成しています。
ENTRYPOINTは自分が理解しきれてないので指定していません。
なくても動きます。

後、重要なのはmysqlのvolumesとして/etc/mysql/conf.dがいったん必須です。
mysql8からデフォルトの認証方式がパスワードになっていないので、それをパスワードにするために永続化して設定ファイルを入れてあげないと困ります。
認証方式変わったということは、それをデフォルトとして他を変えるのが本筋ですが、公開するサービスではないので、いったんパスワードにしています。

version: '3'
services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '13306:3306'
    volumes:
      - ./mysql:/var/lib/mysql
      - ./mysql-confd:/etc/mysql/conf.d
  rails:
    build:
      context: .
      dockerfile: ruby/Dockerfile
    command: bash -c "bundle exec rails s -p 3000 -b '0.0.0.0'"
    environment:
      - "DATABASE_HOST=db"
      - "DATABASE_PORT=13306"
      - "DATABASE_USER=root"
      - "DATABASE_PASSWORD=password"
    volumes:
      - .:/app
    ports:
      - "13000:3000"

Dockerfile

こちらはRails6からwebpackerを入れる必要があるため、その設定は入れています。
今回はAPIを作るようのアプリケーションにするため、この設定はまるごといらないのですが、削除しなくても困らないので残しています。
APIモードで作らない場合はdocker-compose.ymlのcommandでインストールしてあげるかDockerfile内でインストールしてあげればOKです。

あと重要なのは最後にRUN bundle installを入れておくぐらいでしょうか。
Gemを書き換えた際にはイメージを書き換えないと駄目っぽいのですが(本当か?)、これやっておかないと起動時にGemがないと言われて怒られます。
1個目の方はRails関連のGemを入れるようなので、別にやっておかないとアプリケーションの方のGemの読み直してくれません。

※
ここも色々参考にさせていただいた記事があるのですが、見つけ次第追記します。
どの記事だっけなぁ・・・

FROM ruby:2.6.5

ENV TZ='Asia/Tokyo'

RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ENV LANG C.UTF-8

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && apt-get install -y nodejs yarn

RUN gem install bundler

WORKDIR /tmp
ADD ruby/Gemfile Gemfile
ADD ruby/Gemfile.lock Gemfile.lock
RUN bundle install

ENV APP_HOME /app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD . $APP_HOME

RUN bundle install

Gemfile

バージョンは適宜読み替えてください。
Gemfile.lockは参考にある通り空ファイルです。

Gemfileはrailsアプリケーションを生成した際に上書きされると困るので、rootフォルダとは別の場所に置いてあります。

source 'https://rubygems.org'

gem 'rails', '~> 6.0.1'

railsアプリケーションの生成

ファイルの準備が出来たらアプリケーションを生成します。

docker-compose run rails rails new . --api --force --no-deps --database=mysql

config/database.yml

立ち上げる前にデータベースの設定を書き換えます。
passwordとhostの書き換えだけです。

docker-composeでアプリケーションを作成するとネットワークをいい感じで作ってくれるため、hostをサービス名で設定しておけばいいので楽ですね。
docker-composeで設定したportsもこのネットワーク外部から繋いだ時のものでネットワーク内部からは普通のportで接続となります。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

database作成

rakeコマンド使うなり自力で作るなりしてDB作っておいてください。

docker起動

ビルドして起動します。
中々に時間がかかるので、間違えてやり直しをするのが結構辛かったです。

docker-compose build
docker-compose up -d

起動確認

localhost:13000につないで画面出ればオッケーです。
駄目ならエラー見つつ頑張ってください。

routes.rbの設定

起動したので、routesを書いてURLを生成します。
適当でも良いですが、せっかくなので少し真面目に書きます。
resourcesをnewsにしているのは作ろうとしているアプリケーションのURLだからです。

routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :news
    end
  end
end

routeの確認

railsの操作をする際は色んなことをやっていくことになるので、内部で操作していった方が楽だと思います。外からでも操作できるので、どっちでも良いです。
入ってやる方は以下のコマンドで入れます。

docker-compose exec rails bash

確認したらこんな感じで出ると思います。
rails初期から存在するrouteと共に設定したrouteが見れます。

これで上のrouteが正しく設定されたことを確認します。

root@69c223a51ae2:/app# rails routes
                               Prefix Verb   URI Pattern                                                                              Controller#Action
                    api_v1_news_index GET    /api/v1/news(.:format)                                                                   api/v1/news#index
                                      POST   /api/v1/news(.:format)                                                                   api/v1/news#create
                          api_v1_news GET    /api/v1/news/:id(.:format)                                                               api/v1/news#show
                                      PATCH  /api/v1/news/:id(.:format)                                                               api/v1/news#update
                                      PUT    /api/v1/news/:id(.:format)                                                               api/v1/news#update
                                      DELETE /api/v1/news/:id(.:format)                                                               api/v1/news#destroy
        rails_mandrill_inbound_emails POST   /rails/action_mailbox/mandrill/inbound_emails(.:format)                                  action_mailbox/ingresses/mandrill/inbound_emails#create
        rails_postmark_inbound_emails POST   /rails/action_mailbox/postmark/inbound_emails(.:format)                                  action_mailbox/ingresses/postmark/inbound_emails#create
           rails_relay_inbound_emails POST   /rails/action_mailbox/relay/inbound_emails(.:format)                                     action_mailbox/ingresses/relay/inbound_emails#create
        rails_sendgrid_inbound_emails POST   /rails/action_mailbox/sendgrid/inbound_emails(.:format)                                  action_mailbox/ingresses/sendgrid/inbound_emails#create
         rails_mailgun_inbound_emails POST   /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)                              action_mailbox/ingresses/mailgun/inbound_emails#create
       rails_conductor_inbound_emails GET    /rails/conductor/action_mailbox/inbound_emails(.:format)                                 rails/conductor/action_mailbox/inbound_emails#index
                                      POST   /rails/conductor/action_mailbox/inbound_emails(.:format)                                 rails/conductor/action_mailbox/inbound_emails#create
        rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#show
                                      PATCH  /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#update
                                      PUT    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#update
                                      DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#destroy
rails_conductor_inbound_email_reroute POST   /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)                      rails/conductor/action_mailbox/reroutes#create
                   rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
            rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
                   rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
            update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
                 rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

controllerの作成

これもコマンドで作成します。
Controllerの名前はroutesで確認したControllerと同じにする必要があるので、その点だけ注意ください。

rails generate controller api/v1/news

作ったら中身です。

news_controller.rb
class Api::V1::NewsController < ApplicationController

  def index
    render json: { status: 'SUCCESS'}
  end

end

とりあえずのレスポンスを返却するためだけの中身です。
途中でキャッシュ関連で変更が反映されないという問題に直面しましたが、その際はこちらを参考にして解決しました。
Rails6でも同様の方法で解決できるようです。

終わりに

Rails使うならここまではぱぱっと出来るようになりたいですね。
一通りやってみて環境構築周りの注意点とか分かったので、やっぱり手を動かして作るって大事です。

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

Ruby学習ログ:その1【do...end = {...} , etc.】

Rubyを学び始めて2ヶ月ほどの初学者です。
アウトプットも兼ねて学習したことを投稿していこうと思います。
早速いきましょう。

「do...end」は、「{...}」に書き換えられる

hoge = [1, 2, 3]
hoge.each do |hogey|
   puts "オラは#{hogey}歳だ!"
end
puts "========================================"
hoge = [1, 2, 3]
hoge.each {|hogey|puts "オラは#{hogey}歳だ!"}
実行結果
オラは1歳だ!
オラは2歳だ!
オラは3歳だ!
========================================
オラは1歳だ!
オラは2歳だ!
オラは3歳だ!

同じ結果になりました

いつでも使えるか?

ところでデメリットはないのか?と思って調べてみたらどうやらあるっぽい
→ブロックをdo…endで書くか{…}で書くかにより挙動が変わる例

上の記事の中で「戻り値」に関して理解が曖昧だったので
→【Ruby】メソッドの戻り値ってなんだ
この中の演習をやってみました

def shipping_free?(price)
 return price >= 5000
end

if shipping_free?(10000)
 puts "5000円以上のお買い上げなので送料はいただきません"
else
 puts "追加で送料をいただきます"
end
実行結果
5000円以上のお買い上げなので送料はいただきません

「戻り値」の解説ならこちらの方がわかりやすいかも

→関数の「戻り値」って何?C言語とRubyを比較して理解する!:Rubyの関数の戻り値
*戻り値(返り値)は関数が返す「値」のこと (何か関数に値を入れたとき、関数から出てくる値)

所感

わからないワードをググって調べると、また新たにわからないワードが出てきてプログラミングは奥深いなと感じた。基礎へ立ち返ることも必要であるし、内容の質は置いておいて、頭を整理する意味でも定期的に文章としてアウトプットすることは大切だと思った。

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

Codr0 : TwiterでコードをシェアできるWebアプリを作ろうと思った

たぶん、長い記事

きっかけ(こんな呟きを見かけた

作成の過程で得られたもの。

  • Active Record Storage
  • Twitter Login方法と仕組み、そのたTwitterあれこれ
  • JSの基礎
  • AWS S3の使い方
  • XSS対策

作成要件

Untitled.png

  • マークダウン投稿、シンタックスハイライト表示
    • gem: redcarpet, rouge
  • 投稿から画像生成
  • クラウドストレージに保存
    • AWS S3を使用

作成の流れ:予定

  1. rails new codr, git init, heroku create、Active Storage
  2. AWS S3あれこれ
  3. twitter登録、ログイン機能作成

開発環境

  • vm : Linux Ubuntu (virtualbox + vagrant)
    • Ruby 2.5.1p57
    • Rails 5.2.3
    • Postgresql

実作業: アプリ作成、諸準備

rails new codr -d postgresql
# DB設定等は割愛

Gem

今回は公開にまで至る予定なので、railsやdeviseの日本語化等も。が、想定ユーザはエンジニアだしと思い、最終的には英語オンリーのサイトになった。

Gemfile
gem 'mini_racer'   # uncomment
gem 'rails-i18n'   # japanize

# authetication
gem 'devise' # login
gem 'omniauth' # SNS login
gem 'omniauth-twitter' # twitter login
gem 'devise-i18n'  # japanize devise
gem 'devise-i18n-views'

gem 'redcarpet'   # for markdown
gem 'rouge' # for syntax highlight

gem 'meta-tags'
gem 'aws-sdk-s3' # for aws s3

gitignore => rails.credentials.yml

当初は.gitignoreとgem 'dotenv'等を使っていた。が、作成途中でRails5.2からのrails.credentials.ymlを知り、利用した。rails.credentials.ymlは暗号化されており、なお、復号化には/config/master.key`を利用。

irb
# editor setting
 EDITOR="vi" bin/rails credentials:edit
# edit credentials.yml
rails credentials:edit
# show credential.yml
rails credentials.yml:show

# herokuにmaster.keyを環境変数として指定
heroku config:set ENV_VAR="環境変数" --app "アプリ名"

# 追加した変数を使用するには
Rails.application.credentials.dig(:twitter, :API_Key)

rails gあれこれ

# devise
# install devise
rails g devise:install
rails g devise User name:String

# Add Admin column to User
rails g migration AddAdminToUsers
# add setting at /db/migrate/20191103141531_add_admin_to_users.rb
add_column :users, :admin, :boolean, default: false

# add views and controllers to modify devise
rails g devise:controllers users
rails g devise:views users

# japanize
# add at /config/application.rb
config.i18n.default_locale = :ja
=> create /config/locale/devise.view.ja.yml
# scaffold post
rails g scaffold Post user:references name:string content:text date:datetime

Active Record Associations関連付け

/app/model/user.rb
has_many :posts
/app/model/post.rb
belongs_to :user

投稿関連

マークダウン投稿

基本:Redcarpet::Markdown.new(renderer, extensions = {}).render(@post.content)
オプションやXSS対策等を追加したく、helperメソッドを作成した。

app/helpers/posts_helper.rb
Module PostsHelper
  require 'rouge/plugins/redcarpet'
  class RougeRedcarpetRenderer < Redcarpet::Render::HTML
    include Rouge::Plugins::Redcarpet

    def header(text, level)  # #や##等がh2、h3となるようにした。
      level += 1
      "<h#{level}>#{text}</h#{level}>"
    end
  end

  def markdown(text)
    render_options = {
      filter_html: true,  # do not allow any user-inputted HTML in the output.
      hard_wrap: true,
    }

    extensions = {
      autolink: true, # <>で囲まれていない時は、リンクとして認識しない
      fenced_code_blocks: true,   #  ```\n ```内をコード部分と見做す
      lax_spacing: true, 
      no_intra_emphasis: true,
      strikethrough: true,
      superscript: true,
      tables: false,  # テーブルを認識しない
      highlight: true,
      disable_indented_code_blocks: true,
      space_after_headers: false # #の後にスペースが無くても、h1等とする。
    }
    renderer = RougeRedcarpetRenderer.new(render_options)
    Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
  end
end

html_safe => sanitize

html_safeではXSS対策としては駄目と知った。名前詐欺である。
sanitizeヘルパーを使用した。ホワイトリスト方式。要参照

app/views/posts/index.html.erb
# sanitize(html, options = {})
 <div id="capture" class="content">
    <%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %>
</div>

投稿内容のデータ化、AWSへの画像保存

最初はTwitterAPIを利用して、投稿から作成、DBに直接保存した画像でTwitter投稿しようとした。だが、Herokuでは画像が保持されない事、TwitterAPIの変更などいろいろ面倒なことが発生したので、最終的には画像をAWS S3に保存し、og:imageに添付する形を取った。

  1. Webアプリ内で通常投稿
  2. showページ表示(同時にhtml2canvasでBase64としてデータ取得、hidden_fieldに収納
  3. Tweetボタン押す(Postされ、postモデル内でbase64をデコード
  4. Active Storageを通して、AWS S3に保存
  5. 保存画像urlをog:imageに添付

Active Storage

Rail5.2からの機能で、今まで常用されてたと言うcarrievaveやpaperclip等を使わずに、クラウドストレージ等へのアップロードが容易になる。今回はAWS S3を使った話。

irb
# set up
rails active_storage:install
# 今回は画像が紐づくPostテーブルが既にあるので、不要
# rails g resource comment content:text
rails db:migrate
app/models/post.rb
class Post < ApplicationRecord
# 今回は1つの投稿につき、1枚の画像なので。複数なら => has_many_attached :prtscs
  has_one_attached :prtsc
end
app/config/enviroments/
# ファイル保存先変更
# development.rb
config.active_storage.service = :local
# production.rb
config.active_storage.service = :amazon

rails credentials:editでAWSアクセスキーとシークレットキーを追加。

config/credentials.yml.enc
aws:
  access_key_id: 
  secret_access_key: 
config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: ap-northeast-1
  bucket: codr0
Gemfile
# gemが必要
gem 'mini_magick'
# 今回は不要だったので、入れず。
gem 'aws-sdk-s3', require: false

html2canvas

参考にしたページ

  1. Tweetボタン押下時に、画像をPostするためのフォーム、hidden_fieldを用意
  2. html2canvas.jsをapp/assets/javascriptsディレクトリ配下に保存。
  3. html上に置くscriptコードを改修
app/views/posts/show.html.erb
<%= form_with(model: @post, local: true) do |form| %>
  <%= form.hidden_field :id, value: @post.id %>
  <%= form.hidden_field :prtsc, value: "" %> # idはpost_prtscになる。
  <%= form.submit "Post", class:"btn btn-outline-dark", id:"tweet", value:"tweet" %>
<% end %>

showページ描画後に、Redcarpetで出力された<div id="capture"></div>内にある<pre><code>を選択し、html2canvasの基本機能でBase64データ化し、最後に<input type="hidden" id="post_prtsc">のvalueにセットするようにした。

app/views/layouts/application.html.erb
<script type="text/javascript">
  html2canvas(document.querySelector("#capture"),{scale:1, width:600}).then(canvas => {
    var base64 = canvas.toDataURL('image/jpeg', 1.0);
    document.getElementById('post_prtsc').setAttribute('value', base64);
 });
</script>

Base64デコード

app/models/post.rb
attr_accessor :img

def parse_base64(img)
  if img.present?
    # data:image/jpeg;base64,/9j/4AAQSkZJRgABA・・・から/9j/4AA以降を選択取得
    content = img.split(',')[1]
    # 今回は、ユーザによる画像アップロード投稿ではなく、拡張子が決まっている
    filename = Time.zone.now.to_s + '.jpg'
    decoded_data = Base64.decode64(content)
    # String.IO.newにより、アプリ内に一時ファイルを作成しなくて済む
    prtsc.attach(io: StringIO.new(decoded_data), filename: filename)
  end
end

あとはposts_controllerで、paramsから受け取ったBase64データを上のparse_base64(img)で変換し、保存すれば完了。

AWS S3

AWS上での登録、設定、バケット作成等は割愛。

Tweet button

公式で生成されるTweetボタンのURLを利用し、押下時にwindow.openでTweet投稿ページを開くようにした。rubyonrailsで用意した変数をjsに渡すgem 'gon'も考えたが、見送った。

app/views/layouts/application.html.erb
<script>
  var base = 'https://twitter.com/intent/tweet?url=';
  var pageUrl = 'https://codr0.herokuapp.com/posts/' + document.getElementById('post_id').value;
  var option = '&button_hashtag=Codr0&ref_src=twsrc%5Etfw';
  var href = base + pageUrl + option;
  var twit = document.getElementById('tweet');
  twit.addEventListener('click', function() {
        window.open( href );
      });
</script>

Twitterログイン

TwitterDeveloperAccountが必要。割愛。

なお、omniauthは脆弱性が見つかっており、githubの方でもアラートが来るのだが、パッチが無いのだが。クックパッドの人が対処してくれたので、感謝したい。

app/models/user.rb
# 参考ページと同じ基礎的な所は割愛する。
class User < ApplicationRecord
  def self.from_omniauth(auth)
    find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user|
      # 一部割愛
      user.username = auth['info']['nickname']
      # SNS登録時は、ダミーメールを登録
      user.email = User.dummy_email(auth)
    end
   end

  # SNS登録(providerが存在する)時は、パスワード要求をしない
  def password_required?
    super && provider.blank?
  end

  def self.new_with_session(params, session)
    if session['devise.user_attributes']
      new(session['devise.user_attributes']) do |user|
        user.attributes = params
      end
    else
      super
    end
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end

Twitterのニックネームが取得できるようになったので、元からあるUserのnameテーブルは削除した。

デザイン

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

【Ruby初心者】シンボルの使い方でつまづいたこと

はじめに

元々PHPをやっていたのですが、Rubyに触ってみて一週間経ちました。
参考書を2冊読んだのですが、一番わからないポイントがシンボルについてでした。
今回はシンボルについての記事です。

:apple?

参考書を60ページくらい読んだら出てきました。

:apple.class #=> Symbol
'apple'.class #=> String

初めてみる記法です。
Ruby使いは左側にコロンのついてるものをシンボルと呼ぶらしいです。

主な特徴

同じシンボルであれば全く同じオブジェクトである

str1 = "string"
str2 = "string"
p str1.equal?(str2) #=> false
p str1.object_id    #=> 70332245439420
p str2.object_id    #=> 70332245439400

sym1 = :symbol
sym2 = :symbol
p sym1.equal?(sym2) #=> true
p sym1.object_id    #=> 901148
p sym2.object_id    #=> 901148

シンボルは1つのオブジェクトを参照しているものなので、右側の値が同じならシンボルは同じものになる。
→ String型に比べて、メモリを消費しない

参考記事:Rubyの文字列とシンボルの違いをキッチリ説明できる人になりたい
https://qiita.com/Kta-M/items/53a13ef60e14fcb41193

シンボルの使い方

連想配列のキーとして使われる

#文字列をキーにする連想配列
{'japan' => 'yen', 'us' => 'dollar', 'india' => 'rupee'}

#これのキーをシンボルにすると次のようになる
{:japan => 'yen', :us =>'dollar', :india => 'rupee'}

配列のキーをシンボルにすることによって、データの抽出が高速になる
(理由:Rubyのプログラムはシンボルのデータを整数として持っているので、データの処理が速くなる)

シンボルと同名のメソッドを呼び出す使い方

class User
  attr_reader :first_name, :last_name, :age

  def initialize(first_name, last_name, age)
    @first_name = first_name
    @last_name = last_name
    @age = age
  end
end

シンボルが出てきました。

attr_reader :first_name, :last_name, :age

attr_readerは書き換えはできないけど、インスタンスにおいて指定したプロパティを読み込めますという設定にするものみたいです。
でもなんでattr_readerで指定してるのがシンボルなのかわからなくなりました。

・・・

どうやらこれは、同名のメソッドを呼び出ししているみたいです。
:first_name → first_nameのメソッド

attr_reader :first_nameを設定すると、

class User
  def initialize(first_name)
    @first_name = first_name
  end

  def first_name
    @first_name
  end
end

とした時と、同じことになるみたいです。(attr_readerを設定すると同名のゲッターメソッドを作成する)

今回はattr_readerで設定をしているので、first_nameのゲッターを呼び出しているが、
attr_accessorで設定をすると、ゲッターとセッターで呼び出すみたいです。

これまでとは違ったシンボルの使い方ですね

似たような使い方で

['ruby', 'java', 'perl'].map(&:upcase)
#を次のように変更(条件付き)
['ruby', 'java', 'perl'].map{|s| s.upcase}

がありました。これもシンボルと同名のメソッドを呼び出しているみたいですね。

参考記事: Rubyのmap(&:name)というのはどういう意味?
https://qa.atmarkit.co.jp/q/35

まとめ

2つのシンボルの使い方があることを理解しました。
1、連想配列のキーにつかう
2、シンボルと同名のメソッドを呼び出す

以上

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

ABC083B - Some Sums

問題

https://atcoder.jp/contests/abs/tasks/abc083_b
スクリーンショット 2019-11-26 14.48.59.png

1回目

回答

N,A,B = gets.chomp.split(" ").map(&:to_i)

res = 0
for num in 1..N do
  numList = num.to_s.chars.map(&:to_i)
  sum = 0
  for n in numList do
    sum += n
  end
  if sum >= A && sum <= B
    res += num
  end
end

puts res

結果

スクリーンショット 2019-11-26 14.51.25.png

2回目

回答

N,A,B = gets.chomp.split(" ").map(&:to_i)

res = 0

for num in 1..N do
  s = 0
  n = num
  while n > 0 do
    s += n % 10
    n /= 10
  end
  if s >= A && s <= B
    res += num
  end
end

puts res

結果

スクリーンショット 2019-11-26 14.51.52.png

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

Windowsにてp "日本語"で"日本語"を表示させる方法

Ruby on WindowsのFAQシリーズ

初学者からよく聞く

nihongo.rb
p "日本語"
p ["日本語"]
p "日本語": :"日本語"

が

"\u65E5\u672C\u8A9E"
["\u65E5\u672C\u8A9E"]
{:"\u65E5\u672C\u8A9E"=>:"\u65E5\u672C\u8A9E"}

となる問題です。

ちなみに脳筋回答

コンソールに日本語表示させる必要なんて無いんじゃない?
(割と真面目に、Rubyのpによる日本語表示をwindowsでやる必要は無いと思ってます)

真面目回答

出力が真の意味で文字化けする危険性があるが
スクリプトの頭に

Encoding.default_internal = __ENCODING__

付けとけ

p "日本語"とは?

Kernel.#p

引数を人間に読みやすい形に整形して改行と順番に標準出力 $stdout に出力します。主にデバッグに使用します。

引数の inspect メソッドの返り値と改行を順番に出力します。つまり以下のコードと同じです。

print arg[0].inspect, "\n", arg[1].inspect, "\n", ...

つまり、p("日本語")はputs "日本語".inspectとなります。

p "日本語".encoding         # => #<Encoding:UTF-8>
p "日本語".inspect          # => "\"\\u65E5\\u672C\\u8A9E\""
p "日本語".inspect.encoding # => #<Encoding:Windows-31J>

なので、"日本語".inspectの時点で\u表記への変換が行われている事
および、その理由が文字コードを変換しつつ元の情報を損なわないための措置という事がわかります。

"日本語".encodingはるりま: 多言語化に

リテラルのエンコーディング
文字列リテラル、正規表現リテラルそしてシンボルリテラルから生成されるオブジェクトのエンコーディングは スクリプトエンコーディングになります。

現在のスクリプトエンコーディングは __ENCODING__ により取得することができます。

とある通り、スクリプトエンコーディング(__ENCODING__)によって取得できます。
また、Ruby2.0以降のスクリプトエンコーディングの既定はUTF-8です

では、"日本語".inspectは?というと
るびマ Ruby M17N の設計と実装

Encoding.default_internal が設定されている場合は、全ての入力された String のエンコーディングは Encoding.default_internal の返すエンコーディングと等しいと仮定することが可能になります。この場合、ライブラリが返す文字列も Encoding.default_internal になっているべきです。

ライブラリが返す文字列は、Encoding.default_internalになっているべき という事なのでEncoding.default_internalによって指定できる事がわかります。

結論

Encoding.default_internal = __ENCODING__

を行うことによって、String#inspectが返す文字列を文字列リテラルと同じエンコーディングにできる。

問題点

Windowsではlocaleがcp932なので
文字列内に絵文字などのcp932に変換できない文字を含むと文字化けします。
この問題のためにdefault_internalがnilの場合はdefault_externalを代替エンコーディングとして使っていると推測できます。

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

Railsを使ってチャットアプリを作る

はじめに

Railsを使って簡単なチャットアプリを作ったので、そのときの手順をまとめました。

chat-app.gif

環境

$ ruby -v
ruby 2.5.1p57 (2018-03-29 version 63029) [x86_64-darwin19]

$ bin/rails -v
Rails 5.2.3

参考

チャットアプリを作る際、以下を参考にさせて頂きました。
この記事で掲載しているソースコードの大半は下記の2つのページの内容が基になっています。

Action Cableでリアルタイムチャットアプリの作成方法 (Rails 5.1.4にて)(その1) herokuで動かす!
Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より)

ソースコード

チャットアプリのソースコードは以下で確認できます。
Neufo/rails-sample-app/ChatApp

手順

1. アプリケーションを作る

まずはチャット機能を実装するアプリケーションを作ります。

$ rails new ChatApp

2. コントローラーを作る

ChatAppディレクトリに移動して以下のコマンドを実行します。

$ bin/rails generate controller rooms show

これでChatApp/app/controllers/にrooms_controller.rbが作られます。

3. モデルを作る

ユーザーが入力したメッセージを保存するモデルを作ります。

$ bin/rails generate model message content:text
$ bin/rails db:migrate

4. メッセージの一覧を表示する

RoomsControllerのshowメソッドでメッセージの一覧を取得します。

ChatApp/app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end

取得したメッセージを表示するためにViewを編集します。1

ChatApp/app/views/rooms/show.html.erb
<h1>Chat room</h1>

<div id="messages">
  <%= render @messages %>
</div>

messagesの各要素をページに埋め込むために部分テンプレートを作ります。
(ChatApp/app/views/ に messagesフォルダを作り、その中にファイルを保存します)

ChatApp/app/views/messages/_message.html.erb
<div class="message">
  <p><%= message.content %></p>
</div>

5. チャンネルを作る

クライアント/サーバ間の通信処理のひな形を作ります。

$ bin/rails generate channel room speak

コマンドを実行するとクライアント用、サーバ用のファイルが作られます。

  • ChatApp/app/assets/javascripts/channels/room.coffee (クライアント)
  • ChatApp/app/channels/room_channel.rb (サーバ)

サーバを起動して動作を確認してみます。

$ bin/rails s

ブラウザで/rooms/showを開き、コンソールでApp.room.speak()と入力します。trueが返ってくれば成功です。
スクリーンショット 2019-11-24 22.53.29.png

6. jQueryを有効にする

クライアントがメッセージを送受信するときにjQueryを使うので準備しておきます。

ChatApp/Gemfile
gem 'jquery-rails'

上記をGemfileに追記したらbundle updateを実行します。

jQueryのライブラリを読み込むようapplication.jsに追記します。

ChatApp/app/assets/javascripts/application.js
(省略)
//= require jquery
//= require jquery_ujs
//= require_tree .

7. メッセージの送受信処理を作る

以下の機能を作ります。

  • クライアント側
    • サーバからメッセージを受信する (received)
    • ユーザが入力したメッセージをサーバに送信する (speak)
ChatApp/app/views/rooms/show.html.erb
<form>
  <label>Say something:</label><br>
  <!-- room_speaker からサーバへメッセージを送信する -->
  <input type="text" data-behavior="room_speaker">
</form>
ChatApp/app/assets/javascripts/channels/room.coffee
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
    # Enterキーが押されたらサーバへメッセージを送信する
    if event.keyCode is 13 # return = send
        App.room.speak event.target.value
        event.target.value = ''
        event.preventDefault()

App.room = App.cable.subscriptions.create "RoomChannel",
  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) ->
    # メッセージ一覧の末尾に受信したメッセージを追加する
    $('#messages').append data['message']

  speak: (message) ->
    # サーバのspeakメソッドを呼び出す
    @perform 'speak', message: message
  • サーバ側
    • room_channelを購読する (subscribed)
    • クライアントから受信したメッセージを保存する (speak)
    • room_channelに参加しているクライアントにメッセージをブロードキャストする (perform)
ChatApp/app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # room_channelからデータを受信する
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    # データベースにクライアントから受信したメッセージを保存する
    Message.create! content: data['message']
  end
end

メッセージをブロードキャストするジョブを定義します。

$ rails generate job MessageBroadcast
ChatApp/app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    # room_channelにメッセージをブロードキャストする
    ActionCable.server.broadcast 'room_channel', message: render_message(message)
  end

  private
    def render_message(message)
      ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
    end
end

データベースにメッセージを保存したら、ジョブを登録します。

ChatApp/app/models/message.rb
class Message < ApplicationRecord
    # データベースにメッセージが保存されたらジョブを登録する
    after_create_commit { MessageBroadcastJob.perform_later self }
end

登録されたジョブは自動的に実行されます。
MessageBroadcastJobのperformメソッドが呼び出され、すべてのクライアントにメッセージが送信されます。
クライアントはreceivedでメッセージを受け取り、メッセージ一覧の末尾に受信したメッセージを追加します。
これでチャットアプリの作成は終了です。


  1. <%= render @messages %>は<%= render partial: "messages/message", collection: @messages %>を省略形です。参考:コレクションをレンダリングする ↩

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