20200701のRubyに関する記事は20件です。

.sort.uniq か、.uniq.sort か

配列をソートしてユニークするのに、.uniq.sort.sort.uniq どっちが良いのか。

長いものには巻かれようと思ったものの、破壊的に実行すると、.uniq!.sort! は実行出来ないケースがある事が判明 @2.7.1

test.rb
r = {
  's' => [5,1,3,53,2],
  't' => [5,3,4,4,3,2],
}
r.each_key{|k|
  #r[k].sort!.uniq!
  r[k].uniq!.sort!
  #  => ダメ
}
p r
$ ruby test.rb
Traceback (most recent call last):
    2: from hoge2.rb:5:in `<main>'
    1: from hoge2.rb:5:in `each_key'
test.rb:7:in `block in <main>': undefined method `sort!' for nil:NilClass (NoMethodError)

.sort!.uniq! では実行出来る。

普通に、

test2.rb
a = [5,3,4,4,3,2]
#a.sort!.uniq!
a.uniq!.sort!

p a

な場合はエラーになんない。

ベンチが遅かろうと、.sort.uniq の方を使うかな1


  1. こっちの方が、シェルで馴染み深いし。 

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

Time.strptime で、%j (年中の通算日)がアレ

他人様のコードをメンテする必要があって、コードを追ってると、%j な年月日を UNIX time に変換するのに、面倒な事やってた。

で、嫌気が差して、「ruby でも strptime あるだろ?」と適当に探すと、ビンゴ!!

問題発生

差し替える前に、テスト。

以下の様なコードを実行

$ ruby -e 'require "time" ; p Time.strptime("2003,070,12:21:51 JST", "%Y,%j,%H:%M:%S %z").to_i'
1041391311
# ↑ 2003/01/01 12:21:51 JST

問題発生。

え? %j 解釈してくれないの?

ググって見ても答えは得られず1

色々試して、、、

結論

古い rubyTime クラス(モジュール?)の .strptime は、%j 解釈してくれないっぽい2

検証では、2.7.1p83 はOKだった3

検証

以下のコードを、1.9.2p290, 2.0.0p481, 2.7.1p83 で、それぞれ実行

test.rb
require 'date'
require 'time'

# コントロール用の自作関数
def manu(t)
  arr = t.split(/[,: ]/).map(&:to_i)
  Time.mktime(arr[0],1,1,arr[2],arr[3],arr[4]) + ( arr[1] - 1) * 24 * 60 * 60
end

str = "2003,070,12:21:51 JST"
# DateTime.strptime の結果
p DateTime.strptime(str, "%Y,%j,%H:%M:%S %z").strftime("%s").to_i
# 自作関数の結果
p ( manu str ).to_i
# Timme.strptime の結果
p Time.strptime(str, "%Y,%j,%H:%M:%S %z").to_i

1.9.2 と、2.0.0 では、Time.strptime%j を解釈しない。DateTime.strptime は解釈してくれる。

1.9.2p290,2.0.0p481
$ ruby test.rb
1047352911
1047352911
1041391311  # <= 2003/01/01 12:21:51 JST

に対し、2.7.1Time.strptime も解釈してくれてる。

2.7.1p83
$ ruby test.rb
1047352911
1047352911
1047352911

ruby 版の delta みたいなのがあれば、それ読めば一発なんだろうけど、そこまでの情熱は湧かない、、、


  1. そもそも %j なんて使う事って特殊だし、、、 

  2. で、コード差し替えに関しては、 DateTime 使うのも色々と問題があり、結局検証用に作った自作コードを埋め込む形に、、、  

  3. 2.6.3p62 でもOK。 

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

三項演算子で、複数変数への代入

10年ぶりくらいに ruby のコードに立ち向かわなくてはならなくなった

$ ruby -e 'i, j = 1 ? [1,2]:[3,4] ; p i, j'
1
2

こんなので数時間悩んだのがアホみたい

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

Error ExecJS::RuntimeUnavailable: 発生時の対処法

発生現象

AWSのEC2でWebサーバ、アプリケーションサーバの設定時に、環境変数の設定をする際の$ rake secretを実行した際に下記Errorが発生。

