- 投稿日:2022-02-14T22:31:55+09:00
Railsのscopeとは
scope(スコープ)とは 特定のSQL文をメソッドのように呼び出すことができる機能のことです。 下記のように、モデル内にscopeを定義することで使用することができます。 hoge_hoge.rb class HogeHoge < ApplicationRecord scope :active, -> { where(deleted: false) } scope :by_account, -> lambda{|account_id|where(account_id: account_id)} end # 使用例 HogeHoge.active HogeHoge.by_account(params[:account_id]) HogeHoge.by_account(params[:account_id]).active 何が便利なのか? ・コードが短くなる。 ・適切なscope名をつけることで、直感的に読みやすいコードになる。 ・何度も同じようなクエリを使いまわしている場合、 scopeを使用していれば、一箇所修正するだけで済む。 参考記事: https://pikawaka.com/rails/scope https://qiita.com/ozin/items/24d1b220a002004a6351 https://qiita.com/ngron/items/14a39ce62c9d30bf3ac3
- 投稿日:2022-02-14T22:05:58+09:00
rails heroku上のDBをリセットする
結論 heroku上のDBをリセットするには、以下のコマンドを実行する。 ターミナル. heroku run rails db:migrate:reset DISABLE_DATABASE_ENVIRONMENT_CHECK=1 環境 rails 6.0.4.4 ruby 2.6.5 MySQL 5.6.51 heroku heroku runでDBリセットのコマンドを実行 heroku上のDBをリセットしようと以下のコマンドを実行した。 ターミナル. heroku run rails db:reset 以下のエラーが発生。 ターミナル. ActiveRecord::ProtectedEnvironmentError: You are attempting to run a destructive action against your 'production' database. If you are sure you want to continue, run the same command with the environment variable: DISABLE_DATABASE_ENVIRONMENT_CHECK=1 エラー内容をgoogle翻訳で訳すと以下のようになる。 ターミナル. ActiveRecord :: ProtectedEnvironmentError:「本番」データベースに対して破壊的なアクションを実行しようとしています。 続行することが確実な場合は、環境変数を使用して同じコマンドを実行します。 DISABLE_DATABASE_ENVIRONMENT_CHECK = 1 「データベースに対して破壊的なアクション」が実行されるので、本当に実行する場合は「DISABLE〜(以下省略)」を使用してください、ということらしい。 「DISABLE〜(以下省略)」を追記してコマンドを実行する。 ターミナル. heroku run rails db:migrate:reset DISABLE_DATABASE_ENVIRONMENT_CHECK=1 heroku上のDBが予定通りにリセットされ、アプリの挙動も問題なし。 ローカル環境でDBをリセットした後に、本番環境のDBも忘れずにリセットする必要がある。 参考 https://qiita.com/yusuke_s221/items/cefb1808fdf481e6e445 https://qiita.com/saika_0/items/7afe389471adb4e69f9a
- 投稿日:2022-02-14T21:17:29+09:00
解決方法_Webpacker::Manifest::MissingEntryError
初めに 原因は細かに追求していません。(調べたけどわかりませんでした) あくまでも解決方法をまとめています。 エラー内容 Railsアプリを新規作成し、諸々初期設定を終え、いざbin/rails sしてブラウザで表示すると、下記のエラー文が表示。 エラー内容は、1.You〜に書いてある通り,webpackerの設定がコンパイルしないようになっていることが原因のよう。 javascript_pack_tagがレイズされてるのはなぜなのか、、。 webpackerでコンパイルしない設定になっているのに、JSをwebpackerでコンパイルする記述になっているから? Webpacker::Manifest::MissingEntryError Webpacker can't find application in /Users/"ユーザ名"/app/public/packs/manifest.json. Possible causes: 1. You want to set webpacker.yml value of compile to true for your environment unless you are using the webpack -w or the webpack-dev-server. 2. webpack has not yet re-run to reflect updates. 3. You have misconfigured Webpacker's config/webpacker.yml file. 4. Your webpack configuration is not creating a manifest. Your manifest contains: { } <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 開発環境 ruby 2.6.5 rails 6.0.0 解決までの道筋 1. webpackerをインストールして、webpackerを用いたコンパイルを実行 ターミナルにて対象のRailsアプリのディレクトリで下記コマンドを実行 rails webpacker:install rails webpacker:compile しかし、解決せず。同様のエラーが発生。 2. Gemfileを書き換え gem 'webpacker', '~> 5.0’ に書き換え 3. package.jsonに"@rails/webpacker": "5.4.3"を追記 package.json "dependencies": { 略 "@rails/webpacker": "5.4.3" 4. yarn.lock削除 yarn installする前にyarn.lockを削除。 削除せずにyarn installすると、先ほどの変更が反映されないため。 (yarn.lockを元にyarn installされてしまう。) 5. yarn install と yarn add node-sass を実行 3.で行った変更を反映させる。 6. 再度コンパイル実行 rails webpacker:install rails webpacker:compile 7. 解決! webpackerでコンパイルできるようになりました。 終わりに 本エラーは2回再現されたため、メモとして残す。 せっかくなのでなぜこのエラーが発生するのかまで追求したかったのですが、分からず。 以下調べた内容をまとめる。 Rails6はデフォルトの設定だとSprocketsとWebpackerを併用することにより、アセットファイルのコンパイルを行っている。WebpackerはJavaScriptファイル、Sprocketsはその他のアセット(CSSや画像など)のコンパイル。 今回のエラーはWebpackerでのコンパイルが動作していなかった。 参考記事 https://qiita.com/negisys/items/2bf88659f584fe45b686 https://qiita.com/hryau6/items/820d0b404325688574ba
- 投稿日:2022-02-14T19:58:32+09:00
【RSpec】なぜテストを書くのか?
伊藤さんのスライドの中で「テストの品質」についてこう書かれていた。 テストの役割 安全ネットとしての役割 実装の正しさを証明する役割 コードの品質を向上させる役割 省力化ツールとしての役割 バグを叩き潰す役割 設計を支援する役割 説明書としての役割 その中でも実際にテストを書いていて、特に実感したものを所感とともに挙げていく。 その1 バグを叩き潰す役割 バグが起きたときは修正する前にテストを書く。 修正しちゃんと実装されたらこう動くというテストを書いて落としておく →バグ改修 →実装が正しければテストがグリーンになる 上記の手順でバグ改修を行うことで、目視だけではなく実装として正しく修正されたことの証明にもなる。 (たまに書いたテスト自体が間違っていた場合もある…) その2 説明書としての役割 テストは仕様書にもなる。 過去の自分や他の人が書いたコードを読み解くとき、すでに走っているプロジェクトに途中で入って各機能を理解しなくてはいけないときなどにテストを読むことでだいたいを理解するのに役立った。 おわりに まだ独学で開発していた頃はテストを書いていなかったため、テストがあることでの安心感など分からなかったが、今となってはテストなしではいられなくなった。「テストなしでバグ改修とか無理!」とまでになった。 参考
- 投稿日:2022-02-14T17:07:58+09:00
Ruby on Rails チュートリアル第8章をやってみて
基本的なログイン機構 ■第8章 基本的なログイン機構を構築していく。 8.1 セッション HTTPは状態のないプロトコル。HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われる。 ログインするとcreateでセッションを作成して保存。 ログアウトするとdestroyでセッションを破棄。 8.1.1 Sessionsコントローラ ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。 セッションコントローラの作成。 $ rails generate controller Sessions new リソースを追加して標準的なRESTfulアクションをgetできるようにする。 以下のコードを追記。 routes.rb get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' Sessionsコントローラのテストで名前付きルートを使うようにする。 sessions_controller_test.rb require 'test_helper' class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end 8.1.2 ログインフォーム 新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。 ログインフォームのコード new.html.erb <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div> 8.1.3ユーザーの検索と認証 ログインでセッションを作成する場合、入力が無効な場合の処理を最初に行う。 最小限のcreateアクションをSessionsコントローラで定義し、空のnewアクションとdestroyアクションも作成する。 sessions_controller.rb class SessionsController < ApplicationController def new end def create render 'new' end def destroy end end ユーザー認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる。 Active Recordが提供する User.find_byメソッドでデータベースからユーザーを探し、has_secure_passwordが提供する authenticateメソッドでパスワードをチェックする。authenticateメソッドは認証に失敗したときにfalseを返す。 最終的にはこんな感じに。 sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else # エラーメッセージを作成する render 'new' end end def destroy end end 8.1.4 フラッシュメッセージを表示する セッションではActive Recordのモデルを使っていないので、ログインに失敗したときには代わりにフラッシュメッセージを表示する。 ここで書いたコードは誤りで、フラッシュメッセージが消えない状態になるので、一旦次へ。 8.1.5 フラッシュのテスト 統合テストを作成する。 $ rails generate integration_test users_login 以下の流れでテストコードを実装していく ・ログイン用のパスを開く ・新しいセッションのフォームが正しく表示されたことを確認する ・わざと無効なparamsハッシュを使ってセッション用パスにPOSTする ・新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する ・別のページ(Homeページなど)にいったん移動する ・移動先のページでフラッシュメッセージが表示されていないことを確認する フラッシュメッセージの残留をキャッチするテスト。 users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: "", password: "" } } assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end end このままだとテスト失敗になるので、テストをパスさせるに、本編のコードでflashをflash.nowに置き換える。後者はその後リクエストが発生したときに消滅する。 8.2 ログイン cookiesを使った一時セッションでユーザーをログインできるようにする。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。 ほんとはセッションの実装はめちゃくちゃ大変だけど、Rubyのモジュール機能使えば楽にできる。 application_controller.rb class ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper end 8.2.1 log_inメソッド 同じログイン手法を様々な場所で使いまわせるようにするために、 sessionsヘルパーにlog_inという名前のメソッドを定義。 ログインメソッド sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end end 上から3行目のコードで、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。 sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy end end createアクションを完了し、ユーザーのプロフィールページにリダイレクトする準備ができた。 8.2.2 現在のユーザー ユーザーIDを一時セッションの中に安全に置けるようになったので、今度は、ユーザーIDを別のページで取り出すことにする。 current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。 ユーザーIDが存在しない状態でfindを使うと例外が発生するので、find_byメソッドを使う。 sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end end current_userメソッドが動作するようになったので、ユーザーがログインしているかどうかに応じてアプリケーションの動作を変更するための準備ができた。 8.2.3 レイアウトリンクを変更する ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではないという状態を指すので、否定演算子!を使ってチェックをしていく。 sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end end 8.2.4 レイアウトの変更をテストする 以下の手順でテストを作成する。 ・ログイン用のパスを書く ・セッション用パスに有効な情報をpostする ・ログイン用リンクが表示されなくなったことを確認する ・ログアウト用リンクが表示されていることを確認する ・プロフィール用リンクが表示されていることを確認する 上の変更の確認のため、テスト時に登録済みユーザーとしてログインしておく必要がある。 テスト用データをfixture (フィクスチャ) で作成できる。現時点のテストでは、ユーザーは1人いれば十分。 fixture向けのdigestメソッドを追加する。 以下のコードを追加。 user.rb # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end end digestメソッドができたので、有効なユーザーを表すfixtureを作成できるようになった。 ユーザーのログインで使うfixture。 users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> このコードでテストユーザー用の有効なパスワードが作成できる。 <%= User.digest('password') %> fixtureは生のパスワードを参照できないので、テスト用のfixtureでは全員同じパスワード「password」にする。 assert_redirected_to @user 上のコードはリダイレクト先が正しいかどうかをチェックしている。 テストでも問題ないことが確認できた。 8.2.5 ユーザー登録時にログイン ユーザー登録中にログインを済ませておくことにする。Usersコントローラのcreateアクションにlog_inを追加するだけで済むらしい。 8.3 ログアウト ログアウト機能を追加していく。ログアウト用リンクは作成済なので、ユーザーセッションを破棄するための有効なアクションをコントローラで作成する済む。 log_outメソッド。以下を追記。 sessions_helper.rb # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end 2行目のコードで、現在のユーザーをnilに設定できる。 ここで定義したlog_outメソッドは、Sessionsコントローラのdestroyアクションでも同様に使っていく。 以下を追記。 sessions_controller.rb # 現在のユーザーをログアウトする def destroy log_out redirect_to root_url end テストの内容も弄って、無事テストGREENに。 感想 ログイン・ログアウト機能の実装はバックエンドよりの作業なのかな? HTTPだったりcookiesの話は応用情報の時にもちょっと触ったので懐かしいですね。 フワフワしたまま進んでいっていますが、頑張ります。
- 投稿日:2022-02-14T15:31:00+09:00
enumerizeのオプション・メソッドまとめ
enumerizeを使っておきながら便利なオプションやメソッドを活用しきれていなかったので、 備忘録も兼ねてメソッドとオプションをまとめました! 最もベーシックな使用方法 class User extend Enumerize enumerize :status, in: [:student, :employed, :retired] end これだけでモデルにinclusionバリデーションが追加されます(便利ぃ) ここから更に便利なオプションやメソッドを紹介します! 目次 skip_validationsオプション defaultオプション scopeオプション multipleオプション predicatesオプション _textメソッド valuesメソッド その他(enumerizeの値をカスタマイズする) Rspec オプション skip_validationsオプション デフォルトで追加されるバリデーションをskipする class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], skip_validations: true end defaultオプション デフォルトの値を設定できる ActiveRecord::Enumでデフォルト値を設定する場合はDB側で設定する必要があるので、やっぱりenumerize優秀 class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], default: :student end scopeオプション scope: :true scopeを生成し、with_やwithout_で絞り込みができるようになる class User < ActiveRecord::Base extend Enumerize enumerize :status, in: [:student, :employed, :retired], scope: true end User.with_status(:student) # SELECT "users".* FROM "users" WHERE "users"."status" IN ('student') User.without_status(:student) # SELECT "users".* FROM "users" WHERE "users"."status" NOT IN ('student') scope: :shallow enumerizeで宣言した値を直接メソッドのように使用し絞り込みができる class User < ActiveRecord::Base extend Enumerize enumerize :status, in: [:student, :employed, :retired], scope: :shallow end User.student # SELECT "users".* FROM "users" WHERE "users"."status" = 'student' 以下のmultipleオプションを使用している場合、scopeオプションは使用できま multipleオプション 配列のように複数の値を保有できる class User extend Enumerize enumerize :interests, in: [:student, :employed, :retired], multiple: true end user = User.new user.interests << :employed user.interests << :retired predicatesオプション booleanで判定できるようになる class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], predicates: true end user = User.new user.student? # => false user.employed? # => false user.status = :student user.student? # => true user.employed? # => false predicatesオプションやscopeオプションはprefixで名付けすることも可能です メソッド 主にI18nのサポートをする為のメソッド *_text / .text I18nで定義している文言を取得する @user.status_text # or @user.status.text values enumerizeで定義している値を全て取得 User.status.values # or User.enumerized_attributes[:status].values # => ['student', 'employed', 'retired'] その他 enumerizeの値はintなどカスタマイズが可能です class User extend Enumerize enumerize :status, in: [:student => 1, :employed => 2, :retired => 3] end user = User.new user.status = :student user.status #=> 'student' user.status_value #=> 1 User.status.find_value(:student).value #=> 1 User.status.find_value(:employed).value #=> 2 Rspec 便利なマッチャを利用すればテストを簡単に書くことができます ベーシックなテスト class User extend Enumerize enumerize :status, in: [:student, :employed, :retired] end describe User do it { is_expected.to enumerize(:status) } end in 指定した値がenumerizeに定義されているかのテスト class User extend Enumerize enumerize :status, in: [:student, :employed, :retired] end describe User do it { should enumerize(:status).in(:student, :employed, :retired) } end with_default デフォルト値が設定されているかのテスト class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], default: :student end describe User do it { should enumerize(:status).in(:student, :employed, :retired).with_default(:student) } end with_predicates predicatesオプションが適用されているかのテスト class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], predicates: true end describe User do it { should enumerize(:status).in(:student, :employed, :retired).with_predicates(true) } end with_scope scope scopeオプションが適用されているかのテスト class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], scope: true end describe User do it { should enumerize(:status).in(:student, :employed, :retired).with_scope(true) } end with_multiple multipleオプションが適用されているかのテスト class User extend Enumerize enumerize :status, in: [:student, :employed, :retired], multiple: true end describe User do it { should enumerize(:status).in(:student, :employed, :retired).with_multiple(true) } end
- 投稿日:2022-02-14T14:41:42+09:00
Webpacker::Manifest::MissingEntryErrorへの対処法まとめ
はじめに Webpacker::Manifest::MissingEntryErrorが発生した際の対処法です。 個人的的に大変頭を悩ませたエラーでしたのでこちらに備忘録を残していきます。 対象者 下記リンクの偉大な先人達の解決策でも解決できなかった方へ 現状発生している問題・エラーメッセージ あれは忘れもしませぬ、拙僧がRailsの教科書を行なっていた時のこと $ rails new helloworld $ rails g controller hello index $ rails s このように作成を行いサーバーを起動、ブラウザを使い作成したURLへアクセスすると下記のようなエラーが出た。 Webpacker::Manifest::MissingEntryError in Hello#index Showing /Users/damayan/my_web_apps/helloworld/app/views/layouts/application.html.erb where line #9 raised: Webpacker can't find application in /Users/damayan/my_web_apps/helloworld/public/packs/manifest.json. Possible causes: 1. You want to set webpacker.yml value of compile to true for your environment unless you are using the `webpack -w` or the webpack-dev-server. 2. webpack has not yet re-run to reflect updates. 3. You have misconfigured Webpacker's config/webpacker.yml file. 4. Your webpack configuration is not creating a manifest. Your manifest contains: { } Extracted source (around line #9): 7 8 <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 9 <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 10 </head> 11 12 <body> 試したこと 知能と文化の破壊エンジンことGoogle先生の翻訳機能を使いエラー内容を確認 以下和訳 Hello#indexのWebpacker :: Manifest :: MissingEntryError 9行目で/Users/damayan/my_web_apps/helloworld/app/views/layouts/application.html.erbを表示しています。 Webpackerは/Users/damayan/my_web_apps/helloworld/public/packs/manifest.jsonでアプリケーションを見つけることができません。 考えられる原因: 1.ご使用の環境でcompileのwebpacker.yml値をtrueに設定します `webpack-w`またはwebpack-dev-serverを使用している場合を除きます。 2. webpackは、更新を反映するためにまだ再実行されていません。 3.Webpackerのconfig / webpacker.ymlファイルを誤って構成しました。 4.Webpack構成がマニフェストを作成していません。 マニフェストには次のものが含まれます。 {{ } エラー内容からwebpackerのコンパイルがうまくいってないかと思い、上記記載のリンクを参照し色々試すも解決せず、9行目の <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> をコメントアウトすればエラーが出ないことも確認したが根本的な解決にはならず途方に暮れる。 結論 node.jsのバージョン変更にて解決。 詳細 何かエラーがないかを確認。そうすると最初のrails new 実行時に e > urix@0.1.0: Please see https://github.com/lydell/urix#deprecated [2/4] ? Fetching packages... [3/4] ? Linking dependencies... [4/4] ? Building fresh packages... [-/3] ⠠ waiting... [-/3] ⠠ waiting... error /Users/damayan/my_web_apps/helloworld/node_modules/node-sass: Command failed. Exit code: 1 Command: node scripts/build.js ん? error /Users/damayan/my_web_apps/helloworld/node_modules/node-sass: Command failed. というエラーメッセージがあることに気づいた。もしやnode.jsのバージョンとwebpackの依存ライブラリの噛み合わせが原因で表示できないのではと思いnode.jsのバージョンを変更した。 node.js変更後に新しくrails new にて作成すると無事、作成したURLへアクセスすることが出来た! ※個人的にNode.jsのバージョンをv17.5.0からv14.0.0へ変更することで改善することが出来た。Node.jsのバージョン変更にて使用したものについては下記、参照URLの欄に記載しています。 おわりに いかがだったでしょうか。もしもお役に立てれば幸いです。 参照URL: 上記「対象者」項目にて記載させていただいた5つと下記を参照させていただきました。 ◆エラー内容について ◆node.jsのバージョン変更について
- 投稿日:2022-02-14T14:16:45+09:00
Ruby on Rails チュートリアル第7章をやってみて
ユーザー登録 ■第7章 Userモデルができあがったので、ユーザー登録機能を追加していく。 この第7章からは難易度が少しずつ上がっていくっぽい。 7.1 ユーザーを表示する ユーザーの名前とプロフィール写真を表示するためのページを作成していく。 7.1.1 デバッグとRails環境 debugメソッドとparams変数を使って、各プロフィールページにデバッグ用の情報が表示されるようにする。 application.html.erbに追記。 <%= debug(params) if Rails.env.development? %> if以降のコードは「開発環境?」という意味。 デバッグ出力をきれいに整形するために、cssを弄る。 @import "bootstrap-sprockets"; @import "bootstrap"; /* mixins, variables, etc. */ $gray-medium-light: #eaeaea; @mixin box_sizing { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } . . . /* miscellaneous */ .debug_dump { clear: both; float: left; width: 100%; margin-top: 45px; @include box_sizing; } Sassのミックスイン機能 (ここではbox_sizing) を使っている。ミックスイン機能を使うことで、CSSルールのグループをパッケージ化して複数の要素に適用できる。 7.1.2 Usersリソース 6章での操作で、データベースに1人のユーザーがいる。データの作成、表示、更新、削除をリソース (Resources) として扱う。 /users/1 のURLを有効にするために、routesファイル (config/routes.rb)に次の1行を追加します。 resources :users サンプルアプリケーションにこの1行を追加すると、ユーザーのURLを生成するための多数の名前付きルートと共に、RESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになる。便利すぎ。 show用のファイルを手動で作成し、ユーザー表示ビューが正常に動作するためには、Usersコントローラ内のshowアクションに対応する@user変数を定義する。 User.find(params[:id]) このコードでid=1のユーザーを検索できる。 7.1.3 debuggerメソッド byebug gemによるdebuggerメソッドでもっと直接的にデバッグできる。 今後Railsアプリケーションの中でよく分からない挙動があったら、上のようにdebuggerを差し込んで調べてみる。 7.1.4 Gravatar画像とサイドバー 今度は各ユーザーのプロフィール写真のあたりをもう少し肉付けし、サイドバーも作り始める。 gravatar_forヘルパーメソッドを使ってGravatarの画像を利用できるようにする。show.html.erbを書き換え。 <% provide(:title, @user.name) %> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> gravatar_forヘルパーメソッドを定義する。 module UsersHelper # 引数で与えられたユーザーのGravatar画像を返す def gravatar_for(user) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end end Gravatarの画像タグにgravatarクラスとユーザー名のaltテキストを追加したものを返す。 ユーザーのshowビューにサイドバーを追加する。またshow.html.erbを書き換え。 <% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> </aside> </div> HTML要素とCSSクラスを配置したおかげで、プロフィールページにスタイルを与えられるように。 7.2ユーザー登録フォーム 今度はユーザー登録フォームを作成。 7.2.1 form_forを使用する フォームを作るために、form_forヘルパーメソッドを使う。 @user変数の定義。 users_controller.rb class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end end 新規ユーザーのためのユーザー登録フォーム。 new.html.erb <% provide(:title, 'Sign up') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :email %> <%= f.email_field :email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div> CSSもいじったらこんな感じに。 7.2.2 フォームHTML フォームを理解していく。 <%= form_for(@user) do |f| %> . . . <% end %> 変数fは “form” のf。このfオブジェクトは、HTMLフォーム要素に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返す。 ユーザーの作成で重要なのはinputごとにある特殊なname属性。params変数経由で取りに行くため。 次に重要な要素は、formタグ自身。actionとmethodが重要。 7.3ユーザー登録失敗 入力が失敗したときにエラーを表示するようにする。 7.3.1 正しいフォーム ユーザー登録フォームを動かす。 以下のコードを追加。 users_controller.rb def create @user = User.new(params[:user]) # 実装は終わっていないことに注意! if @user.save # 保存の成功をここで扱う。 else render 'new' end end 実装の出発点は完了。 7.3.2 Strong Parameters 実装していく。Strong Parametersを使う。これで、必須のパラメータと許可されたパラメータを指定することができる。 @user = User.new(params[:user]) これだとセキュリティ上かなり危険らしいので、次のようにする。 users_controller.rb class UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save # 保存の成功をここで扱う。 else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end privateキーワードを使って外部から使えないようにする。 やっぱり難易度が上がったこともあって、結構何言ってるかわかんないとこがチラホラ出てきました。 7.3.3 エラーメッセージ エラーメッセージを追加していく。ブラウザで表示するために、ユーザーのnewページでエラーメッセージのパーシャル (partial) を出力する。 qiita.rb <% provide(:title, 'Sign up') %> <h1>Sign up</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div> shared/error_messagesというパーシャルをrenderする。 複数のビューで使われるパーシャルは専用のディレクトリsharedによく置かれる。ただ、ディレクトリが存在しないので作る必要がある。 $mkdir app/views/shared パーシャルを作っておく。 _error_messages.html.erb <% if @user.errors.any? %> <div id="error_explanation"> <div class="alert alert-danger"> The form contains <%= pluralize(@user.errors.count, "error") %>. </div> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> countメソッドはいくつあるか数え、any?メソッドはempty?メソッドと逆で存在していればtrue。 7.3.4 失敗時のテスト Railsではフォーム用のテストを書くことができる。無効な送信をしたときの正しい振る舞いについてテストを書いていく。 users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' end end このコードと等価になるらしい。 before_count = User.count post users_path, ... after_count = User.count assert_equal before_count, after_count ブロックの実行前後で引数が変わらないことをテストしている。またgetメソッドを使っておらず、ユーザー登録ページにアクセスしなくても、直接postメソッドを呼び出してユーザー登録ができることを意味している。 7.4 ユーザー登録成功 新規ユーザーを実際にデータベースに保存できるようにし、ユーザー登録フォームを完成させていく。 7.4.1 登録フォームの完成 ーザー登録に成功した場合はページを描画せずに別のページにリダイレクト するようにしてみる。 users_controller.rb def create @user = User.new(user_params) if @user.save redirect_to @user else render 'new' end end redirect_toメソッドに着目する。次のコードと等価。 redirect_to user_url(@user) 7.4.2 flash 登録完了後に表示されるページにメッセージを表示し、2度目以降にはそのページにメッセージを表示しないようにする。Railsではこういった情報の表示のためにflashという変数を用いる。 users_controller.rb def create @user = User.new(user_params) if @user.save flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end flash変数に代入したメッセージは、リダイレクトした直後のページで表示できるようになる。 application.html.erb <!DOCTYPE html> <html> . . . <body> <%= render 'layouts/header' %> <div class="container"> <% flash.each do |message_type, message| %> <div class="alert alert-<%= message_type %>"><%= message %></div> <% end %> <%= yield %> <%= render 'layouts/footer' %> <%= debug(params) if Rails.env.development? %> </div> . . . </body> </html> これでflash変数の内容をWebサイトのレイアウトに追加できる。 7.4.3実際のユーザー登録 ここまでの変更の確認のため、データベース初期化。 $ rails db:migrate:reset そしてサーバーを起動して、ユーザー登録してみる。無事成功。フラッシュメッセージも表示されました。 7.4.4 成功時のテスト 有効な送信に対するテストを書いてみる。これでアプリケーションの振る舞いを検証し、もし今後バグが埋め込まれたらそれを検知できるようになる。 users_signup_test.rb test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' end 7.5 プロのデプロイ 初めてデータを操作できるようにするデプロイを行う。 7.5.1 本番環境でのSSL SSLとはローカルのサーバーからネットワークに流れる前に、大事な情報を暗号化する技術。 本番環境ではSSLを使うように修正する。以下のコードを追記。 production.rb config.force_ssl = true 7.5.2 本番環境用のWebサーバー Herokuのデフォルトサーバーの、Rubyだけで実装されたWEBrickでなくPumaに置き換える。 7.5.3 本番環境へのデプロイ いつものプッシュしてからマイグレーション。 SSLの設定をしたので鍵付きに。 感想 さらに難しくなった感じがしました。なんとなくサーバー上のやりとりについて書いてるんだろうなーくらいしかわかんないままでしたが、それでもエラーなく進めることができました。
- 投稿日:2022-02-14T13:46:34+09:00
更に pagepank で じゃんけん と トランプ と 軍人将棋 をやってみた
はじめに 前回の記事を投稿後、Ruby版networkxに携わることがあったので、更にやってみた。 じゃんけん require_relative 'lib/networkX' h = { 1 => [2], 2 => [3], 3 => [1] } g = NetworkX::DiGraph.new h.each do |k, v| v.each do |ve| g.add_edge(ve - 1, k - 1) end end puts NetworkX.pagerank(g) # output 0.3333333 0.3333333 0.3333333 じゃんけんは三つ巴ですので、同じ得点になります。 トランプ h = { 1 => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 2 => [1], 3 => [2], 4 => [2, 3], 5 => [2, 3, 4], 6 => [2, 3, 4, 5], 7 => [2, 3, 4, 5, 6], 8 => [2, 3, 4, 5, 6, 7], 9 => [2, 3, 4, 5, 6, 7, 8], 10 => [2, 3, 4, 5, 6, 7, 8, 9], 11 => [2, 3, 4, 5, 6, 7, 8, 9, 10], 12 => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 13 => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } 要素hのみ表示しております。 index 絵柄 得点 1 A 0.2160026 2 2 0.0769230 3 3 0.0222672 4 4 0.0389576 5 5 0.0408504 6 6 0.0445303 7 7 0.0488713 8 8 0.0542792 9 9 0.0612033 10 10 0.0704314 11 J 0.0834375 12 Q 0.1033930 13 K 0.1388527 トランプのルールとして、数の大きい方が勝ちとしますが、Aのみ2を除くすべての数に勝ちます。 すると、Aの得点が一番大きいのですが、2の得点が10より大きく算出されています。 軍人将棋 h = { 1 => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15], 2 => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 3 => [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 4 => [5, 6, 7, 8, 9, 12, 13, 14, 15], 5 => [6, 7, 8, 9, 12, 13, 14, 15], 6 => [7, 8, 9, 12, 13, 14, 15], 7 => [8, 9, 12, 13, 14, 15], 8 => [9, 12, 13, 14, 15], 9 => [12, 13, 14, 15], 10 => [4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15], 11 => [4, 5, 6, 7, 8, 9, 12, 14, 15], 12 => [13, 14, 15], 13 => [11, 14, 15], 14 => [1, 15], 15 => [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14] } index 絵柄 得点 1 大将 0.1585483 2 中将 0.1036655 3 少将 0.0807769 4 大佐 0.0485714 5 中佐 0.0433120 6 少佐 0.0391522 7 大尉 0.0357737 8 中尉 0.0329713 9 少尉 0.0306064 10 飛行機 0.0629422 11 戦車 0.0528652 12 騎兵 0.0285820 13 工兵 0.0334844 14 スパイ 0.0575546 15 地雷 0.1911934 軍人将棋に馴染みのない方もいらっしゃると思いますが、引き分けを双方向のエッジとしています。 予想通り、飛行機やスパイの得点が高いです。 勿論、ここでは移動能力(地雷は動かない、飛行機は飛び越せる等)を評価していないです。 後、一般的な将棋の場合、手番のある方が勝つ(駒をとれる)ので、pagerankの手法ですと得点がすべて同じになるため、別の手法が必要になると思われます。 まとめ pegerank に更に詳しくなった
- 投稿日:2022-02-14T07:09:23+09:00
【個人開発】Twitterで映える!ファンレターを交換・シェアして楽しめるサービス『ご縁箱』をリリースしました✧˖°。
はじめに... ▼ 皆さんはTwitterを使う上で このように感じたことはありませんか? 『 今ある ご縁を大切にしたい 』 『 フォロワーさんと感謝の気持ちを伝えたり、交流を深めたい! 』 『 あの人に話しかけてみたい! 』 けどきっかけがない..ちょっと億劫かも... こういった問題も楽しみながら解決出来る『 ご縁箱 』というサービスを開発しました。(Rails × Vue.js) ▼ ご縁箱を使うとどうなる? 実際に使ってみて... まだ話したことがなかったフォロワーさんと話すきっかけになった 色んなメッセージを頂いてほっこりした気分になった 自分の率直な気持ちをレターを送って伝えることで喜んでもらえた ご飯の約束ができた(全部焼き肉) 仲良しユーザーとの交流も深めることも出来ますが、まだ交流が出来ていないユーザーともコミュニケーションを取るきっかけにすることも出来るサービスです。 【今回作ったサービス】 https://goenbako.com 【私のご縁箱ページ】 https://goenbako.com/outputky スマホ・PC対応(※若干スマホ寄りのレイアウトです) サービスを作ったきっかけ 私は2021年の4月から10月初旬まで、オンラインのプログラミングスクール(RUNTEQ)で受講していました。 色んな方と交流を深めて刺激を受けたり、勉強会で新たな気付きを得た中で、 『そんな人達に普段は言わないようなことや感謝の気持ちを伝えたい!』 『一緒に楽しめて、その時の大事な気持ちを忘れないで残しておけるようなサービスを作ろう!!』 という想いでサービスを構想していきました。 ▼ 発想のタネ? 自分のmixiアカウントの紹介文を見たのがきっかけです。 mixiの紹介文は『 プロフィールに自分を紹介する書き込みをしてもらうことが出来る機能 』です。 ( いわゆる他己紹介 ) 現在、自分の周りは誰もINしていませんが紹介文一覧の不思議なアルバムのような空間がとても好きです。 人との繋がり、自身の感情の変化や当時から現在の軌跡が分かり、そこにとても価値を感じました。 Twitterの検索で 『mixi 紹介文』とワードサーチしたところ、同じように感じている方や、そういったサービスを求めている方が実際に多くいること知り、需要があることを確信しました。 この価値をTwitterというプラットフォームで表現・改良出来ないか?と考え、サービスのリリースに至っています。 サービス概要 ご縁箱はTwitter認証したユーザー同士でファンレターを交換・共有することが出来るSNS型のサービスであり、Twitter上で使えるコミュニケーションツールでもあります。 私は説明があまりお上手ではありません!!...百聞は一見に如かず。という言葉に頼って画像をよく使ってイメージをお伝えするという形を取らせていただきます。 認証しなくても一部の機能を楽しんでいただけますが、ここではTwitter認証してご縁箱を開設したことを前提とさせていただきます。 使い方 1. 自分のご縁箱ページをTwitterでシェア ツイート機能で共有するかプロフィールにリンクを載せることで、フォロワーさんに自分のページを共有します。 ▼ 例えば、これは私のご縁箱ページです。 https://goenbako.com/outputky Twitterでご縁箱ページを公開するか、このように公開している相手に対してレターを書くことでもSNSのようにご縁箱の中で繋がっていくことが出来ます。 2. レターを書く 交流したいユーザー、または応援している・気になるユーザーにレターを書くことが出来ます。 話題を自由に選ぶことができ、全て埋める必要もなく、入力した話題だけが送ったレターとして反映されます。 ただ漠然と 『 レターを書いてください! 』 と言われても書きづらくなる問題と 関係性問わず、誰でも負担少なく書けるように..という考えから、テーマを厳選しています。 ちなみに『ホーム』 ▶ 『送ったレター』から後で編集や削除も出来ます。 レターを書くのは楽しいです! 普段とは違った角度からその人のことを思い浮かべたり、その人の良い部分やポジティブな感情を届けるところがとても良いです。 つい真面目に書いてしまうのですが、適当にボケたりする風潮も好きです。 あなたのフォロワーさんがご縁箱を始めていたら是非、気軽に書いてみてください! 3. 受け取ったレターのシェア ( デザインOGP ) ※任意 受け取ったレターの項目を一つ選んでシェアすることが出来ます。 このタイミングでOGP画像を生成しているため遷移するまでに少しインターバルが発生します。 (具体的な処理内容については『 苦労した点 』で後述いたします) この間はユーザーに待たせることになるため、退屈に感じないように非同期処理で "移動しています..." というアナウンスを表示 & アニメーションを加えるようにしました。 受け取ったレターがデザインされたOGP画像となって反映されます。 Twitterでも気持ちの共有やリアクションをして話題を広げたり楽しむことが出来ます。 その他の機能紹介 レターの詳細 Twitter上でシェアされたツイートからアクセスすると ログインしていなくてもレターの全文を見たり、両者のユーザーページにも飛ぶことが出来ます。 シェアされたレターを通じてさらに第三者がファンレターを書くきっかけに少しでもなればいいなと思い、URL画像デザインとレター詳細ページのレイアウトも拘りました(^^) この一連の流れをユーザー同士で楽しみつつ、その過程で他のユーザーにも参加してもらい、ユーザーの縁が深まれば...というコンセプトです。 Twitterでフォロワーさんと交流を深めたい全ユーザーがご縁箱のターゲット層です!! ランダム訪問機能 ユーザーのページをワンタップでランダムにアクセスします。 新規のユーザーでも、ご縁箱の使われ方や世界観を感覚的に伝える目的と、 何か少し遊びを入れたいと思い実装しました。 単にユーザーのレコードをランダムに取得して画面遷移しているだけですが、意外と好評な機能です。 ユーザー検索機能 & 開設リクエスト機能 ユーザー検索機能はTwitterのIDにマッチするユーザーページに遷移します。 検索成功時の挙動(GIF画像) ▼ 検索失敗時は開設リクエストを案内 ユーザー検索でユーザーが存在しなかった場合、 『 見つかりませんでした。 』とただ表示されるだけだと、少し寂しくありませんか? (はい! 寂しいです!!) 検索したということはそのユーザーに対して興味やファンレターを書きたいという感情に近いものがあるはず.. その気持ちがなるべく無駄にならないように..なにか良い表現はないのか..?と考えた結果 検索ヒットしなかったユーザーにご縁箱をリプライで招待する機能を用意し、案内表示することにしました。 メール通知機能 ・ 設定ページ こちらはリリース後に要望/フィードバックを頂き、取り急ぎ実装いたしました。 RailsのActionMailerを使用しています。 実際に通知が来ると気持ち的に嬉しくなるので、実装して改めて良かったと思います(^^) 他にもプロフィール情報を更新したいという要望もいただき、リリース後に実装しました。 ひとまずは手動編集ではなく、twitter Gem を使って最新のクライアント情報を取得して更新するという仕様にしています。 実装について 使用技術 Ruby 2.7.4 Rails 6.0.4.1 JavaScript Vue.js 2.6.14 Gem sorcery carrierwave meta-tags twitter rubocop ライブラリ vuetify vue-router vuex vuex-persistedstate axios js-cookie vee-validate vue-gtag eslint その他 TwitterAPI heroku Amazon S3 ER図 細かめなこだわり ▼ 初心者ユーザーのみログイン時に、一度だけ使い方カードが表示される。 使い方がわかりづらいサービスのため、私のような感覚派かつ受動的なユーザーでも少しでも馴染みやすいように意識しました。 「受け取ったレターが0件の場合」という条件式と、Vuexで「1度表示したかどうか」のステートを保持させることで実現しています。 ▼ ログインしていなくてもレターを書くボタンを配置 ログインしていない場合はそもそもボタンを表示させないという実装が一番容易ではありましたが、導線を用意しておかないのは勿体ないと思い、ログインガイダンスを用意しました。 『登録する』『レターを書く』という明確な意志を持ったユーザーであれば別かもしれませんが、 ボタンすらない場合、興味本位で見に来てくれた新規ユーザーは本当に見るだけになってしまうのでは? 無意識でもファンレターを書くボタンを認識してもらうことには意味があると考え、このような設計にしました。 ハッシュタグを有効活用する 少しでもTwitterで認知していただきやすくするためにハッシュタグを一意でわかりやすいものにし、タグ一覧を見るだけでサービスの概要が掴めるようにしました。 Twitter単体で宣伝する際にも活用出来るものですので、最初にどう表現するかという部分まで慎重に決めました。 その他 全体的なレイアウト、デザイン(タイトルロゴ、ご縁箱イラストは例外) カラーやフォントで世界観を統一し、合わせて細かく素材を配置しました。 少しでもパフォーマンスの低下を抑えたかったため、Adobe Illustratorを使ってSVGのデザイン画像を自作しています。 デザインに関しては完全に素人でしたが、やってみたいと思い、試行錯誤を重ねつつ挑戦することにしました。 ログイン状態を保持しておく Twitterを行き来するサービスですのでvuex-persistedstateでログイン状態を保持するようにしました。 苦労した点 受け取ったレターをシェアする機能 デザインしたSVG画像をレターの内容をVueで反映させ、画像化した上でOGPに動的に反映させるという一連の処理の実装です。 この部分の実装です (Gif画像)画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 SVG要素の扱い方、Canvas、フォントの適用やCSSの適用 OGP画像の仕組みやbase64画像をアップロードする方法 PC、スマホ、開発、本番 とそれぞれの環境下で所々動かないケース など、検証や問題の切り分けと根本的な知識の土台が必要となり、かなりの苦戦と時間を要しました。 具体的な処理の流れは以下のようになりました。 SVGの見えない部分の処理本番ではhiddenにしていますが2番までの処理は以下の画像のようになっています。 最初から表示してあるのが用意しておいたSVGのデザインフレームで、上に表示される画像がbase64画像化したものです。 画像のように文字を書き換えなくても画像はシェアはできますがツイート画面の状態で表示させるために書き換えています。 色々な記事を参考にしましたが、情報の掴み方が難しく、 一番の解決策はリファレンスなどを読み、土台となる知識を固めて挑むことでした。 初めて扱うオブジェクトや概念が多く、時間的な焦りや、厳しいと思うことが多々ありましたが、しっかり把握しながら実装すると着実に進めていけたため、とても楽しかったです。 終わりに 最後まで読んでいただき、ありがとうございました。 本記事もURL追記など改良していけたらと思いますので興味がある、良かったと思う方は是非ストックしていただいて、ご要望や感想などもコメントやTwitterで共有して頂けますと励みになります(^^) 良ければフォロワーさんに紹介していただいて、楽しんでください♪ https://goenbako.com
- 投稿日:2022-02-14T00:07:53+09:00
Rails7のapiモードでGraphQLを使ってみた
目的 Rails で GraphQL を使うときはどんな感じに書くのかを確認する 必要なもの Docker(今回使用バージョン: Docker version 20.10.12, build e91ed57) VSCode(Remote Develpment 拡張機能を使用, 今回使用バージョン: Version: 1.64.1) ブラウザ(今回は Chrome バージョン: 98.0.4758.80(Official Build) を使用) 開発環境構築 VSCode を起動して Dockerfile を作成し仮想環境を起動する VSCode で開発用の空フォルダを開く(ここでは /dev/rails_graphql とする) Ruby 環境の Dokerfile を作成する /dev/rails_graphsql/Dockerfile FROM ruby VSCode のコマンド Reopen in Container で From Dockerfile から仮想環境に切り替える rails インストール用の Gemfile を作成する /Gemfile source "https://rubygems.org" gem 'rails' VSCode の terminal を開き gem をインストールする terminal bundle install ruby と rails のバージョン確認 terminal ruby -v 実行結果 ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux] terminal rails -v 実行結果 Rails 7.0.2.2 rails プロジェクトを作成 terminal rails new --api --minimal . 動作確認 サーバ起動 terminal rails s http://localhost:3000 にアクセスすれば rails が動作しているのが確認できます GraphQL 用の gem を追加 terminal bundle add graphql bundle add --group development graphiql sass-rails ※ graphiql は開発用ツール, sass-rails は graphiql 動作に必要 graphql 関連ファイルの生成 terminal rails g graphql:install graphiql が動作するよう設定を調整 セッション有効化 application.rb に2行追加 config/application.rb config.middleware.use ActionDispatch::Cookies config.middleware.use ActionDispatch::Session::CookieStore route に設定を追加 routes.rb if Rails.env.development? mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" end 動作確認 rails s でサーバを起動して http://localhost:3000/graphiql にアクセス テスト用に生成されているクエリを実行 GraphQL のクエリ作成 今回はイベント予定表のAPIを想定して作ってみます まずイベントの予定を管理するモデルと REST API を生成します terminal rails g scaffold Event title:string:index start_at:datetime:index end_at:datetime place:string:index tags:string memo:text canceled:boolean:index 生成結果 invoke active_record create db/migrate/20220213115441_create_events.rb create app/models/event.rb invoke test_unit create test/models/event_test.rb create test/fixtures/events.yml invoke resource_route route resources :events invoke scaffold_controller create app/controllers/events_controller.rb invoke resource_route invoke test_unit create test/controllers/events_controller_test.rb 次に参加者分を生成します terminal rails g scaffold Member name:string:index email:string:uniq 生成結果 invoke active_record create db/migrate/20220213115747_create_members.rb create app/models/member.rb invoke test_unit create test/models/member_test.rb create test/fixtures/members.yml invoke resource_route route resources :members invoke scaffold_controller create app/controllers/members_controller.rb invoke resource_route invoke test_unit create test/controllers/members_controller_test.rb 最後にイベントの参加者分を生成します terminal rails g scaffold EventMember event:references member:references presented:boolean:index 生成結果 invoke active_record create db/migrate/20220213115822_create_event_members.rb create app/models/event_member.rb invoke test_unit create test/models/event_member_test.rb create test/fixtures/event_members.yml invoke resource_route route resources :event_members invoke scaffold_controller create app/controllers/event_members_controller.rb invoke resource_route invoke test_unit create test/controllers/event_members_controller_test.rb not null や 既定値を追加設定します db/migrate/20220213115441_create_events.rb class CreateEvents < ActiveRecord::Migration[7.0] def change create_table :events do |t| t.string :title, null: false t.datetime :start_at, null: false t.datetime :end_at t.string :place t.string :tags t.text :memo t.boolean :canceled, null: false, default: false t.timestamps end add_index :events, :title add_index :events, :start_at add_index :events, :place add_index :events, :canceled end end db/migrate/20220213115747_create_members.rb class CreateMembers < ActiveRecord::Migration[7.0] def change create_table :members do |t| t.string :name, null: false t.string :email, null: false t.timestamps end add_index :members, :name add_index :members, :email, unique: true end end db/migrate/20220213115822_create_event_members.rb class CreateEventMembers < ActiveRecord::Migration[7.0] def change create_table :event_members do |t| t.references :member, null: false, foreign_key: true t.boolean :presented, null: false, default: false t.timestamps end add_index :event_members, :presented end end migration を実行します terminal rails db:migrate 実行結果 == 20220213115441 CreateEvents: migrating ===================================== -- create_table(:events) -> 0.0098s -- add_index(:events, :title) -> 0.0016s -- add_index(:events, :start_at) -> 0.0016s -- add_index(:events, :place) -> 0.0015s -- add_index(:events, :canceled) -> 0.0015s == 20220213115441 CreateEvents: migrated (0.0166s) ============================ == 20220213115747 CreateMembers: migrating ==================================== -- create_table(:members) -> 0.0125s -- add_index(:members, :name) -> 0.0014s -- add_index(:members, :email, {:unique=>true}) -> 0.0015s == 20220213115747 CreateMembers: migrated (0.0157s) =========================== == 20220213115822 CreateEventMembers: migrating =============================== -- create_table(:event_members) -> 0.0138s -- add_index(:event_members, :presented) -> 0.0013s == 20220213115822 CreateEventMembers: migrated (0.0154s) ====================== GraphQL 用の型を生成 terminal rails g graphql:object Event 実行結果 create app/graphql/types/event_type.rb 以下のように events テーブルから情報を取得して自動生成できていますね app/graphql/types/event_type.rb # frozen_string_literal: true module Types class EventType < Types::BaseObject field :id, ID, null: false field :title, String, null: false field :start_at, GraphQL::Types::ISO8601DateTime, null: false field :end_at, GraphQL::Types::ISO8601DateTime field :place, String field :tags, String field :memo, String field :canceled, Boolean, null: false field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :updated_at, GraphQL::Types::ISO8601DateTime, null: false end end 同じように member, event_member の分も生成します。 terminal rails g graphql:object Member 実行結果 create app/graphql/types/member_type.rb terminal rails g graphql:object EventMember 実行結果 create app/graphql/types/event_member_type.rb Event 全件取得の GraphQL クエリを追加 app/graphql/types/query_type.rbへ以下を追加 field :events, [Types::EventType], null: false def events Event.all end graphiql で動作確認 rails s でサーバを起動して、 http://localhost:3000/graphiql にアクセスします event の id, title, startAt(start_atだとエラーになる) を取得するクエリを実行 クエリ { events { id title startAt } } まだデータが一件もないので空の配列になっています テストデータを rails c で投入 terminal rails c rails_console Event.create! title: '勉強会', start_at: '2022/02/18 19:00', end_at: '2022/02/18 21:00' 実行結果 (13.6ms) SELECT sqlite_version(*) TRANSACTION (0.1ms) begin transaction Event Create (19.8ms) INSERT INTO "events" ("title", "start_at", "end_at", "place", "tags", "memo", "canceled", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) [["title", "勉強会"], ["start_at", "2022-02-18 19:00:00"], ["end_at", "2022-02-18 21:00:00"], ["place", nil], ["tags", nil], ["memo", nil], ["canceled", 0], ["created_at", "2022-02-13 13:05:15.499816"], ["updated_at", "2022-02-13 13:05:15.499816"]] TRANSACTION (45.7ms) commit transaction => #<Event:0x00007f680970b048 id: 1, title: "勉強会", start_at: Fri, 18 Feb 2022 19:00:00.000000000 UTC +00:00, end_at: Fri, 18 Feb 2022 21:00:00.000000000 UTC +00:00, place: nil, tags: nil, memo: nil, canceled: false, created_at: Sun, 13 Feb 2022 13:05:15.499816000 UTC +00:00, updated_at: Sun, 13 Feb 2022 13:05:15.499816000 UTC +00:00> 再度 graphql クエリ実行 投入したデータが返ってきますね 登録用の Mutation を作成 Mutation 用のファイルを生成 terminal rails g graphql:mutation CreateEvent 実行結果 create app/graphql/mutations/create_event.rb 以下のファイルが生成されました /app/graphql/mutations/create_event.rb module Mutations class CreateEvent < BaseMutation # TODO: define return fields # field :post, Types::PostType, null: false # TODO: define arguments # argument :name, String, required: true # TODO: define resolve method # def resolve(name:) # { post: ... } # end end end これを修正します /app/graphql/mutations/create_event.rb修正後 module Mutations class CreateEvent < BaseMutation field :event, Types::EventType, null: false argument :title, String, required: true argument :start_at, GraphQL::Types::ISO8601DateTime, required: true argument :end_at, GraphQL::Types::ISO8601DateTime, required: false argument :place, String, required: false argument :tags, String, required: false argument :memo, String, required: false def resolve(**args) { event: Event.create!(**args) } end end end Mutation の動作確認 ブラウザで mutation を実行します mutation mutation { createEvent( input:{ title: "合宿" startAt: "2022-02-26T09:00:00Z" endAt: "2022-02-27T18:00:00Z" place: "有馬温泉" tags: "Ruby 合宿 温泉" } ){ event { id title } } } クエリで登録内容を確認してみます query { events { id title startAt endAt place } } 正常に登録できていますね Member 用の Query と Mutation を追加 以下の Query を追加します /app/graphql/types/query_type.rbへ追加 field :members, [Types::MemberType], null: false def members Member.all end ブラウザで Query の確認をします query { members { id name email } } Mutation を追加します terminal rails g graphql:mutation CreateMember 実行結果 create app/graphql/mutations/create_member.rb Mutation ファイルを修正します。 app/graphql/mutations/create_member.rb修正後 module Mutations class CreateMember < BaseMutation field :member, Types::MemberType, null: false argument :name, String, required: true argument :email, String, required: true def resolve(**args) { member: Member.create!(**args) } end end end ブラウザで Mutation の確認をします mutation mutation { createMember( input:{ name: "山田 太郎" email: "yamada.taro@example.com" } ){ member { id name email } } } イベント参加者の Query と Mutation を作成します Event と Member は 多対多の関係にあるので先に Model クラスに関連を設定しておきます。 /app/models/event.rb class Event < ApplicationRecord has_many :event_members has_many :members, through: :event_members end /app/models/member.rb class Member < ApplicationRecord has_many :event_members has_many :events, through: :event_members end 先に event_members にデータを投入しておきます terminal rails c rails_console Member.create! name: '鈴木 花子', email: 'suzuki.hanako@example.com' rails_console Event.first.then{|event| Member.all.each {|member| event.event_members.create! member: }} 実行結果 Event Load (2.0ms) SELECT "events".* FROM "events" ORDER BY "events"."id" ASC LIMIT ? [["LIMIT", 1]] Member Load (2.0ms) SELECT "members".* FROM "members" TRANSACTION (0.1ms) begin transaction EventMember Create (21.4ms) INSERT INTO "event_members" ("event_id", "member_id", "presented", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["event_id", 1], ["member_id", 1], ["presented", nil], ["created_at", "2022-02-13 13:57:02.526484"], ["updated_at", "2022-02-13 13:57:02.526484"]] TRANSACTION (8.1ms) commit transaction TRANSACTION (0.1ms) begin transaction EventMember Create (27.6ms) INSERT INTO "event_members" ("event_id", "member_id", "presented", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["event_id", 1], ["member_id", 2], ["presented", nil], ["created_at", "2022-02-13 13:57:02.561104"], ["updated_at", "2022-02-13 13:57:02.561104"]] TRANSACTION (8.0ms) commit transaction => [#<Member:0x00007efedf8ebfb8 id: 1, name: "山田 太郎", email: "yamada.taro@example.com", created_at: Sun, 13 Feb 2022 13:36:46.345436000 UTC +00:00, updated_at: Sun, 13 Feb 2022 13:36:46.345436000 UTC +00:00>, #<Member:0x00007efedf8ebec8 id: 2, name: "鈴木 花子", email: "suzuki.hanako@example.com", created_at: Sun, 13 Feb 2022 13:55:30.446672000 UTC +00:00, updated_at: Sun, 13 Feb 2022 13:55:30.446672000 UTC +00:00>] GraphQLのクエリを追加します /app/graphql/types/event_type.rbへ追加 field :members, [Types::MemberType], null: false /app/graphql/types/member_type.rbへ追加 field :events, [Types::EventType], null: false ブラウザで動作確認します query { members { id name email events { id title startAt } } } query { events { id title startAt members { id name email } } } member ごとの events や event ごとの members が取得できていますね Mutation の修正 event 登録時に参加者も指定できるよう Mutation を修正します /app/graphql/mutations/create_event.rb module Mutations class CreateEvent < BaseMutation field :event, Types::EventType, null: false argument :title, String, required: true argument :start_at, GraphQL::Types::ISO8601DateTime, required: true argument :end_at, GraphQL::Types::ISO8601DateTime, required: false argument :place, String, required: false argument :tags, String, required: false argument :memo, String, required: false argument :member_ids, [Integer], required: false def resolve(member_ids:, **args) event = Event.create!(**args).tap do|event| member_ids.each{|member_id| event.event_members.create! member_id: } end { event: } end end end ※ argument :member_ids...の行追加と def resolve を修正しています 動作確認 mutation mutation { createEvent( input: {title: "懇親会", startAt: "2022-02-23T19:00:00Z", memberIds: [1, 2]} ) { event { id title members { id name email } } } } うまくできました Queryに抽出条件を追加 events, members の Query に id 指定できるように query_type.rb を修正します /app/graphql/types/query_type.rbを修正 module Types class QueryType < Types::BaseObject # Add `node(id: ID!) and `nodes(ids: [ID!]!)` include GraphQL::Types::Relay::HasNodeField include GraphQL::Types::Relay::HasNodesField # Add root-level fields here. # They will be entry points for queries on your schema. # TODO: remove me field :test_field, String, null: false, description: "An example field added by the generator" def test_field "Hello World!" end field :events, [Types::EventType], null: false do argument :ids, [Integer], required: false end def events(ids: nil) if ids Event.where(id: ids) else Event.all end end field :members, [Types::MemberType], null: false do argument :ids, [Integer], required: false end def members(ids: nil) if ids Member.where(id: ids) else Member.all end end end end field :events, と fields :members にブロックをつけて受け入れる引数を追加し、メソッド内で引数が指定されている場合は抽出条件を指定するようにしました 引数は ids: nil とすれば省略可能になります さいごに 使った感想としては REST API を作るよりコード量は少なくてすみそうなのと、エンドポイントは1つだけなので散らからないところが良いと思いました。