- 投稿日:2019-07-14T23:01:58+09:00
7/14 今日学んだ事(登録機能)
会員登録機能の実装
作業内容
1.usersテーブルにnicknameカラムを追加
2.ビューを追加
3.ストロングパラメータを追加
4.バリテーションの追加1.usersテーブルにnicknameカラムを追加する
usersテーブルにnicknameカラムをstring型で追加する
ターミナル.$ rails g migration Addカラム名Toテーブル名 カラム名:データ型 となるので $ rails g migration AddNicknameToUseres nickname:string とターミナルで実行マイグレーションの実行
ターミナル.$ rake db:migrate・RailsはAdd ~~ ToUsersのように最後に記述するテーブル名でカラムを追加するテーブルを判断しているので、ファイル名は正しく打ち込む
2.ビューの追加
サインアップ画面にnicknameを入力するフォームと、アイコン画像をアップロードできるフォームを作成。
・サインアップ画面はRailsのform_forメソッドを使って記述する。
・ニックネームは『テキストフィールド』のフィールドを生成する
・アイコン画像は『ファイルフィールド』のフィールドを生成する生成方法
テキストフィールド
new.html.erb<%= f.text_field :カラム名 %>ファイルフィールド
new.html.erb<%= f.file_field :カラム名 %>のフォームタグを利用すると生成される。
※new.html.erbは新規作成するテキスト3.ストロングパラメータを追加
まずnicknameとimageが設定されていないため、deviseのstrong_parametersに新しく許可するパラメーターを追加する必要がある。
そこで、deviseのdevise_parameter_sanitizerメソッドを使用する。●devise_parameter_sanitizer
・strong_parametersに対してパラメータをー追加できる。
・before_actionに設定する※
・記述するのはDeviseのコントローラを継承したコントローラかもしくはApplicationControllerコード
devise_parameter_sanitizer.permit(追加したいメソッドの種類, keys: [追加したいパラメーター名])引数の値(処理)
1. :sign_up(新規登録時)
2. :sign_in(ログイン時)
3. :account_update(レコードの更新時)複数パラメータを追加する場合
devise_parameter_sanitizer.permit(追加したいメソッドの種類, keys: [:パラメーター1, :パラメーター2,..])『,』カンマで区切る。
注意
※直接before_actionに設定してはいけない
devise_parameter_sanitizerを呼び出すためのメソッドを作成してそのメソッドを呼び出すようにする。例
application_controller.rbbefore_action :configure_permitted_parameters def configure_permitted_parameters # devise_parameter_sanitizerメソッドを呼び出す endすべてのコントローラがApplicationControllerを継承しているため、この記述ではすべてのコントローラのアクションの前でdevise_parameter_sanitizerメソッドが呼び出される。
devise_parameter_sanitizerメソッドはdeviseで追加されたメソッドなので、Deviseのコントローラ以外で呼び出すことができない。よって、before_actionを適応するコントローラを指定する。before_actionではifというオプションを指定することができる。これはbefore_actionを呼び出す条件を指定するもの。今回はコントローラの種類を指定するので以下のように記述する。
例
application_controller.rbbefore_action :メソッド名, if: :コントローラ名?例
application_controller.rbbefore_action :configure_permitted_parameters, if: :devise_controller?devise_controllerが動いた時のみこのアクションを事前に動かしたいので、ifオプションを利用してそのように設定する。
※configure_permitted_parametersはdeviseで利用出来るパラメーターを設定でるdevise関連のストロングパラメーターの管理メソッド。4.バリテーションの追加
バリテーションとは
オブジェクトがDBに保存される前に、そのデータが正しいかどうかを検証する仕組み今回やりたい事
・nicknameが入力されていないとエラーがでるようにバリテーションを設定する。
・nicknameの入力が必須と分かるようテキストフィールドにプレイスホルダーを設定する●validation(検証)
入力フォームを通じてビューからサーバーへ側へパラメーターが送られてきた際、正常な値か検証できる機能。
●validates :カラム名, presence: true
フォームの中身があるか検出し、無い場合は保存しない。
例
userのnicknameの入力を必須にする場合user.rbclass User < ApplicationRecord validates :nickname, presence: trueと記述する。
● placeholder: ''
読み:(プレイスホルダー)
・フォームの値が空の時に、''で囲んだ文字を薄く表示しておくことができる。
・入力する人が、何を入力するか分かりやすくなる
今回はnicknameの入力欄に反映させたいので、text_fieldメソッドのオプションとして、以下のように利用する。new.html.erb<%= f.text field :nickname, placeholder:'ニックネームを入力(必須)' %>・ニックネームを設定していないとエラーがでるようにした
・ニックネームの入力欄にプレイスホルダーを設定できた以上
- 投稿日:2019-07-14T21:00:40+09:00
Rails newするときの流れ
新しくRailsプロジェクトを作る時の流れをメモとしてざっくり残しておきます。
- Railsをインストール -
$ mkdir ○◯◯ $ cd ◯◯◯ $ bundle init
bundle init
によりGemfileが作成されるので、Gemfileを編集。Gemfile# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails" #ここのコメントアウトを外すそしたら、
$ bundle install --path vendor/bundle
。
--pathでパスを指定することによって、指定されたパスにRailsがインストールされる。- Rails new する -
bundle exec rails new . -B -d mysql -T
Rails new。
Overwrite /Users/apple/Desktop/App/Gemfile? (enter "h" for help) [Ynaqdhm]
と、Gemfileを上書きしていいか聞かれるので、Yes。
Rspecを使いたいのでテストはスキップ。- DBを作成する -
usernameとpasswordを指定。
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: #ユーザー名 password: #パスワード socket: /tmp/mysql.sockターミナルで
$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl/include" $ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"この二つのコマンドを実行し、もう一度
bundle install
します。そして
bin/rails db:create
。上記の2つのコマンドを入力してbundle installという工程を飛ばすとエラーが出ます(自分の環境だと)。
- Rspecの設定をする -
Gemfile・・・ group :development, :test do gem 'rspec-rails' #それぞれ、必要があればバージョン指定をする。 gem 'factory_bot_rails' gem 'spring-commands-rspec' end ・・・Gemfileに上記の3つを追加し、
bundle install
。
$ bin/rails generate rspec:install
RSpecをインストール。.rspec--require spec_helper --format documentation #この行を追加するとテストを実行した際にログが読みやすくなる。次は
$ bundle exec spring binstub rspec
。
このコマンドを打つとbin/rspec
コマンドでテストが実行できるようになる。config/application.rbmodule Test class Application < Rails::Application ... config.generators do |g| g.test_framework :rspec, fixtures: true, view_specs: false, helper_specs: false, routing_specs: false, request_specs: false end ... end end上記の行を追加。
これでRSpecのセットアップは完了です。- Slimを導入する -
Gemfile... gem 'slim-rails' gem 'html2slim'
bundle install
でgemをインストール。
bundle exec erb2slim app/views/layouts --delete
erbファイルを削除。- Bootstrapをインストール -
Gemfilegem 'bootstrap;
bundle install
。
$ rm app/assets/stylesheets/application.css
sassを使うために、application.cssを削除。
application.scssを作成し、application.scss@import 'bootstrap';追加。
- i18nで日本語化 -
$ wget https://raw.githubusercontent.com/svenfuchs/rails-18n/master/rails/locale/ja.yml --output-document=config/locales/ja.ymlrawファイルをja.ymlにダウンロード。
config/initializers下にlocale.rbファイルを作成。config/initializers/locale.rbI18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s] I18n.config.available_locales = :ja I18n.default_locale = :jaこれで完了です。
- 終わり -
こんな感じで一通り終了です。
いつも忘れてしまうので、ザックリまとめてみました。
ここをこうした方が良いよとか、アドバイスがあったら教えていただけると嬉しいです。
- 投稿日:2019-07-14T20:42:02+09:00
rails ページネーションの使い方
はじめに
ページネーションとは
1ページに表示する情報が多いときに、分割して複数のページを作成しアクセスをしやすくすることです。
Googleで検索したとき、検索結果が多いと複数のページに渡って検索結果があると思うのですが、あれがページネーションになります。ページ下のGoooooooooogleってなってるやつです。
実装方法
kaminari
Gemの一種である「kaminari」をインストールすることで、ページネーションを簡単に実装できます。
kaminariのインストール
以下のように、Gemfileにkaminariを記述し、ターミナルでbundle installを実行します。
Gemfilegem 'kaminari'$ bundle installこれでページネーションを実装するためのメソッドを使用できるようになります。
ファイルの編集
ページネーションを実装するにはコントローラーファイル、ビューファイルを編集する必要があります。
編集の例としては以下のようになります。
コントローラーファイル編集例class TweetsController < ApplicationController def index @tweets = Tweet.all.page(params[:page]).per(5) end endここでページネーションのために使用するメソッドはpageメソッドとperメソッドになります。
perメソッド
perメソッドの引数には1ページに表示したい要素の数を持たせます。つまり1ページに表示する検索結果を決めることができる。
pageメソッド
kaminariをインストールすることでparamsにpageのキーが追加されます。そのためpageメソッドの引数にparams[:page]を持たせることで現在のページ番号を指定できます。
ビューファイル編集例<%= paginate(@tweets) %>ビューファイルには以上の記述をすることで記述をした場所にページネーションのリンクを表示させることができます。
おわりに
ページネーションが使えるようになるとサイト作ってるなぁって感じがします笑
なんでGemの名前がkaminariなのか気になります。
わかったらまた追記したいと思います。
- 投稿日:2019-07-14T20:19:21+09:00
RailsアプリをHerokuにデプロイ(Nginx・Unicorn使用)
目次
1. エラーとの遭遇
めっちゃニッチな需要だけど、簡単なBlogのCRUDを実装したRailsアプリをHerokuにデプロイする際にNginxとUnicornを使用したい状況があった。
これまではheroku-buildpack-nginxのREADMEにある
Existing App
とNew App
通りにやればできたハズ、、、
だったけど、この前同じ手順でデプロイをしたところ、App crushed
が出てしまった...まあ、herokuのrestartでもすればなおるだろうと試してみたが、ダメだった?♀️
とりあえずログを見ようと
$ heroku logs -t
を試したらH10
の文字が...
H10
てたまに出るけど、エラーを直すだけでH10
自体の意味をあまり考えたことがなかった。
H10 - App crashed
なるほど...??確かweb dynoってHTTPのリクエストとかを捌いてんだっけ、、って感じになったので原因を探ってみることにした。
H10
が出た時はとりあえず下記コマンドからconsoleを開いて、BlogをcountしてみたりするとよくみるRailsのエラーが表示されることがある。Terminal$ heroku run rails c
・・・エラーが出ない?
じゃあCRUD機能は問題ないと仮定して、どこがダメなのかな〜とあちこち確認を始めた。
この時はまだ余裕があったので、とりあえず
Procfile
とかconfig/
の中とか確認してみればいっかなーとか考えてbuildpackのドキュメント通りにconfigファイル書くときにタイポでもしたかと目を皿にして確認。・・・ちゃんとできてる?
早いけどこの辺で長期化を覚悟し始めた
- buildpackの読み込みの順番を変えてみる
- Gemfileの確認
- herokuのログの再確認
- rails newから再度やり直し
とかそれぞれ何回もやったけど、ダメだった。
herokuのログには下記の内容が毎秒吐き出されてる...???2019-07-14T02:22:44.859554+00:00 app[web.1]: buildpack=nginx at=app-initialization 2019-07-14T02:22:45.861308+00:00 app[web.1]: buildpack=nginx at=app-initialization 2019-07-14T02:22:46.863727+00:00 app[web.1]: buildpack=nginx at=app-initializationここを確認してみたけど、多分buildpackに従ってNginxを使おうとしてるけど、肝心のNginxが反応ないのかな...?
とか生意気に仮定は立てるけど、どうしたら動くのかが思いつかない...
ここまでで既に数時間経っていたので、先輩に聞いてみた。半日くらいして、眠気で目を血走らせた先輩から「できた」と言われ、マジかと思いつつ確認してみると
確かにできてる...?
よーわかんないけどを連発しながら仮定を説明してもらった。(自分も本当によーわからんかった。??)
とりあえず言われた通り、下記のようにしたらうまく動くようになりました。
2. デプロイまでの手順
- Railsのプロジェクト内で下記を実行。
Terminal$ heroku create # herokuにアプリ枠を作成 $ heroku buildpacks:add heroku/ruby # Rubyのbuildpackを追加 $ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-nginx # HerokuでNginxを使用するbuildpackの追加
ここまでは今までやってきたことと一緒 ?♀️
3. Nginxのconfigurationファイルの作成
ここからが、先輩から教えてもらって追加した設定☝️
config/nginx.conf
を作成する。そういえば今までNginxの設定は特にしてなかったな...?とか思いながら言われた通りにやる。
下記のコードはこの記事からココを参考にしたみたい。(この人の記事よくみる気がする...?)
nginx.confworker_processes <%= ENV['NGINX_WORKERS'] || 4 %>; events { use epoll; accept_mutex on; worker_connections 1024; } http { gzip on; gzip_comp_level 3; gzip_min_length 150; gzip_proxied any; gzip_types text/plain text/css text/json text/javascript application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/xml font/opentype image/svg+xml text/xml; server_tokens off; log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id'; access_log logs/nginx/access.log l2met; error_log logs/nginx/error.log; include mime.types; default_type application/octet-stream; sendfile on; # Must read the body in 5 seconds. client_body_timeout 5; upstream app_server { server unix:/tmp/nginx.socket fail_timeout=0; } server { listen <%= ENV["PORT"] %>; server_name _; keepalive_timeout 5; root /app/public; # path to your app location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://app_server; } location ~* ^/documentation/(.*) { set $s3_bucket 'my_bucket.s3-website-us-east-1.amazonaws.com'; set $url_full '$1'; resolver 8.8.8.8 valid=300s; resolver_timeout 10s; index index.html; proxy_hide_header x-amz-id-2; proxy_hide_header x-amz-request-id; proxy_hide_header Set-Cookie; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $s3_bucket; proxy_ignore_headers "Set-Cookie"; proxy_buffering off; proxy_intercept_errors on; proxy_redirect off; proxy_pass http://$s3_bucket/$url_full; } location ~* \.(eot|oft|svg|ttf|woff)$ { add_header Access-Control-Allow-Origin *; expires max; log_not_found off; access_log off; add_header Cache-Control public; } location ~* ^/assets/ { gzip_static on; # Per RFC2616 - 1 year maximum expiry expires 1y; add_header Cache-Control public; # Some browsers still send conditional-GET requests if there's a # Last-Modified header or an ETag header even if they haven't # reached the expiry date sent in the Expires header. add_header Last-Modified ""; add_header ETag ""; break; } } }先輩曰く必要ない記述もありそうとのこと。とりあえず動かすだけだから今は気にしないでおく。
4. Unicornの設定
- Gemfileからpumaを削除し、unicornを追記
Gemfilegem 'unicorn'
pumaは使用しないので
config/puma.rb
を削除する。Gemfile.lock更新のため、
$ bundle
しておくここも元々やってたことと一緒だった ?♀️
config/unicorn.rb
を作成する。ここもこれまでと一緒 ?♀️...と思っていたらちょっと違った。
これまではbuildpackドキュメントのUpdate Unicorn Configを参考にしてたけど、下記のようにするみたい。
unicorn.rbworker_processes Integer(ENV["WEB_CONCURRENCY"] || 3) timeout 15 preload_app true worker_processes 4 listen 'unix:///tmp/nginx.socket', backlog: 1024 before_fork do |server,worker| FileUtils.touch('/tmp/app-initialized') end
FileUtils.touch
については、さっきの記事のConfiguring Pumaを読む限り、それぞれの順番を守もらうための設定らしい。?こんな感じ?
The start-nginx script from the Nginx Buildpack will wait until the /tmp/app-initialized file is present before Nginx binds to the public port.
アプリケーションサーバの起動を待ってからNginxと連携するみたい。
- 次に、
Procfile
を作成する。
config/unicorn.rb
を読み込む設定。Procfileweb: bin/start-nginx bundle exec unicorn -c config/unicorn.rb5.デプロイ
やっとデプロイ??
ここはいつも通り、addしてcommitしてpushすればOK?♀️
- herokuでDBを作成する。
Terminal$ heroku run rake db:migrate RAILS_ENV=productionこれもいつも通りなはず。
ここまでできたら、あとは
$ heroku open
などでアプリを開いて実際に動作確認。
ここでエラーが出ている場合は、Railsアプリ内で何か間違っているか、ここまでの手順に何か抜け漏れがあるハズ?6. Nginxの確認方法
しっかりとheroku上でアプリが動くことを確認できたら、いよいよNginxがしっかり動いているか確認してみる。
確認方法は複数あると覆いますが、ここでは
wget
コマンドを使用して確認していきます。下記を実行してみると、下記のような結果が返ってくるハズ。
Terminal$ wget -S --spider <HerokuアプリのURL> Spider mode enabled. Check if remote file exists. --2019-07-14 17:29:09-- <HerokuアプリのURL> Resolving <HerokuアプリのURL>... Connecting to <HerokuアプリのURL>|... connected. HTTP request sent, awaiting response... HTTP/1.1 200 OK Connection: keep-alive Server: nginx # ここにNginxが表示される。 Date: Sun, 14 Jul 2019 08:29:09 GMT Content-Type: text/html; charset=utf-8 X-Frame-Options: SAMEORIGIN X-Xss-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: none Referrer-Policy: strict-origin-when-cross-origin Cache-Control: max-age=0, private, must-revalidate X-Runtime: 0.090119 Via: 1.1 vegur Length: unspecified [text/html] Remote file exists and could contain further links, but recursion is disabled -- not retrieving.Spiderモードだって?
カッコいいけどよく分かんないから調べてみると、こんな感じらしい。
--spider
はwgetコマンドのオプションみたい。これまで何も考えずに使ってた?ファイルをダウンロードせず、URLの存在だけチェックする(“Web spider”として動作する)。例えば、ブックマークをチェックするなら「wget --spider --force-html -i bookmarks.html」のように指定する
- wgetコマンドが使用できない場合は、brewからインストールしましょう。
7. まとめ
長くなりましたが、これでHerokuにデプロイしたアプリでNginxが動いていることがしっかりと確認できました!
先輩のおかげで助かった...
自分だけだと多分ここまでくるのに何日もかかったハズ?※今回herokuにデプロイするために作成したRailsプロジェクト
README
にも簡潔に一連の内容を書いてみました。
https://github.com/kawaaa26/herokuapp_nginx
- 投稿日:2019-07-14T18:16:14+09:00
ReactでHTMLを作成し、Clickした際にそれぞれの情報をモーダルに表示・非表示させる(Railsで)
はじめに
ReactでHTMLを作成するまでは案外すんなりと行ったのですが、クリックした際に「それぞれの情報を持たせてモーダルを表示する」ことに苦労したので忘備録としてまとめておきます。
最終的にはこのような感じになります。
たくさんの方法を試したので不必要なものもあるかもしれません。
その点は指摘していただけると幸いです。
下の画像をクリックした際にその情報を表示させています。
表示させる内容は個人で変更してください。
1.gemの導入とインストール
この2つのgemをGemfileに加え、
bundle install
してくださいGemfilegem 'react-rails' gem 'webpacker'その後、それぞれインストールしてください
Terminal$ bundle install $ rails webpacker:install $ rails webpacker:install:react $ rails generate react:installするとapp以下に
app/javascript/components
フォルダが作成されます。
私自身、app/assets
以下にJavascriptフォルダがあるのに大丈夫なのかと思いましたが、問題ありません。2. application.html.hamlにtagを追加
application.html.hamlに以下の記述を加えてください
application.html.haml= javascript_pack_tag 'application'この際にTerminalでyarnがどうこうというエラーが出るかもしれません。
その場合$ yarn install
等、各自で調べて解決してください。これらでRails上でReactを使う準備ができましたので、いよいよjsファイルに記述していきます。
3. Components以下にjsファイルの作成
Components以下に
App.js
とGraduate.js
を作成します。
これらの名前は自分で決めてくださって結構です。
ではこちらのコードをコピーしてくださいApp.jsimport React from 'react'; import Graduate from './Graduate'; const lessonList = [ { name: '開成太郎(2017)', school: '一条高校', image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg', introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました', }, { name: '開成花子(2016)', school: '奈良高校', image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg', introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました', }, { name: '開成三郎(2019)', school: '郡山高校', image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg', introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。', }, { name: '開成四郎(2015)', school: '登美ケ丘高校', image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg', introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。', } ]; class App extends React.Component { render() { return ( <div className="performance"> {lessonList.map((lessonItem) => { return ( <Graduate name={lessonItem.name} image={lessonItem.image} school={lessonItem.school} introduction={lessonItem.introduction} /> ); })} </div> ); } } export default App;それでは解説していきます。
import React from 'react'; import Graduate from './Graduate';まずimportとは輸入という意味です。
その名の通り、一行目ではReactを二行目ではGraduateファイルを読み込んでいます。
このおかげでReactを使うことができ、またGraduateにパラメーター(props)を渡すことができます。const lessonList = [ { name: '開成太郎(2017)', school: '一条高校', image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg', introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました', }, { name: '開成花子(2016)', school: '奈良高校', image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg', introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました', }, { name: '開成三郎(2019)', school: '郡山高校', image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg', introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。', }, { name: '開成四郎(2015)', school: '登美ケ丘高校', image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg', introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。', } ];この部分ではそれぞれのハッシュを配列に入れています。これをあとでmapメソッドで一つずづ表示させていきます。
class App extends React.Component { render() { return ( <div className="performance"> {lessonList.map((lessonItem) => { return ( <Graduate name={lessonItem.name} image={lessonItem.image} school={lessonItem.school} introduction={lessonItem.introduction} /> ); })} </div> ); } }この部分でComponentを作成しています。(extendsは広げるという意味)
ConmonentはJavascriptの関数のようなものです。
その中のreturnでHTMLを返し、表示させています。またこのような記法をJSX
と言います。
また、JSXには約束事があり、複数の要素を返すことができません。なので図の場合、performanceクラスを親要素としその中に色々と要素を追加しています。
{lessonList.map((lessonItem)
ではlessonListの数だけ繰り返し、returnを読み込んでいます。
return内でとすることでGraduateコンポーネントを呼び出しています。これが可能なのはimport Graduate from './Graduate';
のおかげです。
sosite
呼び出す際にはname,school,image,introductionのパラメーターを渡しています。(props)
このパラメーターをGraduate.js側で使うわけです。ちなみにJSX内でjavascriptの記法を用いる際は中括弧が必要なため、中括弧内に書いてあります。export default App;App.jsの最後の行ですが、exportとは輸出という意味です。
これのおかげでHTMLファイルでApp.jsを呼び出すことができ、returnとして要素をHTMLに追加することができます。App.jsがGraduateを二行目でimportできているのもGraduate.jsで最後にexport default Graduate;
としているからです。Graduate.jsimport React from 'react'; class Graduate extends React.Component { constructor(props) { super(props); this.state = {isModalOpen: false}; } handleClickLesson() { this.setState({isModalOpen: true}); } handleClickClose() { this.setState({isModalOpen: false}); } render() { let modal; if (this.state.isModalOpen) { modal = ( <div className='modal-area'> <div className='modal-inner'> <div className='modal-header'></div> <div className='modal-introduction'> <h2>{this.props.name}</h2> <p>{this.props.introduction}</p> </div> <button className='modal-close-btn' onClick={() => this.handleClickClose()} > とじる </button> </div> </div> ) }; return ( <div className="graduate"> <img className="graduate__image" src={this.props.image} onClick={() => {this.handleClickLesson()}} /> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div> ); } } export default Graduate;この部分で、実際に表示されるHTMLを作成しています。App.jsから渡されたパラメータ(props)を使って書いていきます。
constructor(props) { super(props); this.state = {isModalOpen: false}; }新しくstate(状態)というものが出てきましたが、propsとstateは少し異なり、stateはそのComponent内で保持されるものであって、propsみたいにComponentからComponentに渡すことはできません。詳しくはこちら
この部分でモーダルの表示・非表示を管理しています。handleClickLesson() { this.setState({isModalOpen: true}); } handleClickClose() { this.setState({isModalOpen: false}); }ここで先ほどのstateを関数(
handleClickLesson
orhandleClickClose
)が呼ばれた時に更新しています。この時に注意して欲しいのが、更新する際はsetState
としないといけないことです。let modal; if (this.state.isModalOpen) { modal = ( <div className='modal-area'> <div className='modal-inner'> <div className='modal-header'></div> <div className='modal-introduction'> <h2>{this.props.name}</h2> <p>{this.props.introduction}</p> </div> <button className='modal-close-btn' onClick={() => this.handleClickClose()} > とじる </button> </div> </div> ) };ここで
isModalOpen
がtrueの場合のみ、変数modalに値を代入し、表示させます。
そしてモーダルの中にonClick={() => this.handleClickClose()}
があると思いますが、このボタンを押すことでisModalOpen
がfalseになり再び非表示になります。
またApp.jsからもらってきたパラメータ(props)を{this.props.name}とすることで代入することができます。
またReactではclassをclassNameと記載します。return ( <div className="graduate"> <img className="graduate__image" src={this.props.image} onClick={() => {this.handleClickLesson()}} /> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div> );最後ですね。
この部分で常時表示させるHTMLをApp.jsからもらったpropsを使って作成しています。
そして{modal}
の部分で先ほどの変数を代入しているわけですね。imgクラスにonClickが設定されているため、画像をクリックするとisModalOpen
がtrueになり、値が代入されたmodalが表示されるわけです。
では最後にHTML側でApp.jsを呼んであげましよう。hamlの場合は
index.haml.haml= react_component("App")htmlの場合は
index.html.erb<%= react_component("App") %>これで完成です。
CSSだけ記載しておきます。
お好みで変更してください。stylesheet.css.performance { .graduate { padding: 30px 0; display: inline-block; width: 25%; text-align: center; &__student { font-size: 15px; padding-top: 10px; text-align: right; padding-right: 20px; } &__school { font-size: 20px; padding-top: 10px; } &__image { cursor: pointer; height: 160px; width: 160px; border-radius: 50%; } .modal-area { z-index: 2; position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.6); .modal-inner { position: absolute; top: 8%; right: 0; left: 0; width: 480px; padding-bottom: 60px; margin: auto; background-color: rgb(255, 255, 255); .modal-header { margin-bottom: 60px; } .modal-introduction p { color: #5876a3; width: 384px; line-height: 32px; text-align: left; margin: 36px auto 40px; } .modal-close-btn { font-size: 13px; color: #8491a5; width: 200px; padding: 16px 0; border: 0; background-color: #f0f4f9; cursor: pointer; } .modal-close-btn:hover { color: #8491a5; background-color: #ccd9ea; transition: .3s ease-in-out; } } } } }番外編
Click時にモーダルが表示されるが非表示にならない場合
モーダル実装時にモーダルが閉じないというバグが起こりました。
原因を調べてみると閉じるボタンを押した際に一回閉じてから再度開いています。
これがその時のコードです。<div className="graduate" onClick={() => {this.handleClickLesson()}}> <img className="graduate__image" src={this.props.image}/> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div>何が問題かというと一番の親要素であるgraduateにopenmodalを設定しているせいで、その子要素であるモーダル内のclossmodalを押した際に同時に親要素のopenmodalも呼ばれてしまうからです。
なのでそれぞれのClick機能は親要素、子要素の関係に注意しましょう。参考資料
React公式HP
Progate
https://qiita.com/k-penguin-sato/items/e3cc04f787cf3254cfae
https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50
- 投稿日:2019-07-14T17:31:40+09:00
RailsとElasticsearchで検索機能をつくり色々試してみる - サジェスト機能の追加
はじめに
elasticsearch-railsを使うことが前提の記事になります。記事の中で出てくるサンプルや環境は、RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成をもとにしています。
サジェストを実装する上でのElasticsearchのデータスキーマやアナライザー等の設計部分はElasticsearch キーワードサジェスト日本語のための設計の記事を参考にさせてもらっています。
本記事では、サジェスト機能をelasticsearch-railsを使ってRailsアプリケーションに追加していく方法に焦点を当てて紹介していきます。最終的なソースコードはGitHubに上げておきます。
完成イメージ
こんな感じで検索窓に2文字以上入力すると候補となるキーワードをサジェストする機能を追加します。
全体像
検索履歴を保存してサジェストワードとして使用
実際のアプリケーションを想定してユーザーの検索したキーワードをサジェストワードとして使用するようにします。
要件によっては検索してほしいキーワードをサジェストワードに登録していく方法などいろんな方法がが考えられますが今回はこの方法でいきます。Railsアプリを経由してサジェストワードを取得
ユーザーの環境からElasticsearchが公開されているような場合は、ブラウザから直接Elasticsearchにリクエストを送ることも考えられますが、今回はRailsアプリケーションのみからElasticsearchが公開されている場合を想定して、Railsアプリケーションを経由してサジェストワードを返却していきます。
検索ワードの保存
ここから実装に入っていきます。まずは検索ワードを保存するテーブルを追加し保存していきます。
後ほどElasticsearchにキーワードを登録する際に、検索にひっかからないワードはサジェストのワードからは除外できるように検索されたワードだけでなくhitした件数も保存するようにします。保存用のテーブル追加
$ bundle exec rails g migration create_search_word_logキーワードとhitした件数を保存するカラムを追加します。
db/migrate/20190601132134_create_search_word_log.rbclass CreateSearchWordLog < ActiveRecord::Migration[5.2] def change create_table :search_word_logs do |t| t.string :word t.integer :hit_number t.timestamps end end end$ bundle exec rails db:migrate対応するモデルを追加します。
app/models/search_word_log.rbclass SearchWordLog < ApplicationRecord end保存処理追加
コントローラに検索履歴をためるための処理を追加していきます。
app/controllers/mangas_controller.rbclass MangasController < ApplicationController before_action :set_manga, only: [:show, :edit, :update, :destroy] + after_action :save_search_log, only: [:index] def index @mangas = if search_word.present? Manga.es_search(search_word).page(params[:page] || 1).per(5).records else Manga.page(params[:page] || 1).per(5) end end private + def save_search_log + return if search_word.blank? + SearchWordLog.create(word: search_word, hit_number: @mangas.size) + endサジェスト用のindexの定義と検索クエリの作成
ここまででサジェストに登録するワードの準備できたので、Elasticsearchにどうのように登録していくかの定義と同時に検索のクエリを追加していきます。
検索機能で追加したものと同様にconcernを作成します。app/models/concerns/search_word_log_searchable.rbmodule SearchWordLogSearchable extend ActiveSupport::Concern included do include Elasticsearch::Model # a. サジェスト用のindex index_name "es_search_log_#{Rails.env}" # b. self.analyzer_settingsで下のほうに定義したanalyzerを使えるようにする。 settings analysis: self.analyzer_settings do mappings dynamic: 'false' do indexes :id, type: 'text', analyzer: 'kuromoji' # c. マルチフィールドを定義する indexes :word, type: 'text', fielddata: true, analyzer: 'keyword_analyzer' do indexes :autocomplete, type: 'text', analyzer: 'autocomplete_index_analyzer', search_analyzer: 'autocomplete_search_analyzer' indexes :readingform, type: 'text', analyzer: 'readingform_index_analyzer', search_analyzer: 'readingform_search_analyzer' end indexes :hit_number, type: 'integer' indexes :created_at, type: 'date', format: 'YYYY-MM-dd kk:mm:ss' end end def as_indexed_json(*) attributes .symbolize_keys .slice(:id, :word, :hit_number) .merge( created_at: created_at.strftime("%Y-%m-%d %H:%M:%S") ) end end class_methods do def create_index! client = __elasticsearch__.client client.indices.delete index: self.index_name rescue nil client.indices.create(index: self.index_name, body: { settings: self.settings.to_hash, mappings: self.mappings.to_hash }) end # d. 検索クエリの定義 def es_search(query) __elasticsearch__.search({ size: 0, query: { bool: { should: [{ match: { "word.autocomplete" => { query: query } } }, { match: { "word.readingform" => { query: query, fuzziness: "AUTO", operator: "and" } } }] } }, aggs: { keywords: { terms: { field: "word", order: { _count: "desc" }, size: "10" } } } }) end # e. analyzerの定義 def analyzer_settings { analyzer: { keyword_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "keyword", filter: ["lowercase", "trim", "maxlength"] }, autocomplete_index_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "keyword", filter: ["lowercase", "trim", "maxlength", "engram"] }, autocomplete_search_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "keyword", filter: ["lowercase", "trim", "maxlength"] }, readingform_index_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces"], tokenizer: "japanese_normal", filter: ["lowercase", "trim", "readingform", "asciifolding", "maxlength", "engram"] }, readingform_search_analyzer: { type: "custom", char_filter: ["normalize", "whitespaces", "katakana", "romaji"], tokenizer: "japanese_normal", filter: ["lowercase", "trim", "maxlength", "readingform", "asciifolding"] }, }, filter: { readingform: { type: "kuromoji_readingform", use_romaji: true }, engram: { type: "edgeNGram", min_gram: 1, max_gram: 36 }, maxlength: { type: "length", max: 36 } }, char_filter: { normalize: { type: "icu_normalizer", name: "nfkc_cf", mode: "compose", }, katakana: { type: "mapping", mappings: [ "ぁ=>ァ", "ぃ=>ィ", "ぅ=>ゥ", "ぇ=>ェ", "ぉ=>ォ", "っ=>ッ", "ゃ=>ャ", "ゅ=>ュ", "ょ=>ョ", "が=>ガ", "ぎ=>ギ", "ぐ=>グ", "げ=>ゲ", "ご=>ゴ", "ざ=>ザ", "じ=>ジ", "ず=>ズ", "ぜ=>ゼ", "ぞ=>ゾ", "だ=>ダ", "ぢ=>ヂ", "づ=>ヅ", "で=>デ", "ど=>ド", "ば=>バ", "び=>ビ", "ぶ=>ブ", "べ=>ベ", "ぼ=>ボ", "ぱ=>パ", "ぴ=>ピ", "ぷ=>プ", "ぺ=>ペ", "ぽ=>ポ", "ゔ=>ヴ", "あ=>ア", "い=>イ", "う=>ウ", "え=>エ", "お=>オ", "か=>カ", "き=>キ", "く=>ク", "け=>ケ", "こ=>コ", "さ=>サ", "し=>シ", "す=>ス", "せ=>セ", "そ=>ソ", "た=>タ", "ち=>チ", "つ=>ツ", "て=>テ", "と=>ト", "な=>ナ", "に=>ニ", "ぬ=>ヌ", "ね=>ネ", "の=>ノ", "は=>ハ", "ひ=>ヒ", "ふ=>フ", "へ=>ヘ", "ほ=>ホ", "ま=>マ", "み=>ミ", "む=>ム", "め=>メ", "も=>モ", "や=>ヤ", "ゆ=>ユ", "よ=>ヨ", "ら=>ラ", "り=>リ", "る=>ル", "れ=>レ", "ろ=>ロ", "わ=>ワ", "を=>ヲ", "ん=>ン" ] }, romaji: { type: "mapping", mappings: [ "キャ=>kya", "キュ=>kyu", "キョ=>kyo", "シャ=>sha", "シュ=>shu", "ショ=>sho", "チャ=>cha", "チュ=>chu", "チョ=>cho", "ニャ=>nya", "ニュ=>nyu", "ニョ=>nyo", "ヒャ=>hya", "ヒュ=>hyu", "ヒョ=>hyo", "ミャ=>mya", "ミュ=>myu", "ミョ=>myo", "リャ=>rya", "リュ=>ryu", "リョ=>ryo", "ファ=>fa", "フィ=>fi", "フェ=>fe", "フォ=>fo", "ギャ=>gya", "ギュ=>gyu", "ギョ=>gyo", "ジャ=>ja", "ジュ=>ju", "ジョ=>jo", "ヂャ=>ja", "ヂュ=>ju", "ヂョ=>jo", "ビャ=>bya", "ビュ=>byu", "ビョ=>byo", "ヴァ=>va", "ヴィ=>vi", "ヴ=>v", "ヴェ=>ve", "ヴォ=>vo", "ァ=>a", "ィ=>i", "ゥ=>u", "ェ=>e", "ォ=>o", "ッ=>t", "ャ=>ya", "ュ=>yu", "ョ=>yo", "ガ=>ga", "ギ=>gi", "グ=>gu", "ゲ=>ge", "ゴ=>go", "ザ=>za", "ジ=>ji", "ズ=>zu", "ゼ=>ze", "ゾ=>zo", "ダ=>da", "ヂ=>ji", "ヅ=>zu", "デ=>de", "ド=>do", "バ=>ba", "ビ=>bi", "ブ=>bu", "ベ=>be", "ボ=>bo", "パ=>pa", "ピ=>pi", "プ=>pu", "ペ=>pe", "ポ=>po", "ア=>a", "イ=>i", "ウ=>u", "エ=>e", "オ=>o", "カ=>ka", "キ=>ki", "ク=>ku", "ケ=>ke", "コ=>ko", "サ=>sa", "シ=>shi", "ス=>su", "セ=>se", "ソ=>so", "タ=>ta", "チ=>chi", "ツ=>tsu", "テ=>te", "ト=>to", "ナ=>na", "ニ=>ni", "ヌ=>nu", "ネ=>ne", "ノ=>no", "ハ=>ha", "ヒ=>hi", "フ=>fu", "ヘ=>he", "ホ=>ho", "マ=>ma", "ミ=>mi", "ム=>mu", "メ=>me", "モ=>mo", "ヤ=>ya", "ユ=>yu", "ヨ=>yo", "ラ=>ra", "リ=>ri", "ル=>ru", "レ=>re", "ロ=>ro", "ワ=>wa", "ヲ=>o", "ン=>n" ] }, whitespaces: { type: "pattern_replace", pattern: "\\s{2,}", replacement: "\u0020" }, }, tokenizer: { japanese_normal: { mode: "normal", type: "kuromoji_tokenizer" }, engram: { type: "edgeNGram", min_gram: 1, max_gram: 36 } }, } end end end作成したconcernをモデルで使えるようにinclude
app/models/search_word_log.rbclass SearchWordLog < ApplicationRecord + include SearchWordLogSearchable end定義の解説
b. analyzerの登録
self.analyzer_settings
の箇所を追加することでanalyzer_settings
メソッド内に独自に定義したアナライザーを登録しています。c.マルチフィールドの定義
Elasticsearchへの登録、検索、読み仮名での登録、検索、検索結果の集計とキーワードの表示で別のAnalyzerを使いたいため、マルチフィールドを使いその中で登録と検索で別のAnalyzerを使うようにマッピングを定義しています。
それぞれの用途は以下の表のようになります。各Analyzerで具体的にどのようにキーワードがアナライズされるかの例を記事の最後に載せていますので詳しくはそちらを参照ください。
filed analyzer 用途 word keyword_analyzer 検索した結果の集計とサジェストとして表示する文字列を登録するためのアナライザー word.autocomplete autocomplete_index_analyzer 検索ワードを加工して登録していくためのアナライザー word.autocomplete autocomplete_search_analyzer 前方一致検索を行う際に使用するのアナライザー word.readingform readingform_index_analyzer 読み仮名でhitできるように検索ワードを加工して登録していくためのアナライザー word.readingform readingform_search_analyzer 読み仮名で前方一致検索を行う際に使用するのアナライザー それぞれどこで使用されているかは、全体像で使った図でいうと以下のようなイメージになります。
キーワード登録時のアナライザーのイメージ
検索時のアナライザーのイメージ
docker imageの修正
さきほどのanalyzerの定義でchar_filterに
icu_normalizer
を指定しているのでanalysis-icu
のプラグインを使えるように以下を追加してイメージをビルドしておきます。docker/es/DockerfileRUN bin/elasticsearch-plugin install analysis-icuindex追加
indexの定義が作成できたのでrakeタスクを追加してindexを作成します。
lib/tasks/elasticsearch.rakenamespace :elasticsearch do + desc 'サジェスト用のindex作成' + task :create_suggest_index => :environment do + SearchWordLog.create_index! + end end$ bundle exec rake elasticsearch:create_suggest_indexテスト用に検索履歴を追加
画面上でぽちぽち登録していくか、スクリプトを書いて
search_word_logs
テーブルにデータ登録していきます。とりあえずサンプルとして動かすなら数十件ほど追加すればOKかと思います。データがたたまったところでindexに登録していきます。
検索にhitしたキーワードのみをサジェストワードとして使いたいので、scopeを使用します。
app/models/search_word_log.rbclass SearchWordLog < ApplicationRecord include SearchWordLogSearchable + scope :searchable_word, -> { + where('hit_number > 0') + } endrakeタスクを登録して実行します。
rb:lib/tasks/elasticsearch.rakenamespace :elasticsearch do + desc 'サジェスト用のキーワードを登録' + task :import_suggest_word => :environment do + SearchWordLog.__elasticsearch__.import scope: 'searchable_word' + end endbundle exec rake elasticsearch:import_suggest_word
サジェストを返却するAPIの追加
GET /mangas/suggest?word={keyword}
でサジェストワードを取得できるように修正していきます。ルーティング追加
config/routes.rbRails.application.routes.draw do resources :mangas do + collection do + get :suggest + end end endコントローラーにアクション追加
app/controllers/mangas_controller.rbclass MangasController < ApplicationController ・・・ + def suggest + # concerns に追加した `es_search` メソッドで検索 + suggest_words = SearchWordLog.es_search(params[:word]).aggregations["keywords"]["buckets"] + render json: { suggest_words: suggest_words.map{|word| word["key"]} } + end private ・・・ endポイントとしては、今回定義した
es_search
メソッドではaggregationsで集計してhitした件数が多いキーワードを取得しているため、aggregations
メソッドを使うことで結果を取り出すことができます。
es_search
で以下のような形式のjsonが返却されaggregations["keywords"]["buckets"]
で対象のキーワードの配列を取得しています。{ "took" : 9, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 6, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "keywords" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "スライム", "doc_count" : 3 }, { "key" : "スラムダンク", "doc_count" : 3 } ] } } }View側の修正
最後にview側に修正を入れて行きます。今回はライブラリを使わずにシンプルなJavascriptを追加します。
js追加
app/assets/javascripts/suggest.jsdocument.addEventListener('turbolinks:load', function () { // turbolinksを使用しない場合はここは不要 let timeout = null; function selectSuggest(e) { document.querySelector("#search_word").value = e.target.textContent; document.querySelector(".SearchForm").submit(); } function displayNoneSuggestList () { let classList = document.querySelector(".dropdown").classList; if (classList.contains("is-active")) { classList.remove("is-active") } } function displaySuggestList() { let classList = document.querySelector(".dropdown").classList; if (!classList.contains("is-active")) { classList.add("is-active") } } // apiのレスポンスを元にhtmlを成形してサジェストリストを表示する function updateSuggestWords(words) { if (words.length > 0) { const dropdownMenu = document.querySelector("#dropdown-menu"); let wordList = ""; words.forEach(word => { wordList += `<div class="dropdown-item" role="option">${word}</div>`; }); const html = `<div id="sugget-list" class="dropdown-content">${wordList}</div>`; dropdownMenu.innerHTML = html; displaySuggestList(); const suggestList = document.querySelectorAll(".dropdown-item"); if (suggestList) { // サジェストワードが選択された場合にそのワードで検索を行うようにイベントリスナーに登録 suggestList.forEach(element => { element.addEventListener("click", selectSuggest); }); } } else { displayNoneSuggestList(); } } function getSuggetWords(e) { const value = e.target.value; clearTimeout(timeout); // 2文字以上入力された場合にサジェストワードを取得 if (value.length > 1) { // setTimeoutを使ってapiが呼ばれるまでに時間を置く timeout = setTimeout(function () { fetch(`http://localhost:3003/mangas/suggest?word=${value}`) .then(res => { return res.json(); }) .then(resJson => { updateSuggestWords(resJson.suggest_words); }) .catch(error => console.log(error)) }, 300); } else { displayNoneSuggestList() } } // 検索ワードの入力をイベントリスナーに登録 const inputWord = document.querySelector("#search_word"); inputWord.addEventListener("input", getSuggetWords); });解説
検索ワードが入力されたらapiからサジェストワードを取得してリストを更新するメソッド(
getSuggetWords
)をイベントリスナーに登録します。
getSuggetWords
では入力の度にapiが呼ばれてサジェストが何度も変わるのは使いづらいので、2文字以上入力された場合に0.3秒後にapiを呼ぶようにしています。
apiからのレスポンスをupdateSuggestWords
に渡してhtmlを成型してサジェストリストを追加しています。追加するリストにイベントリスナーを登録してキーワードが選択された場合にselectSuggest
メソッドを呼び検索を実行するようにしています。
このサンプルアプリではCSSは、bulmaを使用っているので、bulmaのdropdownのクラスを使ってスタイルを整えています。view修正
jsで使用するクラスや要素を追加し、jsを読み込みます。
app/views/mangas/index.html.erb<div class="container" style="margin-top: 30px"> - <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered") do %> - <div class="control"> + <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered SearchForm", autocomplete: "off") do %> + <div class="control SearchInput"> <%= text_field_tag :search_word, @search_word, class: "input", placeholder: "漫画を検索する" %> + <div class="dropdown"> + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + </div> </div> </div> <div class="control"> <%= submit_tag "検索", class: "button is-info" %> </div> <% end %> </div> ・ ・ ・ + <%= javascript_include_tag 'suggest' %>追加したjsを読み込むためにinitializer修正
config/initializers/assets.rb+ Rails.application.config.assets.precompile += %w(suggest.js)以上で完成です!
おまけ:analyzerの動作確認
今回追加したanalyzerが実際にどのように動作するかを「転生」という単語を例に見ていきます。
documentの登録
まずはdocument登録する際のanalyzerの動きを確認します。
「転生」という単語を登録する場合以下のようにanalyzeされてindexされています。それぞれのAnalyzer APIでの確認結果は以下のようになります。
keyword_analyzer
requestPOST /es_search_log_development/_analyze { "analyzer": "keyword_analyzer", "text": "転生" }response{ "tokens" : [ { "token" : "転生", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }autocomplete_index_analyzer
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_index_analyzer", "text": "転生" }resoponse{ "tokens" : [ { "token" : "転", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "転生", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }readingform_index_analyzer
requestPOST /es_search_log_development/_analyze { "analyzer": "readingform_index_analyzer", "text": "転生" }response{ "tokens" : [ { "token" : "t", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "te", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "ten", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "tens", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "tense", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 }, { "token" : "tensei", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }documentのsearch
続いてsearch用のanalyzerの動きを見ます。
今度は「転生」、「てんせ」、「ten」のワードで確認します。
以下のイメージのようにanalyzeされています。autocomplete_search_analyzer
「転生」
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_search_analyzer", "text": "転生" }{ "tokens" : [ { "token" : "転生", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }「てんせ」
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_search_analyzer", "text": "てんせ" }{ "tokens" : [ { "token" : "てんせ", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0 } ] }「ten」
requestPOST /es_search_log_development/_analyze { "analyzer": "autocomplete_search_analyzer", "text": "ten" }{ "tokens" : [ { "token" : "ten", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0 } ] }readingform_search_analyzer
「転生」
POST /es_search_log_development/_analyze { "analyzer": "readingform_search_analyzer", "text": "転生" }{ "tokens" : [ { "token" : "tensei", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 0 } ] }「てんせ」
POST /es_search_log_development/_analyze { "analyzer": "readingform_search_analyzer", "text": "てんせ" }{ "tokens" : [ { "token" : "tense", "start_offset" : 0, "end_offset" : 3, "type" : "word", "position" : 0 } ] }「ten」
POST /es_search_log_development/_analyze { "analyzer": "readingform_search_analyzer", "text": "ten" }{ "tokens" : [ { "token" : "ten", "start_offset" : 0, "end_offset" : 4, "type" : "word", "position" : 0 } ] }サジェストワード検索時のイメージ
indexとsearchのanalyzerの動きをふまえると、「転生」をindexして「転生」、「てんせ」、「ten」を入力した場合、全て「転生」がサジェストされることがわかります。
- 投稿日:2019-07-14T16:49:09+09:00
デバックツール(pry-rails)について binding.pryの使い方
はじめに
デバックツールとは
一般的にデバックツールとはプログラムの作業を行う際に、バグの有無、エラーの原因箇所、コード上の問題がないかなどを確認するために用いるツールのことをいいます。
pry-rails
pry-railsとは
Rails用に使われるデバックツールのことをいい、Gemの一種であるため、使用をする際は以下の様にGemfileに追加する必要があります。
Gemfilegem 'pry-rails'Gemの追加を行ったため、ターミナルで以下のようにbundle installを忘れずに行いましょう。
$ bundle installこれでpry-railsが使えるようになります。
binding.pryとは
pry-railsを導入したことでpry-railsの機能が使えるようになりました。pry-railsの機能の一つにbinding.pryがあります。
binding.pryはコード上にbinding.pryを記述することで
binding.pryの書かれている箇所までの処理を実行し、
binding.pryの書かれている箇所で処理を一時的に止めることができます。使用方法
以下の様に、処理の確認をしたいコードの箇所に
binding.pryを記述します。tweets_controller.rbclass TweetsController < ApplicationController def index @tweets = Tweet.all binding.pry end end記述をし、実行を行うとターミナルのローカルサーバー画面が以下のような表示になり、
binding.pryの記述している箇所で処理が止まります。3: def index 4: @tweets = Tweet.all => 5: binding.pry 6: end [1] pry(#<TweetsController>)>この状態ではコンソールと似た操作ができるので
@tweets.find(6)のようなApplicationRecordメソッドを使い、binding.pryが書かれている箇所までの処理の値を確認することができます。処理停止状態を終了させるには以下のようにexitを入力することで停止状態が終了し、binding.pry以降の処理を実行します。
[1] pry(#<TweetsController>)>exitちなみにexit!を入力するとローカルサーバーも終了します。
おわりに
以上pry-railsの説明とpry-railsでよく使うbinding.pryについてまとめました。
デバックツールはプログラミングをする上で非常にお世話になると思うので覚えておこうと思います。
- 投稿日:2019-07-14T15:31:49+09:00
わたしがRailsチュートリアルで学んだこと【7章】
注意:プログラミング歴34日の初心者が書いています
注意:間違っていたら優しく教えてください(喜びます)
「Ruby on Rails チュートリアル実例を使ってRailsを学ぼう」
https://railstutorial.jp/素晴らしいチュートリアルに感謝します。
8.1 セッション
HTTPは、その場限り。以前の情報を全く持たないリクエストです。
そのため、ログイン情報の保持には、セッションという接続を使います。Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
セッションについても例に漏れず、Sessionsコントローラで操作します。
コントローラを追加したら、
routes.rb
に対応するルーティングを追加します。今回は、
new 新しいセッションのページ (ログイン画面)
create 新しいセッションの作成 (実際のログイン)
destroy セッションの削除 (ログアウト)
という3つのアクションを、
/login
というURLに紐づけます。
- 投稿日:2019-07-14T15:31:49+09:00
わたしがRailsチュートリアルで学んだこと【8章】
注意:プログラミング歴34日の初心者が書いています
注意:間違っていたら優しく教えてください(喜びます)
「Ruby on Rails チュートリアル実例を使ってRailsを学ぼう」
https://railstutorial.jp/素晴らしいチュートリアルに感謝します。
8.1 セッション
HTTPは、その場限り。以前の情報を全く持たないリクエストです。
そのため、ログイン情報の保持には、セッションという接続を使います。Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。
セッションについても例に漏れず、Sessionsコントローラで操作します。
コントローラを追加したら、
routes.rb
に対応するルーティングを追加します。今回は、
new 新しいセッションのページ (ログイン画面)
create 新しいセッションの作成 (実際のログイン)
destroy セッションの削除 (ログアウト)
という3つのアクションを、
/login
というURLに紐づけます。8.1.2 ログインフォーム
フォームに入力されたログイン情報を保存したい、でもセッションにはUserのときのようなモデル、データベースはありません。
form_for(:session, url: login_path)そのため上記のように、データを保存するリソース名(ここでは
:session
)とそのパスを指定ながら、form_for
でformタグを生成します。rb <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %>送信されたデータは、
params[:session][:email]
params[:session][:password]
で受け取ることができます。こうしてみると、リソース名(ここでは
:session
)をシンボルで記述する意味がわかりますね。"session" => { "email" => "foo@bar.com", "password" => "something", }送信されるデータはこういうハッシュになってるということですね。
これが、
login_path
のPOST、つまりcreate
アクションに受け渡されるということです。8.1.3 ユーザーの検索と認証
session
コントローラのcreate
アクションでparamsからデータを受け取ることができます。
render
とredirect_to
について
flash
で表示させるエラーメッセージは、redirect_to
の場合は次のページ移動で消えるのに、rederではページ移動しても消えずに残ったままになってしまう。def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else flash[:danger] = 'Invalid email/password combination' # 本当は正しくない render 'new' end endrenderとは
render
は指定したviewを呼び出します。def new endこれも実は
render
が動いています。
GETアクション内になにも記述がない場合は、def new render 'new' endに勝手に補完されています。
redirect_toとは
redirect_to
はHTTPリクエスト(GET)をサーバーに送ります。def create @user = User.new(user_params) if @user.save flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end endわかりにくいですが、ここでもRailsの自動補完が効いているのでした。
redirect_to
の行は、以下と同じ意味です。redirect_to user_url(@user)
- 投稿日:2019-07-14T13:46:28+09:00
親クラスも含めて、メソッドの定義場所を特定する
経緯
このモンキーパッチを解読していたところ、元の
columns
がどのクラスで定義されているかを知りたくなった。module ActiveRecordInvisibleColumn INVISIBLE_COLUMNS = { 'foos' => ['bar'], }.freeze def columns(table_name) super.delete_if { |column| INVISIBLE_COLUMNS.fetch(table_name, []).include?(column.name) } end end ActiveSupport.on_load :active_record do ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(ActiveRecordInvisibleColumn) endハマった罠
initializeに引数が必要らしく、面倒。
irb(main):036:0> ActiveRecord::ConnectionAdapters::Mysql2Adapter.new ArgumentError: wrong number of arguments (given 0, expected 4) irb(main):037:0> ActiveRecord::ConnectionAdapters::Mysql2Adapter.new(nil, nil, nil, nil) NoMethodError: undefined method `fetch' for nil:NilClass結論
これで解決した。
allocateはinitializeをskipしてインスタンス化できるメソッド。irb(main):035:0> ActiveRecord::ConnectionAdapters::Mysql2Adapter.allocate.method(:columns).source_location => ["/Users/xxxx/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.11.1/lib/active_record/connection_adapters/abstract_mysql_adapter.rb", 469]参考
https://qiita.com/jnchito/items/fc8a61b421d026a23ffe
https://techracho.bpsinc.jp/hachi8833/2019_07_04/75432
https://eagletmt.hateblo.jp/entry/2017/09/24/004709共同編集者
- 投稿日:2019-07-14T13:25:05+09:00
【Ruby】prependの使い道
まとめ
- prependしたクラスの挙動の前後に、処理を追加したい
- prependしたクラスの挙動を完全に上書きしたい
1. prependしたクラスの挙動の前後に、処理を追加したい
モンキーパッチではこのパターンがよく使われている。
module Hoge def execute pp 'モジュール' super end end class Human def execute pp 'クラス' end end Human.prepend(Hoge) Human.ancestors => [Hoge, Human, ....] Human.new.execute #=> 'モジュール' 'クラス'2. prependしたクラスの挙動を完全に上書きしたい
module Hoge def execute pp 'モジュール' end end class Human def execute pp 'クラス' end end Human.prepend(Hoge) Human.ancestors => [Hoge, Human, ....] Human.new.execute #=> 'モジュール'2'. prependしたクラスの挙動を完全に上書きしたい -> includeではダメ
module Hoge def execute pp 'モジュール' end end class Human include Hoge def execute pp 'クラス' end end Human.ancestors => [Human, Hoge, ....] Human.new.execute #=> 'クラス'おまけ: 継承している場合
superを呼ぶと、Humanだけでなく、Humanの親クラスであるAnimalまで探索される。
module Hoge def execute pp 'モジュール' super end end class Animal def execute pp '親クラス' end end class Human < Animal end Human.prepend(Hoge) Human.ancestors => [Hoge, Human, Animal, ....] Human.new.execute #=> 'モジュール' '親クラス'参考
https://eagletmt.hateblo.jp/entry/2017/09/24/004709
共同編集者
- 投稿日:2019-07-14T12:50:43+09:00
【Ruby】methodsとinstance_methodsの違い
- 投稿日:2019-07-14T12:47:47+09:00
【個人開発】チャットBotのAppStoreが1週間で出来上がるまで(rails)
はじめに
チャットBotや音声アプリのダウンロードサイトを作りました。2022年にはチャットボットの市場規模は132億円規模になる!アメリカでは成人の1/4にスマートスピーカーが普及している!など、このあたりの産業は高騰してくのが目に見えていますので。
今のうちに、こういう横断的なサイトを立ち上げておけばまだ間に合うかもしれない。そもそも、それっぽいのが国内には見当たらなかったので作ってみよう!と決めて、1週間で仕上げました。今回は、コードを書き始めて、リリースするまでをオープンに書いていきたいと思います。
「個人開発に挑戦してみたいな。」と考えている方にオススメです!
Botcampとは
AppStoreや、Google Play Storeってありますよね?これらのプラットフォームでは、スマホアプリをダウンロードできますが、BotcampではチャットBotや音声アプリをサイトからダウンロードできます。
できることは、まず「Botを登録する」。運営元じゃなくても、会員登録しなくても、Bot登録はできるような仕様にします。何といっても、このBot数こそがサイトを成り立たせるために必須条件となるので、なるべく登録していただきたいところ。
次に、「Botをダウンロードする」です。といっても、各プラットフォームが提供しているURLをBotごとに設定しておくだけです。
最後に、「Botをレビューする」になります。App Storeのデザインにだいぶ寄せてます(笑)Botやアプリの品質を評価できる部分なので、Bot数を増やしていったあとに、力を入れて増やしていきたい部分です。
おまけに、Slackのワークスペースをコミュニティとして用意してみました。とりあえず、会員登録した人を全員ワークスペースに招待していくという状況を作ってみようかと思います。これに関しては、どうなるかは分からないですが、Botマニアたちが情報交換できるコミュニティ的になれれば嬉しいですね。ちょっと実験的に入れてみた感じです。
1週間で出来上がるまで
Day1
Gitを確認してみたところ、2019年7月8日から開発をスタートしています。初日はRails newして、トップのデザインをコーディングしただけでした。デザインに関しては、ベースは「開発会議」や「個人開発のフリマ」で使っているものを流用しています。
MVPを作る過程において、デザインに力を入れる必要があるプロダクトではないので、毎回流用してリソースをカットしています。ただ、どうしてもこだわり的にレスポンシブサイトが好きになれなくて、pcとspを分けています。
昔は
gem jpmobile
というライブラリを使っていたのですが、Rails 4.1.0で追加されたActionPack Variantsを使うようになりました。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :detect_device_variant private def detect_device_variant request.variant = :sp if is_mobile? end endapp/models/concerns/user_agent.rbmodule UserAgent extend ActiveSupport::Concern def is_mobile? request.user_agent =~ /iPhone|iPad|Android/ end endこれで、HTML側は切り替わってくれるので、スタイルシートもpcはpc直下のディレクトリのみ、spはsp直下のディレクトリのみ読み込んでくれるように設定を変更しておきます。
app/views/layouts/application.html.erb<%= stylesheet_link_tag "application", :media => "all" %> ## pc直下のみに変更 <%= stylesheet_link_tag 'pc/style', media: 'all', "data-turbolinks-track" => true%>初日で本番環境(Heroku)にはあげました。ドメイン購入しましたし、Amazon s3(画像サーバー)、SES(メールサーバー)も設定しました。こういう本番を意識した開発をDay1からするのは、個人開発の精神面で大切な気がしますね。
Day2
Userの認証〜管理までを実装しました。
gem devise
を使っていて、ライブラリとしては賛否両論ありますが、管理画面rails_admin
と連携するところもありますし、初心者が爆速でローンチするんだったら、使ってokだと思います。このあたりは、完全に今まで作ってきたサービスのコピペです。振り返ってみると、コミュニティ系のサイト作るならここまでは毎回同じなのでどっかに置いておいても良いですね。
Day3
Bot関連のCRUDを作ってみました。少しいつもと違ったのが、Botを作成する際に運営元(Botを配信している会社/個人開発者)を追加するのですが、これを既存のものからか、新しく追加できるのを選べるようにするパターンがあるのです。
まず、
Maker
モデルを用意しました。Bot
モデルとは一対多の関係になります。もっと良いやり方がありそうですが、attr_accessor
を使って、これがある場合には運営元を更新して、新しいものを追加するようにしました。app/models/bot.rbattr_accessor :bot_maker attr_accessor :bot_maker_urlapp/controllers/bots_controller.rbdef create @bot = Bot.create(create_params) if @bot.bot_maker.present? @maker = Maker.create({ name: @bot.bot_maker, website: @bot.bot_maker_url }) @maker.save @bot.maker_id = @maker.id end if @bot.save flash[:success] = "新しくボットが追加されました!" redirect_to @bot else flash[:alert] = "ボットの追加に失敗しました。" redirect_to new_bot_path end endカテゴリと、お気に入り機能はチュートリアルにあるくらいの感覚で実装しました。いつもと同じです。
Day4
この日、Mac Book Proが急に死にます。Apple Storeへ重病の我が子を看てもらうように急ぎましたが、4時間待たされた上に完治するまで7営業日かかると宣告されました。スペアPCの開発に急遽移行へ。
Review
と、Type
(プラットフォーム)、BotImage
(ボットの複数画像)を実装しました。プラットフォームはLINEや、MessengerなどのプラットフォームごとのダウンロードURLを入力するところです。Botを入力するときに、同時に入力できるようにしたいので、gem cocoon
を使いました。
Type
は、Bot
と多対多。BotImage
は一対多で関係を構築しました。Type
の方の例を載せておきます。app/views/bots/new.html.erb<section class="feed"> <div class="box"> <h2>プラットフォーム</h2> <div class="formContainer"> <div class="imageContainer"> <%= f.fields_for :bot_types do |t| %> <div id="types"> <%= link_to_add_association "プラットフォームを追加", f, :bot_types, partial: 'type' %> </div> <% end %> </div> </div> </div> </section>app/views/bots/_type.html.erb<div class="nested-fields"> <div class="formItem flexContainer typeContainer"> <div class="label"> <%= f.collection_select :type_id, Type.all, :id, :name, {class: "selectType"} %> </div> <div class="formInput"> <%= f.text_field :url, placeholder: "ホームページや、ダウンロードリンクを入力してください" %> </div> </div> <p><%= link_to_remove_association "プラットフォームを削除", f %></p> </div>Day5
夜ご飯を料理してたら、シェアメイトのオリーブオイルが棚から落ちてきて派手に割れる、右中指からまあまあ出血しました。キーを打つのが極めて困難に。
この日は結構微調整が多かったですね。検索機能をつけたり、カテゴリ、プラットフォーム、運営元のURL指定がIDになっていたので、slugを作成して、それに変更しました。
botcamp.jp/types/
をbotcamp.jp/types/line
に変えた感じですね。config/routes.rbresources :types, param: :slugconfig/controllers/types_controller.rbclass TypesController < ApplicationController def show @type = Type.find_by_slug(params[:slug]) end endDay6
この日はほとんどコードを書いていません。デザインや、仕組みの微調整をしたくらいですね。ひたすら、ボットを探していました。スクレイピングをするのもよぎったのですが、なかなか良い感じのスクレイピング先が思い浮かばなくて。とりあえず、手で探し、手で入力するような、労働集約的な作業をしました。それでも、40ちょっとしか見つからなくて泣きそうになりました。
100は欲しかったのですが、気が滅入りそうなので、とりあえず、出して、毎日ちょこちょこ入れていこうと決意しました。
Day7
リリースします。リリース時にやることはそこまでないのですが、開発会議の週一で送っているニュースレターを一日ずらして、リリースのことを含んだ文書にする。もちろん、開発会議も更新する。
そして、TwitterとこのQiita記事を投稿するくらいです。あと、落ち着いたら、Crieitにも、個人開発寄りの記事をまとめて報告しようと思っています。
さいごに
公開するつもりでやってなかったので、コミットメッセージ分かりづらくてすみません。自分がわかればそれで良いのが、個人開発の甘えなので(笑)
一週間あれば、プロダクトはリリースできます。むしろ、アイデアからリリースまでに一週間以上かかっていたら、遅すぎるのではないでしょうか。とはいえ、自分がリモートワーク/フリーランスなので、一般的なスタイルではないかもです。(もちろん、日中は仕事ありますが)
この記事を読んでいただいた方が、個人開発面白いなと思ってくれたら嬉しいです!!あと、BotcampにぜひBotやアプリを追加したり、サイトを楽しんでもられば最高です!!!
P.S.
リリース Day1から、個人開発のフリマでサイト丸ごと販売します。サービス開発の勉強として、あるいは、Botビジネスに興味ある人は入札してみてはいかがでしょうか?(笑)
- 投稿日:2019-07-14T09:52:19+09:00
TECH DAY 1 Ruby
勉強した項目: Ruby
時間: 4:30
内容:
【メソッド】
メソッド:オブジェクトをメソッドと呼ばれる処理によって自身の形を変えたりすること
【lengthメソッド】
文字列が利用できるメソッドで、文字列の文字の数を数えてくれる
返り値として数値オブジェクトの数字で返してくれる
【to_sメソッド】
文字列と数値は別のオブジェクトなので、連結することができないため、to_sメソッドを利用して数値を文字列に変換する【Rubyの演算子】
+(足し算)
-(引き算)
* (かけ算)
/(割り算)
%(剰余)比較演算子(>, <, ==) 式が正しければtrue、間違っていればfalse,等しい場合は==
比較演算子(>=, <=) 比較演算子>,<のあとに=を続けることで〜以上、〜以下を表す
not演算子(!) !(エクスクラメーションマーク)はnot演算子と呼ばれ、否定の意味
!と=を合わせた!=は==と反対で、値同士が等しくない場合にtrueを返す【変数】
変数とは、オブジェクトの入れ物、またはオブジェクトを識別する名札のようなもの
=を挟んで右側においたオブジェクトの返り値を、左側の変数の中に入れるイメージ変数 = 格納するオブジェクト
変数の名前は、原則として小文字から
putsの返り値は「nil」となります。これは、オブジェクトが何もない、という意味
【変数の命名規則】
・原則、小文字から始める
((アンダーバー)から始めることも可能ですが、特に理由がない場合は避けましょう。)
・名前の1文字目でなければ大文字や数字、(アンダーバー)を使ってもよい
・名前にスペースが入ってはいけない
・予約語(Rubyによって最初から用途を与えられた単語)と同一の名前は使ってはいけない【プログラミングにおける=の意味】
Rubyにおいて=が1つの式は必ず『右側の値を左の変数に代入する』という意味になる【名札としての変数】
変数にはオブジェクトが何であるかを識別する名札という役割があります。変数を定義することにより、ソースコード中でオブジェクトをわかりやすく使える
、
【定数】
定数は、一度オブジェクトを代入したら再代入することをせずに、定義するときは名前の最初を大文字にする。
hello world ではなく Hello World【アプリケーションの作成】
putsメソッドは最後に自動で改行する
putsの改行はputs""でも可能【バックスラッシュ記法】
バックスラッシュの打ち方は option + \
記法 意味 \n 改行 \t タブ \b バックスペース \ バックスラッシュ バックスラッシュ記法が適応されるのは文字を"(ダブルクォテーション)で囲んだときのみ
バックスラッシュ記法で改行は、
puts "吾輩は猫である\n\n名前はまだない" で表せる【式展開】
式展開の書き方は文字列中で#{式}とするだけ
ポイントは、"(ダブルクォテーション)で囲む必要があります。'(シングルクォテーション)で囲んだ場合は式展開が行われない【getsメソッド】
getsメソッドはユーザーからターミナルへ入力できるようにするメソッド
返り値はユーザーが入力した値の文字列オブジェクト
getsメソッドが呼ばれるとターミナル画面は入力待ちの状態になる【chompメソッド】
chompメソッドは文字列の末尾の改行文字を取り除いた新しい文字列を返してくれるメソッド
getsメソッドで返ってきた文字列オブジェクトは勝手に文末で改行してしまうのを防ぐgets.chompのように後方につける
【ハッシュ】
ハッシュという複数の情報を扱うことのできるオブジェクト【ハッシュオブジェクト】
ハッシュオブジェクトは、自身の中にデータとそれに対応するキーのセットを所持することができる【キーバリューストア】
ハッシュのように、保存したいデータ(バリュー)とそれに対応する標識(キー)を対応させてペアで保存する方式のこと【ハッシュオブジェクトの変数を作る】
hash = {}ハッシュは{} (中括弧)を使って記述します。また左辺のhashは変数名であり、任意の名前を定義
【シンボルオブジェクト】
シンボルオブジェクト(略称:シンボル)とは今回のハッシュのキーのような名前を識別するためのラベル
基本的に先頭に接頭語:をつける# どちらも同じ
hash1 = {:title => "時をかける少女"}
hash2 = {"title" => "時をかける少女"}ハッシュではシンボルを使うようにする、実行の速度がシンボルの方が早いため、ハッシュのキーにはシンボルの使用が推奨
例:変数をハッシュオブジェクトpostで生成する
post = {} # 空のハッシュの宣言#以下で要素の追加
post[:genre] = "マンガ"
post[:title] = "るろうに剣心"
post[:review] = "面白い!"
puts post # => {:genre=>"マンガ", :title=>"るろうに剣心", :review=>"面白い"}【条件分岐 if文】
if 条件式 then
# 条件式が真(true)のときに実行する処理
end条件式とは基本文法で出てきた(3 > 0)などのような返り値がtrueかfalseの式のことです。
if 条件式 thenとendの間にはその条件式がtrueのときに実行する処理を書く【条件式が偽の場合の処理】
条件式が偽のときに処理を行うには以下のようにelseを使う
if 条件式 then
# 条件式が真(true)のときに実行する処理
else
# 条件式が偽(false)のときに実行する処理
end複数の条件分岐を書くときはelsifという文法を使う
if 条件式1 then
# 条件式1が真(true)のときに実行する処理
elsif 条件式2 then
# 条件式1が偽(false)のとき、かつ
# 条件式2が真(true)のときに実行する処理
else
# 条件式1と条件式2がどちらとも偽(false)のときに実行する処理
end【to_iメソッド】
文字列オブジェクトに対してto_iメソッドを使うとその文字列を数値オブジェクトに変換することstring = "30" # 文字列の30
number = string.to_i # 数値の30に変換
puts number + 20 # => 50が出力【メソッド】
メソッドとは、ある処理をまとめること。定義したメソッドを呼び出すことで同じ処理を何度も実行すること【メソッドの定義方法】
def メソッド名
# 実行する処理
endメソッドを呼び出すときは、メソッド名を記述
def say_hello # メソッドの定義
puts "Hello World"
endsay_hello # メソッドの実行
- 投稿日:2019-07-14T01:20:09+09:00
herokuにアップする画像を永続的に表示させる方法【Rails】
heroku に画像をアップできるアプリケーションをデプロイしたとしても、アップした画像は数時間で消えるかデプロイするたびに消えてしまいます。
しかし、画像が消えるということを回避できる方法があります。それは、画像をデータベースに格納するときに
バイナリデータ
として保存する方法です。ですが、この記事の方法は「モックアップ用の簡易なアプリケーション」や「アクセスがなく自分しか使わないアプリケーション」といったケースでやるのが好ましいです。
下記の注意点を読んでから、アプリケーションの状況に応じて実施を決断するようにしてください。注意点
以下のサイトの回答者の方の答えが非常にわかりやすくて、画像をデータベースに保存するデメリット(とメリット)についてまとめられているので、参考にしてみてください。
データベースに画像を保存するのはありでしょうか? - teratail
テーブルを作成
今回は、既存の
Postテーブル
に紐付けしたPhotoテーブル
のなかに画像を保存していくとします。
Postテーブル
とPhotoテーブル
の関係は、1対多です。まずは、画像を保存するためのテーブルの作成から行います。
画像を保存するためのカラムは、bynary型
にします。
コンソールに下記のコマンドを打ち込み実行します。$ rails g model Photo
そうすることで、マイグレーションファイルが生成されるので下記のように編集します。
xxxxxxxxxxxxxx_create_photos.rbclass CreateUsers < ActiveRecord::Migration def change create_table : photos do |t| t.bynary :image, nill: false t.references :post, foreign_key: true, null: false t.timestamps null: false end end endマイグレーションファイルを編集したら、下記のコマンドを実行してテーブルを作成しましょう。
$ rails db:migrate
作成した全てのテーブル情報は「schema.rb」ファイルでも確認することができます。
コントローラを編集
コントーラファイルには、「画像をデータベースに保存する」と「画像をデータベースから取り出す」といった2つの動きを追加していきます。
画像をデータベースに保存する
画像をデータベースに保存するには、
read
というメソッドが重要になります。
「photos_controller.rb」ファイルを作成して、下記を追加します。photos_controller.rbdef create @post = Post.new(post_params) # バイナリ化した画像の呼び出し params[:post][:photos_attributes]["0"][:image].each_with_index do |item, i| if i != 0 @post.photos.build end @post.photos[i].image = item.read end end private def post_params params.require(:post).permit(photos_attributes: [:image]) end画像をデータベースから取り出す
画像をデータベースから取り出すには、
send_data
というメソッドが重要です。photos_controller.rbdef send_img tmpbin = Photo.find(params[:id]) send_data(tmpbin.image, :type => 'image/jpeg', :disposition => 'inline') endビューを編集
ビューには、「画像のアップロード」と「画像の表示」とこれまた2つの動きを追加していきます。
画像のアップロード
画像をアップできるようにフォームを設置します。
new.html.erb<%= form_with model: @post, multipart: true do |f| %> <%= f.field_for :photos do |i| %> <%= i.file_field :image, type: :file %> <% end %> <%= f.submit "投稿する" %> <% end %>画像の表示
次に、データベースに保存した画像が表示されるように下記を追加します。
index.html.erb<% @post.photos.each do |photos| %> <%= image_tag url_for(:controller => 'photos', :action => 'send_img', id => photos.id %> <% end %>ルートの設定を追加
最後の仕上げとして、「routes.rb」に下記を追加します。
routes.rbget '/photos/send_img/:id', to: 'photos#send_img'これでバイナリデータとした画像の保存から表示までを終わらせることができました。
heroku にデプロイするアプリケーションで画像投稿機能が付いているものでも、永続的に画像を表示させ続けられることでしょう!参考にさせてもらった記事