terminal
ExecJS::RuntimeUnavailable: Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes.
/var/www/chat-space/config/application.rb:7:in `<top (required)>'
/var/www/chat-space/Rakefile:4:in `require_relative'
/var/www/chat-space/Rakefile:4:in `<top (required)>'
(See full trace by running task with --trace)

→Javascriptがうまく走っていないので、Node.jsをinstallする。

install確認

local環境にて

terminal
$ node --version
v12.16.1

AWSの本番環境にもinstallする

terminal
sudo yum install nodejs --enablerepo=epel  ←実行
読み込んだプラグイン:priorities, update-motd, upgrade-helper
amzn-main                                                          | 2.1 kB  00:00:00     
amzn-updates                                                       | 3.8 kB  00:00:00     
epel/x86_64/metalink                                               | 5.3 kB  00:00:00     
epel                                                               | 4.7 kB  00:00:00     
nodesource                                                         | 2.5 kB  00:00:00     
(1/3): epel/x86_64/group_gz                                        |  74 kB  00:00:00     
(2/3): epel/x86_64/updateinfo                                      | 789 kB  00:00:00     
(3/3): epel/x86_64/primary_db                                      | 6.1 MB  00:00:00     
1073 packages excluded due to repository priority protections
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ nodejs.x86_64 2:6.17.1-1nodesource を インストール
--> 依存性の処理をしています: python >= 2.6 のパッケージ: 2:nodejs-6.17.1-1nodesource.x86_64
--> トランザクションの確認を実行しています。
---> パッケージ python26.x86_64 0:2.6.9-2.89.amzn1 を インストール
--> 依存性の処理をしています: libpython2.6.so.1.0()(64bit) のパッケージ: python26-2.6.9-2.89.amzn1.x86_64
--> トランザクションの確認を実行しています。
---> パッケージ python26-libs.x86_64 0:2.6.9-2.89.amzn1 を インストール
--> 依存性解決を終了しました。

依存性を解決しました

==========================================================================================
 Package              アーキテクチャー
                                    バージョン                    リポジトリー       容量
==========================================================================================
インストール中:
 nodejs               x86_64        2:6.17.1-1nodesource          nodesource         13 M
依存性関連でのインストールをします:
 python26             x86_64        2.6.9-2.89.amzn1              amzn-main         5.8 M
 python26-libs        x86_64        2.6.9-2.89.amzn1              amzn-main         697 k

トランザクションの要約
==========================================================================================
インストール  1 パッケージ (+2 個の依存関係のパッケージ)

総ダウンロード容量: 20 M
インストール容量: 59 M
Is this ok [y/d/N]: y
Downloading packages:
警告: /var/cache/yum/x86_64/latest/nodesource/packages/nodejs-6.17.1-1nodesource.x86_64.rpm: ヘッダー V4 RSA/SHA512 Signature、鍵 ID 34fa74dd: NOKEY
nodejs-6.17.1-1nodesource.x86_64.rpm の公開鍵がインストールされていません
(1/3): nodejs-6.17.1-1nodesource.x86_64.rpm                        |  13 MB  00:00:00     
(2/3): python26-libs-2.6.9-2.89.amzn1.x86_64.rpm                   | 697 kB  00:00:00     
(3/3): python26-2.6.9-2.89.amzn1.x86_64.rpm                        | 5.8 MB  00:00:01     
------------------------------------------------------------------------------------------
合計                                                       16 MB/s |  20 MB  00:00:01     
file:///etc/pki/rpm-gpg/NODESOURCE-GPG-SIGNING-KEY-EL から鍵を取得中です。
Importing GPG key 0x34FA74DD:
 Userid     : "NodeSource <gpg-rpm@nodesource.com>"
 Fingerprint: 2e55 207a 95d9 944b 0cc9 3261 5ddb e8d4 34fa 74dd
 Package    : nodesource-release-el7-1.noarch (installed)
 From       : /etc/pki/rpm-gpg/NODESOURCE-GPG-SIGNING-KEY-EL
上記の処理を行います。よろしいでしょうか? [y/N]y
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  インストール中          : python26-libs-2.6.9-2.89.amzn1.x86_64                     1/3 
  インストール中          : python26-2.6.9-2.89.amzn1.x86_64                          2/3 
  インストール中          : 2:nodejs-6.17.1-1nodesource.x86_64                        3/3 
  検証中                  : 2:nodejs-6.17.1-1nodesource.x86_64                        1/3 
  検証中                  : python26-2.6.9-2.89.amzn1.x86_64                          2/3 
  検証中                  : python26-libs-2.6.9-2.89.amzn1.x86_64                     3/3 

インストール:
  nodejs.x86_64 2:6.17.1-1nodesource                                                      

依存性関連をインストールしました:
  python26.x86_64 0:2.6.9-2.89.amzn1        python26-libs.x86_64 0:2.6.9-2.89.amzn1       

完了しました!

以上で本番環境でもjavascriptが走るようになりました。

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

railsのバリデーションとnull: false 自分用メモ

バリデーションとは、リクエスト側が送信した特定のカラムの値が空のままだったり、意図しない形で保存されるようなことを防ぐために、予めモデルに記述しておく、門番のような役割。

valid.png

上記の例だと、

text「通りまーす」
validates「待て」
text「えっ」
validates「お前何も入力されてないじゃないか。やり直し」
text「ぴえん」

こんな感じ。

一方でnull: falseとは。
実はこちらも空のままのデータを受け入れないための門番、その2。
null.png

じゃあどっちが良いのかっていうと、どっちも書くが正解。

なぜか。

理由は、前述した二つの要素にはひとつ、決定的な違いがあって、それは、「守っている場所が違う」こと。

バリデーションくんは、railsアプリの中で張っている門番。フォームからの送信などに対して取り締まってくれる。
対してnull: falseくんは、データベースの前で待ち構えている門番。こちらはデータベースへの直接の書き込みを防いでくれる。

銀行の窓口と、金庫前の警備員、のような関係。例えた結果、逆に分かりにくくなっている。

バリデーションが無いとアプリケーションが意図した挙動をしてくれなかったり、null: falseが無いと何らかの方法でデータベースに意図しないデータが送られた時に素通りしてしまう。

なので、二人仲良くデータベースを守っていって欲しい。

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

Railsの存在確認メソッド 使い分けメモ (any?/empty?/present?(!blank?)/nil?)

はじめに

Railsのビューで、「画像があったら表示させる」みたいな処理を書く場面がありました。
存在確認のためのメソッドは、Rubyのもの、Railsのもの含め、いくつかあります。
ちょっと調べて、「よし、これで行こう!」とメソッドを選択してレビューに出したところ、おもいっきり玉砕したので、今後間違えないように備忘録として残しておきます。

環境

macOS Catalina Version 10.15.5
Ruby: 2.6.5
Ruby on Rails: 6.0.3

調査対象

  • any?(Ruby)
  • empty?(Ruby)
  • nil?(Ruby)
  • present?(Rails)

動機

過去にも類似の記事はあり、実際に大部分、「rubyの真偽判定メソッド(nil?/empty?/blank?/present?)を検証してみた結果、興味深いことがわかった」に乗っからせていただきました。そのうえで、今回投稿に至った理由は、以下となります。

  • 上記投稿においてany?メソッドが触れられていなかったが、any?の挙動についても確認したかった(今回直面したのが、配列絡みの問題だったため)
  • 上記投稿のまとめの表では、
     -「ある かどうかを聞くメソッド(present?等。何かあればtrueを返す)」
     -「無い かどうかを聞くメソッド(empty?等。何も無ければtrueを返す)」
    が並べて表記されていたが、「何かある場合に〇〇を返す」というように、条件の方向を揃えた表(present?と not empty?が比較できるような表)が欲しかった
    ※もちろん上記の表のメリットとして、present?とblank?が裏の関係になっていることがわかりやすい、等があります

なお、blank?は除外しています(present?の定義が!blank?のため)。

調査方法

rails consoleを使います。

test = XXX ←ここに色々代入していく

test.any?
!test.empty?
test.present?
!test.nil?

調査結果

any? !empty?
(empty?の否定)
present?
(!blank?)
!nil?
(nil?の否定)
Ruby/Rails Ruby Ruby Rails Ruby
1
数字
NoMethodError NoMethodError true true
"foo"
文字列
NoMethodError true true true
{key: value}
ハッシュ
true true true true
["foo"]
配列
true true true true
true NoMethodError NoMethodError true true
false NoMethodError NoMethodError false true
nil NoMethodError NoMethodError false false
""
NoMethodError false false true
" "
半角スペース
NoMethodError TRUE FALSE true
{}
空ハッシュ
false false false true
[]
空配列
false false false true
[nil]
配列(要素がnil)
FALSE TRUE TRUE TRUE

any?はEnumerableモジュールで定義されているため、配列やハッシュ以外で使おうとしてもNoMethodErrorとなります

要点

この結果から今後気を付けなきゃなと思ったところは、表内で大文字で強調していますが、改めて以下に記載しておきます。

  1. 半角スペースは、!empty?では存在するもの空ではないとみなされtrueを返し、present?では空とみなされfalseを返す
    ↑(修正)@scivola 様よりコメントをいただき、修正および追記しました。ありがとうございました。
     (追記)present?(もといblank?)は、空判定の出る範囲が広めであることに注意
         例:false、nil、半角スペース以外に、全角スペース、タブ(\t)、改行(\n)、Unicode(\unnnn)等も空と判定される
  2. 配列にany?を適用すると、真の要素(=nilでない要素)がある場合にtrueを返す。それ以外のメソッドは、真か否かに関係なく、要素があればtrueを返す。
  3. 素の nil!nil?って聞いてもfalseを返すが、 [nil]でできた配列に!nil?と聞くとtrueを返す

その他

Railsで、exists?というメソッドもあります。これは、データベースに特定の条件のデータが存在するか確かめるときに使うようで、ActiveRecord::Baseで定義されているメソッドとのことでした。そのことをつゆ知らず、当初調査対象に含めていましたが、すべてNoMethodErrorを返されました。

学び

Qiita初投稿となりました。シンプルでごく当たり前の内容だとは思いますが、アウトプットのために調べる、整理する、といった過程を通じて理解を深める良い機会となりました。
定期的に、今後の自分(≒他人)にとって役に立つ記事を投稿していきたいと思います。

参考URL

rubyの真偽判定メソッド(nil?/empty?/blank?/present?)を検証してみた結果、興味深いことがわかった

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

【Rails】Bootstrap3を用いた画像スライドショーの実装

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
投稿機能実装
画像複数アップロード機能実装

実装

1.ビューを編集

books/show.html.slim
/ 追記
.row
  #sampleCarousel.carousel.slide data-ride='carousel'
    ol.carousel-indicators
      li.active data-target='#sampleCarousel' data-slide-to='0'
      li data-target='#sampleCarousel' data-slide-to='1'
      li data-target='#sampleCarousel' data-slide-to='2'
    .carousel-inner role='listbox'
      - @book.images.each.with_index(1) do |image, index|
        - if index == 1
          .item.active
            = image_tag image.to_s, class: 'img-responsive img-rounded carousel-image'
        - else
          .item
            = image_tag image.to_s, class: 'img-responsive img-rounded carousel-image'
    a.left.carousel-control href='#sampleCarousel' role='button' data-slide='prev'
      span.glyphicon.glyphicon-chevron-left aria-hidden='true'
      span.sr-only
        | 前へ
    a.right.carousel-control href='#sampleCarousel' role='button' data-slide='next'
      span.glyphicon.glyphicon-chevron-right aria-hidden='true'
      span.sr-only
        | 次へ

【解説】

① 本に登録されている画像一覧を繰り返し処理し、indexを付与する。

- @book.images.each.with_index(1) do |image, index|

② 1枚目に表示する画像を設定する。

今回は、indexが 1 の画像を設定しています。

- if index == 1
  .item.active
    = image_tag image.to_s, class: 'carousel-image'

③ 2枚目以降の画像を設定する。

- else
  .item
    = image_tag image.to_s, class: 'carousel-image'

2.application.scssを編集

application.scss
.carousel-image {
  width: 30%; // スライドに対する画像の幅を設定
  margin: 0 auto; // 画像を中央に配置
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ruby mapメソッド

mapメソッドとは

配列に対して変更を加えて配列を返すメソッド。

書き方

オブジェクト.map { |変数|
  # 実行したい処理
}

もしくは

オブジェクト.map(&:メソッド名)

実行例

書き方1の場合

a=[2,5,3]

b=a.map{ |hoge| 

  hoge*2   
}

#結果 b=[4,10,6]

まず、a[0]の2がhogeに代入されます。そしてhoge*2で4がb[0]に入ります。
次にa[1]の5がhogeに代入されます。そしてhoge*2で10がb[1]に入ります。
最後にa[2]の3がhogeに代入されます。そしてhoge*2で6がb[2]に入ります。

結果として b=[4,10,6]という配列が生まれます。

書き方2 オブジェクト.map(&:メソッド名) の例

a=["リンゴ","ゴマフアザラシ","シカ"]

b=a.map(&:length)

結果 b=[3,7,2]

lengthメソッドは文字列の長さを返すメソッドです。

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

【Windows】gem install sqlite3ができない

環境

ターミナル: MSYS2(mintty+bash+pacman)
Ruby 2.6 + DevKit
Windows10 64bit

SQLiteのバージョンは問わないという人

SQLite 1.3.13をインストールしよう
コンパイル済だからエラー出ることはないはず。

gem install sqlite3 --version 1.3.13 --platform ruby

mkmf.logを見て

①~package configuration for sqlite3 is not found篇~
②~Cannot create temporary file in C:\篇~
模索中...

出てくるエラー

C:\Users\foobar>gem install sqlite3 --platform ruby
Temporarily enhancing PATH for MSYS/MINGW...
Installing required msys2 packages: mingw-w64-x86_64-sqlite3
警告: mingw-w64-x86_64-sqlite3-x.xx.x-x は最新です -- スキップ
Building native extensions. This could take a while...
ERROR:  Error installing sqlite3:
        ERROR: Failed to build gem native extension.

    current directory: C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/sqlite3-1.4.0/ext/sqlite3
C:/Ruby26-x64/bin/ruby.exe -r ./siteconfyyyymmdd-foooo-baaaaar.rb extconf.rb
*** 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=C:/Ruby25-x64/bin/$(RUBY_BASE_NAME)
        --with-sqlcipher
        --without-sqlcipher
        --with-sqlite3-config
        --without-sqlite3-config
        --with-pkg-config
        --without-pkg-config
C:/Ruby26-x64/lib/ruby/2.6.0/mkmf.rb:467:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
        from C:/Ruby26-x64/lib/ruby/2.6.0/mkmf.rb:552:in `try_link0'
        from C:/Ruby26-x64/lib/ruby/2.6.0/mkmf.rb:570:in `try_link'
        from C:/Ruby26-x64/lib/ruby/2.6.0/mkmf.rb:672:in `try_ldflags'
        from C:/Ruby26-x64/lib/ruby/2.6.0/mkmf.rb:1832:in `pkg_config'
        from extconf.rb:35:in `<main>'

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

  C:/Ruby26-x64/lib/ruby/gems/2.6.0/extensions/x64-mingw32/2.6.0/sqlite3-x.x.x/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in C:/Ruby25-x64/lib/ruby/gems/2.5.0/gems/sqlite3-x.x.x for inspection.
