- 投稿日:2020-10-25T23:59:44+09:00
意外と簡単にできた。パスワードを入力させずにユーザー情報を更新する方法
概要
今回の記事は表題の通り「パスワードを入力させずにユーザー情報を更新する方法」です。
Railsでとあるアプリケーションを作成中に、どうにかしてパスワードを入力させずにユーザー情報を更新できないものかと考えました。
updateメソッドではパスワードの入力を必須としているようです。
で、よく考えてみたらSNSやECサイトなどの多くのWEBアプリケーションは、ユーザー名やプロフィールなどをパスワードを入力せずとも更新可能です。
updateメソッドをオーバーライドとかしなくても、意外と簡単に出来るのでは...と思い調べてみました。実際にやったこと
update_without_passwordという、そのまんまな名前をしているメソッドがありました。
これを用いて今回は下記のように実装しました。
単純にパラメータに含まれるパスワードが空かどうかで、使用するメソッドを振り分けています。# パラメータに含まれるパスワードが空の場合、 if params[:password].blank? # パスワードなしでユーザー情報を更新 @user.update_without_password(user_params) # パラメータにパスワードが含まれていた場合 else @user.update(user_params)注意点
モデルに以下のような記載があると、update_without_passwordメソッドをつかった際にエラーになります。
パスワードの入力を必須にする、という意味のバリデーションですね!app/models/user.rb
validates :password, presence: trueonオプションでバリデーションをかけるアクションを指定しましょう。
以下のようにすれば、ユーザーを作成する際のみバリデーションがかかります。validates :password, presence: true, on: :create
- 投稿日:2020-10-25T23:26:48+09:00
[Rails]Modelの関連付け(アソシエーション)
Railsのモデルの(アソシエーション)の種類の忘備録です
参考資料:Railsガイド
https://railsguides.jp/association_basics.html関連付け(アソシエーション)を行う理由
2つのActive Recordモデルの繋がりを関連付け(アソシエーション)という。
以下、アソシエーションは関連付けでと統一して記述していく。
関連付けをする理由は
・モデル間の共通操作を可能とし、コードの記述がシンプルで簡単になる
・上記により、コードの見通しが良くなる
為である。
※主キー、外部キーの詳細は省く。Example
簡単なタスク管理アプリを例にして、ユーザー(User)とタスク(Task)の関連付けを書いていく。(Railsガイドの沿って記述していく)
class User < ApplicationRecord end class Task < ApplicationRecord endユーザーは新しいタスクを追加する場合とユーザーを削除する場合を関連付け無しで記述すると以下のような実装となると思われる。
#新しいタスクの追加 @task = Task.create(task: "買い物", user_id: @user.id) #ユーザーの削除(この場合、削除されるユーザーのタスクも一緒に全て削除しなければ、いつまでもDBに意味のないデータが残されてしまう) @tasks = Task.where(user_id: @user.id) @tasks.each do |task| task.destroy end @user.destroyRailsのモデルに明示的に関連付けを追加することで、より簡潔にコードを記述することが可能となる。
まず、モデルの関連付けを定義する。class User < ApplicationRecord #ユーザーは複数のタスクを持つよ, ユーザーが削除されたら、tasksも全て削除してねと定義 has_many :tasks, dependent: :destroy end class Task < ApplicationRecord #タスクは1人のユーザーから生み出されるよ belong_to :user endこれで関連付けは完了である。
has_manyとbelong_toを、かなりざっくりと解説するとUserから見ると、Task_a,Task_b,Task_cと複数タスクを持てるけど、
Taskから見ると、Userは1人しかいない
1(User)対多(Task)の関係だとRailsに定義した。ちなみに、dependent: :destroyオプションは、
ユーザーが削除されたら、そのユーザーのタスクも漏れなく全て削除してねという意味である。上記のように関連付けを行ったことにより、
新しいタスクの追加とユーザーの削除は下記のよう簡潔に記述できるようになった。#新しいタスクの追加 @task = @user.tasks.create(task: "買い物") #ユーザーの削除(dependent: :destroyオプションでユーザーのタスクも一緒に削除される。) @user.destroy特に削除の部分は5行が1行で済む。
コードを見ても、すぐに何をしているのか理解が可能で見通しも良くなった。関連付けの種類
belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many説明
belong_to
1対1の関連付けが設定される。
宣言を行ったモデルのすべてのインスタンスは、他方のモデルのインスタンスに「従属(belongs to)」する。
Exampleの章を例にすると1つのTaskに対して1人のユーザーを割り当てる関係を表現している。
belongs_to関連付けで指定するモデル名は必ず「単数形」にしなければならない。#ユーザーは1人しかいない為、単数形出なければならない。 #Railsの自動推測でエラーとなる。 class Task < ApplicationRecord belong_to :user endhas_one
1対1の関連付けが設定される。belong_toとの違いは、
宣言が行われているモデルのインスタンスが、他方のモデルのインスタンスを「まるごと含んでいる」または「所有している」ことを示す。
国民健康保険の保険証を例にとる(分かりづらい(笑)?)1人の人が1つの保険証を所有している。 class people < ApplicationRecord has_one :insurance_card end 1人の人に保険証は所有されている。 class insurance_card < ApplicationRecord belong_to :people endhas_many
「1対多」のつながりがあることを示す。
has_many関連付けが使われている場合、そのモデルのインスタンスは、反対側のモデルの「0個以上の」インスタンスを所有する。
Exampleの章を例にすると1人のユーザーが複数のタスクを持っている関係を表現できる。
has_many関連付けを宣言する場合、相手のモデル名は「複数形」にする必要がある。#ユーザー1人に対して、タスクは複数所有できる為、複数形にて記述しなければならない。 #Railsの自動推測でエラーとなる。 class User < ApplicationRecord has_many :tasks endhas_many :through
「多対多」のつながりを設定する場合によく使われる。
この関連付けは、2つのモデルの間に「第3のモデル(中間モデル)」を作成する。それによって、相手モデルの「0個以上」のインスタンスとマッチする。
複数の授業と複数の生徒から、特定の授業に出る生徒を限定することが可能となる。生徒は、複数の授業を受けている。 class Student < ApplicationRecord has_many :members has_many :class_works, through: :members end 複数の授業と、複数の生徒のidを保存することで、特定の授業に出てる生徒を限定することが出来る。 class Member < ApplicationRecord belongs_to :student belongs_to :class_work end 授業は複数の生徒が受けている。 class Class_work < ApplicationRecord has_many :members has_many :students, through: :members endhas_one :through
「1対1」のつながりを設定する。
この関連付けは、2つのモデルの間に「第3のモデル(中間モデル)」を作成する。
それにより、相手モデルの1つのインスタンスとマッチする。
思いつかないのでRailsガイドをそのまま例にとる。1人の提供者(supplier)が1つのアカウントに関連付けられ、さらに1つのアカウントが1つのアカウント履歴に関連付けられる場合、supplierモデルは以下のような感じになります。
#Supplierはaccountを持ち、accountを通じてaccout_historyを持つ。 class Supplier < ApplicationRecord has_one :account has_one :account_history, through: :account end #accountはsupplierに属していて、account_historyを一つ持つ class Account < ApplicationRecord belongs_to :supplier has_one :account_history end #AccountHistoryはaccoutに属している class AccountHistory < ApplicationRecord belongs_to :account endhas_and_belongs_to_many
「多対多」のつながりを作成する。しかしthrough:を指定した場合と異なり、第3のモデル(中間モデル)がない。
完成車(assembly)と部品(part)があり、1つの完成車に多数の部品が対応し、逆に1つの部品にも多くの完成車が対応するのであれば、モデルの宣言は以下のようになります。#完成車はたくさんの部品(parts)が取り付けられて車となる。 class Assembly < ApplicationRecord has_and_belongs_to_many :parts end #部品は複数あり、複数のの完成車に取り付けられている。 class Part < ApplicationRecord has_and_belongs_to_many :assemblies end
- 投稿日:2020-10-25T23:12:14+09:00
Spree:: Taxonのカラムを解説
solidus使っていると初心者にとっては「。。。ん?」みたいな状態ですよね。
この記事を読まれていると言うことは少し全体像が掴めて、「Spree::Taxon」にはどう言ったカラムが存在するんだ?」と言うところなのかなと思います。
そこで今回は「Spree::Taxon」内のカラムについて簡潔に紹介と説明をしていきます。Solidusのバージョン
solidus 2.0.9
カラムの紹介と説明
それではここにあるカラムについて説明します。
id
id。
parent_id
例えばこのような入れ子構造になっていた場合、以下のtaxonのparent_idをそれぞれ示します。
Category ▶︎ parent_id:nil
Clothing,Shoes ▶︎ parent_id:1
T-shirts,Shirts,Socks ▶︎ parent_id:2
Boots,Sandals,Sneakers ▶︎ parent_id:3
つまり、その商品が所属する一つ上のtaxonのid。position
画像リスト中のImageの配置場所を設定する。
例として,「2」の値では,「1」の値の後で表示される。name
分類(taxon)名。
permalink
その商品のtaxon内でのurl。
taxonomy_id
所属するtaxonomyのid。
Clothing,Shoesを含めそれ以下 ▶︎ taxonomy_id:1(Category)
NIKE,Ferrari,GUUCI ▶︎ taxonomy_id:2(Brands)lft
商品の階層構造の中にある位置。
awesome_nested_set-Gem参照。rgt
商品の階層構造の中にある位置。
awesome_nested_set-Gem参照。icon_file_name
調査中。
icon_content_type
調査中。
icon_file_size
調査中。
icon_updated_at
調査中。
description
分類(taxon)紹介文。
create_at
分類(taxon)が作られた時間。
update_at
分類(taxon)が更新された時間。
meta_title
HTMLのタグ。空の場合はnameが使用される。
meta_description
SEO用で、サーチエンジン向けの説明。
meta_keywords
SEO用で、サーチエンジン向けのキーワード。
depth
そのtaxonの所属するディレクトリの深さ。
例えば
Category,Brands ▶︎ depth:0>
Clothing,Shoes,NIKE,Ferrari,GUCCI ▶︎ depth:1>駆け出しsolidus開発者にとってSpree::Taxonモデルを理解する糧になれば幸いです!
- 投稿日:2020-10-25T22:52:21+09:00
【Rails】amCharts4を用いたグラフ描画における第2縦軸の作成及び日本語化 他
はじめに
本記事では、JavaScriptのグラフ描画ライブラリのamChartを用いて、複数軸の線グラフの実装する際の第2縦軸の作成方法や日本語化などのカスタム方法を共有します。
開発環境
Ruby 2.5.1
Rails 5.2.4.4amChartsとは
amChartsは、javascriptのグラフ描画ライブラリで、様々な種類の高機能なグラフを描画することができます。
公式リファレンス:https://www.amcharts.com/docs/v4/
日本語の情報が少ないため、カスタマイズする際は、公式リファレンスを参照することをおすすめします。
完成イメージ
現在、体重と体脂肪率を記録してグラフ化する機能を持つアプリを開発中で、
1つのグラフに横軸を日付、第1縦軸に体重、第2縦軸に体脂肪率を描画するため、amCharts4を使用しました。
- 横軸:日付、第1縦軸:体重、第2縦軸:体脂肪率
- カーソル上のデータをTooltipで表示
- スクロールバーで拡大
- タイトルをマウスホバーするとTooltipを表示
実装手順/解説
amChartsの導入
amChartsの導入方法はこちらの記事が参考になります。
amcharts 4 Demos を使ってグラフを作成横軸の値が不連続な場合のグラフの作成
本記事の横軸の値が不連続になる場合のグラフの作成は、下記の記事を参考にさせていただきました。
Railsにて不連続な間隔(日付など)で投稿された値をamChartsを使って折れ線グラフを作成する。デモデータの準備
csvファイルをseedして以下のようなデモデータを準備します。
id date weight body_fat_percentage 1 2020/06/08 72 15 2 ・・・ ・・・ ・・・ 完成サンプルコード
解説の前にサンプルコードを貼っておきます。
Rails側の記述は今回割愛します。
record.html.erb
<style> #chartdiv { width: 700px; height: 300px; } </style> //必要なJSファイルの読み込み <script src="https://www.amcharts.com/lib/4/core.js"></script> <script src="https://www.amcharts.com/lib/4/charts.js"></script> <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script> <script src="//www.amcharts.com/lib/4/lang/ja_JP.js"></script> <script> am4core.ready(function() { am4core.useTheme(am4themes_animated); var chart = am4core.create("chartdiv", am4charts.XYChart); chart.dateFormatter.language = new am4core.Language(); chart.dateFormatter.language.locale = am4lang_ja_JP; chart.language.locale["_date_day"] = "MMMdd日"; chart.language.locale["_date_year"] = "yyyy年"; const weights = <%== JSON.dump(@weights) %>; const body_fat_percentages = <%== JSON.dump(@body_fat_percentages) %>; const dates = <%== JSON.dump(@dates) %>; var firstDate = new Date(dates[0]) var lastDate = new Date(dates.slice(-1)[0]) var termDate = (lastDate - firstDate) / 1000 / 60 / 60 / 24 + 1 function generateChartData() { var chartData = []; for (var j = 0; j < weights.length; j++ ) { for (var i = 0; i < termDate; i++ ) { var newDate = new Date(firstDate) newDate.setDate(newDate.getDate() + i); if ((new Date(dates[j])) - (newDate) == 0 ){ weight = weights[j] body_fat_percentage = body_fat_percentages[j] chartData.push({ date1: newDate, weight: weight, date2: newDate, body_fat_percentage: body_fat_percentage }); } } } return chartData; } chart.data = generateChartData(); //グラフタイトルの設定 var title = chart.titles.create(); title.text = "体重・体脂肪率の推移"; //グラフタイトルの設定 title.fontSize = 15; //グラフタイトルのフォントサイズの設定 //タイトルをマウスホバーした際に表示させるTooltipの表示内容設定 title.tooltipText = "スクロールバーで拡大できます。"; //第1横軸の設定 var dateAxis = chart.xAxes.push(new am4charts.DateAxis()); dateAxis.renderer.grid.template.location = 0; dateAxis.renderer.labels.template.fill = am4core.color("#ffffff"); //第2横軸の設定 var dateAxis2 = chart.xAxes.push(new am4charts.DateAxis()); dateAxis2.tooltip.disabled = true; //Tooltipの非表示設定 dateAxis2.renderer.grid.template.location = 0; dateAxis2.renderer.labels.template.fill = am4core.color("#000000"); //第1縦軸の設定 var valueAxis = chart.yAxes.push(new am4charts.ValueAxis()); valueAxis.tooltip.disabled = true; valueAxis.renderer.labels.template.fill = am4core.color("#e59165"); valueAxis.renderer.minWidth = 60; valueAxis.renderer.labels.template.adapter.add("text", function(text) { return text + "kg"; }); valueAxis.renderer.fontWeight = "bold"; //軸の値を太字に変更 //第2縦軸の設定 var valueAxis2 = chart.yAxes.push(new am4charts.ValueAxis()); valueAxis2.tooltip.disabled = true; valueAxis2.renderer.grid.template.strokeDasharray = "2,3"; valueAxis2.renderer.labels.template.fill = am4core.color("#dfcc64"); valueAxis2.renderer.minWidth = 60; valueAxis2.renderer.labels.template.adapter.add("text", function(text) { return text + "%"; }); valueAxis2.renderer.opposite = true; //第2縦軸を右側に設定 valueAxis2.renderer.fontWeight = "bold"; //軸の値を太字に変更 //第1縦軸用の値の設定 var series = chart.series.push(new am4charts.LineSeries()); series.name = "体重"; series.dataFields.dateX = "date1"; series.dataFields.valueY = "weight"; series.tooltipText = "{valueY.value}kg"; series.fill = am4core.color("#e59165"); series.stroke = am4core.color("#e59165"); series.smoothing = "monotoneX"; series.strokeWidth = 2; //系列のポイントの設定(第1縦軸) var bullet = series.bullets.push(new am4charts.Bullet()); var circle = bullet.createChild(am4core.Circle); circle.width = 5; circle.height = 5; circle.horizontalCenter = "middle"; circle.verticalCenter = "middle"; //第1縦軸用の値の設定 var series2 = chart.series.push(new am4charts.LineSeries()); series2.name = "体脂肪率"; series2.dataFields.dateX = "date2"; series2.dataFields.valueY = "body_fat_percentage"; series2.yAxis = valueAxis2; series2.xAxis = dateAxis2; series2.tooltipText = "{valueY.value}%"; //ツールチップの表示設定 series2.fill = am4core.color("#dfcc64"); //ツールチップの色 series2.stroke = am4core.color("#dfcc64"); //グラフの線の色 series2.smoothing = "monotoneX"; series2.strokeWidth = 2; //系列のポイントの設定(第2縦軸) var bullet2 = series2.bullets.push(new am4charts.Bullet()); var circle2 = bullet2.createChild(am4core.Circle); circle2.width = 5; circle2.height = 5; circle2.horizontalCenter = "middle"; circle2.verticalCenter = "middle"; chart.scrollbarX = new am4core.Scrollbar(); //スクロールバーの設定 //カーソルの設定 chart.cursor = new am4charts.XYCursor(); chart.cursor.xAxis = dateAxis2; //凡例の設定 chart.legend = new am4charts.Legend(); chart.legend.parent = chart.plotContainer; chart.legend.zIndex = 100; chart.legend.position = "top"; chart.legend.contentAlign = "right"; //グリッド線の設定 valueAxis2.renderer.grid.template.strokeOpacity = 0.07; dateAxis2.renderer.grid.template.strokeOpacity = 0.07; dateAxis.renderer.grid.template.strokeOpacity = 0.07; valueAxis.renderer.grid.template.strokeOpacity = 0.07; }); </script> <div id="chartdiv"></div>解説
複数縦軸の設定
サンプルコードの通り、複数軸のグラフの場合、各軸及び各値の設定が必要になります。
それぞれの使用するデータなどの設定を行います。
- 第1横軸:dateAxis
- 第2横軸:dateAxis2
- 第1縦軸:valueAxis
- 第2縦軸:valueAxis2
- 体重:series
- 体脂肪率:series2
日本語化
横軸が日付の場合、デフォルトの表記が米国式のため下図のように英語表記になります。
そのままでも問題は無いのですが、もし「◯月◯日」という表記にしたい場合は、以下の設定を追加します。
標準の翻訳設定では、例えば「Aug」を「8月」に翻訳はできますが、何日の方は「〇〇日」とは翻訳されないため、独自ルールを以下のように追加します。年も同様に行えます。<script src="//www.amcharts.com/lib/4/lang/ja_JP.js"></script> //localeファイルの呼び出し <script> //中略 chart.dateFormatter.language = new am4core.Language(); //標準の翻訳設定 chart.dateFormatter.language.locale = am4lang_ja_JP; //標準の翻訳設定 chart.language.locale["_date_day"] = "MMMdd日"; 独自ルールで上書き chart.language.locale["_date_year"] = "yyyy年"; 独自ルールで上書き //中略 </script>【参考リンク】
https://www.amcharts.com/docs/v4/concepts/locales/
https://github.com/amcharts/amcharts4/blob/master/src/lang/ja_JP.ts第2縦軸の設定
デフォルトの設定ですと第1縦軸と第2縦軸は両方左側にあります。
少し見にくいので、第2縦軸を右側に変更したい時は、以下の設定を追加します。valueAxis2.renderer.opposite = true; //第2縦軸を右側に設定他の追加設定を紹介
- データポイントの設定
各系列毎にデータポイントの設定が行えます。circleをsquareに変えると四角に変更できます。
var bullet = series.bullets.push(new am4charts.Bullet()); var circle = bullet.createChild(am4core.Circle); circle.width = 5; circle.height = 5; circle.horizontalCenter = "middle"; circle.verticalCenter = "middle";【参考リンク】
https://www.amcharts.com/docs/v4/concepts/bullets/
- グラフの線を曲線に変更
series.smoothing = "monotoneX";
- 色の変更
series2.fill = am4core.color("#dfcc64"); //ツールチップの色 series2.stroke = am4core.color("#dfcc64"); //グラフの線の色まとめ
amChartsを使うと高機能なグラフを描画できます。
折れ線グラフ以外にも様々なグラフを作ることができます。
日本語の情報が少ないので、カスタマイズしたい場合は、公式リファレンスを参照することをおすすめします。参考URL
amcharts 4 Demos を使ってグラフを作成
Railsにて不連続な間隔(日付など)で投稿された値をamChartsを使って折れ線グラフを作成する。
- 投稿日:2020-10-25T21:45:09+09:00
[Rails]例外処理を書く方法とは?
例外処理とは、、?
例外処理とは、エラーがでたときの処理を行うことです。
エラー原因の特定やシステム側で不具合を起こさないためにも例外処理を書けば便利です。例外処理の書き方
begin,rescueの基本形
エラーの対象となりそうな箇所をbeginで囲い、エラーが発生した場合の処理をrescueの中に書く。
begin 100 / 0 rescue p "0で割れません" end puts "おはようございます"ワンライナーで書く
ワンライナーで書くことも可能です。
sample_1 = 10 / 0 rescue 0 sample_2 = 10 / nil rescue 0 puts sample_1 #=>0 puts sample_2 #=>0エラー内容を取得する(e)
また、rescueの後に引数を指定してあげて、変数の中にエラー内容を格納できます。
begin 10 / 0 rescue => e puts e #=> divided by 0 endエラーごとに条件分岐する
他にも、rescueの後にエラーメッセージを指定することで、
どのエラーが出た時にどの処理をするか,というのを条件分岐ができます。begin 10 / 0 rescue NoMethodError puts "メソッドはありません" rescue ZeroDivisionError puts "0で割れません" endraiseの使い方
パラメータが想定されたものではない時や、不正なアクセスがきたといった場合に、明示的にエラーを発生させ、処理を中断させたいときに使います。
begin raise NoMethodError # 発生させたい例外のクラス rescue => e puts e endエラーメッセージを出力することも可能です。
begin raise RuntimeError, "実行時エラーです" rescue => e puts e endretryの使い方
エラーが起きても、再度beginに戻って実行することができます。
num = 0 begin puts 10 / num rescue ZeroDivisionError => e puts e num = 1 retry #beginブロックを再度実行 end puts "終了しました"ensureの使い方
例外が発生してもしなくても実行される処理を書くことができる。
begin puts "例外なし" rescue => e puts e ensure puts "ここは絶対実行する!" end
- 投稿日:2020-10-25T21:21:52+09:00
[linux]プロセスを終了するkillコマンド
killコマンドとは?
指定したプロセスIDのプロセスを終了させるコマンド。
$ kill プロセス番号(PID)使い方
1.以下のコマンドを実行してプロセスIDを調べる。
$ ps # 現在起動しているプロセスを確認する $ ps ax | grep gedit # プロセスに”gedit”の名称がついたプロセスのみを表示 16619 ? Sl 0:01 gedit2.killコマンドでプロセスIDを指定してプログラムを終了する。
$ kill 166193.強制終了したい場合は以下のコマンドを実行する。
$ kill -9 16619主なオプション
コマンド 概要 -s シグナル 指定したシグナル名またはシグナル番号を送信する -シグナル 指定したシグナル名またはシグナル番号を送信する -l [] シグナル名とシグナル番号の対応を表示する
- 投稿日:2020-10-25T21:17:17+09:00
画像が表示出来ない時の意外な落とし穴
作業内容
投稿された画像を表示したい
画像以外は、表示されるという状況でエラーに悩んでました
構文エラー
ActionView::SyntaxErrorInTemplate in PrototypesController#index
エラー箇所
エラー部のみ記載<%= image_tag prototype.image, if prototype.image.attached? %>最初はもう少し記述がありましたが、わすれました。
参考にした記述が
<%= image_tag message.image, class: 'message-image' if message.image.attached? %>違いはクラスが入っているという所です。
クラス抜いたバージョンで記述したのですが、エラーがでました。
なぜ、エラーがでたかわかりますか?
スペルが違うわけでも、ありません。
解決方法
どこが違うかわかりますか?
<%= image_tag prototype.image if prototype.image.attached? %>エラー文の原因は単純な事が多いので、解決した時はなーんだという感じですが
悩んでいる時は、正直つらいですよね。
でも、乗り越えたら成長できるので!
頑張っていきましょう。
「,」がいらなかったんですね。
If文の時は使わない、クラスを使う時に必要だったんですね。「、」は盲点でした。
必要だと思っていたので、必要だと思ったやつでも外してみたりすることも大切だと学ばせていただきました。
ありがとうございます!
- 投稿日:2020-10-25T21:03:26+09:00
問題解決するには2つの記事を参考にすると解決できる!!!
どうも、三町哲平です!!
プロミングをしているととにかく分からない事が出まくりますよね!?
初めて行う実装やエラー解決の為にあれやこれやすると思いますが、そういう時は、本で調べたり、人に聞いたりすることがあるでしょうが、やっぱり一番するのって、ググって調べる事じゃないでしょうか...?少なくとも私は9割以上の問題に対し、ググって解決しています。
しかし...しかしですよ。
例えばRuby on Railsで出た問題に対してあれやこれや調べたとしても開発環境が違ったり、データベースが違ったり、バージョンが違ったり、アプリ名が違ったりと全く一緒の条件でアプリ開発している可能性なんて限りなく0です。必ずどこかに違いがあります。
そんな微妙な違いによってただコピペしただけだは、中々実装できなかったり、エラー解決できなかったりして最終的には嫌になって止めてしまう...。
そうならない為にも問題に対峙する時にこれに気を付ければ上手く解決できるかもね...というやり方を一つ見つけたので、そのご紹介です。
問題解決するには2つの記事を参考にすると解決できる!!!
タイトル通りなのですが、答えは、
「問題解決するには2つの記事を参考にすると解決できる!!!」です。
もうこれが今回の結論なのですが、これだけじゃ分かりにくいので参考例をどうぞ!1. やり方が分からない...。
まず、ググって調べるに当たって直面している壁といったらとにかくやり方が分からないって所です。
- 何で、このエラーは出たのだろう?
- どうやってこの機能を実装すればいいのだろう? など、など...
その疑問に対して調べまくった結果、訳わからんって状態になる中で今回は、ja.ymlの書き方が分からないという問題の解決方法を模索していました。
状態としては、投稿フォームで、紹介文というテキスト欄が空欄だった場合に表示するエラーメッセージを全て日本語表示にしたい。
つまり、Contentを入力してくださいのContentを日本語にしたいという話です。
ちなみに、Contetは保存したいデータベース(postテーブル)のカラム名になります。
2. ググって行き着いた記事
ググると沢山似たような記事が検索に引っかかる事があります。その中で自分自身にとって分かりやすく、状況が似ている記事を参考にしていく中で私は、
Railsのバリデーションエラーのメッセージの日本語化 - Qiita
この記事を参考にさせて頂きました。
参考にした結果↑この様にja.ymlを作成して、入力しました。これは、Railsのバリデーションエラーのメッセージの日本語化 - Qiita
のカラム名の日本語化のコードをコピペしただけです。この結果が実は先ほどお見せした投稿フォームの画像になります。↓再掲しています。
まあ...つまりは、コードをコピペしただけでは、今回のカラムを日本語化したいという問題は、解決しなかった訳ですね。
3. ja.ymlの書き方が分からないからまたググってみる
ja.ymlの何処かが間違っている...何が違うんだ...!?
そんな疑問の中、エラーメッセージの日本語化を再度調べていく中で、
ActiveRecordのvalidatesで表示されるエラーメッセージのフォーマットを変更する - Qiita
こちらの記事を発見!!そして、ja.ymlで、
config/local/models/ja.ymlja: activerecord: models: user: ユーザー attributes: user: name: 名前↑上記のコードを発見!
config/local/models/ja.ymlja: activerecord: models: event: イベント attributes: event: name: イベント名 place: 開催場所 content: イベント内容↑これが現在使用しているコードです。
ここで、タイトルにもどります。
問題解決するには2つの記事を参考にすると解決できる!!!
に戻ります。2つのコードを見ている中で、これって、
config/local/models/ja.ymlja: activerecord: models: user: ユーザー attributes: user: name: 名前↑このコードだと、user:が、
config/local/models/ja.ymlja: activerecord: models: event: イベント attributes: event: name: イベント名 place: 開催場所 content: イベント内容↑このコードだと、event:の部分が、テーブル名だと気づいた私は...
[Before]
↓
[After]
この様に変更して...無事、エラーメッセージを全て日本語化できました!!
結果
繰り返しになりますが、
「問題解決するには2つの記事を参考にすると解決できる!!!」はこれにて完了です。...ご参考までに。
- 投稿日:2020-10-25T21:02:16+09:00
mergeメソッドについて改めて理解を深めた
はじめに
formオブジェクトを用いて、フォームから複数のテーブルに情報を保存する機能を実装する過程で、いちばん悩んだエラーについて、忘れないために記録しておく。
想定している場面
ユーザーが商品を購入する。formに入力したものをデータベースに保存すると同時に、どの商品をどのユーザーが購入したかも保存する。つまり、フォームで「購入」を押した時に、2つのテーブルに保存される。
mergeメソッドについて
ストロングパラメーターを設定するときに、使うメソッド。
使用例
controllers.rbprivate def user_order_params params.require(:user_order).permit(:postal_code, :prefecture_id).merge(user_id: current_user.id, item_id: params[:item_id]) end
user_id: current_user.id
のcurrent_userメソッドが使えるのは、deviseのGemを導入しているため。
item_id: params[:item_id]
で値を入れることができるのは、ルーティングをネストし、URLにitem_id
を含めているため。
ストロングパラメーターはprivateメソッド以下に記述する。
require
の引数は、モデル名。
permit
の引数は、DBのカラム名。ターミナルで確認できるパラメーター
"user_order"=>{"hoge"=>"", "postal_code"=>"", "prefecture_id"=>"1"}, "commit"=>"購入", "controller"=>"orders", "action"=>"create", "item_id"=>"7"}mergeメソッドを使う場面
form_withでユーザーが記入した内容はハッシュの中に、キーと一緒に入っているが、ユーザーが記入しない内容も保存したい時。
例えば、ユーザーのidやその商品のidについては、ユーザーが直接入力することはないが、パラメーターに含めて、DBの保存したい。そのような時に、mergeメソッドを使って、パラメーターに含めたいキーと値を記述する。
上記の例では、user_idをcrrent_user.idから、item_idをURLに含めたparamsから取ってきて、パラメーターに含めている。最後に
エラーが解決できた時、マージかぁと一人呟いたとさ…。
- 投稿日:2020-10-25T20:58:08+09:00
画像プレビュー機能の実装
railsでの画像プレビュー機能の実装
環境
Rails 5.2.4.4前提
userテーブルにimage_idカラム手順
①image_tagにidを与える
JavaScriptで処理するためにidを与える。
ここではid: "img_prev"
とする。<%= attachment_image_tag @user, :image, size: "300x300", fallback: "no_profile.jpg", size: "300x300" , id: "img_prev"%>②同ページの下部にJavaScriptの記述をする
画像ファイルフィールドの値が変化したときに、
image_tagで画像ファイルのURLを読み込み、画像を表示をする。<script> $(function(){ $('#user_image').on('change', function (e) { #idからの情報取得 var reader = new FileReader(); #既存の画像urlの取得 reader.onload = function (e) { $(".image").attr('src', e.target.result); } #ここまでが画像取得のため reader.readAsDataURL(e.target.files[0]); #取得したurlにアップロード画像のurlを挿入 }); }); </script>以上で画像プレビュー機能の実装ができた。
- 投稿日:2020-10-25T20:57:10+09:00
「API開発 + Swagger UIを利用したAPI検証」な環境をDockerで構築する
Swaggerを利用することでREST APIの仕様をドキュメント化できます。
SwaggerではREST APIの仕様をドキュメントしたファイルをSwagger Specと呼びます。Swagger UIとはSwagger Specの情報を反映させた静的ページを生成するツールのことを言います。
Swagger UIはSwagger Specの情報を可視化するだけでなく、画面上からREST APIを実行する機能も提供しています。今回は「開発中のAPIをSwagger Specでドキュメント化 → Swagger Specの情報が反映されたSwagger UIの画面からAPIリクエストの検証」という一連の作業が行えるDocker環境の構築手順について紹介します。
今回作成するDocker環境について
仕様は以下の通りです。
- docker-compose upだけで環境が準備できる
- 「/swagger-ui」でSwagger UIの画面が表示される
- 「/swagger-ui」以外はAPI用のエンドポイントとする
- Swagger Spec編集後、リロードでSwagger UIに変更内容が反映される
- APIはRuby on RailsのAPIモードで作成する
- DBはMySQLを利用
nginxをリバースプロキシとして利用することでAPIの開発環境とSwagger UIを組み合わせます。
図で表現すると以下のようになります。今回の利用する各種バージョンは以下の通りです。
- Ruby on Rails: 6.0.3.2
- Ruby: 2.7.1
- MySQL: 8.0.21
- nginx: 1.19.3
API開発環境をDockerに作成する
APIモードで作成する『Rails 6 x MySQL 8』Docker環境構築手順を参考に、RailsのAPIモードを利用してAPI開発環境を作成します。
Dockerfileとdocker-compose.ymlは以下の通りです。
DockerfileFROM ruby:2.7.1 # 作業ディレクトリを/rails_api_swaggerに指定 WORKDIR /rails_api_swagger # ローカルのGemfileをDokcerにコピー COPY Gemfile* /rails_api_swagger/ # /rails_api_swaggerディレクトリ上でbundle install RUN bundle installdocker-compose.ymlversion: '3' services: api: # Ruby on Railsが起動するコンテナ build: . ports: - '3000:3000' # localhostの3000ポートでアクセスできるようにする volumes: - .:/rails_api_swagger # アプリケーションファイルの同期 depends_on: - db command: ["./wait-for-it.sh", "db:3306", "--", "./start.sh"] db: # MySQLが起動するコンテナ image: mysql:8.0.21 volumes: - mysql_data:/var/lib/mysql # データの永続化 - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d command: --default-authentication-plugin=mysql_native_password # 認証方式を8系以前のものにする。 environment: MYSQL_USER: 'webuser' MYSQL_PASSWORD: 'webpass' MYSQL_ROOT_PASSWORD: 'pass' MYSQL_DATABASE: 'rails_api_swagger_development' volumes: mysql_data: # データボリュームの登録RailsのAPIモードでは静的ページを生成する機能は除外されているため、Swagger UIを直接Railsアプリケーションに組み込むことはできませんが、今回の方法を利用すればAPIモードでもSwagger UIを利用できます。
サンプルとなるAPIを作成します。
# コンテナをバックグランドで起動 $ docker-compose up -d # Eventを操作する機能(モデル、ビュー、コントローラー)を一括作成 $ docker-compose exec api rails g scaffold event title:string # eventsテーブルを作成 $ docker-compose exec api rails db:migrate # rails consoleでeventsのレコードを作成 $ docker-compose exec api rails c > event = Event.new(title: 'サンプルイベント') > event.save
localhost:3000/events
にアクセスして以下のようなレスポンスが返ってくればOKです。リバースプロキシの設定を行い、nginx経由でAPIにアクセスできるようにする
nginx経由でRailsアプリケーションにアクセスできるようリバースプロキシの設定を行ます。
default.confserver { listen 80; server_name localhost; # "/"にアクセスがあったときの処理 location / { proxy_set_header Host localhost; # アクセス元のホストをlocalhostにする proxy_pass http://api:3000; # apiコンテナの3000ポートにリクエストを送る } }nginxの設定ファイルは
/etc/nginx/nginx.conf
です。
設定ファイルにinclude /etc/nginx/conf.d/*.conf;
という記述があることからも分かるように、設定ファイルでは/etc/nginx/conf.d
配下の.conf
という拡張子の設定も読み込んでいます。つまり、
/etc/nginx/conf.d
配下に今回作成した設定ファイルを配置することで、nginxコンテナをリバースプロキシとして利用できます。docker-compose.ymlにnginxコンテナを追加します。
docker-compose.ymlservices: api: (略) db: (略) nginx: image: nginx:1.19.3 ports: - '80:80' command: [nginx-debug, '-g', 'daemon off;'] volumes: - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf depends_on: - api volumes: mysql_data:
[nginx-debug, '-g', 'daemon off;']
はデバッグモードによる起動方法です。1なお、swagger-uiのDockerイメージもnginxを利用していますが、
nginx.conf
にinclude /etc/nginx/conf.d/*.conf;
の記述がありません。
ですので、swagger-uiのDockerイメージを利用する場合だと今回のアプローチはうまくいかないので注意してください。コンテナ起動後、
localhost:80/events
にアクセスして以下のようなレスポンスが返ってくればOKです。Swagger UIをnginxに組み込む
/swagger-ui
にアクセスをしたらSwagger UIの画面が表示されるようにnginxにSwagger UIを組み込んでいきます。Swagger UIの画面はswagger-ui/distによって構成されています。
dist
配下のファイルをローカルにコピーし、nginxコンテナにバインドマウントすることで、Swagger UIをnginxに組み込みます。
dist
配下のファイルをすべてコピーしてきてもよいのですが、unpkgを利用することでindex.html
のみをコピーするだけでSwagger UIの画面が作成できます。 2index.html<!-- HTML for static distribution bundle build --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Swagger UI</title> - <link rel="stylesheet" type="text/css" href="./swagger-ui.css" > + <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css" > <style> html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; } *, *:before, *:after { box-sizing: inherit; } body { margin:0; background: #fafafa; } </style> </head> <body> <div id="swagger-ui"></div> - <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script> + <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script> - <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script> + <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-standalone-preset.js" charset="UTF-8"> </script> <script> window.onload = function() { // Begin Swagger UI call region const ui = SwaggerUIBundle({ url: "https://petstore.swagger.io/v2/swagger.json", dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }) // End Swagger UI call region window.ui = ui } </script> </body> </html>docker-compose.ymlを修正し、作成した
index.html
をnginxのデフォルトの公開ディレクトリである/usr/share/nginx/html
配下に配置します。docker-compose.ymlservices:![スクリーンショット 2020-10-25 18.23.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140792/22cf7096-e237-7c2e-3aa5-0959d4776657.png) api: (略) db: (略) nginx: image: nginx:1.19.3 ports: - '80:80' command: [nginx-debug, '-g', 'daemon off;'] volumes: - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf + - ./nginx/html/swagger-ui:/usr/share/nginx/html/swagger-ui depends_on: - api volumes: mysql_data:
/swagger-ui
にアクセスしたらindex.html
が表示されるようnginxの設定を追記します。default.confserver { listen 80; server_name localhost; location / { proxy_set_header Host localhost; proxy_pass http://api:3000; } # "swagger-ui"にアクセスがあったときの処理 location /swagger-ui { alias /usr/share/nginx/html/swagger-ui; } }コンテナ起動後、
localhost:80/swagger-ui
にアクセスして以下のような画面が表示されればOKです。ローカルのSwagger SpecがSwagger UI上に反映されるようにする
Swagger UIの
url
を変更することで参照するSwagger Specを変更できます。ローカルのSwagger Specを参照するように変更します。
index.html- url: "https://petstore.swagger.io/v2/swagger.json", + url: "./api.yml",上記の変更でローカル環境に配置された
./nginx/html/swagger-ui/api.yml
の内容がSwagger UIへ反映されます。なお、
./nginx/html/swagger-ui/
ディレクトリはバインドマウントされているので、ローカルでSwagger Spec編集後、リロードすればコンテナのSwagger UIに変更内容が反映されます。サンプルとして作成した
GET /events
を実行するSwagger Specは以下の通りです。api.ymlopenapi: 3.0.2 info: title: サンプルAPI version: 1.0.0 servers: - url: http://localhost:3000 tags: - name: イベント paths: /events: get: tags: - イベント description: イベント一覧取得 responses: 200: description: 成功 content: application/json: schema: type: array description: イベントの配列 items: $ref: "#/components/schemas/Event" components: schemas: Event: type: object properties: id: description: ID type: integer format: int64 example: 1 title: description: タイトル type: string example: サンプルイベント created_at: description: 作成日 type: string format: date-time example: 2020-04-01 10:00 updated_at: description: 更新日 type: string format: date-time example: 2020-04-01 10:00コンテナ起動後、以下のような画面が表示されればOKです。
CORSの設定をする
Swagger UIは
localhost:80
、APIはlocalhost:3000
で起動しています。
この状態でSwagger UIからAPIにリクエストを送るとオリジンをまたがっているためAccess to fetch at 'http://localhost:3000/events' from origin 'http://localhost' has been blocked by CORS policy
というエラーが発生します。CORSの設定を行い、Swagger UIからAPIリクエストが送れるようにします。
今回はrack-corsを利用してCORSの設定を行ます。Gemfilegem 'rack-cors'config/initializers/cors.rbRails.application.config.middleware.insert_before 0, Rack::Cors do unless Rails.env.production? allow do origins(['localhost', /localhost:\d+\Z/]) resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end endコンテナ起動後、リクエストが正常に返ってくればOKです。
参考: CRUD操作を行うSwagger Spec
- GET /events
- POST /events
- GET /events/{id}
- PATCH /events/{id}
- DELETE /events/{id}
上記のエンドポイントに関するSwagger Specは以下の通りです。
api.ymlopenapi: 3.0.2 info: title: サンプルAPI version: 1.0.0 servers: - url: http://localhost:3000 tags: - name: イベント paths: /events: get: tags: - イベント description: イベント一覧取得 responses: 200: description: 成功 content: application/json: schema: type: array description: イベントの配列 items: $ref: "#/components/schemas/Event" post: tags: - イベント description: イベント登録 requestBody: content: application/json: schema: type: object properties: title: type: string example: サンプルイベント responses: 201: description: 作成 /events/{event_id}: get: tags: - イベント description: イベント詳細 parameters: - name: event_id in: path description: イベントID required: true schema: type: integer format: int64 example: 1 responses: 200: description: 成功 content: application/json: schema: type: object $ref: "#/components/schemas/Event" 404: description: event not found patch: tags: - イベント description: イベント更新 parameters: - name: event_id in: path description: id required: true schema: type: integer format: int64 example: 1 requestBody: content: application/json: schema: type: object properties: title: type: string example: サンプルイベント responses: 200: description: 成功 content: application/json: schema: type: object properties: activity: $ref: "#/components/schemas/Event" delete: tags: - イベント description: イベント削除 parameters: - name: event_id in: path description: id required: true schema: type: integer format: int64 example: 1 responses: 204: description: No Content components: schemas: Event: type: object properties: id: description: ID type: integer format: int64 example: 1 title: description: タイトル type: string example: サンプルイベント created_at: description: 作成日 type: string format: date-time example: 2020-04-01 10:00 updated_at: description: 更新日 type: string format: date-time example: 2020-04-01 10:00画面は以下のようになります。
まとめ
以上でAPIとSwagger UIを統合した開発環境の構築手順の紹介を終わります。
- nginxを利用することでSwagger UIとAPIを組み合わせる
- nginxのリバースプロキシ設定は『/etc/nginx/conf.d』配下に作成
- オリジンをまたがるリクエストをする際はCORSの設定が必要になる
- 自作のSwagger SpecをSwagger UIに反映させるにはindex.htmlのurlを変更する
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!
- 投稿日:2020-10-25T20:39:48+09:00
Impressionistを用いてPV数(閲覧数)取得~Rails
目標
この記事ではImpressionistというgemを用い、投稿のPV(page view)数取得とPV数ランキングを実装します。
開発環境
・Ruby: 2.6.2
・Rails: 6.0.3
・OS: windows前提
Tweetモデルにて、投稿機能一式作成済み。
実装
1.
impressionist
を導入gemfileに下記一行を追加します。
Gemfilegem 'impressionist'ターミナル$ bundle install次にPV数をカウントするテーブルを作成します。
ターミナル$ rails g impressionistターミナル$ rails db:migrate以下のようなimperssionsテーブルができます。
schema.rbcreate_table "impressions", force: :cascade do |t| t.string "impressionable_type" t.integer "impressionable_id" t.integer "user_id" t.string "controller_name" t.string "action_name" t.string "view_name" t.string "request_hash" t.string "ip_address" t.string "session_hash" t.text "message" t.text "referrer" t.text "params" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["controller_name", "action_name", "ip_address"], name: "controlleraction_ip_index" t.index ["controller_name", "action_name", "request_hash"], name: "controlleraction_request_index" t.index ["controller_name", "action_name", "session_hash"], name: "controlleraction_session_index" t.index ["impressionable_type", "impressionable_id", "ip_address"], name: "poly_ip_index" t.index ["impressionable_type", "impressionable_id", "params"], name: "poly_params_request_index" t.index ["impressionable_type", "impressionable_id", "request_hash"], name: "poly_request_index" t.index ["impressionable_type", "impressionable_id", "session_hash"], name: "poly_session_index" t.index ["impressionable_type", "message", "impressionable_id"], name: "impressionable_type_message_index" t.index ["user_id"], name: "index_impressions_on_user_id" end2.Tweetsテーブルにカウント数のカラムを追加
ターミナル$ rails g migration AddImpressionsCountToTweets impressions_count:integer以下のmigrationファイルに
default: 0
を追記します。~_add_impressions_count_to_tweets.rbclass AddImpressionsCountToTweets < ActiveRecord::Migration[6.0] def change # 「default: 0」を追記 add_column :users, :impressions_count, :integer, default: 0 end endターミナル$ rails db:migrate3.モデルを編集
tweet.rb# 追記 is_impressionable counter_cache: true
is_impressionable
➡︎ Tweetモデルでimpressionist
を使用できるようにします。
counter_cache: true
➡︎ impressions_countカラムがupdateされるようにします。4.コントローラーを編集
tweets_controller.rbdef index @tweets = Tweet.all @rank_tweets = Tweet.order(impressions_count: 'DESC') # ソート機能を追加 end def show @tweet = User.find(params[:id]) impressionist(@tweet, nil, unique: [:ip_address]) # 追記 end
Tweet.order(impressions_count: 'DESC')
➡︎ tweet一覧をPV数の多い順に並び替える。
impressionist(@user, nil, unique: [:ip_address])
➡︎ tweet詳細ページにアクセスするとPV数が1つ増える。※自主的にPV数を伸ばす事が出来ないように、今回はip_addressにてPV数をカウントします。
※rails sしてlocalhostで試す場合、ip_addressはデフォルトの::1が入るため、PV数は1を超えません。しかしデプロイ後はきちんと動作することが確認できていますので、ご安心ください。
5.ビューを編集
app/tweets/index.html.erb<h3> 投稿一覧 </h3> <% @tweets.each do |t| %> <%= t.~ %> # PV数 <%= t.impressions_count %> <% end %> <h3> PV数ランキング </h3> <% @rank_tweets.each do |t| %> <%= t.~ %> # PV数 <%= t.impressions_count %> <% end %>
- 投稿日:2020-10-25T17:35:31+09:00
【並び替え】あるユーザーのフォロー、フォロワーをフォローした、フォローされた順(降順)に並び替える!
概要
あるユーザーのフォロー、フォロワーをフォローした、フォローされた順(降順)に並び替えた時のことを備忘録として記録します。
環境
・ruby '2.5.7'
・rails '5.2.3'前提
・ユーザーのフォロー機能は実装済であること
【参考】
第14章 ユーザーをフォローする - Railsチュートリアル過程
1.実装することの確認
「あるユーザー(
@user
)のフォロー、フォロワーを(@users
)フォローした、フォローされた順(降順)に並び替える(order("relationships.created_at DESC")
)」これを具体的にコードにしていきます!
2.following,followersアクションを定義する
users_controllerにfollowing,followersアクションを定義していきます。
controllers/users_controller.rbclass UsersController < ApplicationController (省略) def following @title = "フォロー" @user = User.find(params[:id]) get_follower_user_ids = Relationship.where(follower_id: @user.id).pluck(:followed_id) @users = User.includes(:passive_relationships).where(id: get_follower_user_ids).order("relationships.created_at DESC").paginate(page: params[:page]) render 'show_follow' end def followers @title = "フォロワー" @user = User.find(params[:id]) get_followed_user_ids = Relationship.where(followed_id: @user.id).pluck(:follower_id) @users = User.includes(:active_relationships).where(id: get_followed_user_ids).order("relationships.created_at DESC").paginate(page: params[:page]) render 'show_follow' end (省略) endコードを順番に説明していきます!(followingアクションのみ説明します)
①
@user = User.find(params[:id])
で表示されているユーザーを@user
に代入します。②
get_follower_user_ids = Relationship.where(follower_id: @user.id).pluck(:followed_id)
で@user
にフォローされているユーザーのidをget_follower_user_ids
に代入します。③
@users = User.includes(:passive_relationships).where(id: get_follower_user_ids).order("relationships.created_at DESC").paginate(page: params[:page])
で@user
にフォローされているユーザーを@users
に代入します。ここで、
includes(:passive_relationships)
とすることで、Relationshipモデルを参照できるようになり、order("relationships.created_at DESC")
で並び替えることができます。結果
これで、あるユーザーのフォロー、フォロワーをフォローした、フォローされた順(降順)に並び替えることができました!
- 投稿日:2020-10-25T17:30:25+09:00
【Rails】パフォーマンス改善にすぐに役立つTips集
はじめに
Railsにおけるパフォーマンス改善に役立つTipsを集めてみました。
すぐに使えるものから、少し改善に時間がかかるものまで幅広く集めています。開発中のアプリのパフォーマンス改善のお役に立てれば幸いです。
【Tips1】 N+1を改善する
何はともあれN+1が発生していたら、それを解消するようにしましょう。
大抵の場合、そこがアプリのパフォーマンスのボトルネックになっているはずです。books_controller.rbclass BooksController < ApplicationController def index @book = Book.all end endindex.html.erb<% @book.each do |book| <%= book.title %> <%= book.user.name %> # ここでN+1が起きている <% end %>上のコード例だとbookの関連先のuserを読み込むところでN+1が起きています。
下記のようにコントローラーを書き換えましょう。
books_controller.rbclass BooksController < ApplicationController def index @book = Book.includes(:user) end end
モデル.all
で全てのモデルを取得してきている場所はN+1が起きている可能性が高くなり、大抵allは使わなくなることが多いです。
N+1を検知するためにgemのbullet
をアプリへ導入するのもおすすめです。https://github.com/flyerhzm/bullet
また、上の例だと
includes
を使用していますが、preload
とeager_load
を使い分けられるようになるといいですね。ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
【Tips2】 countではなくてsizeを使用する
countを使用するとSQLを発行してしまいます。
そのため、モデルの数などを調べたい時はsize
などで代用しましょう。意外とやってしまいがちなケースですが、
count
をsize
へ置き換えるだけなので、手軽にできます。countを使用した場合
user = User.all user.count # count関数を用いたSELECT文が発行されてしまう (4.6ms) SELECT COUNT(*) FROM `users` => 100sizeを使用した場合
user = User.all user.size # SQLクエリは発行されない => 100【Tips3】exist?の使用を控える
count
と同じくexist?
はモデルオブジェクトに使用すると、sqlを発行してしまいます。
存在するかを確認したい場合などはpresent?
などで代用しましょう。exsit?を使用した場合
user = User.where(deleted: true) user.exist? # SQLが発行される User Load (5.4ms) SELECT `users`.* FROM `users` WHERE `users`.`deleted` = TRUE => []present?を使用した場合
user = User.where(deleted: true) user.present? # SQLは発行されない => []【Tips4】allで取得した結果をeachで回さない
全ユーザーを
all
で取得してそれをeachで回す・・・のようなパターンです。
バッチ処理とかにありがちなパターンですね。User.all.each do |user| # 何かuserのオブジェクトを使用して処理をするコード endN+1のTipsでも触れましたが、にallが処理に入ってきたときは一度そのコードを疑ってかかりましょう。
上述の実装だとUserの全件をメモリに展開してから、ひとつひとつの処理をeachで行っていくため、メモリ消費が激しくなります。
allのかわりにfind_eachを使いましょう。
User.find_each do |user| # 何かuserのオブジェクトを使用して処理をするコード endfind_eachはレコードを1000件取得ずつ取得し、その取得したレコードを1件ずつ処理してきてくれます。
1000件取得し終わったらまた、次の1000件を取得してきて・・・の繰り返しとなります。
ちなみにレコード展開数を指定したかったらその姉妹メソッドであるfind_in_batchesを使用しましょう。メソッドの引数で、レコード数を指定できます。
【Tips5】不必要なActive Recordオブジェクトの生成
上述のN+1問題や、eachパターンほどではないですが、これも気をつけないとやってしまいがちな実装となります。
user_names = User.all.map(&:name)上述のmapを使用したやり方は不必要なActive Recordオブジェクトを生成してしまい、パフォーマンスがあまりよくありません。
Active Recordオブジェクトは膨大な数のモジュールやメソッドをラップしているので、生成コストが高く、それだけでメモリを費やしてしまいます。
Active Recordオブジェクトを生成しなくてもよいやり方があるならば、そちらのやり方を考えましょう。
user_names = User.pluck(:name)pluckを使用することで、不要なオブジェクト生成を回避できました。
【Tips6】 キャッシュや非同期処理の導入を検討する
パフォーマンス悪化箇所にはキャッシュを使ったり、処理を非同期にする方法もあります。
キャッシュを使う
Redisなどの導入を検討する。マスター系のデータ読み込みなどに検討してみるといいかも。
ただ、導入箇所をよく検討しないとバグの原因になりがちです。処理を非同期にする
gemのsidekiq
やdelayed job
などで重い処理を非同期にしてしまう。
メール送信処理を非同期にする方法をよく見かけます。終わりに
いかがだったでしょうか。
以上、Railsでコーディングする上で意識するだけで簡単にパフォーマンス改善ができるTipsを紹介しました。他にもこんなやり方あるよだったり、ここが間違っているよなどありましたらコメント欄でそっとお知らせください笑
- 投稿日:2020-10-25T16:03:30+09:00
[Rails]Google Maps APIによるGoogle Mapの表示と複数地点間のルート検索
はじめに
参考になる記事がとても少なく、ポートフォリオ作成で一番苦戦した部分なので、自分の学習のためアウトプットとして残す、とともにだれかの役に立てればいいなと思ったので書きました!
初学者なりにこの記述の意味はなんだ?と思う部分はしっかり説明したつもりです。
当たり前だろ!と思う部分も多々あると思いますがご了承ください。目標
Google Maps APIでGoogle Mapを表示させるとともに、マーカーの吹き出しから任意にルート検索リストに追加でき、複数地点のルートを検索する機能を目標とします。
開発環境
・Ruby: 2.5.1
・Rails: 5.2.1
・OS: macOS前提
・Slimの導入
・公式のGoogle Maps Platformで以下のAPIの有効化
・Maps JavaScript API
→ GoogleMapの表示
・Geocoding API
→ 住所から緯度経度の算出
・Directions API
→ ルート検索設定
1. 必要なgemをインストール
Gemfilegem 'dotenv-rails' # APIキーを環境変数化 gem 'gon' # コントローラーで定義したインスタンス変数をJavaScript内で使用出来るようにする。 gem 'geocoder' # 住所から緯度経度を算出する。ターミナル$ bundle install2. APIキーを環境変数化
アプリケーション直下に「.env」ファイルを作成
ターミナル$ touch .env自身のAPIキーを' 'の中に記述
.envGOOGLE_MAP_API = '自身のコピーしたAPIキー'.gitignore/.env3. turbolinksの無効化
Gemfilegem 'turbolinks' # この行を削除app/assets/javascripts/application.js//= require turbolinks // この行を削除
data-turbolinks-track':'reload'属性の削除
app/views/layouts/application.html.slim= stylesheet_link_tag 'application', media: 'all' = javascript_include_tag 'application'4. Geocoding APIを使用できるようにする
geocorderの設定ファイルを作成し、編集
ターミナル$ touch config/initializers/geocoder.rbconfig/initializers/geocoder.rb# 追記 Geocoder.configure( lookup: :google, api_key: ENV['GOOGLE_MAP_API'] )これで設定は終了です。ここからGoogleMapを表示していく実装に入ります。
GoogleMapの表示
1. 追加したいモデルにカラムを追加
自分のアプリの場合はPlaceモデルにaddressカラムを追加します。
latitude, longitudeカラムはGeocoding APIによってaddressカラムの値から算出された経度・緯度の値です。小数の値なので型はfloatを使います。ターミナル$ rails g migration AddColumnsToPlaces address:string latitude:float longitude:floatターミナル$ rails db:migrate2. モデルを編集
models/place.rb# 追記 geocoded_by :address # addressカラムを基準に緯度経度を算出する。 after_validation :geocode # 住所変更時に緯度経度も変更する。3. コントローラーを編集
controllers/places_controller.rbdef index @place = Place.all gon.place = @place # 追記 end private def place_params # ストロングパラメーターに「address」を追加 params.require(:place).permit(:name, :description, :image, :address) end4. ビューを編集
①application.html.slimを編集
CSSとJavaScriptより先に、gonを読み込むよう記述します。views/layouts/application.html.slimdoctype html html head title | app_name = csrf_meta_tags = csp_meta_tag = include_gon # 追記 = stylesheet_link_tag 'application', media: 'all' = javascript_include_tag 'application'②新規登録画面に住所入力フォームを追加
views/places/new.html.slim= f.label :address, '住所' = f.text_field :address, class: 'form-control'③GoogleMapを表示するファイルに記述
views/places/index.html.slimdiv id = 'map_index' # idを付与, この部分にjsファイルで記述したGoogle Mapが埋め込まれる - google_api = "https://maps.googleapis.com/maps/api/js?key=#{ ENV['GOOGLE_MAP_API'] }&callback=initMap".html_safe script{ async src = google_api } .map-route < ルート検索リスト > ul id = "route-list" class = "list-group" # jsファイルで吹き出しの追加ボタンによってその場所がli要素に追加される div id = 'directions-panel' # 距離・時間が埋め込まれる < 各地点間の距離・時間 > ul id = "display-list" class = "display-group" .map-search = button_tag "ルート検索", id: "btn-search", class: "btn btn-primary", onclick: "search()" # クリック処理でsearch()関数を呼び出す[ google_api = 〜〜〜〜の部分について ]
→ callback処理で読み込み時にinitMap関数を呼び出す。
→ .html_safeはエスケープ処理
→ async属性によって非同期でJavaScriptを読み込みレンダリングを早くする。④GoogleMapで表示したいサイズをscssに記述
stylesheets/application.scss#map_index{ height: 400px; width: 400px; }5. JavaScriptのファイルを編集
ここが肝です。
assets/javascripts直下に新たなファイルを作成し、記述します。
だいぶ長く見にくいかと思いますが、変数を定義したのち、関数の定義をそれぞれ行っているだけです。
関数は、
・initMap
・markerEvent( i )
・addPlace(name, lat, lng, number)
・search()
の順で4つがあります。
わかりにくい部分やポイントは、コメントアウトで説明していますので参考にしてください。assets/javascripts/googlemap.jsvar map var geocoder var marker = []; var infoWindow = []; var markerData = gon.places; // コントローラーで定義したインスタンス変数を変数に代入 var place_name = []; var place_lat = []; var place_lng = []; // GoogleMapを表示する関数(callback処理で呼び出される) function initMap(){ geocoder = new google.maps.Geocoder() // ビューのid='map_index'の部分にGoogleMapを埋め込む map = new google.maps.Map(document.getElementById('map_index'), { center: { lat: 35.6585, lng: 139.7486 }, // 東京タワーを中心 zoom: 9, }); // 繰り返し処理でマーカーと吹き出しを複数表示させる for (var i = 0; i < markerData.length; i++) { // 各地点の緯度経度を算出 markerLatLng = new google.maps.LatLng({ lat: markerData[i]['latitude'], lng: markerData[i]['longitude'] }); // マーカーの表示 marker[i] = new google.maps.Marker({ position: markerLatLng, map: map }); // 吹き出しの表示 let id = markerData[i]['id'] place_name[i]= markerData[i]['name']; place_lat[i]= markerData[i]['latitude']; place_lng[i]= markerData[i]['longitude']; infoWindow[i] = new google.maps.InfoWindow({ // 吹き出しの中身, 引数で各属性の配列と配列番号を渡す content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">` }); markerEvent(i); } } } // マーカーをクリックしたら吹き出しを表示 function markerEvent(i) { marker[i].addListener('click', function () { infoWindow[i].open(map, marker[i]); }); } // リストに追加する function addPlace(name, lat, lng, number){ var li = $('<li>', { text: name[number], "class": "list-group-item" }); li.attr("data-lat", lat[number]); // data-latという属性にlat[number]を入れる li.attr("data-lng", lng[number]); // data-lngという属性にlng[number]を入れる $('#route-list').append(li); // idがroute-listの要素の一番後ろにliを追加 } // ルートを検索する function search() { var points = $('#route-list li'); // 2地点以上のとき if (points.length >= 2){ var origin; // 開始地点 var destination; // 終了地点 var waypoints = []; // 経由地点 // origin, destination, waypointsを設定する for (var i = 0; i < points.length; i++) { points[i] = new google.maps.LatLng($(points[i]).attr("data-lat"), $(points[i]).attr("data-lng")); if (i == 0){ origin = points[i]; } else if (i == points.length-1){ destination = points[i]; } else { waypoints.push({ location: points[i], stopover: true }); } } // リクエストの作成 var request = { origin: origin, destination: destination, waypoints: waypoints, travelMode: google.maps.TravelMode.DRIVING }; // ルートサービスのリクエスト new google.maps.DirectionsService().route(request, function(response, status) { if (status == google.maps.DirectionsStatus.OK) { new google.maps.DirectionsRenderer({ map: map, suppressMarkers : true, polylineOptions: { // 描画される線についての設定 strokeColor: '#00ffdd', strokeOpacity: 1, strokeWeight: 5 } }).setDirections(response);//ライン描画部分 // 距離、時間を表示する var data = response.routes[0].legs; for (var i = 0; i < data.length; i++) { // 距離 var li = $('<li>', { text: data[i].distance.text, "class": "display-group-item" }); $('#display-list').append(li); // 時間 var li = $('<li>', { text: data[i].duration.text, "class": "display-group-item" }); $('#display-list').append(li); } const route = response.routes[0]; // ビューのid='directions-panel'の部分に埋め込む const summaryPanel = document.getElementById("directions-panel"); summaryPanel.innerHTML = ""; // 各地点間の距離・時間を表示 for (let i = 0; i < route.legs.length; i++) { const routeSegment = i + 1; summaryPanel.innerHTML += "<b>Route Segment: " + routeSegment + "</b><br>"; summaryPanel.innerHTML += route.legs[i].start_address + "<br>" + " ↓ " + "<br>"; summaryPanel.innerHTML += route.legs[i].end_address + "<br>"; summaryPanel.innerHTML += "<" + route.legs[i].distance.text + ","; summaryPanel.innerHTML += route.legs[i].duration.text + ">" + "<br>"; } } }); } }吹き出しの内容のcontent部分の補足:(データの受け渡しの方法で苦戦したので)
content: `<a href='/places/${ id }'>${ markerData[i]['name'] }</a><input type="button" value="追加" onclick="addPlace(place_name, place_lat, place_lng, ${i})">`addPlace(place_name, place_lat, place_lng, ${i})
この関数の呼び出しでは前の3つの引数は配列として渡しています。4つ目の引数は配列の中でどの情報かを表すための番号(インデックスと呼びます。)を式展開したものです。JavaScriptでの式展開はこの形だそうです。
このような引数を用意することで、関数addPlace(name, lat, lng, number)は正常にどのデータであるかという情報を処理できるのです。最後に
最後まで読んでくださり、ありがとうございます。
自分自身、現在ポートフォリオが完成に近づき就職活動を本格的に始め出したような状態です!
目標を持ってポートフォリオ作成、転職活動など行っている方を心から応援しています、共に頑張りましょう!!参考
- 投稿日:2020-10-25T14:59:24+09:00
Railsチュートリアル第4版:第2章 Toyアプリケーション
Railsチュートリアル第4版:第2章 Toyアプリケーション
2.1 アプリケーションの計画
Terminal-> % rails new toy_app create create README.md create Rakefile create .ruby-version create config.ru create .gitignore create Gemfile run git init from "." Initialized empty Git repository in /Users/**********/environment_2/toy_app/.git/ create package.json create app create app/assets/config/manifest.js create app/assets/stylesheets/application.css create app/channels/application_cable/channel.rb create app/channels/application_cable/connection.rb create app/controllers/application_controller.rb create app/helpers/application_helper.rb create app/javascript/channels/consumer.js create app/javascript/channels/index.js create app/javascript/packs/application.js create app/jobs/application_job.rb create app/mailers/application_mailer.rb create app/models/application_record.rb create app/views/layouts/application.html.erb create app/views/layouts/mailer.html.erb create app/views/layouts/mailer.text.erb create app/assets/images create app/assets/images/.keep create app/controllers/concerns/.keep create app/models/concerns/.keep create bin create bin/rails create bin/rake create bin/setup create bin/yarn create config create config/routes.rb create config/application.rb create config/environment.rb create config/cable.yml ・ ・ ・ ├─ retry@0.12.0 ├─ select-hose@2.0.0 ├─ selfsigned@1.10.8 ├─ serve-index@1.9.1 ├─ serve-static@1.14.1 ├─ sockjs-client@1.4.0 ├─ sockjs@0.3.20 ├─ spdy-transport@3.0.0 ├─ spdy@4.0.2 ├─ strip-eof@1.0.0 ├─ thunky@1.1.0 ├─ type-is@1.6.18 ├─ unpipe@1.0.0 ├─ utils-merge@1.0.1 ├─ wbuf@1.7.3 ├─ webpack-dev-middleware@3.7.2 ├─ webpack-dev-server@3.11.0 └─ ws@6.2.1 ✨ Done in 10.52s. Webpacker successfully installed ? ?Gemfilesource 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.3', '>= 6.0.3.4' # Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.4' # Use Puma as the app server gem 'puma', '~> 4.1' # Use SCSS for stylesheets gem 'sass-rails', '>= 6' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '~> 4.0' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.7' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Active Storage variant # gem 'image_processing', '~> 1.2' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '~> 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]Terminal-> % bundle install --without production [DEPRECATED] The `--without` flag is deprecated because it relies on being remembered across bundler invocations, which bundler will no longer do in future versions. Instead please use `bundle config set without 'production'`, and stop using this flag The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`. Using rake 13.0.1 Using concurrent-ruby 1.1.7 Using i18n 1.8.5 Using minitest 5.14.2 Using thread_safe 0.3.6 Using tzinfo 1.2.7 Using zeitwerk 2.4.0 Using activesupport 6.0.3.4 ・ ・ ・ Using webdrivers 4.4.1 Using webpacker 4.3.0 Bundle complete! 17 Gemfile dependencies, 74 gems now installed. Gems in the group production were not installed. Use `bundle info [gemname]` to see where a bundled gem is installed.Terminal-> % git init Reinitialized existing Git repository in /Users/**********/environment_2/toy_app/.git/ -> % git add -A -> % git commit -m "Initialize repository" [master (root-commit) 28e8b26] Initialize repository 93 files changed, 9253 insertions(+) create mode 100644 .browserslistrc create mode 100644 .generators create mode 100644 .gitignore create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README.md create mode 100644 Rakefile create mode 100644 app/assets/config/manifest.js create mode 100644 app/assets/images/.keep create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/channels/application_cable/channel.rb create mode 100644 app/channels/application_cable/connection.rb create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/helpers/application_helper.rb create mode 100644 app/javascript/channels/consumer.js ・ ・ ・ create mode 100644 test/models/.keep create mode 100644 test/system/.keep create mode 100644 test/test_helper.rb create mode 100644 tmp/.keep create mode 100644 tmp/pids/.keep create mode 100644 vendor/.keep create mode 100644 yarn.lock
Terminal-> % git remote add origin https://github.com/**********/toy_app.git -> % git push origin master Enumerating objects: 105, done. Counting objects: 100% (105/105), done. Delta compression using up to 4 threads Compressing objects: 100% (87/87), done. Writing objects: 100% (105/105), 149.29 KiB | 4.82 MiB/s, done. Total 105 (delta 3), reused 0 (delta 0) remote: Resolving deltas: 100% (3/3), done. To https://github.com/**********/toy_app.git * [new branch] master -> masterapp/controllers/application_controller.rbclass ApplicationController < ActionController::Base def hello render html: "hello, world!" end endconfig/routes.rbRails.application.routes.draw do # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html root 'application#hello' endTerminal-> % heroku create Creating app... done, ⬢ dry-headland-50008 https://dry-headland-50008.herokuapp.com/ | https://git.heroku.com/dry-headland-50008.git -> % git push heroku master Enumerating objects: 112, done. Counting objects: 100% (112/112), done. Delta compression using up to 4 threads Compressing objects: 100% (94/94), done. Writing objects: 100% (112/112), 149.82 KiB | 3.12 MiB/s, done. Total 112 (delta 7), reused 0 (delta 0) remote: Compressing source files... done. ・ ・ ・ remote: An error occurred while installing sqlite3 (1.4.2), and Bundler cannot continue. remote: Make sure that `gem install sqlite3 -v '1.4.2' --source 'https://rubygems.org/'` remote: succeeds before bundling. remote: remote: In Gemfile: remote: sqlite3 remote: remote: ! remote: ! Failed to install gems via Bundler. remote: ! Detected sqlite3 gem which is not supported on Heroku: remote: ! https://devcenter.heroku.com/articles/sqlite3 remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: Verifying deploy... remote: remote: ! Push rejected to dry-headland-50008. remote: To https://git.heroku.com/dry-headland-50008.git ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/dry-headland-50008.git'2.1.1 ユーザーのモデル設計
2.1.2 マイクロポストのモデル設計
2.2 Usersリソース
Terminal-> % bin/rails g scaffold User name:string email:string Running via Spring preloader in process 17885 invoke active_record create db/migrate/20201023134008_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb invoke test_unit create test/controllers/users_controller_test.rb create test/system/users_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder create app/views/users/_user.json.jbuilder invoke assets invoke scss create app/assets/stylesheets/users.scss invoke scss create app/assets/stylesheets/scaffolds.scssTerminal-> % bin/rails db:migrate == 20201023134008 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0026s == 20201023134008 CreateUsers: migrated (0.0026s) =============================Terminal-> % bin/rails s => Booting Puma => Rails 6.0.3.4 application starting in development => Run `rails server --help` for more startup options2.2.1 ユーザーページを探検する
- 演習
- 1. CSSを知っている読者へ: 新しいユーザーを作成し、ブラウザのHTMLインスペクター機能を使って「User was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?
- 2. emailを入力せず、名前だけを入力しようとした場合、どうなるでしょうか?
これは何を意図した問題だ?てっきり入力してくださいアラート出るのかの思った。
- 3. 「@example.com」のような間違ったメールアドレスを入力して更新しようとした場合、どうなるでしょうか?
- 4. 上記の演習で作成したユーザーを削除してみてください。ユーザーを削除したとき、Railsはどんなメッセージを表示するでしょうか? "Are you sure?"
2.2.2 MVCの挙動
config/routes.rbRails.application.routes.draw do resources :users root 'users#index' end
- 演習
- 1. 図 2.11を参考にしながら、/users/1/edit というURLにアクセスしたときの振る舞いについて図を書いてみてください。
省略- 2. 図示した振る舞いを見ながら、Scaffoldで生成されたコードの中でデータベースからユーザー情報を取得しているコードを探してみてください。
app/controllers/users_controller.rb
の@users = User.all
とか@user = User.find(params[:id])
かな、自信ないけど。- 3. ユーザーの情報を編集するページのファイル名は何でしょうか?
app/views/users/edit.html.erb
一旦コミットしたよ
Terminal-> % git add . -> % git commit -m "Usersリソース" ・ ・ ・ -> % git push origin master2.2.3 Usersリソースの欠点
2.3 Micropostsリソース
2.3.1 マイクロポストを探検する
Terminal-> % bin/rails g scaffold Micropost content:text user_id:integer Running via Spring preloader in process 19890 invoke active_record create db/migrate/20201024030447_create_microposts.rb create app/models/micropost.rb invoke test_unit create test/models/micropost_test.rb create test/fixtures/microposts.yml invoke resource_route route resources :microposts invoke scaffold_controller create app/controllers/microposts_controller.rb invoke erb create app/views/microposts create app/views/microposts/index.html.erb create app/views/microposts/edit.html.erb create app/views/microposts/show.html.erb create app/views/microposts/new.html.erb create app/views/microposts/_form.html.erb invoke test_unit create test/controllers/microposts_controller_test.rb create test/system/microposts_test.rb invoke helper create app/helpers/microposts_helper.rb invoke test_unit invoke jbuilder create app/views/microposts/index.json.jbuilder create app/views/microposts/show.json.jbuilder create app/views/microposts/_micropost.json.jbuilder invoke assets invoke scss create app/assets/stylesheets/microposts.scss invoke scss identical app/assets/stylesheets/scaffolds.scssTerminal-> % bin/rails db:migrate == 20201024030447 CreateMicroposts: migrating ================================= -- create_table(:microposts) -> 0.0032s == 20201024030447 CreateMicroposts: migrated (0.0033s) ========================
- 演習
- 1. CSSを知っている読者へ: 新しいマイクロポストを作成し、ブラウザのHTMLインスペクター機能を使って「Micropost was successfully created.」の箇所を調べてみてください。ブラウザをリロードすると、その箇所はどうなるでしょうか?
- 2. マイクロポストの作成画面で、ContentもUserも空にして作成しようとするどうなるでしょうか?
保存できちゃう。
- 3. 141文字以上の文字列をContentに入力した状態で、マイクロポストを作成しようとするとどうなるでしょうか? (ヒント: WikipediaのRubyの記事にある設計思想の引用文が140文字を超えているので、これをコピペしてみましょう)
- 4. 上記の演習で作成したマイクロポストを削除してみましょう。
一旦コミットしたよ
Terminal-> % git add . -> % git commit -m "Micropostsリソース" ・ ・ ・ -> % git push origin master2.3.2 マイクロポストをマイクロにする
app/models/micropost.rbclass Micropost < ApplicationRecord validates :content, length: { maximum: 140 } end
- 演習
- 1. 先ほど2.3.1.1の演習でやったように、もう一度Contentに141文字以上を入力してみましょう。どのように振る舞いが変わったでしょうか?
- 2. CSSを知っている読者へ: ブラウザのHTMLインスペクター機能を使って、表示されたエラーメッセージを調べてみてください。
一旦コミットしたよ
Terminal-> % git add . -> % git commit -m "文字数のvalidation追加" ・ ・ ・ -> % git push origin master2.3.3 ユーザーはたくさんマイクロポストを持っている
app/models/user.rbclass User < ApplicationRecord has_many :microposts # <= 追加 endapp/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user # <= 追加 validates :content, length: { maximum: 140 } end
- 演習
- 1. ユーザーのshowページを編集し、ユーザーの最初のマイクロポストを表示してみましょう。同ファイル内の他のコードから文法を推測してみてください (コラム 1.1で紹介した技術の出番です)。うまく表示できたかどうか、/users/1 にアクセスして確認してみましょう。
app/controllers/users_controller.rb# GET /users/1 # GET /users/1.json def show @first_micropost = @user.microposts.first # <= 追加 endapp/views/users/show.html.erb<p> <strong>Email:</strong> <%= @user.email %> </p> # 以下を追加 <p> <strong>Micropost:</strong> <%= @first_micropost.content %> </p> <%= link_to 'Edit', edit_user_path(@user) %> | <%= link_to 'Back', users_path %>
- 2. リスト 2.16は、マイクロポストのContentが存在しているかどうかを検証するバリデーションです。マイクロポストが空でないことを検証できているかどうか、実際に試してみましょう (図 2.16のようになっていると成功です)。
app/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user validates :content, length: { maximum: 140 }, presence: true end
- 3. リスト 2.17のFILL_INとなっている箇所を書き換えて、Userモデルのnameとemailが存在していることを検証してみてください (図 2.17)。
app/models/user.rbclass User < ApplicationRecord has_many :microposts validates :name, presence: true validates :email, presence: true end
2.3.4 継承の階層
- 演習
- 1. Applicationコントローラのファイルを開き、ApplicationControllerがActionController::Baseを継承している部分のコードを探してみてください。
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base # <= ここのはず def hello render html: "hello, world!" end end
- 2. ApplicationRecordがActiveRecord::Baseを継承しているコードはどこにあるでしょうか? 先ほどの演習を参考に、探してみてください。ヒント: コントローラと本質的には同じ仕組みなので、app/modelsディレクトリ内にあるファイルを調べてみると...?)
app/models/application_record.rbclass ApplicationRecord < ActiveRecord::Base # <= ここのはず self.abstract_class = true end2.3.5 アプリケーションをデプロイする
ビューを一部修正。
コミットTerminal-> % git add . -> % git commit -m "first_micropost部分をコメントアウト" [master 59064e0] first_micropost部分をコメントアウト 1 file changed, 4 insertions(+), 4 deletions(-) -> % git push origin master Enumerating objects: 11, done. Counting objects: 100% (11/11), done. Delta compression using up to 4 threads Compressing objects: 100% (6/6), done. Writing objects: 100% (6/6), 531 bytes | 531.00 KiB/s, done. Total 6 (delta 5), reused 0 (delta 0) remote: Resolving deltas: 100% (5/5), completed with 5 local objects. To https://github.com/**********/toy_app.git 71fcd86..59064e0 master -> masterTerminal-> % git push heroku Enumerating objects: 215, done. Counting objects: 100% (215/215), done. Delta compression using up to 4 threads Compressing objects: 100% (192/192), done. Writing objects: 100% (215/215), 161.74 KiB | 3.94 MiB/s, done. Total 215 (delta 55), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: ・ ・ ・ remote: Gem files will remain installed in remote: /tmp/build_ef1fbbcc/vendor/bundle/ruby/2.7.0/gems/sqlite3-1.4.2 for inspection. remote: Results logged to remote: /tmp/build_ef1fbbcc/vendor/bundle/ruby/2.7.0/extensions/x86_64-linux/2.7.0/sqlite3-1.4.2/gem_make.out remote: remote: An error occurred while installing sqlite3 (1.4.2), and Bundler cannot continue. remote: Make sure that `gem install sqlite3 -v '1.4.2' --source 'https://rubygems.org/'` remote: succeeds before bundling. remote: remote: In Gemfile: remote: sqlite3 remote: remote: ! remote: ! Failed to install gems via Bundler. remote: ! Detected sqlite3 gem which is not supported on Heroku: remote: ! https://devcenter.heroku.com/articles/sqlite3 remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: Verifying deploy... remote: remote: ! Push rejected to dry-headland-50008. remote: To https://git.heroku.com/dry-headland-50008.git ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/dry-headland-50008.git'gemを修正しわすれていたので、
git push heroku
できなかったので修正。ここ修正忘れててつまずく人でてくるかも。(参考)Gemfilegroup :production do gem 'pg' endhttps://github.com/fukadashigeru/toy_app/commit/1d0d5c80762438ef72c2c507b240dda9819edf2a
Terminal-> % git push heroku Enumerating objects: 215, done. Counting objects: 100% (215/215), done. Delta compression using up to 4 threads Compressing objects: 100% (192/192), done. Writing objects: 100% (215/215), 161.75 KiB | 3.76 MiB/s, done. Total 215 (delta 55), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: remote: ・ ・ ・ remote: ###### WARNING: remote: remote: There is a more recent Ruby version available for you to use: remote: remote: 2.7.2 remote: remote: The latest version will include security and bug fixes. We always recommend remote: running the latest version of your minor release. remote: remote: Please upgrade your Ruby version. remote: remote: For all available Ruby versions see: remote: https://devcenter.heroku.com/articles/ruby-support#supported-runtimes remote: remote: ###### WARNING: remote: remote: No Procfile detected, using the default web server. remote: We recommend explicitly declaring how to boot your server process via a Procfile. remote: https://devcenter.heroku.com/articles/ruby-default-web-server remote: remote: remote: -----> Discovering process types remote: Procfile declares types -> (none) remote: Default types for buildpack -> console, rake, web remote: remote: -----> Compressing... remote: Done: 77.8M remote: -----> Launching... remote: Released v6 remote: https://dry-headland-50008.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/dry-headland-50008.git * [new branch] master -> masterアクセスすると、なんかわからんけど落ちてるくさい。
Terminal-> % heroku run rails db:migrate Running rails db:migrate on ⬢ dry-headland-50008... up, run.3253 (Free) D, [2020-10-25T05:13:31.252614 #4] DEBUG -- : (13.5ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL PRIMARY KEY) D, [2020-10-25T05:13:31.264484 #4] DEBUG -- : (8.8ms) CREATE TABLE "ar_internal_metadata" ("key" character varying NOT NULL PRIMARY KEY, "value" character varying, "created_at" timestamp(6) NOT NULL, "updated_at" timestamp(6) NOT NULL) D, [2020-10-25T05:13:31.302300 #4] DEBUG -- : (1.3ms) SELECT pg_try_advisory_lock(2791976884009269180) D, [2020-10-25T05:13:31.321970 #4] DEBUG -- : (2.6ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC I, [2020-10-25T05:13:31.323672 #4] INFO -- : Migrating to CreateUsers (20201023134008) == 20201023134008 CreateUsers: migrating ====================================== -- create_table(:users) D, [2020-10-25T05:13:31.328518 #4] DEBUG -- : (1.2ms) BEGIN D, [2020-10-25T05:13:31.337948 #4] DEBUG -- : (9.1ms) CREATE TABLE "users" ("id" bigserial primary key, "name" character varying, "email" character varying, "created_at" timestamp(6) NOT NULL, "updated_at" timestamp(6) NOT NULL) -> 0.0114s == 20201023134008 CreateUsers: migrated (0.0115s) ============================= D, [2020-10-25T05:13:31.348980 #4] DEBUG -- : primary::SchemaMigration Create (1.4ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20201023134008"]] D, [2020-10-25T05:13:31.352253 #4] DEBUG -- : (2.9ms) COMMIT I, [2020-10-25T05:13:31.352397 #4] INFO -- : Migrating to CreateMicroposts (20201024030447) == 20201024030447 CreateMicroposts: migrating ================================= -- create_table(:microposts) D, [2020-10-25T05:13:31.356681 #4] DEBUG -- : (1.2ms) BEGIN D, [2020-10-25T05:13:31.365711 #4] DEBUG -- : (8.8ms) CREATE TABLE "microposts" ("id" bigserial primary key, "content" text, "user_id" integer, "created_at" timestamp(6) NOT NULL, "updated_at" timestamp(6) NOT NULL) -> 0.0123s == 20201024030447 CreateMicroposts: migrated (0.0124s) ======================== D, [2020-10-25T05:13:31.367853 #4] DEBUG -- : primary::SchemaMigration Create (1.2ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20201024030447"]] D, [2020-10-25T05:13:31.370689 #4] DEBUG -- : (2.5ms) COMMIT D, [2020-10-25T05:13:31.381377 #4] DEBUG -- : ActiveRecord::InternalMetadata Load (1.3ms) SELECT "ar_internal_metadata".* FROM "ar_internal_metadata" WHERE "ar_internal_metadata"."key" = $1 LIMIT $2 [["key", "environment"], ["LIMIT", 1]] D, [2020-10-25T05:13:31.394744 #4] DEBUG -- : (1.1ms) BEGIN D, [2020-10-25T05:13:31.396634 #4] DEBUG -- : ActiveRecord::InternalMetadata Create (1.5ms) INSERT INTO "ar_internal_metadata" ("key", "value", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "key" [["key", "environment"], ["value", "production"], ["created_at", "2020-10-25 05:13:31.392814"], ["updated_at", "2020-10-25 05:13:31.392814"]] D, [2020-10-25T05:13:31.399052 #4] DEBUG -- : (2.1ms) COMMIT D, [2020-10-25T05:13:31.400599 #4] DEBUG -- : (1.3ms) SELECT pg_advisory_unlock(2791976884009269180)https://dry-headland-50008.herokuapp.com/
- 演習
- 1. 本番環境で2〜3人のユーザーを作成してみましょう。
- 2. 本番環境で最初のユーザーのマイクロポストを作ってみましょう
https://dry-headland-50008.herokuapp.com/microposts
- 3. マイクロポストのContentに141文字以上を入力した状態で、マイクロポストを作成してみましょう。リスト 2.13で加えたバリデーションが本番環境でもうまく動くかどうか、確認してみてください。
2.4 最後に
省略
2.4.1 本章のまとめ
省略
質問ある方どうぞ
駆け出しエンジニアの方が多いかと思います。私でもお力になれることもあるかもしれないので、何か質問ありましたら聞いて下さい。
- 投稿日:2020-10-25T14:23:15+09:00
【Rails】sessionによるデータの一時保持
はじめに
sessionは「ウィザード形式」等でデータを保持させた状態で遷移先に移動する時に使用します。
目次
- セッションについて
- 実装例
- 最後に
1. セッションについて
セッションは情報を一時的に記憶しておく仕組みです。
Railsにおいてセッションは、sessionというオブジェクトにハッシュのような形でデータが格納されます。例えば、会員登録の際ページが切り変わって進んでいくようなサイトを想定します。
この場合、セッションを使い入力した情報を一旦sessionに格納して、次のページに遷移してそこで展開させる必要があります。2実装例
前提
前提としてdeviseを用いてページを遷移しながら会員情報を保存する事を想定してます。
開発環境
ruby 2.6.5
rails 6.0.0
devise 4.7.32.1 データの保持
それでは実装してみますー
registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController def new @user = User.new end def create @user = User.new(sign_up_params) unless @user.valid? render :new and return end session["devise.regist_data"] = {user: @user.attributes} @address = @user.build_address render "new_address" end他いろいろ記述してますがsessionに注目します。
session["devise.regist_data"] = {user: @user.attributes}{user: @user.attributes}はsessionにハッシュオブジェクトの形で情報を保持させたい時、attributesメソッドを用いてデータを整形しています。
つまり、attributesされた@userのデータがハッシュの状態でsessionに入ってます。
これで、session["devise.regist_data"]に入力情報が代入され、保持さてた状態になります。2.2 展開させる
次に、データを保持した状態でページを遷移して展開します。
registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController #省略 def create_address @user = User.new(session["devise.regist_data"]["user"]) @address = Address.new(useraddress_params) if @address.valid? @user.build_address(@address.attributes) @user.save session["devise.regist_data"]["user"].clear sign_in(:user, @user) else render "new_address" end endこちらもいろいろ記述してますが一番上の@userに注目。
@user = User.new(session["devise.regist_data"]["user"])session["devise.regist_data"]["user"]について説明します。
{user: 〇〇}でデータを持すと ["user"]を使う必要がある。
また、sessionは配列で情報を入れるので以下のように情報を持す事もできます。
session["devise.regist_data"] = {user: @user.attributes, address: @user.address}これで無事遷移先のページでsessionが展開できて@userに代入ができました。
あとは@userに代入されてるデータを使って保存などが可能です。
まとめ
今回は「ウィザード形式」でdeviseを用いた際のsessionの使いた方を紹介しました。
最後に
私はプログラミング初学者ですが、同じ様に悩んでる方々の助けになればと思い、記事を投稿しております。
それでは、また次回お会いしましょう〜
- 投稿日:2020-10-25T13:37:58+09:00
Unicorn環境でRubyをアップデートする
Unicorn と capistrano を使った Railsアプリケーション で ruby を 2.3 から2.5にアップデートしたので、
その手順をまとめてみました。1. rbenv の更新
updateしたい対象のrubyのバージョンをinstall し、global で対象のバージョンを指定しておく
$ rbenv install 2.5.8 $ rbenv global 2.5.8
rbenv install --list
でアップデートしたい対象のバージョンが出てこない時は、
rbenv install
ができないので、下記の手順でrbenvを更新すると、installできるはずです。$ cd ~/.rbenv/plugins/ruby-build $ git pull2. bundler の install と bundle install をしておく
デプロイ時に bundle install でこけないように予め bundler と 他のライブラリを install しておく
# gemfile.lock を確認して、同じ version の bundler を指定する $ gem install bundler -v 1.17.3 $ bundle install3. デプロイ
capistrano で通常通りデプロイする
しかし、この時必要なのが、 ruby のバージョンを切り替えるには、再起動が必要であり、
一度 unicorn を kill して立ち上げ直す必要があった。
preload_app: true
の設定をしている型は注意が必要です$ kill -QUIT `cat /path/to/unicorn.pid` $ bundle exec unicorn_rails -E production -D
- 投稿日:2020-10-25T13:04:26+09:00
Railsチュートリアル 第11章 SendGridでの送信者ID未認証による、メールが送信できない事象と対応
事象
Rails tutorialの「11.4 本番環境でのメール送信」において、herokuからユーザ登録のためのメール送信を実行してみたところ、「We're sorry, but something went wrong.」と表示された。
heroku logs
でログを確認すると、以下のようなエラーログが表示されており、
メールの内容等の情報が出力されていたので、SendGridによる送信処理の部分にエラーがありそう。Net::SMTPFatalError (550 The from address does not match a verified Sender Identity. Mail cannot be sent until this error is resolved. Visit https://sendgrid.com/docs/for-developers/sending-email/sender-identity/ to see the Sender Identity requirements)調査
現在チュートリアルの記載そのままで設定しているので、送信元として設定している「noreply@example.com」が認証されず、SendGridからメールが届かない?
ググってみるものの、Railsチュートリアルで同様の事象は見つけられなかった。試しに
heroku run rails console
より以下コマンドを実行するが、上記と同じエラーが発生し、メールが届かなかった。ActionMailer::Base.mail(from: "noreply@example.com", to: "<受信用メールアドレス>", subject: "subject", body: "body").deliver_nowログに示されたURL等を見て色々調べてみたが、SendGridでの送信元の認証が必要であるよう。
https://sendgrid.com/docs/for-developers/sending-email/sender-identity/
https://sendgrid.com/docs/ui/sending-email/sender-verification/ただ、「noreply@example.com」は自分で作成したドメインであるわけでもなく、このアドレスでの認証の仕方が不明なので、今回は個人用のアドレスを送信元に設定し、送信することとした。
対応
個人で使用しているメールアドレスを送信用メールアドレスとして、SendGridで送信元として認証し、使用する。
以下の記事を参考に、SendGridへログインし、「Single Sender Verification」を設定。
https://sendgrid.kke.co.jp/docs/Tutorials/B_Marketing_Mail/marketing_campaigns1.html認証後、以下のコマンドを実行すると、送信用メールアドレスから受信用メールアドレスにメールが届いた。
ActionMailer::Base.mail(from: "<送信用メールアドレス>", to: "<受信用メールアドレス>", subject: "subject", body: "body").deliver_nowそのため、メール送信部の処理は下記の通り変更し、テスト部の送信元も併せて変更したところ、heroku上で事象が解決し、メール送信が問題なく動作した。
/sample_app/app/mailers/application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: "<送信用メールアドレス>" layout 'mailer' end私見
実際のwebサービスでは、所有するメールアドレスやドメインを送信元として使用すると思われるため、所有しない「noreply@example.com」による送信は、解決を見送りました。
ただ、所有しないアドレスでの送信の可否等、まだまだ理解が追いついていないので、何か見つけた時は追記します。
- 投稿日:2020-10-25T01:45:41+09:00
Railsのvalid?とinvalid?
Ruby on Rails学習における忘備録です。
参照記事:Railsガイド
https://railsguides.jp/active_record_validations.htmlバリデーション
バリデーションは正しいデータをDBに保存するために行われる。
RailsはオブジェクトをActiveRecordオブジェクトに保存する前にバリデーションを実行する。
そこでエラーが発生するとオブジェクトは保存されない。
要は、DBの制約に対して保存条件を満たしていますか?のチェックをDBレベルで保存前に行う仕組みである。valid?とinvalid?
valid?はバリデーションを手動でトリガすることができる。
保存するオブジェクトにエラーがない場合はtrueを返し
エラーの際はfalseを返す。class User < ApplicationRecord # バリデーション(nameが空は許容しない) validates :name, presence: true end #これはtrue(条件を満たしている) User.create(name: "Gonshiba").valid? #これはfalse(条件を満たしていない→nameが空である) User.create(name: "").valid?invalid?はvalid?の逆チェックになっており、帰ってくるBool値が逆になるだけである。
ちなみに、createメソッドはオブジェクト作成から保存までを一括で行ってくれる。