- 投稿日:2021-08-29T21:18:16+09:00
【Rails】エラーメッセージ表示
目標 エラーメッセージを表示させる ユーザーの登録や投稿がなんで失敗したか知らせてくれるメッセージ 実装 エラーメッセージのファイル作成 app/views/layouts/_error_messeages.html.erb <% if model.errors.any? %> <div id="error_explanation" class="alert alert-warning"> <ul class="mb-0"> <% model.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> バリデーション 新規登録の際の制限を記述。 ここに記述した制限に引っかかった場合にエラーメッセージで表示される。 app/models/user.rb class User < ApplicationRecord before_save { email.downcase! } validates :name, presence: true, length: { maximum: 50 } validates :email, presence: true, length: { maximum: 255 }, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i }, uniqueness: { case_sensitive: false } has_secure_password 新規登録フォームにパーシャルrenderでエラーメッセージファイルを記述する。 <%= render 'layouts/error_messages', model: f.object %> app/views/users/new.html.erb <h1>Sign up</h1> <%= form_with(model: @user) do |f| %> <%= render 'layouts/error_messages', model: f.object %> <div> <%= f.label :name, 'Name' %> <%= f.text_field :name, class: 'form-control' %> </div> <div> <%= f.label :email, 'Email' %> <%= f.email_field :email, class: 'form-control' %> </div> <div> <%= f.label :password, 'Password' %> <%= f.password_field :password, class: 'form-control' %> </div> <div> <%= f.label :password_confirmation, 'Confirmation' %> <%= f.password_field :password_confirmation, class: 'form-control' %> </div> <%= f.submit 'Sign up', class: 'btn btn-primary' %> <% end %> これでエラーメッセージが表示されます、他のフォームも同様の方法で大丈夫です。
- 投稿日:2021-08-29T18:57:56+09:00
rails g devise User を実行の際のエラーの話
背景 rails g devise User を実行の際、以下のエラーが発生 rails aborted! StandardError: An error has occurred, this and all later migrations canceled: PG::DuplicateColumn: ERROR: column "email" of relation "users" already exists エラー文を見ると、どうやらusersテーブルが重複しているようだ。 試した事 rails db:migrate:reset しかし、 rails aborted! ActiveRecord::NoEnvironmentInSchemaError: Environment data not found in the schema. To resolve this issue, run: bin/rails db:environment:set RAILS_ENV=development 以上のようなエラーが発生。 binは一切弄っていないので他に原因がありそう。 解決方法 対象モデルを削除する rails destroy devise user 次にテーブルを削除するために、削除用のmigrationファイルを作成する rails g migration drop_table_users(←適当なファイル名で可) 作成したmigrationファイルにテーブル削除を記述する class DropTableUsers < ActiveRecord::Migration def change drop_table :users end end マイグレーション実行 rails db:migrate
- 投稿日:2021-08-29T18:44:13+09:00
LINE Messaging APIを使ってみる
概要 LINEのMessaging APIを使ってみる。 Railsでサーバー立ち上げて、実際にWebhookでどんな値が連携されるのか見ていきます。 (公式にあるのかもだけど見当たらなかったので) ただ、Messaging APIの動きを見たいだけなので環境とかは適当に作ります。 誰かの役に立てば嬉しいです。 RailsのDockerイメージを作る 以下をまるまる真似してDockerでRailsが動くところまでやった。 - DockerでRuby on Railsの環境構築を行うためのステップ【Rails 6対応】 と、言いたいところだったけど、postgressのイメージバージョンの影響で動かないところがあった。 以下の対応でとりあえず回避した。 - PostgresのDockerImageに変更があって起動ができなくなった話 GrapeでAPIを作る Grapeで最低限リクエスト受け取って動く形にする。 Grapeの導入はREADME見てもらえればわかるかと思います。 実装は以下のようにした。 app/api/line/line.rb module Line class API < Grape::API version 'v1', using: :header, vendor: 'line' format :json prefix :api resource :messaging do desc "user test" post do User.all end end end end Postman使ってローカルのAPI叩けるか確認した。 herokuにデプロイする Messaging APIのリクエストを受け取るためにはデプロイしないといけないので、herokuでデプロイする。 herokuのGetting Startedを参考にすればデプロイまでは簡単にできる。 welcomeページいらないけど、必要みたいなのでとりあえず追加。 Messaging APIのWebhoom URLを設定する herokuでデプロイしたサーバーのAPIのURLを LINE Developersで設定する。 herokuのURLを入れるとverifyが正常に動く。 Messaging APIのWebhookで渡ってくるパラメータ 今回の目的は、Messaging APIのWebhookがどういうパラメータを連携するか知りたいので、grape_loggingを入れてパラメータを確認した。 以下、友だち追加、メッセージ送信、ブロックのときのパラメータ。 Messaging APIのQRを自分のLINEで読み込んでそれぞれの動作を確認。 follow params => { "destination" => "", "events" => [{ "type" => "message", "message" => { "type" => "text", "id" => "", "text" => "てすと" }, "timestamp" =>, "source" => { "type" => "user", "userId" => "" }, "replyToken" => "[FILTERED]", "mode" => "active" }] } text params => { "destination" => "", "events" => [{ "type" => "message", "message" => { "type" => "text", "id" => "", "text" => "てすと" }, "timestamp" =>, "source" => { "type" => "user", "userId" => "" }, "replyToken" => "[FILTERED]", "mode" => "active" }] } unfollow params => { "destination" => "", "events" => [{ "type" => "unfollow", "timestamp" =>, "source" => { "type" => "user", "userId" => "" }, "mode" => "active" }] } さいごに パラメータの構造がイメージできなかったけど、これでテスト書いて実装できそう。 公式とかでわかりやすい記述があったら教えていただけるとたいへん喜びます。
- 投稿日:2021-08-29T17:49:04+09:00
Redmineからメンション付きでSlackにメッセージを送信する
View Customize Pluginを使って、Redmineの注記をSlackで共有する機能をつけてみました。 処理の手順は、以下のようになっています。 View Customize Pluginを使ってSlackに送信するデータを準備する クライアント側から、サーバ側に配置したコントローラをAjax通信で呼び出して実行する サーバ側はデータをSlackにPOSTするだけ 機能のイメージは以下のようになります。 Redmineのチケット詳細画面・履歴の注記に追加した「Slackで共有」ボタンをクリック Slackにメンション付きでメッセージが送信される 1.SlackのIncoming WebHook URLとSlackのメンバーIDが必要になりますので、以下のページを参考にして取得しておいてください。 SlackのWebhook URL取得手順 https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008 SlackのメンバーID取得手順 https://help.receptionist.jp/?p=1100#memberid 2.次に、Redmineの「管理 » カスタムフィールド » 新しいカスタムフィールド » ユーザー 」で、「SlackメンバーID」の入力項目を作成します。 上記1.で取得したSlackのメンバーIDをRedmineの個人設定で登録してください。 3.View Customize Pluginでのコーディング ユーザー情報をまとめて取得するために、Redmine Shared APIプラグインを使用します。 Redmine Shared API https://www.redmine.org/plugins/redmine_shared_api 取得したユーザ情報を、メンション名として注記のテキストエリアに入力補完するための処理を追加します。「@」の入力に対してメニューを表示し、選択したユーザーをメンションする。 テキストエリアで入力補完 (Redmine View Customize Plugin) https://blog.enjoyxstudy.com/entry/2019/03/22/000000 <!-- パスのパターン:/issues/[0-9]+ 挿入位置:全ページのヘッダ 種別:HTML コメント:Slackで注記を共有(メンション名を入力自動補完) --> <style> /*メンション名自動補完用*/ .atwho-view { position: absolute; top: 0; left: 0; display: none; margin-top: 18px; background: white; color: black; border: 1px solid #DDD; border-radius: 3px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); min-width: 120px; z-index: 11110 !important; } .atwho-view .atwho-header { padding: 5px; margin: 5px; cursor: pointer; border-bottom: solid 1px #eaeff1; color: #6f8092; font-size: 11px; font-weight: bold; } .atwho-view .atwho-header .small { color: #6f8092; float: right; padding-top: 2px; margin-right: -5px; font-size: 12px; font-weight: normal; } .atwho-view .atwho-header:hover { cursor: default; } .atwho-view .cur { background: #3366FF; color: white; } .atwho-view .cur small { color: white; } .atwho-view strong { color: #3366FF; } .atwho-view .cur strong { color: white; font: bold; } .atwho-view ul { /* width: 100px; */ list-style: none; padding: 0; margin: auto; max-height: 200px; overflow-y: auto; } .atwho-view ul li { display: block; padding: 5px 10px; border-bottom: 1px solid #DDD; cursor: pointer; /* border-top: 1px solid #C8C8C8; */ } .atwho-view small { font-size: smaller; color: #777; font-weight: normal; } </style> <script src="https://cdnjs.cloudflare.com/ajax/libs/at.js/1.5.2/js/jquery.atwho.min.js" defer></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Caret.js/0.3.1/jquery.caret.min.js" defer></script> <script> $(function() { const apiKey = ViewCustomize.context.user.apiKey; const slacknames = []; //取得したユーザー・SlackメンバーID情報を格納しておく //チケット詳細画面表示時にユーザーリストを準備 getslackuserList().done(function(results) { results.forEach(function(result) { const username = result.firstname + '_' + result.lastname; //カスタムフィールドの番号は環境に合わせてください const memberid = result.custom_fields[7].value; if (memberid !== '') { slacknames.push({ name: username, id: memberid }); } }) //新規の注記コメントでメンション名を自動補完 autocompletesuggestion(); //履歴の注記コメントでメンション名を自動補完 $(document).on('click', 'div[id^="journal-"] .contextual .icon-edit', function() { $(document).ajaxComplete(function() { autocompletesuggestion(); }); }); }) //ユーザー・SlackメンバーID情報取得(Redmine Shared APIプラグイン) function getslackuserList() { const deferred = new $.Deferred(); $.ajax({ type: "GET", url: '/shared/users.json?limit=1000', headers: { 'X-Redmine-API-Key': apiKey }, dataType: "text", contentType: 'application/json', }).done(function(data) { data = JSON.parse(data); data = data["users"]; deferred.resolve(data); }); return deferred; } //メンション入力自動補完 function autocompletesuggestion() { $('textarea.wiki-edit').atwho({ at: '@', data: slacknames, limit: 5, insertTpl: '@' + '${name}', suffix: '' }); } }) </script> 「Slackで共有」ボタンを画面に追加します。アイコン画像はフリー素材からダウンロードして、Redmineサーバの「../images/jstoolbar/」 などに配置してください。 <style> /*Slack共有ボタン用*/ .slack { background-image: url(/images/jstoolbar/slack_btn.png); } .slack { margin-right: 4px; padding: 4px; vertical-align: middle; width: 24px; height: 24px; border-style: none; background-color: #FFFFFF; background-position: 50% 50%; background-repeat: no-repeat; } </style> <script> //画面初期表示時 addSlack_btn(); //Slack共有ボタンを表示 //履歴を変更して保存した際にSlackボタンを再表示 $(document).on('click', 'div[id*="note-"] input[type="submit"]', function() { $(this).parents('div[id*="note-"]').find('.slack').remove(); $(document).ajaxComplete(function() { addSlack_btn(); }) }) //コンテキストメニュー(履歴)にSlackで共有ボタンを追加 function addSlack_btn() { var slack_btn = '<button type="button" title="' + "Slackで共有" + '" class="slack">'; $('div[id^="journal-"] .contextual').each(function() { if ($(this).find('.slack').length === 0) { $(this).append(slack_btn); } }) } </script> 送信用のデータを準備して、「Slackで共有」ボタンクリック時に送信します。 SlackのIncoming Webhooksを使い倒す https://qiita.com/ik-fib/items/b4a502d173a22b3947a0 <script> //Slackにメッセージを送信 $(document).on('click', '.slack', function() { //Slackのチャンネル名とRedmineのプロジェクト識別子を同じに設定してください const channel = ViewCustomize.context.project.identifier; const issueIdno = location.pathname.split('/').pop(); const issueIdtxt = ' #' + issueIdno + ': '; const subject = $('#issue_subject').val(); const noteId = '#' + $(this).parents('div[id^="note-"]').attr('id'); const linkUrl = location.protocol + "//" + location.hostname + "/issues/" + issueIdno + noteId; const journalId = $(this).parents('div[id^=journal-]').attr('id').split('-')[1]; get_note(issueIdno).done(function(result) { const trackername = result.issue.tracker.name; const projectName = '[' + result.issue.project.name + '] '; const journals = result.issue.journals; journals.forEach(function(journal) { if (journal.id !== Number(journalId)) { return; } const notes = journal.notes; const regex = new RegExp('(?<=^|\\s)(@\\w+)', 'g'); const mentions = notes.match(regex); let mentionList = ''; if (mentions !== null) { for (let i in mentions) { const mentionId = convertSlackid(mentions[i]); if (mentionId !== undefined) { mentionList += ' <' + mentionId + '>'; } } } const authorName = journal.user.name; const title = trackername + issueIdtxt + subject; const message = projectName + authorName + ' updated' + ' <' + linkUrl + '|' + title + '>' + mentionList; const pretext = "*内容を確認してください。*"; const data = { "channel": "#" + channel, "text": message, "attachments": [{ "pretext": pretext, "color": "#00FF00", "text": notes }] } //Slack通信用のコントローラ呼び出し $.get('/shared/slack', { data: data }).done(function(resp) { console.log("メッセージを送信しました"); }).fail(function(error) { console.log("メッセージを送信できませんでした"); }) }) }) }) //チケット・注記情報を取得 function get_note(issueIdno) { const deferred = new $.Deferred(); $.ajax({ type: "GET", url: '/issues/' + issueIdno + '.json?include=journals', headers: { 'X-Redmine-API-Key': apiKey }, dataType: "text", contentType: 'application/json', }).done(function(data) { data = JSON.parse(data); deferred.resolve(data); }); return deferred; } //メンション名をSlackメンバーIDに変換 function convertSlackid(mentionName) { for (let j in slacknames) { const slackName = '@' + slacknames[j].name; if (mentionName === slackName) { return '@' + slacknames[j].id; } } } </script> *コードを分けて記述していますが、$(function(){}); 内にまとめてください。 4.Rails(サーバー側)で必要なコード jQueryでAjax通信 × Railsでサーバー処理 https://qiita.com/beanbeenzou/items/54f1a6f0170ab48a0999 コントローラファイルの配置は、上記3.でインストールしたRedmine Shared APIプラグインのフォルダを共有することにします。 SlackへのPOST先は、上記1.で取得したWebhook URLを環境変数 .env に格納するか、 コードに直打ちしてください。 slack_api_controller.rb # ../redmine/plugins/redmine_shared_api/app/controllers/slack_api_controller.rb class SlackApiController < ApplicationController require 'net/http' require 'uri' require 'json' before_action :require_login accept_api_auth :slack def slack data = params[:data] uri = URI.parse(ENV['SLACK_URL']) #.envに格納した場合 # Webhook URLを直打ちの場合: uri = URI.parse('https://hooks.slack.com/services/...') res = Net::HTTP.post_form(uri, {"payload" => data.to_json}) @slack = res.body render text: @slack.to_json end end ルーティングは、View Customize Pluginのコードに記述したリクエストの送信先(/shared/slack)を設定します。Redmine Shared APIプラグインのroutesファイル(../redmine/plugins/redmine_shared_api/config/routes.rb)の最後に追記してください。 routes.rb match 'shared/slack', :controller => 'slack_api', :action => 'slack', :via => [:get] *最後に、設定を有効にするためRedmineを再起動してください。 以上で、Redmineからメンション付きでSlackにメッセージを送信することができるようになったと思います。
- 投稿日:2021-08-29T17:45:32+09:00
Railsで共通の処理をどのようにまとめるか、考えてみた
記事を書くにあたって Railsで実装を行なっている際に同じ処理が複数の箇所に記載されてしまうため、モヤモヤして色々と調べた結果です。他にもこういったものがあるよ! とコメント欄で教えていただけると嬉しいです! ポリモーフィック関連付と単一テーブル継承に関しては私自身あまり理解していないので、今回の項目からは除外しています。(個人的にはメリットよりデメリットを大きく感じる……) Railsで共通の処理はどう実装するの? concern、merge、extendingを紹介いたします! これらを使用すれば、共通の処理をまとめることができるのではないかと考えています! concern、merge、extendingはそれぞれ何を共通化できるの? 機能 共通化できるもの concern モデルのクラスやインスタンスに対しての処理 merge scope(他のモデルのscopeを呼び出すことが可能になる。) extending モデルの集合のインスタンスに対しての処理 説明の元となるモデルに関して モデル名称(JP) 列(JP) MissionCar(MT車) name(名前), manufacturer_id(メーカーID), car_model(車種) AutomaticCar(AT車) name(名前), manufacturer_id(メーカーID), car_model(車種) モデル名称(JP) 列(JP) Manufacturer(メーカー) name(名前) concernに関して 共通で書かない場合 例えば、Enumerizeを使用する場合に複数のモデルに同じ処理を書いたことはありませんか? 下記の例のような形です。 この場合、car_modelに新しく種類を追加して欲しい! と言われた時にMissionCar(MT車)とAutomaticCar(AT車)の両方を修正しないといけませんよね? app/models/automatic_car.rb class AutomaticCar < ApplicationRecord belongs_to :manufacturer extend Enumerize enumerize :car_model, in: %i[sedan suv minivan open] scope: true scope :familiesa_car , lambda{ where(car_model: %w[sedan suv minivan]) } end app/models/mission_car.rb class MissionCar < ApplicationRecord belongs_to :manufacturer extend Enumerize enumerize :car_model, in: %i[sedan suv minivan open] scope: true scope :familiesa_car , lambda{ where(car_model: %w[sedan suv minivan]) } end 共通で書いた場合 Concernを使用して共通の処理を抜き出してみます。 下記のようにすることでenumerizeを一つのmoduleに閉じ込めることができます。 今後、car_modelに追加や削除があってもCarModelに対して修正を行い、MissionCar(MT車)やAutomaticCar(AT車)に修正する必要は無くなりました。 app/models/concerns/car_model.rb module CarModel extend Enumerize extend ActiveSupport::Concern enumerize :car_model, in: %i[sedan suv minivan open] scope: true included do scope :familiesa_car , lambda{ where(car_model: %w[sedan suv minivan]) } end end app/models/automatic_car.rb class AutomaticCar < ApplicationRecord include CarModel belongs_to :manufacturer end app/models/mission_car.rb class MissionCar < ApplicationRecord include CarModel belongs_to :manufacturer end Mergeに関して 共通で書かない場合 例えば、MissionCar(MT車)やAutomaticCar(AT車)がManufacturer(メーカー)のname(名前)に関して検索を行いたいとします。 MissionCar(MT車)やAutomaticCar(AT車)にそれぞれ下記のように書いていませんか? もし、システムの使用が部分一致ではなく完全一致になった場合対象の処理を全て修正しないといけませんよね? app/models/automatic_car.rb class AutomaticCar < ApplicationRecord include CarModel belongs_to :manufacturer scope :where_manufacturer_name, -> (search_name) { eager_load(:manufacturer).where('name LIKE ?', "%{ search_name }%") if name.present? } end app/models/mission_car.rb class MissionCar < ApplicationRecord include CarModel belongs_to :manufacturer scope :where_manufacturer_name, -> (search_name) { eager_load(:manufacturer).where('name LIKE ?', "%{ search_name }%") if name.present? } end 共通で書いた場合 Mergeを使用することで、Mergeで指定したクラスのscopeを使用することが可能になります。 同じscopeを複数の箇所に持たせたいのであれば、Concernを使用すれば良いと思います。 app/models/manufacturer.rb class Manufacturer < ApplicationRecord has_many :mission_cars has_many :automatic_cars scope :where_name, -> (search_name) { where('name LIKE ?', "%{ search_name }%") if name.present? } end app/models/automatic_car.rb class AutomaticCar < ApplicationRecord include CarModel belongs_to :manufacturer # 或いはscopeで直接merge(Manufacturer.where_name(search_name)と呼び出しても良い scope :where_manufacturer_name, -> (search_name) { eager_load(:manufacturer).merge(Manufacturer.where_name(search_name)) } end app/models/mission_car.rb class MissionCar < ApplicationRecord include CarModel belongs_to :manufacturer # 或いはscopeで直接merge(Manufacturer.where_name(search_name)と呼び出しても良い scope :where_manufacturer_name, -> (search_name) { eager_load(:manufacturer).merge(Manufacturer.where_name(search_name)) } end Extendingに関して 共通で書かない場合 この機能が一番知りたかった機能でした! 例えば、Paginationの処理ですが複数のコントローラーに何度もpage(params[:page]).per(20)と貼り付けていますよね? 20件が30件になった時やページネーションのGEMを変更する時を考えると頭が痛くなります。 app/controllers/manufacturers_controller.rb def index @manufacturers = Manufacturer.all.params[:page]).per(20) end 共通で書かいた場合 Railsのextendingを使用することで一つのファイルにまとめることができました! これで変更する際にも修正箇所はPaginationモジュール一つのみになります。 app/extensions/paginate.rb # 実際にこのファイルを作成した場合はRailsを再起動してください。 # 再起動しない場合はファイルをRailsが読み込みません。 module Pagination def paginate(page) page(page).per(10) end end app/controllers/manufacturers_controller.rb def index @manufacturers = Manufacturer.all.extending(Pagination) @manufacturers.paginate(params[:page]) end 参考 Railsガイド
- 投稿日:2021-08-29T17:10:26+09:00
flashについて
なぜ書くのか? flashで表示すべきか?htmlで表示するべきか?を判断するときに、それぞれの技術の性質や役割を理解していないために、どちらを使えば良いのかを判断ができなかった。またそれらの技術の特徴を理解する以前に、基本的なこと(ブラウザのリクエスト、レスポンス、セッション(Cookie))についての理解が曖昧なために、どれだけ説明をよんでもちゃんと理解できなかった。そのため、本記事では研修で学んだ基礎的なことの復習をし、flashの技術の性質を理解するためにかく。 基本的なことのおさらい HTTPとは HTTPとはWebサーバとWebブラウザの間で情報のやり取りをするための通信規則です。(プロトコル) 例えばgoogleでサイトを閲覧するときも、このプロトコルに則ってブラウザがサーバーにリクエストし、その結果サイトを閲覧することができている。 HTTPの特徴として、常にブラウザ側からサーバーをよびだし、1つの要求(リクエスト)には1つの応答(レスポンス)を返すこと。そして、以前に何を要求したかで応答が変わることなく、同じ条件ならば、ある要求に対する応答は常に同じものになることです。(ステートレス) 似た名前でHTTPSがあります。HTTPとHTTPSの違いは通信が暗号化されていないか暗号化されているかの違いで、HTTPSが暗号化されています。( 僕の肌感ですが、だいたいのWebサイトがhttpsになっている気がします。 HTTPリクエスト HTTPメソッドを使ってサーバーに対して、必要なレスポンスの要求をすることです。HTTPリクエストには「HTTPリクエスト行」「HTTPヘッダー」「データ本体」の3つのパートがあります。 HTTPリクエスト行 HTTPリクエストの1行目が、「メソッド」「URL」「HTTPのバージョン」の3つの情報が含まれています。 HTTPヘッダー 主に5つが含まれている ユーザーエージェント名(User-Agent) ブラウザの種類やOSの情報 2.リファラ(Referer) リクエストの発生元のページ名 クッキー(Cookie) Cookieとは、ブラウザに対し特定の情報を保持させておく仕組みです。例えば、ログインしたことあるサイトであればIDとパスワードの情報を打たなくても、入力フォームに情報が入っているなど。 更新されていたら(If-Modified-Since)/同じでなければ(If-None-Match) ブラウザに保存されているローカルキャッシュが変更されているかどうかを問い合わせています 受け取り希望情報(Accept、Accept-Language、Accept-Encoding、Accept-Charset) どのようなデータを受け取りたいのか、言語、文字コード、画像の種類などの情報 データ本体 メソッドに「POST」が指定されている場合、送るデータが記載される 「GET」の場合は空になる HTTPレスポンス HTTPリクエストに従って、必要なレスポンスを返すこと。HTTPレスポンスには「レスポンス状態行」「HTTPヘッダー」「データ本体」の3つのパートがあります。 レスポンス状態行 HTTPステータスコードが含まれている HTTPヘッダー データ本体の各種の状態を示す情報が入れられている部分です。主に6つが含まれています。 コンテンツタイプ(Content-Type) どのようなデータを受け取ったのか?HTMLなのか画像、文字コードなどの情報。 再利用期限(Expires) 取得したデータを再度サーバーに問い合わせなくてもブラウザが再利用していい期限(キャッシュの制御に使われる) データの最終更新日時(Last-Modified)やエンティティ情報(ETag) HTMLや画像などがいつ更新されたものかの情報や、サーバー上のファイルの場所ID、ファイルのサイズ、更新日時などから算出した、更新チェック情報。次回同じデータをリクエストする際に、これらの情報を使って更新されているかどうかを確認します。 キャッシュ制御(Cache-ControlやPragma) ブラウザや通信を橋渡しするプロキシが、データのキャッシュをどう扱うかの情報。 接続状況(Connection) 接続を持続するのか(keep-alive)、毎回接続を切断するのか(close)。ブラウザもサーバーもHTTPバージョン1.1の持続接続(keep-alive)を使える場合、通信のやりとりが効率良くなります。 移動先(Location) リクエストと違う場所からデータを取得するように示す指示。新しい場所のURLが含まれます。いわゆるリダイレクト先を示す情報です。 本題(flashの技術の性質を理解していく) flashって? flashの性質を理解しようと、いくつかのサイトを参考にまとめた結果、下記のように動いていることが分かった。 リダイレクト前にセッションに値を保存して、リダイレクト後のページでセッションから値を取り出して、次のリクエストでセッションに保存した値を削除する ここから2つの疑問がうまれる sessionにいつ保存したん? flash[:key]= "hoge" としか書いてないのに、なぜcookieにflashの値が保存されているの? ブラウザでcookieの中を探しても、hogeらしき文言が見つからない! sessionから、値を取得するための処理をコントローラ内に書かないと!? cookieに保存された値を取得するためのメソッドはどこに書かれているのか? 外部情報の取得 == GETリクエスト (間違った解釈) のはずだから、検証ツールのNetworkを確認すれば、GETのレスポンスの中にhogeがいるのではないか! 疑問(勘違い)を抱いた背景 1の原因 railsチュートリアルでは、ログイン済みユーザーを返すためにに下記のコードを書いていた。 なので、sessionと連携するために、session[:flash] = flashみたいなものを書くべき!と考えた。 # sessionに保存 def log_in(user) session[:user_id] = user.id end flashの中の実装がどうなっているかを知らなかった cookieに保存するときに、値が暗号化されることを忘れてしまっていた 2の原因 外部情報の取得 == GETリクエスト (間違った解釈) HTTPリクエストをちゃんと理解ができていなかった railsとsessionとの関係が理解できていない 下記では、flashの確認の前に、sessionの実体を探っていく Sessionの仕組みについて知る そもそもsession[:key] = "hoge"も、どうやって実装しているんだっけ?を紐解いていく ApplicationControllerから、順に継承元を見ていくと ActionController::Metalというクラスに、初めてsessioの文字を発見(githubリンク) module ActionController ... class Metal < AbstractController::Base ... delegate :session, to: "@_request" ... end ... end ここから、ActionController::Metalがsessionのメソッドを持っていることがわかるが、中身が@_requestで何を示していいるのかがわからない。。 inspectメソッドでクラス名を調べる アクションの中に、raise session.inspectを挿入にしてオブジェクトを調べてみる def show raise session.inspect @task = Task.find(params[:id]) end 結果、sessionがActionDispatch::Request::Sessionであるとことが判明! どのクラスかがわかったので、おってみる(gituhbリンク) module ActionDispatch class Request ... class Session ... def [](key) @delegate[key] end ... def []=(k, v) @delegate[k] = v end ... end end end これでようやくアプリケーションの controllerで、sesion[:hoge]= "hogehoeg"のメソッドが使える理由が判明! (継承クラスを追ってみて思った。結構複雑。日々何気なく、使ってるメソッドも先人の努力の賜物であり、先人に感謝しないとなと思った。) セッションの管理 Railsでは、セッションの管理にCookieStoreがデフォルトで用意されている cookieの他、Redis(インメモリ方式)などがある Session情報を全てsecret_key_baseで暗号化し、クライアントのCookieに保存する リクエストの際に、cookieに保存した暗号かされた情報から、アプリケーション内でsecret_key_baseで復号し、Session情報を取得する flashの実装の中身を見ていく sessionの仕組みがある程度わかったので、flashの実体を追っていく def show raise flash.inspect @task = Task.find(params[:id]) end flashは、ActionDispatch::Flash::FlashHashであることがわかった。この中身を追っていく(github) module ActionDispatch class Flash ... class FlashHash ... def [](key) @delegate[key] end ... def []=(k, v) @delegate[k] = v end ... end end end なるほど!これで、アプリケーションの controller、flash[:hoge]= "hogehoeg"のメソッドが使える理由がわかりました。 ここにflash.nowの実装も書かれていました。 flashをsessionに保存 or 削除 def commit_flash # :nodoc: session = self.session || {} flash_hash = self.flash_hash # flash_hashをsessionに保存 if flash_hash && (flash_hash.present? || session.key?("flash")) session["flash"] = flash_hash.to_session_value self.flash = flash_hash.dup end # sessionに保存していたflashをkeyとするhashを削除 if session.loaded? && session.key?("flash") && session["flash"].nil? session.delete("flash") end end このcommit_flashはActionDispatch::Routing::Redirectionの中で、読み込まれており、リダイレクトのたびに実行される。 4つのポイントに分けて確認する 1. flashメッセージをセッションに登録 コントローラ内のupdateアクションで、flashを作成 ここでは、sessionには保存されていない def update ... flash[:success] = "タスク「#{@task.name}」を更新しました" binding.pry ... end pry(#<TasksController>)> flash => #<ActionDispatch::Flash::FlashHash:0x00007f980022c028 @discard=#<Set: {}>, @flashes={"success"=>"タスク「変更」を更新しました"}, @now=nil> pry(#<TasksController>)> session[:flash] => nil 2. ルーティング後のコントローラ リダイレクトのタイミングで、commit_flashメソッドが実行され、sessionにflashを保存 def index binding.pry @tasks = Task.all ... end pry(#<#<Class:0x00007f95019d9228>>)> session[:flash] => {"discard"=>[], "flashes"=>{"success"=>"タスク「変更」を更新しました"}} 3. Viewでflashを表示 sessionに保存していたflash_hashの情報を取得し、画面に表示 <% binding.pry %> <% flash.each do |key, value| %> <%= content_tag(:p, value, class: "mb-4 alert alert-#{key}") %> <% end %> pry(#<#<Class:0x00007fcb0feecd20>>)> flash => #<ActionDispatch::Flash::FlashHash:0x00007fcb1232e3f0 @discard=#<Set: {"success"}>, @flashes={"success"=>"タスク「変更」を更新しました"}, @now=nil> 4. 2回目のルーティング後のコントローラ リダイレクトのタイミングで、commit_flashが実行され、sessionのflashが削除 flashの中身もすべて削除されている def show binding.pry @task = Task.find(params[:id]) end pry(#<TasksController>)> session[:flash] => nil pry(#<TasksController>)> flash => #<ActionDispatch::Flash::FlashHash:0x00007fcb194be380 @discard=#<Set: {}>, @flashes={}, @now=nil> この結果から、もちろんだが次のviewではflashメッセージは表示されていません。 flashとhtmlの使い分け flash アクションの結果をリダイレクト先で一時的に表示するときに使う html アクションの結果を現在のページで表示したい時(永続的)に使う 結果 研修でなんとなくで読んでいた箇所を今回改めて読むと以前よりも、理解できる箇所やイメージがつく箇所が増えた。正直なんとなくの理解でも、コードは動くことは動く。ただ、なんとなくの理解では、模倣でしかコードを書くことができず、応用がきかなくなりすぐに頭打ちがきそう。数年後の自分のためにも、納得して進めていきたいと思う。
- 投稿日:2021-08-29T16:06:04+09:00
Active Jobについて知る
なぜ書くのか? ユーザー登録時にslackに通知する機能を実装した際に、他の実装を真似てApplicationJobを継承した。実装自体はできたが、Active Jobの役割は非同期処理をするくらいの認識で、その仕組みについてしらないことだらけ。なので、Railsガイドに参考に分からない単語を調べたり、実際に実装してみて確かめながら理解を深めていくことにする。 Active Jobの目的 Active Jobの主要な目的は、あらゆるRailsアプリケーションにジョブ管理インフラを配置することです。これにより、Delayed JobとResqueなどのように、さまざまなジョブ実行機能のAPIの違いを気にせずにジョブフレームワーク機能やその他のgemを搭載することができるようになります。バックエンドでのキューイング作業では、操作方法以外のことを気にせずに済みます。さらに、ジョブ管理フレームワークを切り替える際にジョブを書き直さずに済みます。 railsガイドより引用 色々難しいことを書いているが、ここでは一旦Active Jobは非同期処理を実装するための機能であり、複数のgemと連携して使うこともできると理解しておく。 まずは言葉の理解から Active jobの理解の前に頻繁にでてくる単語、Job(ジョブ)とqueue(キュー)について理解する。技術ブログ ActiveJobとはを参考 jobとは ジョブは現実世界で例えるなら、買い物にいく、料理をする、片付けをするといったような一つ一つのタスクのようなもの。 queueとは キューはタスクを登録するための入れ物のようなイメージ このようにタスクを順番にqueueに登録していき、jobを順番に実行していく。このフローのことをジョブキューと呼ばれ、先に登録されたものから先に実行される。(先入れ先出し) 実装例 例えば、ユーザー登録時にメール送信とslackに通知する場合 キューを利用することで、メール送信とslack通知を待たずに、次の処理に入ることできる。またキューで登録された処理が仮に失敗しても、次の処理を行うことも可能にする。 jobとqueueについて理解できたので、下記では実際にジョブを作成しキューに登録、実行までの流れを確認していく。 簡単なJobを作成して実行して見る User modelを作成し、UserJobを作成 shell $ rails new active_job_sample $ cd active_job_sample $ bin/rails db:create $ bin/rails g model user name $ bin/rails db:migrate #自動で下記のクラスを作成する $ bin/rails generate job user class UserJob < ApplicationJob queue_as :default def perform(name="hoge") # 下記は追記した User.create!(title: name) end end performメソッドの種類について確認 上記のperformは2種類の呼び出し方がある メソッド名 概要 perform_now 同期処理: キューに入ることなく即座に実行される perform_later 非同期処理: ジョブをキューに入れ、キューが空き次第ジョブを実行する rails cでjobの動作確認したいときなどに、perform_nowを使うとよい! Active Jobはキューイングのバックエンドが必要 ? Rails自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけ。 プロセスがクラッシュしたりコンピュータをリセットしたりすると、デフォルトの非同期バックエンドの振る舞いによって主要なジョブが失われてしまう。なので、開発環境や小規模アプリケーションの場合には、ActiveJobのみでも問題なさそうだが、本番環境でアプリケーションを運用していく場合は、キューイングのバックエンドが必要になる。 キューイングライブラリ キューイングライブラリとして有名なのは、Delayed JobやSidekiq、Resqueなど。これらは、Active Jobが導入される以前から、非同期処理を行うgemとして使われていたらしい。 Active Jobはどのgemに対しても、キューイングバックエンドに接続できるアダプタがビルトインで用意されている(優秀)。また、Active Jobを使用せずに上記のgemのみを使用することも可能である。今回は上記のgemの中でも最もdefaultなSidekiqを使用してみる。 Sidekiqって? sidekiqの特長↓ 項目 DB Redis スレッド マルチスレッド リトライ処理 あり Redisとは Redisは、ネットワーク接続された永続化可能なインメモリデータベース。ジョブの情報を保存している。 スレッドとはプログラムの一連の処理のまとまりのこと。マルチスレッドのプログラムは、複数の処理を並行して実行させることが可能。 Job(Worker)の中でraiseをキャッチして、自動でリトライする。 デフォルトの設定では21日間に25回のリトライを行う。リトライは、m2秒後、4秒後、8秒後・・・というように指数関数的に間隔をあけ、行われる。 sidekiqを利用してみる Gemfile gem 'sidekiq' gem 'sinatra', require: false # ダッシュボードを利用するため bundle install application.rb module ActiveJobSample class Application < Rails::Application ... config.active_job.queue_adapter = :sidekiq ... end end config/initializers/sidekiq.rb Sidekiq.configure_server do |config| config.Redis = { url: 'Redis://localhost:6379'} end Sidekiq.configure_client do |config| config.Redis = { url: 'Redis://localhost:6379'} end sidekiq.yml :verbose: false # 対話モード :concurrency: 10 # worker process 数 :queues: # 処理するキュー名 - default 大きなプロジェクトではqueueを複数に分けて、queueごとにsidekiqのプロセス(インスタンス)を分けるが、今回は動作確認程度なのでdefaultにする。 $ bundle exec sidekiq -C config/sidekiq.yml http://localhost:3000/sidekiqにアクセスしてダッシュボードを確認 これでようやく、sidekiqのキューにジョブを登録する準備が整った コンソールからジョブを実行してみる $ rails c irb(main):001:0> UserJob.perform_later Enqueued UserJob (Job ID: 02002483-9077-4832-963b-1058ef6534fe) to Sidekiq(default) => #<UserJob:0x00007ffa08f36f38 @arguments=[], @job_id="02002483-9077-4832-963b-1058ef6534fe", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="26bcc5664da36768f743b851"> 完了が1になった!いい感じにsidekiqが機能し、非同期で実装できていそう! この他にも、setメソッドを使いジョブの実行をスケジュールすることも可能 内部実装を見ていく Activejobかキューイングライブラリsidekiqと連携するまで module ActiveJob # :nodoc: ... class Base include Enqueuing ... end ... end ActiveJob::BaseはActiveJob::Enqueuingをincludeしており、ActiveJob::Enqueuingにクラスメソッドとしてperform_laterメソッドが定義されている module ActiveJob module Enqueuing extend ActiveSupport::Concern ... module ClassMethods def perform_later(...) job = job_or_instantiate(...) enqueue_result = job.enqueue yield job if block_given? enqueue_result end private def job_or_instantiate(*args) # :doc: args.first.is_a?(self) ? args.first : new(*args) end ... end ... def enqueue(options = {}) self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait] self.scheduled_at = options[:wait_until].to_f if options[:wait_until] self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue] self.priority = options[:priority].to_i if options[:priority] self.successfully_enqueued = false run_callbacks :enqueue do if scheduled_at queue_adapter.enqueue_at self, scheduled_at else queue_adapter.enqueue self end self.successfully_enqueued = true rescue EnqueueError => e self.enqueue_error = e end ... end end perform_laterの引数に何も指定しなければ定義したActiveJobがインスタンス化され、enqueueメソッドが呼び出される。enqueueメソッドで、ジョブがscheduledされていないか?やどのキューに登録するかを確認する。queue_adapterはconfig.active_job.queue_adapterで指定した名前から生成されるアダプタクラスになる。つまり、今回では:sidekiq を指定しているので、下記のqueue_adapterメソッドを呼びActiveJob::QueueAdapters::SidekiqAdapterが生成されることになる。 def queue_adapter=(name_or_adapter) case name_or_adapter when Symbol, String queue_adapter = ActiveJob::QueueAdapters.lookup(name_or_adapter).new assign_adapter(name_or_adapter.to_s, queue_adapter) else if queue_adapter?(name_or_adapter) adapter_name = "#{name_or_adapter.class.name.demodulize.remove('Adapter').underscore}" assign_adapter(adapter_name, name_or_adapter) else raise ArgumentError end end end その後、ActiveJob::QueueAdapters::SidekiqAdapter内で、ジョブのクラス名や引数を Sidekiq::Clientに投げている。 def enqueue(job) # :nodoc: # Sidekiq::Client does not support symbols as keys job.provider_job_id = Sidekiq::Client.push \ "class" => JobWrapper, "wrapped" => job.class, "queue" => job.queue_name, "args" => [ job.serialize ] end Sidekiqによるエンキューとデキュー コードが多くなりすぎるので、テキストのみ掲載 Redisのキューにジョブの情報を登録 Sidekiq.client.push処理でsidekiq内の実装で、受け取ったジョブ情報をJsonに直し、ジョブIDの情報を追加して、 Redisのqueues:#{queue_name}キーに追加 Redisのキューから登録したジョブを実行する bundle exec sidekiq を実行し、Sidekiq::Managerがworker作成。その後、workerはそれぞれのスレッドを起動 Sidekiq::Processorがメインの処理で、queueから取り出したjobを実行する process_oneメソッド内で、Redisからjobの情報をdequeueする processメソッドで、取得したjobから各種情報を引き出しロードし、dispatchメソッド内でjobのインスタンスを作成して、jobの処理を実行している。 感想 社内のソースコードを真似すればいい感じに実装されるといったことが多々あり、あたかも自分が簡単に複雑な機能を実装したと勘違いしてしまいやすい。今回のslack通知を実装した際も、なんか非同期になってる、なんかRedisにジョブの情報を保存してるといった曖昧な理解しかできなかった。なんとなくでその場を乗り切ったとしても、自分の実力は上がらないし知識も増えない。だから、今後も日々の業務で理解しきれなかったことを今回のようにミニマムで実際に動かしてみたり、コードリーディングを通して仕組みを理解するようにする。 参考記事
- 投稿日:2021-08-29T13:59:04+09:00
開発環境のRailsアプリをDockerコンテナで起動してみた
目次 1.はじめに 2.前提 3.大まかな手順 4.dockerの設定ファイル作成 5.アプリ用コンテナとデータベース用コンテナの接続設定 6.コンテナ起動 7.まとめ はじめに 先日、Railsで学習系のWebアプリを作成しましたが、環境構築の際は、何かしらエラーが出ては原因を調べて解消の繰り返しでした。様々な知識を得る良い機会ではあったのですが、今後、開発をする際はより効率的にできたらなという思いがあります。解決手段の1つとしては「Docker」が挙げられるかと思います。次にアプリ開発をする際は初期段階からDockerを用いた開発をしたいと考えていますが、まずは今回作成した既存のアプリをDockerコンテナで起動できないか試してみましたので、記録として残します。 前提 ・開発端末にてDockerをインストール済であること ・以下の手順における各ソフトウェアバージョンは次の通り ・Ruby: 2.6.5 ・Rails: 6.0.3 ・MySQL: 5.6 ・今回は既存アプリを2種類のDockerコンテナ(アプリ用コンテナ、データベース用コンテナ)を連携させて起動する 大まかな手順 ①dockerの設定ファイル(Dockerfileとdocker-compose.yml)作成 ②アプリ用コンテナとデータベース用コンテナの接続設定 ③コンテナ起動 dockerの設定ファイル作成 まずは既存アプリのフォルダ階層にて以下、2種類のファイルを作成します。 ■ Dockerfile →Dockerイメージの素となるファイル。ここに記述された内容で、まるっと環境のイメージが生成されます。今回はアプリ用コンテナの素となるように記述していきます。 ■ docker-compose.yml →複数のコンテナを管理する際に使用するファイル。今回はアプリ用コンテナとデータベース用コンテナの定義を記述して管理します。 ファイルを作成したら各々に定義を記述していきます。 ※ 各行で何をしているかはコメントを参照 ■ Dockerfileの内容 #アプリ用コンテナの素 #以下、「memory_tank」の部分は適宜、自分のアプリ名にする #ベースとなるバージョン2.6.5のrubyイメージを公式リポジトリより取得 FROM ruby:2.6.5 #コンテナ内のパッケージ管理を最新状態にする #前提ソフトウェア(nodejs、mysql)や保守用にvimをインストール。「--no-install-recommends」オプションを付け、recommendされた不要なソフトウェアはインストールしない #イメージ軽量化のためにapt-getリストをクリア RUN apt-get update && \ apt-get install -y nodejs default-mysql-client vim --no-install-recommends && \ rm -rf /var/lib/apt/lists/* #アプリ用ディレクトリの作成 RUN mkdir /memory_tank #ワークディレクトリを設定 WORKDIR /memory_tank #ローカルのGemfileをアプリ用コンテナにコピーする ADD Gemfile /memory_tank/Gemfile ADD Gemfile.lock /memory_tank/Gemfile.lock #アプリ用コンテナにgemをインストール RUN gem install bundler RUN bundle install #ローカルのアプリファイルをまるっとアプリ用コンテナにコピー ADD . /memory_tank ■ docker-compose.ymlの内容 docker-compose.yml version: "2" services: db: #データベース用コンテナの定義 image: mysql:5.6 #ベースとなるバージョン5,6のmysqlイメージを公式リポジトリより取得 command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci #文字コードの設定 environment: MYSQL_ROOT_PASSWORD: ****** #データベースユーザ「root」のパスワード MYSQL_DATABASE: memory_tank_development #データベース名 volumes: - mysql-data:/var/lib/mysql #名前付きボリュームでデータを永続化 ※ Dockerの管理下にデータを保管 ports: - "3306:3306" #ポート設定 app: #アプリ用コンテナの定義 build: . #DockerFileを素にコンテナイメージを作成 command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" #railsの起動 volumes: - .:/memory_tank #ローカルのディレクトリをコンテナにマウント - bundle:/usr/local/bundle #bundle install後のbuildを不要にするため ports: - "3000:3000" #ポート設定 depends_on: #作成順序の設定 「db」→「app」 - db volumes: mysql-data: #名前付きボリューム bundle: #bundle install後のbuildを不要にするため アプリ用コンテナとデータベース用コンテナの接続設定 次にアプリ用コンテナとデータベース用コンテナが正常に通信できるよう、「database.yml」を編集します。 ※ socket通信でのDB接続ではなく「host:db」のように、データベース用コンテナを直接指定します。 docker-compose.yml default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password:***** host: db #データベース用コンテナを指定 コンテナ起動 ここまでで各種定義が完了したため、最後にコンテナをバックグラウンドで起動して稼働確認を行います。 xxxx@xxxxnoMacBook-Pro memory_tank % docker-compose up -d Starting memory_tank_db_1 ... done Starting memory_tank_app_1 ... done xxxx@xxxxnoMacBook-Pro memory_tank % 「localhost:3000」でアプリに接続できることを確認 まとめ 今回の作業を通し、ある程度はDockerについての基礎知識が身についてきたため、今後はwebアプリ初期段階でのDocker導入やAWS ECSへの本番デプロイに繋げていけたらと思います。 以上、最後まで読んで頂きありがとうございました!
- 投稿日:2021-08-29T08:03:41+09:00
【Ruby on Rails】Rate.jsを使ったときに星が増えるバグの解消法(jQuery)
対象者 レビュー評価を実装している方 星が増える不具合を実装している方 目的 星が増える不具合を解消して、星を5個で収める 実際の手順と実例 1.結論(解決策) scriptタグに$('').empty();を追加すればOK! 下記例です div.rateの部分は人によって違うと思います。 <script> $(document).on("turbolinks:load", function() { $('div.rate').empty(); #これを追加しました $('div.rate').raty({ size: 14, starOff: '<%= asset_path('star-off.png') %>', starOn : '<%= asset_path('star-on.png') %>', starHalf: '<%= asset_path('star-half.png') %>', half: true, readOnly: true, }); }); </script> 2.原因 星の表示されてるページから 他のページへ遷移 その後、再度星が表示されているページへ この間に星が削除されていない(保存されたまま??)のが原因みたいです。 3..emptyメソッドについて 下記記事によると jQueryオブジェクトで指定した要素の内容(子孫要素やテキストなど)を削除します。jQueryオブジェクトで指定した要素は削除しません。 とありました。 つまり、 ページ再読み込み時に まず子要素である"div.rate"を.emptyで削除 その後、.rateで星を表示 となって星5個の表示が保たれています。 参照 emptyメソッド - 初心者向けのjQuery入門講座 投稿者コメント 今回は「どのような流れでそうなったか」という点に重きを置いて記事を書いてみました。次回以降の投稿でもどのような手順でそうなったかを曖昧にさせないように理解しながら書いていきたいと思います。 星が増えるとなんか笑っちゃいますよね。笑 My Profile プログラミング学習歴3ヶ月目のアカウントです! プログラミングスクールで学んだ内容や自分が躓いた箇所等のアウトプットの為に発信しています。 また、プログラミング初学者の方にわかりやすく、簡潔にまとめて情報共有できればと考えています。 もし、投稿した記事の中に誤り等ございましたら、コメント欄でご教授いただけると幸いです。
- 投稿日:2021-08-29T00:35:01+09:00
例外処理について基本
概要 例外処理についての基本的なことについてまとめです 実務において例外処理行わないことはないので、基礎的な部分を記載しておきます。 基礎構文 begin # エラーが起こりうる処理 rescue # エラーが起きた時にどのような挙動をしてほしいか記述 end エラーを補足したい場合は上記のように begin ~ rescue の間に処理を挟む。 エラーが起きると rescue ~ end の間に処理が移る。 ちなみにメソッド単位なら begin は省略可能 rescue は複数書ける begin # エラーが起こりうる処理 rescue NomethodError puts 'NomethodError' rescue RuntimeError puts 'RuntimeError' rescue StandardError puts 'StandardError' end 特定のエラー起こしたいぜ! つ raise begin raise RuntimeError rescue NomethodError puts 'NomethodError' rescue RuntimeError puts 'RuntimeError' # => ここに処理が飛ぶ rescue StandardError puts 'StandardError' end raise で特定のエラーを引き起こすことができる ここでは raise RuntimeError とすることで rescue RuntimeError に処理が飛ぶ 特定のエラーを作りたいぜ! つ エラーを継承 class FugaError < StandardError; end begin raise FugaError rescue FugaError puts "FugaError" end # 出力 # FugaError StandardError を継承して特定のエラーを作り出すことができる rescue の処理の順番に注意すること class FugaError < StandardError; end class HogeError < FugaError; end begin raise HogeError rescue FugaError puts "FugaError" rescue HogeError puts "HogeError" end # 出力 # FugaError エラーが参照されるのは親から。 StandardError > FugaError > HogeError の順で継承している。 FugaError が HogeError より上に記載されており、かつ HogeError は FugaError の子クラスなので先に参照されてしまう。 HogeError 出したければ逆にすること class FugaError < StandardError; end class HogeError < FugaError; end begin raise HogeError rescue HogeError puts "HogeError" rescue FugaError puts "FugaError" end # 出力 # HogeError ちなみに raise のみ記載しておくと StandardError として補足される begin raise rescue => e p e.class #=> StandardClass end 番外編:例外処理の定義の仕方色々 #パターン1 通常 # begin # raise StopIteration # rescue KeyError => e # p 'きーえらー' # rescue StopIteration => e # p 'すとっぷいてれーしょん' # end # パターン2 横につなげる # begin # raise NoMethodError # rescue KeyError, StopIteration, NoMethodError # p 'きーえらーとストップイテレーションとのーめそっどえらー' # rescue StopIteration => e # p 'すとっぷいてれーしょん' # end # パターン3 スプラッシュ演算子 begin raise NoMethodError rescue *[KeyError, StopIteration, NoMethodError] p 'きーえらーとストップイテレーションとのーめそっどえらー' rescue StopIteration => e p 'すとっぷいてれーしょん' end
- 投稿日:2021-08-29T00:09:31+09:00
[Rails]Mysql2::Error:Unknown column(フォロー機能)
はじめに 本記事では、フォロー機能導入中に起きたエラーの 私が行った対処法を記述します。 昨日もフォロー機能を実装しており、ようやく終わりといった感じです。 前回の記事になります。 エラー内容 ActiveRecord::StatementInvalid in ~ Mysql2::Error:Unknown column 'relationships.user_id' in 'where clause' というエラーになりました。 clause=句 という意味のようです。(Google先生) 内容としては、'relationships.user_id'というカラムは知らんぜ。 と言っています。 ?? コード routes.rb Rails.application.routes.draw do devise_for :users root to: 'foods#index' resources :foods do collection do get :search end resource :likes, only: [:create, :destroy] end resources :users do resources :relationships, only: [:create, :destroy] get :followings, on: :member get :followers, on: :member end end フォロー機能なので、Userモデル user.rb class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :foods, dependent: :destroy has_many :likes, dependent: :destroy has_many :liked_foods, through: :likes, source: :food has_many :relationships has_many :followings, through: :relationships, source: :follower has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: :follower_id has_many :followers, through: :reverse_of_relationships, source: :following has_one_attached :image def already_liked?(food) self.likes.exists?(food_id: food) end def is_followed_by?(user) reverse_of_relationships.find_by(following_id: user.id).present? end end 該当箇所のみ <div class="mypage-follow-info"> <h2 class="mypage-follow">フォロー</h2> <%= link_to @user.followings.count, followings_user_path(@user) %> <h2 class="mypage-follow">フォロワー</h2> <%= link_to @user.followers.count, followers_user_path(@user) %> </div> 原因究明 よくわからないので、とりあえず、怒られているところを丸々消してみました。 コメントアウトしてみた <div class="mypage-follow-info"> <h2 class="mypage-follow">フォロー</h2> <%#= link_to @user.followings.count, followings_user_path(@user) %> <h2 class="mypage-follow">フォロワー</h2> <%= link_to @user.followers.count, followers_user_path(@user) %> </div> すると、ブラウザがコメントアウトしたところ抜いて無事に表示されました。 <%= link_to @user.followers.count, followers_user_path(@user) %> ここは表示されたので、 原因は <%= link_to @user.followings.count, followings_user_path(@user) %> であることが判明。 形は、followersでは問題なかったので、 モデルかコントローラーではないかと探しました。 結果 モデルでした。 user.rb class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :foods, dependent: :destroy has_many :likes, dependent: :destroy has_many :liked_foods, through: :likes, source: :food has_many :relationships ←ここ!! has_many :followings, through: :relationships, source: :follower has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: :follower_id has_many :followers, through: :reverse_of_relationships, source: :following has_one_attached :image def already_liked?(food) self.likes.exists?(food_id: food) end def is_followed_by?(user) reverse_of_relationships.find_by(following_id: user.id).present? end end user.rb class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :foods, dependent: :destroy has_many :likes, dependent: :destroy has_many :liked_foods, through: :likes, source: :food has_many :relationships, foreign_key: :following_id has_many :followings, through: :relationships, source: :follower has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: :follower_id has_many :followers, through: :reverse_of_relationships, source: :following has_one_attached :image def already_liked?(food) self.likes.exists?(food_id: food) end def is_followed_by?(user) reverse_of_relationships.find_by(following_id: user.id).present? end end has_many :relationships foreign_key: :following_idが記述できていませんでした。 外部キー (foreign key) 【SQL入門】外部キーとは?主キーとの関係や作成方法について解説 外部キー(FOREIGN KEY)とは、 関連したテーブル間を結ぶために設定する列のことで、データの整合性をデータベースに保証させるために利用します。 設定し忘れていたので、followingが宙に浮いていた感じですね。 終わりに オリジナルアプリにDockerを取り扱いたいと思ったため、 Dockerの学習を始めました。 ちょっと難しいく頭が混乱しますが、 仮想という言葉が好きらしく、興味は沸いてます。 YouTubeでこの方と一緒に同じことをしていました。 よければ見てみてください。 Docker超入門講座 合併版 | ゼロから実践する4時間のフルコース 今回の参考記事です。 Mysql2::Error: Unknown column 'favorites.true' in 'where clause'の対処について 明日は日曜日ですが、 引き続きDockerの学習も頑張ります! どこかのタイミングでDockerの記事も書きたい!