Results logged to C:/Ruby26-x64/lib/ruby/gems/2.6.0/extensions/x64-mingw32/2.5.0/sqlite3-x.x.x/gem_make.out
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Windows】gem install sqlite3ができない ~package configuration for sqlite3 is not found篇~

原因

SQLiteのソースファイルが欠損している

解決

MSYS2コンソールで以下のコマンドを実行

pacman -S libsqlite-devel

あとはコマンドプロンプトでも良いのでgem install sqlite3 --platform rubyを実行します。

エラー内容

package configuration for sqlite3 is not found
"x86_64-w64-mingw32-gcc -o conftest.exe -IC:/Ruby26-x64/include/ruby-2.6.0/x64-mingw32 -IC:/Ruby26-x64/include/ruby-2.6.0/ruby/backward -IC:/Ruby26-x64/include/ruby-2.6.0 -I. -D__USE_MINGW_ANSI_STDIO=1 -DFD_SETSIZE=2048 -D_WIN32_WINNT=0x0600 -D__MINGW_USE_VC2005_COMPAT -D_FILE_OFFSET_BITS=64  -O3 -fno-fast-math -fstack-protector-strong conftest.c  -L. -LC:/Ruby26-x64/lib -L. -pipe -s -fstack-protector-strong     -lx64-msvcrt-ruby260  -lshell32 -lws2_32 -liphlpapi -limagehlp -lshlwapi  "
checked program was:
/* begin */
1: #include "ruby.h"
2:
3: #include <winsock2.h>
4: #include <windows.h>
5: int main(int argc, char **argv)
6: {
7:   return 0;
8: }
/* end */
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Windows】gem install sqlite3ができない ~Cannot create temporary file in C:\篇~

原因

Windowsのユーザ名に2バイト文字(日本語)が使われている

解決

Tempファイルの位置を移動させる。
コマンドプロンプトで以下を実行

mkdir C:\temp
set TEMP=C:\temp
set TMP=C:\temp

日本語が使われていなければどのディレクトリでも大丈夫です。
あとはコマンドプロンプトでも良いのでgem install sqlite3 --platform rubyを実行します。

エラー内容

"pkg-config --exists sqlite3"
| pkg-config --libs sqlite3
=> "-LC:/msys64/mingw64/lib lsqlite3\n"
"x86_64-w64-mingw32-gcc -o conftest.exe -IC:/Ruby26-x64/include/ruby-2.6.0/x64-mingw32 -IC:/Ruby26-x64/include/ruby-2.6.0/ruby/backward -IC:/Ruby26-x64/include/ruby-2.6.0 -I. -D__USE_MINGW_ANSI_STDIO=1 -DFD_SETSIZE=2048 -D_WIN32_WINNT=0x0600 -D__MINGW_USE_VC2005_COMPAT -D_FILE_OFFSET_BITS=64  -O3 -fno-fast-math -fstack-protector-strong conftest.c  -L. -LC:/Ruby26-x64/lib -L. -pipe -s -fstack-protector-strong     -lx64-msvcrt-ruby260  -lshell32 -lws2_32 -liphlpapi -limagehlp -lshlwapi  "
Cannot create temporary file in C:\Users\??????\AppData\Local\Temp\: Invalid argument
checked program was:
/* begin */
1: #include "ruby.h"
2:
3: #include <winsock2.h>
4: #include <windows.h>
5: int main(int argc, char **argv)
6: {
7:   return 0;
8: }
/* end */
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ncestryによる多階層構造データを表示、投稿!! ~Ajax~

