- 投稿日:2020-07-01T22:58:47+09:00
.sort.uniq か、.uniq.sort か
配列をソートしてユニークするのに、
.uniq.sort
と.sort.uniq
どっちが良いのか。長いものには巻かれようと思ったものの、破壊的に実行すると、
.uniq!.sort!
は実行出来ないケースがある事が判明@2.7.1
test.rbr = { '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.rba = [5,3,4,4,3,2] #a.sort!.uniq! a.uniq!.sort! p aな場合はエラーになんない。
ベンチが遅かろうと、
.sort.uniq
の方を使うかな1
こっちの方が、シェルで馴染み深いし。 ↩
- 投稿日:2020-07-01T22:58:37+09:00
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
色々試して、、、
結論
古い
ruby
のTime
クラス(モジュール?)の.strptime
は、%j
解釈してくれないっぽい2検証では、
2.7.1p83
はOKだった3検証
以下のコードを、
1.9.2p290
,2.0.0p481
,2.7.1p83
で、それぞれ実行test.rbrequire '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.1
はTime.strptime
も解釈してくれてる。2.7.1p83$ ruby test.rb 1047352911 1047352911 1047352911
ruby
版のdelta
みたいなのがあれば、それ読めば一発なんだろうけど、そこまでの情熱は湧かない、、、
- 投稿日:2020-07-01T22:58:29+09:00
三項演算子で、複数変数への代入
10年ぶりくらいに
ruby
のコードに立ち向かわなくてはならなくなった$ ruby -e 'i, j = 1 ? [1,2]:[3,4] ; p i, j' 1 2こんなので数時間悩んだのがアホみたい
- 投稿日:2020-07-01T22:45:58+09:00
Error ExecJS::RuntimeUnavailable: 発生時の対処法
発生現象
AWSのEC2でWebサーバ、アプリケーションサーバの設定時に、環境変数の設定をする際の
$ rake secret
を実行した際に下記Errorが発生。terminalExecJS::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.1AWSの本番環境にもinstallする
terminalsudo 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が走るようになりました。
- 投稿日:2020-07-01T20:36:18+09:00
railsのバリデーションとnull: false 自分用メモ
バリデーションとは、リクエスト側が送信した特定のカラムの値が空のままだったり、意図しない形で保存されるようなことを防ぐために、予めモデルに記述しておく、門番のような役割。
上記の例だと、
text「通りまーす」
validates「待て」
text「えっ」
validates「お前何も入力されてないじゃないか。やり直し」
text「ぴえん」こんな感じ。
一方でnull: falseとは。
実はこちらも空のままのデータを受け入れないための門番、その2。
じゃあどっちが良いのかっていうと、どっちも書くが正解。
なぜか。
理由は、前述した二つの要素にはひとつ、決定的な違いがあって、それは、「守っている場所が違う」こと。
バリデーションくんは、railsアプリの中で張っている門番。フォームからの送信などに対して取り締まってくれる。
対してnull: falseくんは、データベースの前で待ち構えている門番。こちらはデータベースへの直接の書き込みを防いでくれる。銀行の窓口と、金庫前の警備員、のような関係。例えた結果、逆に分かりにくくなっている。
バリデーションが無いとアプリケーションが意図した挙動をしてくれなかったり、null: falseが無いと何らかの方法でデータベースに意図しないデータが送られた時に素通りしてしまう。
なので、二人仲良くデータベースを守っていって欲しい。
- 投稿日:2020-07-01T20:08:29+09:00
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となります要点
この結果から今後気を付けなきゃなと思ったところは、表内で大文字で強調していますが、改めて以下に記載しておきます。
- 半角スペースは、
!empty?
では存在するもの空ではないとみなされtrueを返し、present?
では空とみなされfalseを返す
↑(修正)@scivola 様よりコメントをいただき、修正および追記しました。ありがとうございました。
(追記)present?
(もといblank?
)は、空判定の出る範囲が広めであることに注意
例:false、nil、半角スペース以外に、全角スペース、タブ(\t)、改行(\n)、Unicode(\unnnn)等も空と判定される- 配列に
any?
を適用すると、真の要素(=nilでない要素)がある場合にtrueを返す。それ以外のメソッドは、真か否かに関係なく、要素があればtrueを返す。- 素の
nil
に!nil?
って聞いてもfalseを返すが、[nil]
でできた配列に!nil?
と聞くとtrueを返すその他
Railsで、
exists?
というメソッドもあります。これは、データベースに特定の条件のデータが存在するか確かめるときに使うようで、ActiveRecord::Base
で定義されているメソッドとのことでした。そのことをつゆ知らず、当初調査対象に含めていましたが、すべてNoMethodError
を返されました。学び
Qiita初投稿となりました。シンプルでごく当たり前の内容だとは思いますが、アウトプットのために調べる、整理する、といった過程を通じて理解を深める良い機会となりました。
定期的に、今後の自分(≒他人)にとって役に立つ記事を投稿していきたいと思います。参考URL
rubyの真偽判定メソッド(nil?/empty?/blank?/present?)を検証してみた結果、興味深いことがわかった
- 投稿日:2020-07-01T19:54:42+09:00
【Rails】Bootstrap3を用いた画像スライドショーの実装
目標
開発環境
・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; // 画像を中央に配置 }
- 投稿日:2020-07-01T19:33:06+09:00
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メソッドは文字列の長さを返すメソッドです。
- 投稿日:2020-07-01T19:32:45+09:00
【Windows】gem install sqlite3ができない
環境
ターミナル: MSYS2(mintty+bash+pacman)
Ruby 2.6 + DevKit
Windows10 64bitSQLiteのバージョンは問わないという人
SQLite 1.3.13をインストールしよう
コンパイル済だからエラー出ることはないはず。gem install sqlite3 --version 1.3.13 --platform rubymkmf.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
- 投稿日:2020-07-01T19:30:00+09:00
【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 */
- 投稿日:2020-07-01T19:29:46+09:00
【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 */
- 投稿日:2020-07-01T18:14:13+09:00
ncestryによる多階層構造データを表示、投稿!! ~Ajax~
はじめに
ancestryで作成したカテゴリーデータを用いて、選択肢を動的に変化させる機能を実装しました。
学習メモとして投稿します。
まだ、理解が浅いところもありますが参考になればと思います!完成形
https://gyazo.com/8a5adc080698873d544b8665855c0901
以下が完成コードです!
routesresources :products, except: [:index] do get 'new/children_category', to: 'products#children_category' get 'new/grandchildren_category', to: 'products#grandchildren_category' endpuroducts_controllerbefore_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 endpuroducts/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で処理を行いますのでリクエストが会った際のコントローラーへの通り道を作成します。routesresources :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_controllerbefore_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 endJSの処理
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で確認してみてください!
ありがとうございました!
- 投稿日:2020-07-01T17:55:46+09:00
簡単ログイン機能実装のエラーが一応解決したけど疑問が残る
はじめに
こちらの記事を参考に実装しましたので、コードで不明な点はこちらの記事を見にいってください。
ゲストログイン・簡単ログイン機能の実装方法(ポートフォリオ用)発生している問題・エラーメッセージ
簡単ログイン機能の実装で、ログイン画面に「簡単ログイン」ボタンを設置したが、ボタン押してもログインできない。
エラーメッセージ(ターミナル)Processing by HomesController#new_guest as HTML Parameters:{"authenticity_token"=>"c1Qc02T4i6+77OtxhGDwxJwEQUYO8d9cIncoNjZ/hXa5c3IzxHPjFAr2QleTBCuMpnxd5+1Sk+HRa9RNXLGSkg=="} Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)エラーになった時のソースコード
routes.rbpost '/homes/guest_sign_in', to: 'homes#new_guest'homes_controller.rbclass 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 enddevise/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.rbdevise_scope :user do post 'users/guest_sign_in', to: 'users/sessions#new_guest' enduser.rb(省略) def self.guest find_or_create_by!(email: 'guest@example.com') do |user| user.password = SecureRandom.urlsafe_base64 end endsessions_controller.rbclass Users::SessionsController < Devise::SessionsController def new_guest user = User.guest sign_in user redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。' end enddevise/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でも問題なく遷移できているので、この予想はおそらく間違っています。
もしわかる方がいらっしゃったら教えて頂けると嬉しいです?♂️参考記事
- 投稿日:2020-07-01T17:55:46+09:00
簡単ログイン機能実装のエラー原因は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.rbpost '/homes/guest_sign_in', to: 'homes#new_guest'homes_controller.rbclass 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 enddevise/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.rbdevise_scope :user do post 'users/guest_sign_in', to: 'users/sessions#new_guest' enduser.rb(省略) def self.guest find_or_create_by!(email: 'guest@example.com') do |user| user.password = SecureRandom.urlsafe_base64 end endsessions_controller.rbclass Users::SessionsController < Devise::SessionsController def new_guest user = User.guest sign_in user redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。' end enddevise/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.rbclass HomesController < ApplicationController (省略) end
homes_controller
はapplicatoin_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
で設定しました。参考記事
- 投稿日:2020-07-01T17:52:36+09:00
【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も使うことができます。
- 投稿日:2020-07-01T16:42:38+09:00
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 . endPaginationの機能を追加したい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 endpagination情報を返すため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 } end10件ごとに分割した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のスタイルに合っているので良いのではないかと思います。
- 投稿日:2020-07-01T14:47:44+09:00
rubyの変数をjavascriptで使う。
- 投稿日:2020-07-01T12:42:49+09:00
【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がクエリ結果を返し、通信終了
キャッシュミス時は以下のフロー
①EC2から検索クエリを投げる
②ElastiCacheからキャッシュミスが返る
③RDSへSQLクエリを発行
④RDSからSQLクエリ結果が返ってくる
⑤取得したクエリ結果をElastiCacheに書き込む
作業の流れ
項番 タイトル 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-develsudo yum groupinstall "Development Tools"sudo yum install mysql-develMysqlクライアント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 | +----+-----------+
①クエリ初回実行(キャッシュミスパターン)
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所感
本来はキャッシュを利用してクエリのレスポンスを高速化させたり、データベース負荷を下げることがこのシステムの目的なのですが、
データ数が少なすぎて性能面でのメリットは確認できていないのがなんとも言えない感じです…笑
いずれ時間あったらそこらへんも軽く確認出来たらとは思ってはいます。
- 投稿日:2020-07-01T10:47:27+09:00
Seleniumでnavigator.webdriverの対策をする
enable-automationとかuseAutomationExtensionは期待通りの動作しなかった
とある理由でスクレイピングしたいけど、
navigator.webdriver=true
なブラウザだとNGなサイトだった。
対応したい。できるらしいので。
参考:navigator.webdriver=trueだとロボットだとバレる。その回避法はあるか?puppeteerなら出来そう
でもSelenium使っているしpuppeteer使ったことないし、どうにかできないものか。。動かなかったときの設定
どうやらSeleniumでもできるらしい記事をいくつか見つけた。
やってみた。
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
- 投稿日:2020-07-01T10:47:27+09:00
Seleniumでnavigator.webdriverの対策をしてアクセスする
enable-automationとかuseAutomationExtensionは期待通りの動作しなかった
とある理由でスクレイピングしたいけど、
navigator.webdriver=true
なブラウザだとNGなサイトだった。
対応したい。できるらしいので。
参考:navigator.webdriver=trueだとロボットだとバレる。その回避法はあるか?puppeteerなら出来そう
でもSelenium使っているしpuppeteer使ったことないし、どうにかできないものか。。動かなかったときの設定
どうやらSeleniumでもできるらしい記事をいくつか見つけた。
やってみた。
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