はじめに

ancestryで作成したカテゴリーデータを用いて、選択肢を動的に変化させる機能を実装しました。

学習メモとして投稿します。
まだ、理解が浅いところもありますが参考になればと思います!

完成形

https://gyazo.com/8a5adc080698873d544b8665855c0901

以下が完成コードです!

routes
resources :products, except: [:index]  do 
    get 'new/children_category', to: 'products#children_category'
    get 'new/grandchildren_category', to: 'products#grandchildren_category'
  end
puroducts_controller
before_action :set_categories, only: [:edit, :update]


〜省略〜

  def children_category
    @children_category = Category.where(ancestry: params[:parent_category_id])
    render json:  @children_category
  end

  def grandchildren_category
    @grandchildren_category = Category.where(ancestry: "#{params[:parent_category_id]}/#{params[:children_category_id]}")
    render json: @grandchildren_category 
  end

puroducts/new_html_haml
.input-field__contents
  .input-field__contents-data
    %p.subline
      商品の詳細
    .input-field__contents-image__headline
      .headlabel
        = f.label :category_id, "カテゴリー"
        %span.necessary
           必須
           .sell__about__right__wrap-box.parent
             %select.select-box1#parent
               %option{value: 0} ---
               -  @parents.each do |parent|
                 %option{value: "#{parent.id}"} #{parent.name}

           .child
             %select.select-box2#child
           .grand_child
             .select-box3
               = f.collection_select(:category_id, [], :id, :name, {prompt: "---"}, {id: "grand_child"})

category_js
$(function(){
  let buildPrompt = `<option value>---</option>`
  let buildHtmlOption = function(parent) {
    let option = `<option value ="${parent.id}">${parent.name}</option>`
    return option
  }
  $('#parent').change(function() {
    let parent_id = $(this).val();
    $.ajax({
      type: 'GET',
      url: 'products/new/children_category',
      data: {parent_category_id: parent_id},
      dataType: 'json'
    })
    .done(function(parent) {
      $('.child').css('display', 'block');
        $('#child').empty();
        $('.grand_child').css('display', 'none');
        $('#child').append(buildPrompt);

      parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
        $('#child').append(html_option);
      });
    })
    .fail(function() {
      alert('エラー')
    });
  });
  $(this).on("change", "#child", function() {
    let parent_id = $("#parent").val();
    let child_id = $("#child").val();
    $.ajax({
        type: 'GET',
        url: 'products/new/grandchildren_category',
        data: {
          parent_category_id: parent_id,
          children_category_id: child_id
        },
        dataType: 'json'
    })
    .done(function(parent) {
      $('.grand_child').css('display', 'block');
      $('#grand_child').empty();
      $('#grand_child').append(buildPrompt);

       parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
         console.log(buildHtmlOption(html_option));
        $('#grand_child').append(html_option);
      });
    })
  });
})

考え方

・親カテゴリーを選択しイベントを発火させたら、カテゴリーをappend(追加)する
・子カテゴリーを選択しイベントを発火させたら、カテゴリーをappend(追加)する
・ajaxを使用を子カテゴリー及び孫カテゴリーが表示されるための通り道を作成する
・最終的には孫カテゴリーの値が保存される様にする

ざっくりとこんな感じです。

では、一つ一つ見ていきましょう!

ルーティング

プログラムの処理の流れとして、最終的にviewに子カテゴリーと親カテゴリーを表示させます。
それは実際に、コントローラーとjsで処理を行いますのでリクエストが会った際のコントローラーへの通り道を作成します。

routes
resources :products, except: [:index]  do 
   #children_categoryアクションに行くためのパス
    get 'new/children_category', to: 'products#children_category'
   #grandchildren_categoryアクションに行くためのパス
    get 'new/grandchildren_category', to: 'products#grandchildren_category'
  end

コントローラー

前提として、ajax処理行うのでjsでajax処理が行われたあとはコントローラーに行きます。
その際、コントローラーではカテゴリーの値を探してjsに返してあげる必要があります。
したがって、以下の様に書きます。

puroducts_controller
before_action :set_categories, only: [:edit, :update]


〜省略〜

  def children_category
    #.whereを使ってancestryから値を探して、インスタンス変数に代入する
    @children_category = Category.where(ancestry: params[:parent_category_id])
   #ancestryから探した値をjsに返してあげる
    render json:  @children_category
  end

  def grandchildren_category
  #.whereを使ってancestryから値を探して、インスタンス変数に代入する
    @grandchildren_category = Category.where(ancestry: "#{params[:parent_category_id]}/#{params[:children_category_id]}")
  #ancestryから探した値をjsに返してあげる
    render json: @grandchildren_category 
  end

JSの処理

jsでは、カテゴリーが選択されるたびにイベントが発火する様にします。
具体的に、
カテゴリーが選択されたら、イベントが発火し要素のカテゴリー表示させる
カテゴリーが選択されたら、イベントが発火し要素のカテゴリー表示させる

処理としては、イベントが発火したらajaxでコントローラーから値を取得しforEachで全てを表させる流れになります。

category_js
//①=====HTMLで表示させるviewを定義===========================
$(function(){
  let buildPrompt = `<option value>---</option>`
  let buildHtmlOption = function(parent) {
    let option = `<option value ="${parent.id}">${parent.name}</option>`
    return option
  }
//=================================================



//②=====親カテゴリーが選択され子カテゴリーを呼び出す処理============
  $('#parent').change(function() {
    let parent_id = $(this).val();
    //ajaxでコントローラーに送る
    $.ajax({
      type: 'GET',
      url: 'products/new/children_category',
      data: {parent_category_id: parent_id},
      dataType: 'json'
    })
  //以下はコントローラーからのレスポンス後の処理
    .done(function(parent) {
      $('.child').css('display', 'block');
        $('#child').empty();
        $('.grand_child').css('display', 'none');
        $('#child').append(buildPrompt);

  //コントローラーから取得した値をforEachで全て取得し、.appendでHTML要素に追加する
      parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
        $('#child').append(html_option);
      });
    })
    .fail(function() {
      alert('エラー')
    });
  });
//=============================================


//②=====子カテゴリーが選択され孫カテゴリーを呼び出す処理============
  $(this).on("change", "#child", function() {
    let parent_id = $("#parent").val();
    let child_id = $("#child").val();
  //ajaxでコントローラーに送る
    $.ajax({
        type: 'GET',
        url: 'products/new/grandchildren_category',
        data: {
          parent_category_id: parent_id,
          children_category_id: child_id
        },
        dataType: 'json'
    })
  //以下はコントローラーからのレスポンス後の処理
    .done(function(parent) {
      $('.grand_child').css('display', 'block');
      $('#grand_child').empty();
      $('#grand_child').append(buildPrompt);
  //コントローラーから取得した値をforEachで全て取得し、.appendでHTML要素に追加する
       parent.forEach(function(child) {
        var html_option = buildHtmlOption(child);
         console.log(buildHtmlOption(html_option));
        $('#grand_child').append(html_option);
      });
    })
  });
//=============================================
})

最後には、HTML

HTMLで注意する点は、jsのid属性とHTMLでのid属性に齟齬かないかぐらいです。

ただし、最後の孫カテゴリーの値を保存するためには少し工夫が必要です。

puroducts/new_html_haml
.input-field__contents
  .input-field__contents-data
    %p.subline
      商品の詳細
    .input-field__contents-image__headline
      .headlabel
        = f.label :category_id, "カテゴリー"
        %span.necessary
           必須
           .sell__about__right__wrap-box.parent
             %select.select-box1#parent
               %option{value: 0} ---
                # 親カテゴリーの値を全て表示させる
               -  @parents.each do |parent|
                 %option{value: "#{parent.id}"} #{parent.name}

           .child
        # #childのところにjsで定義したviewが挿入される
             %select.select-box2#child
           .grand_child
             .select-box3
          # id:grand_childのところにjsで定義したviewが挿入される
                # また、選択孫カテゴリーの値が保存正しく保存されるために以下の様に書きます。
               = f.collection_select(:category_id, [], :id, :name, {prompt: "---"}, {id: "grand_child"})

補足で、以下の記述については以下のサイトを参考にしましたのでご確認ください

 f.collection_select(:category_id, [], :id, :name, {prompt: "---"}, {id: "grand_child"})

 #参考記述
 #collection_select(オブジェクト名, メソッド名, 要素の配列, value属性の項目, テキストの項目 [, オプション or HTML属性 or イベント属性])

参考記事:
https://railsdoc.com/page/collection_select

終わりに

処理としては、そこまで複雑ではないため1つ1つ確認しながら行ったら上手く行きました!

もし、エラーや上手く値が取得できていない場合は、binding.pryや、console.log();debuggerで確認してみてください!

ありがとうございました!

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

簡単ログイン機能実装のエラーが一応解決したけど疑問が残る

はじめに

こちらの記事を参考に実装しましたので、コードで不明な点はこちらの記事を見にいってください。
ゲストログイン・簡単ログイン機能の実装方法(ポートフォリオ用)

発生している問題・エラーメッセージ

簡単ログイン機能の実装で、ログイン画面に「簡単ログイン」ボタンを設置したが、ボタン押してもログインできない。

エラーメッセージ(ターミナル)
Processing by HomesController#new_guest as HTML
Parameters:{"authenticity_token"=>"c1Qc02T4i6+77OtxhGDwxJwEQUYO8d9cIncoNjZ/hXa5c3IzxHPjFAr2QleTBCuMpnxd5+1Sk+HRa9RNXLGSkg=="}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)

エラーになった時のソースコード

routes.rb
post '/homes/guest_sign_in', to: 'homes#new_guest'
homes_controller.rb
class HomesController < ApplicationController

  def new_guest
    user = User.find_or_create_by(email: 'guest@example.com') do |user|
      user.password = SecureRandom.urlsafe_base64
    end
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end
devise/sessions/new.html.haml
(省略)
.easyLogin
  = link_to 'かんたんログイン', homes_guest_sign_in_path, method: :post, class:"btn btn-lg btn-success center-block"

考えたこと

・sessionで保持しているものとauthenticity_tokenをキーとして送られたコードが異なる?
・authenticity_tokenはログイン画面のフォームのinputタグの中にあったので、フォーム外に簡単ログインボタンを設置すれば、authenticity_tokenが関係なくなり解決される?

→結果的にどちらも間違っていた

うまくいった時のソースコード

ターミナル
Processing by Users::SessionsController#new_guest as HTML
Parameters: {"authenticity_token"=>"2UEwYr4gIvtHF4GqHNPaXIiWgQSbEXPVjI9kneVS27oTZl6CHqtKQPYNKIwLtwEUsu6dpXiyP2h/k5jmj5zMXg=="}
  User Load (4.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`email` = 'guest@example.com' LIMIT 1
  ↳ app/models/user.rb:31
Redirected to http://localhost:3000/
Completed 302 Found in 10ms (ActiveRecord: 4.0ms)
routes.rb
devise_scope :user do
  post 'users/guest_sign_in', to: 'users/sessions#new_guest'
end
user.rb
(省略)
def self.guest
  find_or_create_by!(email: 'guest@example.com') do |user|
    user.password = SecureRandom.urlsafe_base64
  end
end
sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new_guest
    user = User.guest
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end
devise/sessions/new.html.haml
(省略)
.easyLogin
  = link_to 'かんたんログイン', users_guest_sign_in_path, method: :post, class:"btn btn-lg btn-success center-block"

疑問が残る

これで一応ゲストユーザーでログインできたので、解決はしたのですが、なぜ解決したのかまだちゃんとわかっていません。

自分なりに考えたことは以下の通りです。

・homes_controllerではダメでsessions_controllerだとうまくいく理由は、ログイン機能はもともとsessions_controllerで行っており、その流れに組み込ませる形でないといけないから。

しかし、参考記事だとhomes_controllerでも問題なく遷移できているので、この予想はおそらく間違っています。
もしわかる方がいらっしゃったら教えて頂けると嬉しいです?‍♂️

参考記事

ゲストログイン・簡単ログイン機能の実装方法(ポートフォリオ用)

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

簡単ログイン機能実装のエラー原因はauthenticate_user!だった

はじめに

こちらの記事を参考に実装しましたので、コードで不明な点はこちらの記事を見にいってください。
ゲストログイン・簡単ログイン機能の実装方法(ポートフォリオ用)

発生している問題・エラーメッセージ

簡単ログイン機能の実装で、ログイン画面に「簡単ログイン」ボタンを設置したが、ボタン押してもログインできない。

エラーメッセージ(ターミナル)
Processing by HomesController#new_guest as HTML
Parameters:{"authenticity_token"=>"c1Qc02T4i6+77OtxhGDwxJwEQUYO8d9cIncoNjZ/hXa5c3IzxHPjFAr2QleTBCuMpnxd5+1Sk+HRa9RNXLGSkg=="}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)

エラーになった時のソースコード

routes.rb
post '/homes/guest_sign_in', to: 'homes#new_guest'
homes_controller.rb
class HomesController < ApplicationController

  def new_guest
    user = User.find_or_create_by(email: 'guest@example.com') do |user|
      user.password = SecureRandom.urlsafe_base64
    end
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end
devise/sessions/new.html.haml
(省略)
.easyLogin
  = link_to 'かんたんログイン', homes_guest_sign_in_path, method: :post, class:"btn btn-lg btn-success center-block"

考えたこと

・sessionで保持しているものとauthenticity_tokenをキーとして送られたコードが異なる?
・authenticity_tokenはログイン画面のフォームのinputタグの中にあったので、フォーム外に簡単ログインボタンを設置すれば、authenticity_tokenが関係なくなり解決される?

→結果的にどちらも間違っていた

うまくいった時のソースコード

ターミナル
Processing by Users::SessionsController#new_guest as HTML
Parameters: {"authenticity_token"=>"2UEwYr4gIvtHF4GqHNPaXIiWgQSbEXPVjI9kneVS27oTZl6CHqtKQPYNKIwLtwEUsu6dpXiyP2h/k5jmj5zMXg=="}
  User Load (4.0ms)  SELECT  `users`.* FROM `users` WHERE `users`.`email` = 'guest@example.com' LIMIT 1
  ↳ app/models/user.rb:31
Redirected to http://localhost:3000/
Completed 302 Found in 10ms (ActiveRecord: 4.0ms)
routes.rb
devise_scope :user do
  post 'users/guest_sign_in', to: 'users/sessions#new_guest'
end
user.rb
(省略)
def self.guest
  find_or_create_by!(email: 'guest@example.com') do |user|
    user.password = SecureRandom.urlsafe_base64
  end
end
sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new_guest
    user = User.guest
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end
devise/sessions/new.html.haml
(省略)
.easyLogin
  = link_to 'かんたんログイン', users_guest_sign_in_path, method: :post, class:"btn btn-lg btn-success center-block"

疑問が残る

これで一応ゲストユーザーでログインできたので、解決はしたのですが、なぜ解決したのかまだちゃんとわかっていません。

自分なりに考えたことは以下の通りです。

・homes_controllerではダメでsessions_controllerだとうまくいく理由は、ログイン機能はもともとsessions_controllerで行っており、その流れに組み込ませる形でないといけないから。

しかし、参考記事だとhomes_controllerでも問題なく遷移できているので、この予想はおそらく間違っています。
もしわかる方がいらっしゃったら教えて頂けると嬉しいです?‍♂️

追記(エラー原因がわかりました)

homes_controller.rb
class HomesController < ApplicationController
(省略)
end

homes_controllerapplicatoin_controllerを継承していました。

applicatoin_controller.rb
(省略)
before_action :authenticate_user!

protected

def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
end

before_action :authenticate_user!は、ログイン済みのユーザーだけを許可するので、これが原因で弾かれていました。

before_action :authenticate_user!をコメントアウトしたら、homes_controllerでも問題なく画面遷移しました。

sessions_controllerの方が自然なので、実装はhomes_controllerではなくsessions_controllerで設定しました。

参考記事

ゲストログイン・簡単ログイン機能の実装方法(ポートフォリオ用)

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

【Rails】UrlHelperを拡張する

独自のUrlHelperを定義したい

Rails.application.routes.url_helpers を include したClassを作成するとよいです。

class MyUrlResolver
  class << self
    include Rails.application.routes.url_helpers
  end

  # 独自定義したhelper
  def self.custom_users_path(user)
    if user.admin?
      admin_users_path
    else
      users_path
    end
  end
end

これで、

> MyUrlResolver.admin_users
=> "/admin_users"
> MyUrlResolver.users_path
=> "/users"

標準のUrlHelperに加えて、

> MyUrlResolver.custom_users_path(admin_user)
=> "/admin_users"
> MyUrlResolver.custom_users_path(normal_user)
=> "/users"

独自定義したUrlHelperも使うことができます。

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

GraphQLでのPaginationの実装方法について(for ruby)

背景

弊社サービスのCarelyではサーバサイドはRuby(onRails)で実装されておりフロント(Vue)とのデータのやりとりをgraphQLで実装しています。
RailsでのPaginationはKaminariというgemをよく使うのですが、graphQLの場合はRelay-Style Cursor Paginationがstandardっぽいので両方の実装方法を試してみました。

使っているgem, versionについて(2020/6/26現在)

kaminariはversion 1.2.1
graphql-rubyはversion 1.10.10
です。

Relay-Style Cursor Paginationを使っての実装

(graphql-rubyのPaginationの説明のURLです)
https://graphql-ruby.org/pagination/using_connections.html

サーバサイド実装例

下記のようにSchema ClassにPaginationのPluginを使うための記述をします。

class MySchema < GraphQL::Schema
  .
  .
  use GraphQL::Pagination::Connections
  .
end

Paginationの機能を追加したいQueryの定義に::connection_typeという記述を使用します。

field :users, Types::UserType::connection_type, null: true do
  .
  .
  argument :name, String, "名前", required: false
  .
end

サーバサイドの実装は以上です。

フロントサイド queryの呼び出し例

first(last), after(before) のparameterを指定できるようになります。
下記のqueryだとfirst(先頭)から10件取得でafterを指定することでafterから10件取得になります。
afterに指定する文字列はcursorで取得された文字列を指定します。
また pageInfoというfieldも指定できるようになり、前ページ、次ページがあるか?やstart、endカーソルの位置を取得することができます。


query MyQuery {
  users (first: 10, after: "xxxx") {
    pageInfo {
      hasPreviousPage  
      hasNextPage
      endCursor
      startCursor
    }
   edges {
     cursor
        node {
          firstName
          lastName
          mailAddress
          age
          .
          .
        }
      }
   ## nodesでも取れる
   nodes {
     firstName
     lastName
     mailAddress
     age
     .
     .
   }


## 結果の例
{
  "data": {
    "users": {
      "pageInfo": {
        "hasPreviousPage": false,
        "hasNextPage": true,
        "endCursor": "MTA",
        "startCursor": "MQ"
      },
      "edges": [
        {
          "cursor": "MQ",
          "node": {
            "firstName": "ホゲホゲ",
            "lastName": "フガフガ",
            "mailAddress": "hogehoge@example.com",
            "age": "20"
          }
        },
        {
          "cursor": "Mg",
          "node": {
            "firstName": "ホゲホゲ2",
            "lastName": "フガフガ2",
            "mailAddress": "hogehoge2@example.com",
            "age": "30"
          }
        },
        .
        .
     ],
     "nodes": [
       {
         "firstName": "ホゲホゲ",
         "lastName": "フガフガ",
         "mailAddress": "hogehoge@example.com",
         "age": "20"
       },
       {
         "firstName": "ホゲホゲ2",
         "lastName": "フガフガ2",
         "mailAddress": "hogehoge2@example.com",
         "age": "30"
       },
        .
        .
     ]

Kaminariを使っての実装

kaminariでのPaginationで使う一般的なメソッドは以下になります。

# 10件ごとに分割した1ページ目を取得する
User.page(1).per(10)
# tolal件数
User.page(1).per(10).total_count
# tolalページ数
User.page(1).total_pages
# 1ページの件数
User.page(1).limit_value
# 現在のページ数
User.page(1).current_page
# 次ページ数
User.page(1).next_page
# 前ページ数
User.page(1).prev_page
# 最初のページかどうか
User.page(1).first_page?
# 最後のページかどうか
User.page(1).last_page?

GraphQLでkaminariの機能を使う場合の実装例

以下のようなPagination用のTypeを作成します。

module Types
  class PaginationType < Types::BaseObject
    field :total_count, Int, null: true
    field :limit_value, Int, null: true
    field :total_pages, Int, null: true
    field :current_page, Int, null: true
  end
end

以下のように UserType と複数のUser情報とPaginationを返す UsersType を作成します。

module Types
  class UserType < Types::BaseObject
    field :uuid, String, null: true
    field :first_name, String, null: true
    field :last_name, String, null: true
    field :mail_address, String, null: true
    field :age, String, null: true
    .
    .
  end
end

module Types
  class UsersType < Types::BaseObject
    field :pagination, PaginationType, null: true
    field :users, [UserType], null: true
  end
end


pagination情報を返すためQueryに以下のような処理を追加します。

# 引数でpage, perを渡せるように追加
field :users, Types::UserType, null: true do
  .
  .
  argument :name, String, "名前", required: false
  argument :page, Int, required: false
  argument :per, Int, required: false
  .
end

# 引数 page、perがあればkaminariのpaginationを使用
def users(**args)
  .
  .
  users = User.page(args[:page]).per(args[:per])
  {
     users: users,
     pagination: pagination(users)
  }
end

# kaminariのメソッドを使って件数を返す
def pagination(result)
  {
    total_count: result.total_count,
    limit_value: result.limit_value,
    total_pages: result.total_pages,
    current_page: result.current_page
  }
end

10件ごとに分割した1ページ目を取得するqueryとその結果の例です。


query MyQuery {
  users (per:10, page:1) {
    pagination {
      currentPage
      limitValue
      totalCount
      totalPages
    }
   users {
     firstName
     lastName
     mailAddress
     age
     .
     .
   }
}


## 結果の例
{
  "data": {
    "users": {
      "pagination": {
        "currentPage": 1,
        "limitValue": 10,
        "totalCount": 100,
        "totalPages": 10
      },
      "users": [
        {
          "firstName": "ホゲホゲ",
          "lastName": "フガフガ",
          "mailAddress": "hogehoge@example.com",
          "age": "20"
        },
        {
          "firstName": "ホゲホゲ2",
          "lastName": "フガフガ2",
          "mailAddress": "hogehoge2@example.com",
          "age": "30"
        },
        .
        .
     ]

使い分けについて

Relay-Style Cursor Pagination
APIで情報を検索するだけなら簡単に使えるので良さそう。
ただcursorによる位置情報を持っているだけなのでフロント側でトータル件数、トータルページ数を表示したりするUIを作るのであればカスタムconnectionを作成する必要がありそうです。

kaminari
社内のエンジニアしか使わず、フロント側でトータル件数、トータルページ数を表示するUIを作るのであればkaminariを使った方が工数的にはかからないです。

個人的にはRelay-Style Cursor Paginationを使ってカスタムconnectionを作っていく方がgraphQLのスタイルに合っているので良いのではないかと思います。

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

rubyの変数をjavascriptで使う。

色々方法はあるみたいですが、今回実行した方法。

結論

index.html.erb



@test = { 'hoge' => 'huga', 'hogehoge' => 'hugahuga' }

<div id="hoge" data-hoge-id="<%= @test.to_json %>"></div>

<script>
  var test = $('#hoge').data('hoge-id');
</script>

たったこれだけでした。

他にも"gon"というgemを使っても実装できるとの事。

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

【AWS ElastiCache】AWS ElastiCache(Memcached)を利用してRDSへのクエリをキャッシング

目標

・AWS ElastiCache(Memcached)(※)を利用してRDSへのクエリをキャッシングするシステムを構築すること。
・キャッシングの方式はキャッシュ戦略(説明後述)に沿って構築を行う。

※ElastiCacheに関する基本・詳細情報は以下記事を参照
AWS キャッシュ活用 ElastiCache

はじめに

これまでの記事でEC2⇔RDS、及びEC2⇔ElastiCacheの接続を構築したので、
今度はElastiCacheを利用してRDSへのクエリをキャッシングさせるプログラムをほぼポートフォリオ的なノリで書いてみました。
言語はRubyを利用しました。
実務で経験したことのない実装ですので、何か変なとこあったらコメントください笑

キャッシュ戦略(※)とは

ElastiCacheをキャッシュ利用する際のAWSが推奨するベストプラクティスのこと。
以下2つの方式に分かれ、システムのユースケースに沿った戦略を選択する必要がある。
今回は遅延読み込みを利用した実装を行う。

・遅延読み込み
データ読み込み時にキャッシュを参照し、ヒットしなかった場合にのみデータソースへアクセスし必要なデータを取得してキャッシュに書き込む方式
⇒キャッシュのメモリ使用量を抑えることが可能だが、キャッシュデータが古い可能がある(キャッシュミス時にしかキャッシュを書き換えないため)

・書き込みスルー
データ書き込み時に毎回キャッシュにも書き込みを行う方式
⇒キャッシュのメモリ使用量は多くなってしまうが、常に最新のキャッシュデータを取得可能

※より詳しくはAWSドキュメント参照
キャッシュ戦略
https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/mem-ug/Strategies.html

前提

・EC2とRDS(MySQL)間の接続が確立されていること(※1)。
・EC2とElactiCache(Memcached)間の接続が確立されていること(※2)。

※1 以下記事で構築済み
【RDS】EC2とRDS(MySQL)間の接続を確立する

※2 以下記事で構築済み
【AWS ElastiCache】AWS ElastiCache(Memcached)を構築し、EC2から接続

システム環境

・EC2
OS(AMI) : Amazon Linux 2 AMI (HVM), SSD Volume Type
ソフトウェア: Rubyを利用した自作プログラム

・RDS
エンジン: MySQL

・ElastiChache
エンジン: Memcached

完成フロー

キャッシュヒット時は以下のフロー

①EC2から検索クエリを投げる
②ElaElastiCacheがクエリ結果を返し、通信終了
ElastiCache1 (3).png

キャッシュミス時は以下のフロー

①EC2から検索クエリを投げる
②ElastiCacheからキャッシュミスが返る
③RDSへSQLクエリを発行
④RDSからSQLクエリ結果が返ってくる
⑤取得したクエリ結果をElastiCacheに書き込む
Copy of ElastiCache2 (2).png

作業の流れ

項番 タイトル
1 デプロイ
2 動作検証

手順

1.デプロイ

①EC2にOSログイン

②Ruby実行環境をインストール

$ sudo yum install ruby

③Ruby用Memocacheクライアントのgem(Rubyのライブラリ)をインストール(※)
※参考にしたサイト
16.6.3.7 Ruby での MySQL と memcached の使用

$ gem install Ruby-MemCache

③Mysqlクライアントのgemをインストール(※)
Mysqlクライアントgemを利用する際に必要となるライブラリをインストール
(以下はEC2のAmazon Linux 2を利用した際の手順です。他のディストリビューションでは必要なライブラリが異なる可能性があります。)

※一部参考にした記事
AWS Cloud9のEC2上にmysql2のgemを導入する

$ sudo yum -y install ruby-devel
sudo yum groupinstall "Development Tools"
sudo yum install mysql-devel

Mysqlクライアントgemインストール

gem install mysql2

④自作Rubyスクリプト(※)をEC2に配備
<Elasticache_endpoint><rds_endpoint><db_login_user><db_login_password><db_name>は適宜書き換え

ファイル名: rds_cache.rb
# **********************************************************************************

# 機能概要: AWS ElastiCache(Memcached)を利用して、RDSへのクエリ結果をキャッシングする
# 機能詳細: ElastiCacheにクエリを発行し、キャッシュが存在する場合にはそのバリューを返す。
#       キャッシュが存在しない場合、データソースであるRDS(MySQL)にアクセスし結果表示後、ElastiCacheにキャッシュ保存する。
# スクリプト用法: ruby <スクリプトパス> "<検索SQLクエリ>"

# **********************************************************************************

unless ARGV.size() == 1
    puts "The number of arguments is incorrect."
    exit
end

# パッケージ
require 'base64'
require 'memcache'
require 'mysql2'

# 変数
sql_query = ARGV[0]                                                     # 実行SQLクエリ
cache_host = "<Elasticache_endpoint>"                                   # Elasticacheエンドポイント
cache_port = 11211                                                      # Elasticacheポート番号
db_host = "<rds_endpoint>"                                              # RDSエンドポイント
db_user = "<db_login_user>"                                             # DBログインユーザ
db_password = "<db_login_password>"                                     # DBパスワード
db_name = "<db_name>"                                                   # データベース名

# SQLクエリ(空白除去、小文字変換)をBase64でエンコード(キャッシュのキーとして利用する)
encoded_query = Base64.encode64(sql_query.gsub(" ", "").downcase)

# MemCache、Mysql接続用インスタンス作成
memc_connect = MemCache::new "#{cache_host}:#{cache_port}"
db_connect = Mysql2::Client.new(host: db_host, username: db_user, password: db_password, database: db_name)

# Elacacheからキャッシュを取得
cache_outcome = memc_connect[encoded_query]

if !cache_outcome[0].nil?
    puts "Cache HIT!"
    puts "[Query results from cache]"
    puts cache_outcome[0]
else
    puts "Cache MISS"
    puts "[Query results from datasource]"

    # キャッシュミスした場合、データベースへSQLクエリ発行
    sql_outcome = db_connect.query(sql_query) 

    cache_val = ""
    for row in sql_outcome do
        puts "--------------------"
        cache_val = cache_val + "--------------------\n"
        for key, value in row do
            puts "#{key} => #{value}"

            cache_val = cache_val + "#{key} => #{value}\n"
        end
    end

    # Elasticacheにバリューをセット
    memc_connect[encoded_query] = cache_val
end

※実装方針は以下
・SQLクエリを引数としてスクリプト実行
・引数として指定したSQLクエリを空白除去・小文字変換後、Base64によってエンコードし、Elasticacheのキーとしてキャッシュ検索・保存に利用する。
・キャッシュヒットした場合は、結果を出力しスクリプト終了
・キャッシュミスした場合は、RDS(データソース)にアクセスしSQLクエリを実行し結果を出力。最後にその結果をElasticacheに保存。

2.動作検証

検証用DBデータ

+----+-----------+
| id | Name      |
+----+-----------+
|  1 | Ryosuke   |
|  2 | Tomoharu  |
|  3 | ryosuke   |
|  4 | shunsuke  |
|  5 | sato      |
|  6 | sato      |
|  7 | ryOsuke   |
|  8 | Kawashima |
|  9 | tomoharu  |
| 10 | RYOSUKE   |
+----+-----------+

実行SQLクエリ及びその結果期待値
tempsnip.png

①クエリ初回実行(キャッシュミスパターン)
Cache MISSメッセージが出力され、データソース(RDS)からクエリ結果が適切に表示されているためOK

$ ruby rds_cache.rb "SELECT * FROM test_table WHERE name = 'Ryosuke';"
Cache MISS
[Query results from datasource]
--------------------
id => 1
Name => Ryosuke
--------------------
id => 3
Name => ryosuke
--------------------
id => 7
Name => ryOsuke
--------------------
id => 10
Name => RYOSUKE

②クエリ再実行(キャッシュヒットパターン1)
Cache HIT!が出力され、Elasticacheから適切なクエリ結果が返ってきているためOK

[ec2-user@ip-172-31-34-150 ~]$ ruby rds_cache.rb "SELECT * FROM test_table WHERE name = 'Ryosuke';"
Cache HIT!
[Query results from cache]
--------------------
id => 1
Name => Ryosuke
--------------------
id => 3
Name => ryosuke
--------------------
id => 7
Name => ryOsuke
--------------------
id => 10
Name => RYOSUKE

③小文字化かつスペースをいじったクエリを再実行(キャッシュヒットパターン2)

[ec2-user@ip-172-31-34-150 ~]$ ruby rds_cache.rb "select *            from test_table where name = 'Ryosuke';"
Cache HIT!
[Query results from cache]
--------------------
id => 1
Name => Ryosuke
--------------------
id => 3
Name => ryosuke
--------------------
id => 7
Name => ryOsuke
--------------------
id => 10
Name => RYOSUKE

所感

本来はキャッシュを利用してクエリのレスポンスを高速化させたり、データベース負荷を下げることがこのシステムの目的なのですが、
データ数が少なすぎて性能面でのメリットは確認できていないのがなんとも言えない感じです…笑
いずれ時間あったらそこらへんも軽く確認出来たらとは思ってはいます。

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

Seleniumでnavigator.webdriverの対策をする

enable-automationとかuseAutomationExtensionは期待通りの動作しなかった

とある理由でスクレイピングしたいけど、navigator.webdriver=trueなブラウザだとNGなサイトだった。
対応したい。できるらしいので。
参考:navigator.webdriver=trueだとロボットだとバレる。その回避法はあるか?puppeteerなら出来そう

でもSelenium使っているしpuppeteer使ったことないし、どうにかできないものか。。

動かなかったときの設定

どうやらSeleniumでもできるらしい記事をいくつか見つけた。

参考1:https://stackoverflow.com/questions/53039551/selenium-webdriver-modifying-navigator-webdriver-flag-to-prevent-selenium-detec

参考2:https://help.applitools.com/hc/en-us/articles/360007189411--Chrome-is-being-controlled-by-automated-test-software-notification

やってみた。

    capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
      accept_insecure_certs: true,
      chromeOptions: {
        args: [
          '-window-size=1920,1080',
          '--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
        ],
        excludeSwitches: ['enable-automation'], # 追加
        useAutomationExtension: false # 追加
      }
    )
    driver = Selenium::WebDriver.for(
      :remote,
      url: 'http://chrome:4444/wd/hub',
      desired_capabilities: capabilities,
      http_client: Selenium::WebDriver::Remote::Http::Default.new
    )

実際に動かしてみてもtrueが帰ってくる。ダメだった。

driver.execute_script('return navigator.webdriver')
>>> true

動いた設定

enable-automationとかuseAutomationExtensionは削除。
今まで通りにdriverを設定。

    capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
      accept_insecure_certs: true,
      chromeOptions: {
        args: [
          '-window-size=1920,1080',
          '--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
        ],
      }
    )
    driver = Selenium::WebDriver.for(
      :remote,
      url: 'http://chrome:4444/wd/hub',
      desired_capabilities: capabilities,
      http_client: Selenium::WebDriver::Remote::Http::Default.new
    )

    # 以下を追加
    driver.execute_script('const newProto = navigator.__proto__;delete newProto.webdriver;navigator.__proto__ = newProto;')

実際に動かすとundefinedが帰ってくる。
puppeteerと似たような動作する。
良かった。

driver.execute_script('return navigator.webdriver')
>>> undefined
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Seleniumでnavigator.webdriverの対策をしてアクセスする

enable-automationとかuseAutomationExtensionは期待通りの動作しなかった

とある理由でスクレイピングしたいけど、navigator.webdriver=trueなブラウザだとNGなサイトだった。
対応したい。できるらしいので。
参考:navigator.webdriver=trueだとロボットだとバレる。その回避法はあるか?puppeteerなら出来そう

でもSelenium使っているしpuppeteer使ったことないし、どうにかできないものか。。

動かなかったときの設定

どうやらSeleniumでもできるらしい記事をいくつか見つけた。

参考1:https://stackoverflow.com/questions/53039551/selenium-webdriver-modifying-navigator-webdriver-flag-to-prevent-selenium-detec

参考2:https://help.applitools.com/hc/en-us/articles/360007189411--Chrome-is-being-controlled-by-automated-test-software-notification

やってみた。

    capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
      accept_insecure_certs: true,
      chromeOptions: {
        args: [
          '-window-size=1920,1080',
          '--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
        ],
        excludeSwitches: ['enable-automation'], # 追加
        useAutomationExtension: false # 追加
      }
    )
    driver = Selenium::WebDriver.for(
      :remote,
      url: 'http://chrome:4444/wd/hub',
      desired_capabilities: capabilities,
      http_client: Selenium::WebDriver::Remote::Http::Default.new
    )

実際に動かしてみてもtrueが帰ってくる。ダメだった。

driver.execute_script('return navigator.webdriver')
>>> true

動いた設定

enable-automationとかuseAutomationExtensionは削除。
今まで通りにdriverを設定。

    capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
      accept_insecure_certs: true,
      chromeOptions: {
        args: [
          '-window-size=1920,1080',
          '--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
        ],
      }
    )
    driver = Selenium::WebDriver.for(
      :remote,
      url: 'http://chrome:4444/wd/hub',
      desired_capabilities: capabilities,
      http_client: Selenium::WebDriver::Remote::Http::Default.new
    )

    # 以下を追加
    driver.execute_script('const newProto = navigator.__proto__;delete newProto.webdriver;navigator.__proto__ = newProto;')

実際に動かすとundefinedが帰ってくる。
puppeteerと似たような動作する。
良かった。

driver.execute_script('return navigator.webdriver')
>>> undefined
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む