20210417のRubyに関する記事は18件です。

enumとenum_helpの使い方【rails】

※プログラミング初学者の方向けに記事を書いています。 自分が遭遇したエラーなどに同じように遭遇するかもなのでご参考になれば enumって何?? モデルで数値のデータ型で定義しているカラムを、 文字列型として使えるようにすることができます。 例えば、0は男性、1は女性みたいな感じです。 定義方法 enumはモデルに定義する必要があります。 定義の仕方は主に2つあります。 ・名前だけを定義する方法 ・名前とそれに対応する数値を定義する方法 では、1つずつ見ていきましょう! 1.名前だけを定義する方法 まず、前提としてUserモデルのroleカラムがあるという前提で説明していきます。 models/user.rb class User < ApplicationRecord enum role: [ :general, :admin ] end 上記のように定義します。 「role」の部分がカラム名です。 その横に配列として、使いたい名前を定義していき、左から順番に0から数字が紐づけられていきます。 上の例でいうと、 「general: 0, admin: 1」という風になります。 ちなみ後から下記のように定義を追加した場合、 models/user.rb class User < ApplicationRecord enum role: [ :general, :editor, :admin ] end 「general: 0, editor: 1, admin: 2」と言う風になります。 つまり、後から定義したとしても、数字は前から順に割り振られていきます。 なので、仮にすでにデータベースに「admin」が保存されていた場合、 それが「editor」ということになってしまいます。 2.名前とそれに対応する数値を定義する方法 models/user.rb class User < ApplicationRecord enum role: { general: 0, admin: 1 } end こちらの方法は、明示的に名前と対応する数字を定義します。 ちなみに、こちらの方法は後から定義を追加する場合は 下記のように数字を明示的に指定する必要があります。 models/user.rb class User < ApplicationRecord enum role: { general: 0, editor: 2, admin: 1 } end なので、先程のようにデータが途中から変わってしまうというようなことは起こり得ません。 明示的に指定したほうがわかりやすいので私はこちらの方法を推奨します。 また、一番最初に定義している値、つまり「general: 0」の値をデフォルトとしてモデルに定義すると、より良いです。 マイグレーションファイル class AddRoleToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :role, :integer, null: false, default: 0 end end コンソールで使ってみる コンソール irb(main):001:0> User.roles ※定義済のカラムを参照 => {"general"=>0, "admin"=>1} ※ハッシュ形式で保存されている irb(main):002:0> User.roles[:general] ※ハッシュなので一項目だけ参照することも可能 => 0 irb(main):003:0> User.roles[:admin] => 1 irb(main):004:0> user = User.first irb(main):005:0> user.general? ※このような便利なメソッドも使えるようになります。 => false irb(main):006:0> user.admin? => true enum_helpって何?? enum_helpとはenumで定義した値をi18n化させることができるgemです! 現段階では下記のようになります。 コンソール irb(main):007:0> user.role => "admin" ※これを日本語にしたい! 導入方法 導入方法は簡単! enum_helpというgemを追加して、localeに翻訳を設定するだけです! Gemfile gem 'enum_help' $ bundle install config/locales/activerecord/ja.yml ja: enums: user: role: general: 一般 admin: 管理者 コンソール irb(main):007:0> user.role => "admin" irb(main):008:0> user.role_i18n ※カラム名のあとに「_i18n」をつけて呼び出す => "管理者" enum_helpを導入すると、便利なヘルパーも追加されます。 コンソール irb(main):012:0> User.roles => {"general"=>0, "admin"=>1} irb(main):011:0> User.roles_i18n => {"general"=>"一般", "admin"=>"管理者"} irb(main):013:0> User.roles_i18n.invert => {"一般"=>"general", "管理者"=>"admin"} ※キーとバリューが入れ替わる! このinvertメソッドはセレクトボックスを使う時やransackを利用した時などに有効です! ※間違いなどあればコメントください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6MSAccess

Rails6とMSAccessの接続テスト(OK) https://github.com/JFLABO/Rails6MSAccess Microsoft Access Driver (*.mdb, *.accdb) is the name of Microsoft's ODBC driver for Microsoft Access. It is only available for Windows. require 'dbi' require "nkf" def to_sjis(str) str ? NKF.nkf('-s', str) : "" end def to_utf8(str) str ? NKF.nkf('-w', str) : "" end def create_database_handle # プログラムでODBCの接続文字列を指定する場合 # 上:ローカルドライブ、下:ネットワーク越しの書き方 # dsn = %q(DBI:ODBC:Driver={Microsoft Access Driver (.mdb, *.accdb)};Dbq=c://db/Sample.accdb;) # dsn = %q(DBI:ODBC:Driver={Microsoft Access Driver (.mdb, .accdb)};Dbq=//127.0.0.1/db/Sample.accdb;) dsn = %q(DBI:ODBC:Driver={Microsoft Access Driver (.mdb, *.accdb)};Dbq=j://sample.accdb;) DBI.connect(dsn) end sql = "SELECT * From lins" dbh = create_database_handle sth = dbh.prepare("SELECT * from links") sth.execute() while row = sth.fetch do print "Message", "\n"; print "-------------------\n"; print to_utf8(row["title"]) end sth.finish dbh.disconnect
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecをインストール

【RSpecをインストール】 Dockerで開発したRailsアプリのテストコードを書きます。 RSpecを使用するのでインストール手順を備忘録として残します。 RSpecをインストール Gemfileに下記のように追記し、 $ docker-compose build を実行する。 Gemfile group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'rspec-rails', '~> 5.0.0' end 次に、$ docker-compose run web bundle exec rails g rspec:install を実行し、RSpecに必要なディレクトリや設定ファイルを作成する。 //実行結果 Running via Spring preloader in process 26 create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb ※ spec/spec_helper.rb は、全体的な設定を記述するファイル ※ spec/rails_helper.rb は、Rails特有の設定を記述するファイル
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseを介したユーザー管理機能

gemファイルにdeviseを入力してbundle installするだけで使用できると思っていたので、記事にしておきます。 gem 'devise' をgemfileに入力してbundle install rails g devise:install 上記をターミナルに入力して実行しないとしようできません。 rails g devise モデル名 でdeviseを介したモデルを作成できます
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Postmanを使用してRailsAPIの確認(CRUD)

はじめに RailsでrestfulなAPIを作成したので今回は、postmanで確認する方法を重点に置いて自分用メモとしてまとめていこうと思います! ※API作成手順は以下の記事を参考にさせて頂きました。 Railsで超簡単API Railsガイド postman初期設定 1,「new」を選択し、「collection」を作成。  2,「new」で「request」を作成。 Requestで「name」(任意)を入力し、先ほど作成した「collection」を選択し、「save to create」で作成。 これで下準備は完了し、次からレスポンスの確認にはいる。 GETリクエスト 投稿を全て取得する方法 画面は以下のような感じ 1,「GET」を選択し、URLを入力する。 今回は、(http://localhost:3000/api/v1/posts) を入力する。 ※rails routesで確認する。 2,rails server起動 3,「save」して「send」を実行すると、以下のようにJSON形式で値を取得することができる。 ※rails sしていないと以下の画面が表示される。 POSTリクエスト 1,「POST」を選択し、同じくURLを入力。 2,「body」→「raw」→「JSON」をそれぞれ選択する。 3,JSON形式で値を入力する。 4,「save」→「Send」 5,getに戻り、データが追加されていることが確認できる。 UPDATEリクエスト 1,「PUT」を選択し、同じくURLを入力。 2,paramsで編集する値を指定。 3,「body」→「raw」→「JSON」それぞれ選択し、編集したいデータを入力する。 4,「send」を実行すると、上書きされる。 DELETEリクエスト 1,「DELETE」を選択し、同じくURLを入力。 2,paramsで削除する値を指定し、「send」で実行。 3,getでtitle5が消えていることが確認できる。 まとめ ・簡単ではありましたが、postmanを使用し、CRUDのリクエストを確認していきました。 ・今後もっとうまく活用できるようにしていきたいと思います。 最後まで読んでいただきありがとうございました! 少しでも参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ナビゲーションメニューの色を随時変更する方法

※プログラミング初学者の方用に記事を書いています。 自分が遭遇したエラーなどに同じように遭遇するかもなのでご参考になれば そもそもナビゲーションメニューって何? ナビゲーションメニューとは、サイトやブログの上部やサイドなどに並んでいるリンクのことです。 ※bootstrapの公式サイト↓ たとえば上記の画像だと、自分は「ホーム」のページにいるので 「ホーム」のところが白くなっています。 こんな感じで、サイト上で自分が今いる項目の部分の色を変えるための方法を解説していきます! 完成品の画像 左のナビゲーションバーに注目! ※bootstrapが入っている前提 解説 上記の画像のように色をつけるためには、自分のいるリンクに 「'nav-link active'」というclassをつけることで実装できます。 つまり、「掲示板一覧」にいるときは、そこに対応するリンクのclassに対して「active」を付与し、「ユーザー一覧」にいるときは、そこに対応するリンクのclassに対して「active」を付与するというようにします。 ※実装する前のコード _sidebar.html.erb <!-- Sidebar Menu --> <nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false"> <li class="nav-item"> <%= link_to admin_boards_path, class:"nav-link" do %> <i class="nav-icon far fa-file"></i> <p> 掲示板一覧 </p> <% end %> </li> <li class="nav-item"> <%= link_to admin_users_path, class:"nav-link" do %> <i class="nav-icon far fa-user"></i> <p> ユーザー一覧 </p> <% end %> </li> </ul> </nav> <!-- /.sidebar-menu --> 上記の「link_to」の部分のclassに、状況に応じて'active'というclassを付与するためのヘルバーメソッドを定義します。 helpers/application_helper.rb def active_class(controller_name) controller_name == params[:controller]? 'active' : '' end ※三項演算子なので下記とイコール def active_class(controller_name) return 'active' if controller_name == params[:controller] end 上記のように、引数としてコントローラの名前を渡し、 それがparamsに入っているcontrollerと一致していれば'active'を返すようにします。 「掲示板一覧」をクリックしたときは、'boards'コントローラのindexアクションが動き、 「ユーザー一覧」をクリックしたときは、'users'コントローラが動くので、 状況に応じて'active'を与えることができます。 先程のビューに、定義したメソッドを呼び出すための記述をします。 _sidebar.html.erb <!-- Sidebar Menu --> <nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false"> <li class="nav-item"> <%= link_to admin_boards_path, class:"nav-link #{active_class("boards")}" do %> <i class="nav-icon far fa-file"></i> <p> 掲示板一覧 </p> <% end %> </li> <li class="nav-item"> <%= link_to admin_users_path, class:"nav-link #{active_class("users")}" do %> <i class="nav-icon far fa-user"></i> <p> ユーザー一覧 </p> <% end %> </li> </ul> </nav> <!-- /.sidebar-menu --> 引数には、対応するコントローラの名前を渡します。 これで完成! ※間違いなどあればコメントください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】graphql-rubyでのオフセットベースのページネーションの実装方法【Graphql】

要件からカーソルベースではなくオフセットベースのページネーションを作らないといけなかったがスムーズにいかなかったので当時のメモ。先輩にも助けて頂いた。 使用技術は フロントエンド - nuxt + vuetify バックエンド - rails + Graphql という組み合わせ 実現方法としては - カーソルベースで一度頭から該当ページのカーソルIDを取ってそれを元に1ページの個数をとるという2度APIを取る方法 - graphql-rubyにコードを追加してoffsetやtotal countを返す方法 の二つ考えられるが今回は後者を書く。 app/graphql/offset_extension.rb class OffsetExtension < GraphQL::Schema::Field::ConnectionExtension def apply super field.argument :offset, 'Int', 'Offset value.', required: false end def after_resolve(args) offset = args[:memo][:offset] new_args = offset ? args.merge(value: args[:value].offset(offset)) : args super(new_args) end end app/graphql/types/base_field.rb module Types class BaseField < GraphQL::Schema::Field argument_class Types::BaseArgument connection_extension(::OffsetExtension) end end app/graphql/types/pagination_connection.rb module Types class PaginationConnection < GraphQL::Types::Relay::BaseConnection field :total_count, Int, null: false def total_count object.items.size object.items.unscope(:offset).unscope(:limit).count end end end 参考 GraphQL RubyのPaginationについて|Daiki Tanaka|note GraphQLでのPaginationの実装方法について(for ruby) - Qiita graphql-rubyでページネーションがサクッと実装できたのでGemが何をやっているのか覗いてみた - Qiita RailsでGraphQL APIを作る時に悩んだ5つのこと | スペースマーケットブログ Offset based pagination in GraphQL-ruby - Blog by Abhay Nikam Introduction to pagination in GraphQL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Error: Undefined method `authenticates_with_sorcery!' for User:Class」の対処法

※プログラミング初学者の方用に記事を書いています。 自分が遭遇したエラーなどに同じように遭遇するかもなのでご参考になれば sorceryを導入しようとしたときに遭遇したエラー 公式のwiki通りにGemfileに'sorcery'と記載して下記コマンドを実行。 $ rails g sorcery:install $ rails db:migrate その際にこんなエラーに遭遇。 Error: Undefined method `authenticates_with_sorcery!' for User:Class まあ簡単に訳すと Userクラスに対して`authenticates_with_sorcery!'なんてメソッドは 定義されてないので使えませんよー! みたいな感じです!笑 `authenticates_with_sorcery!'とはなんぞやって話なんですけど、 sorceryをインストールするとUserモデルやマイグレーションファイルが生成されます。 その際に、Userモデルに先程の`authenticates_with_sorcery!'メソッドが定義されます。 models/user.rb class User < ApplicationRecord authenticates_with_sorcery! end このメソッドをUserモデルに定義することで、 認証機能などのsorceryの機能がもろもろ使えるようになります。 なので、sorceryをインストールした時点で勝手に定義されるので、こんなエラーが出るのがおかしいわけなんです笑 そこでuser.rbを確認してみたものの、やはりしっかりと定義されていたんです! あれ?っとなるわけです。 そこで生成されたマイグレーションファイルを確認してみたところ、、、 class SorceryCore < ActiveRecord::Migration[5.2] def change create_table :User do |t| t.string :email, null: false t.string :crypted_password t.string :salt t.timestamps null: false end add_index :User, :email, unique: true end end create_tableのところが「User」になっていたわけです。 通常、generateコマンドでUserモデルを作成すると こんな感じのマイグレーションファイルになると思います。 $ rails g model User name:string email:string class CreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :name t.string :email t.timestamps end end end Userテーブルではなく、usersテーブルが作成されるはずなんです。 つまり今回のエラーの原因はここです。 なので、先程のマイグレーションファイルの 「User」のところを「users」に変えてあげれば、 class SorceryCore < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :email, null: false t.string :crypted_password t.string :salt t.timestamps null: false end add_index :users, :email, unique: true end end $ rails db:migrate 問題なくマイグレートできます! ※間違いなどあればコメントください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]大変便利なFormオブジェクトはご存知でしょうか?

はじめに 突然ですが、「Formオブジェクト」というものを知っていますでしょうか? 便利なものなのですが、意外と使われていない人もいるかと思います。 これを知ってることで、開発がよりスムーズに進無こと間違いなしです!! Formオブジェクトとは? モデルとフォームの責務を切り分けられる事で、単体のモデルに依存しない場合や、フォーム専用の特別な処理をモデルに書きたくない場合に用いたりします。 Railsのフォームは基本的にモデルに依存しています。例えば1つのフォーム送信で複数のモデルの更新をしたい場合バリデーションの責務が曖昧なものとなり、可読性も低下するため、責務を明確にするということで使います。 1つのフォームで複数モデルの操作をしたいときにForm Objectを使うと、処理がすっきりかける。またログインに関する処理など、特定のフォームでしか行わない処理もForm Objectに書くと良い。 メリット modelとformで入力値の検証かビジネスロジックの検証かを分別することができる。 一度のフォーム送信時に複数の ActiveRecordモデルを更新しやすくできる。 同じ処理をmodelに対して行う際にForm層の再利用が可能になる。 ActiveRecordと同じバリデーションを使うことができる。 Formオブジェクトにパラメーターを渡すことができるためエラーメッセージを戻す処理が書きやすい。 ビジネスロジックをcontroller/viewから切り離せる(単一責務の状態)。 ユースケース ユーザからの問い合わせフォームの実装 1つの記事に複数の画像を保存する処理(has_manyな関係) ユーザーのログイン処理 APIのパラメーターに対するバリデーション 使い方 1.appディレクトリにformsディレクトリを作成 $ mkdir app/forms 2.利用したいフォームのFormオブジェクトを作成 $ touch app/forms/sample_form.rb 3.作成したファイルに、バリデーションや使用するメソッドを記述していく。 app/forms/sample_form.rb class SampleForm include ActiveModel::Model # 通常のモデルのようにvalidationなどを使えるようにする include ActiveModel::Attributes # ActiveRecordのカラムのような属性を加えられるようにする attribute :first_name, :string attribute :last_name, :string attribute :email, :string attribute :body, :string validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true validates :body, presence: true def save 処理 end end コントローラーにも記述する。 class SamplesController < ApplicationController ... def new @sample = SampleForm.new end def create @sample = SampleForm.new(sample_params) if @sample.save redirect_to :samples, notice: 'サンプル情報を作成しました。' else render :new end end private def sample_params params.require(:sample).permit(:first_name, :last_name, :email, :body) end end 終わりに 開発の規模が大きくなるほどに、コントローラーやモデルは膨大になり、ごちゃごちゃしてきます。 そういう時に、処理ごとに切り分けてまとめることは可読性も上がってとても重要なことです。 状況を見て利用してみても良いかなと思います! 参考 【Rails】FormObjectを使ってほしい RailsのForm Objectについて 【Rails】Form Objectを使ってModelに依存しないFormを作成する 【Ruby on Rails】フォームオブジェクトを使って検索キーワードをcontrollerに送る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリに非同期通信のいいね機能を実装

はじめに Rialsアプリに非同期通信のいいね機能を実装しました。 方針としては、部分テンプレートを作成してレスポンスをJS形式で返すことで画面の一部のみを更新するというものです。備忘録としてまとめたいと思います。 実装イメージ 非同期でいいねといいねの解除ができる いいねされたらカウントに反映される 開発環境 macOS Catalina Ruby 2.6.5 Ruby on Rails 5.2 前提 userとpost(投稿)モデルを作成済み fontawesomeを導入済み jQueryを導入済み 目次 1.モデルとアソシエーション 2.ルーティング 3.コントローラー 4.view 1. モデルとアソシエーション モデルとアソシエーションの構成は以下の通りです。 ではfavoriteモデルを作成します。 ターミナル $ rails g model favorite user:references post:references $ rails db:migrate 次にアソシエーションを組みます。 user.rb has_many :posts, dependent: :destroy has_many :favorites, dependent: :destroy # すでにいいねしているかを判定するメソッド def already_favorited?(post) self.favorites.exists?(post_id: post.id) end already_favorited?メソッドを定義します。 これはもし投稿にいいねしていたら解除のリンクを表示させ、解除していたらいいねリンクを表示させる条件分岐のために記述しています。 post.rb belongs_to :user has_many :favorites, dependent: :destroy favorite.rb belongs_to :user belongs_to :post 2. ルーティング いいねは投稿ひとつひとつに紐付いているのでルーティングはネストさせて記述します。 ネストさせることでアソシエーション先のレコードのidをparamsに追加してコントローラーに渡せるようになります。 (今回の場合はfavoriteのidを取得できる) routes.rb resources :posts, only: [:index, :new, :create, :show, :destroy] do resource :favorites, only: [:create, :destroy] end 3. コントローラー favorites_controller.rbではcreateとdestroyアクションを定義します。 favorites_controller.rb class FavoritesController < ApplicationController before_action :set_post def create @favorite = Favorite.create(user_id: current_user.id, post_id: @post.id) @favorite.save end def destroy @favorite = Favorite.find_by(user_id: current_user.id, post_id: @post.id) @favorite.destroy end private def set_post @post = Post.find(params[:post_id]) end end set_post ・before_actionを設定することで、アクション実行前にどの投稿に対するものなのかを判断するためにidを取得します。 posts_controller.rb def show @post= Post.find(params[:id]) end 4. view 非同期通信をするために_favorite.html.erbという部分テンプレートを作成します。 更にcreateとdestroyアクション実行時にページの一部を更新するためにcreate.js.erbとdestroy.js.erbというファイルを作成します。 JS形式でレスポンスするためにjs.erbという拡張子になっています。 ディレクトリ構成は以下の通りです。 views |-posts | |-show.html.erb |-favorites |-_favorite.html.erb |-create.js.erb |-destroy.js.erb まずは、投稿詳細ページのviewから記述します。 posts/show.html.erb <div id="favorite_area_<%= post.id %>", class="favoriteArea"> <%= render partial: "favorites/favorite", locals: { post: @post } %> </div> divタグにfavorite_area_<%= post.id %>というidを付与しています。 これはどの投稿に対するものなのかを判別するために記述しています。 またrenderメソッドで_favorite.html.erbファイルを呼び出しています。 localsオプションではpostコントローラーで定義した@postの変数を部分テンプレートのなかでpostとして使用できるように定義しています。 次に部分テンプレートです。 favorites/_favorite.html.erb <% if current_user.already_favorited?(post) %> <%= link_to post_favorites_path(post), method: :delete, class: "goodLink", remote: true do %> <i class="fas fa-heart"></i> <% end %> <% else %> <%= link_to post_favorites_path(post), method: :post, class: "goodLink", remote: true do %> <i class="far fa-heart"></i> <% end %> <% end %> <p class="favoriteCount"><%= post.favorites.count %></p> user.rbで定義したalready_favorited?メソッドを使用しています。 link_toにはremote: tureを記述することで非同期通信をしますという意味になります。これでHTML形式ではな先程作成したjs.erbファイルを返す挙動になります。 最後の行の<%= post.favorites.count %>はcountメソッドを使用していいねされている数を表示しています。 最後にjs.erbファイルの編集です。 favorites/create.js.erb <!-- #favorite_area_<%= @post.id %>この部分のHTMLだけ、renderで部分的に更新するという処理 --> $("#favorite_area_<%= @post.id %>").html("<%= j(render partial: 'favorites/favorite', locals: { post: @post }) %>"); favorites/destroy.js.erb <!-- #favorite_area_<%= @post.id %>この部分のHTMLだけ、renderで部分的に更新するという処理 --> $("#favorite_area_<%= @post.id %>").html("<%= j(render partial: 'favorites/favorite', locals: { post: @post }) %>"); 意味としてはfavorite_area_<%= @post.id %>というidを付与したdivタグの中身を_favorite.html.erbの内容で更新するという内容になります。 以上で非同期のいいね機能を実装できていると思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveHash を使って記事に紐付けた都道府県データを元に、地方別の投稿一覧を作る

前提 自作のアプリケーションで文化施設の情報を投稿できるものを作っていて、 投稿時に入力した都道府県のデータを元に、地方ごとで一覧表示できるようにしました。 そもそも投稿の時に地方の情報も一緒に登録するとか、 地方ごとに同じようなコードを書いているのであまりよろしくないとか改善の余地はたくさんあるとは思いますが、 とりあえず動かすことはできたという記録のために記事を作成します。 都道府県データ prefecture.rb class Prefecture < ActiveHash::Base self.data = [ { id: 1, name: '---' }, { id: 2, name: '北海道' }, { id: 3, name: '青森県' }, { id: 4, name: '岩手県' }, { id: 5, name: '宮城県' }, { id: 6, name: '秋田県' }, { id: 7, name: '山形県' }, { id: 8, name: '福島県' }, { id: 9, name: '茨城県' }, { id: 10, name: '栃木県' }, { id: 11, name: '群馬県' }, { id: 12, name: '埼玉県' }, { id: 13, name: '千葉県' }, { id: 14, name: '東京都' }, { id: 15, name: '神奈川県' }, { id: 16, name: '新潟県' }, { id: 17, name: '富山県' }, { id: 18, name: '石川県' }, { id: 19, name: '福井県' }, { id: 20, name: '山梨県' }, { id: 21, name: '長野県' }, { id: 22, name: '岐阜県' }, { id: 23, name: '静岡県' }, { id: 24, name: '愛知県' }, { id: 25, name: '三重県' }, { id: 26, name: '滋賀県' }, { id: 27, name: '京都府' }, { id: 28, name: '大阪府' }, { id: 29, name: '兵庫県' }, { id: 30, name: '奈良県' }, { id: 31, name: '和歌山県' }, { id: 32, name: '鳥取県' }, { id: 33, name: '島根県' }, { id: 34, name: '岡山県' }, { id: 35, name: '広島県' }, { id: 36, name: '山口県' }, { id: 37, name: '徳島県' }, { id: 38, name: '香川県' }, { id: 39, name: '愛媛県' }, { id: 40, name: '高知県' }, { id: 41, name: '福岡県' }, { id: 42, name: '佐賀県' }, { id: 43, name: '長崎県' }, { id: 44, name: '熊本県' }, { id: 45, name: '大分県' }, { id: 46, name: '宮崎県' }, { id: 47, name: '鹿児島県' }, { id: 48, name: '沖縄県' } ] include ActiveHash::Associations has_many :posts end ActiveHash については割愛しますが、上のように都道府県ごとにid で投稿に紐付けられるようにしています。 ルーティング routes.rb Rails.application.routes.draw do devise_for :users root to: "posts#index" resources :posts do collection do get 'search' get 'search_kanto' end end end モデル post.rb class Post < ApplicationRecord belongs_to :user has_one_attached :image has_many :favorites, dependent: :destroy extend ActiveHash::Associations::ActiveRecordExtensions belongs_to :prefecture # 〜省略〜 def self.search_kanto Post.where(prefecture_id: 9..15) end end where句で指定の都道府県が登録されている投稿を取得しています。 コントローラー posts_controller.rb class PostsController < ApplicationController # 〜省略〜 def search_kanto @posts_kanto = Post.search_kanto.order('created_at DESC') end end @posts_kantoに取得した投稿を代入して、ビューファイルで表示できるようにしています。 学んだこと where句を使って検索条件を指定する部分が自分でやってみて多少は理解できたかなというところです。 最初にも書いたように地方ごとにほぼ同様のコードを書いているので、そこは何とかシンプルにできるようにしたいです。 拙い内容とは思いますが読んでいただいてありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]sessionとcookies。得意になりたくない?

はじめに Railsチュートリアルでも出てきました「session」と「cookies」の使い方がかなり難しいなと感じたのですが、同じ気持ちになった人は、たくさんいるかと思います。。 初心者では、チュートリアル完遂後にすぐ理解して使いこなすのは難しいと思うので、この記事を見れば「使い方がわかった!」というレベルまで持っていけたらと思います! sessionについて sessionとは、ステートフルな通信を実現するための仕組みのことです。 sessionは主にログイン機能に使用されるもので、ログイン状態を持続させるためにあります。 また、sessionの情報はRails標準では、ブラウザ側のクッキーに保存されるということも合わせて覚えておいてください。 sessionは明示的に削除する(又は有効期限切れになる)まで消えないので、不要になった際には削除するようにしましょう。 sessionの使い方 セッションに情報を保存する # session[キー] = 値 session[:user_name] = 'test' #=> sessionに[:user_name]という名前をつけて「'test'」という文字を格納 セッション情報を削除 reset_session #=> 全てのsessionを削除 session[:user_name] = nil session[:user_name].clear session.delete(:user_name) #=> [:user_name]の値を削除(3つとも) 現在のセッション情報を取得 session.session_id #=> 現在保存されているsession情報を取得 実際に使ってみる! よくある使い道として、ログイン機能がありますが、簡単に実装手順を示しました! ※Userモデル作成済の前提で行っています。 1.Gemfileに以下を追記する。 gem 'bcrypt' # データベースにパスワードを保存する際に、暗号化してくれる 追記後は、以下コマンドを実行する。 $ bundle install 2.パスワードの暗号化を有効にするための設定をする。 app/models/user.rb ... has_secure_password ... 3.コントローラーにsessionに関する記述を記載する。 app/controllers/sessions_controller.rb class SessionsController < ApplicationController def index if session[:user_name] @notice = "#{session[:user_name]}でログイン済です。" # セッションに情報が保存されるか判断 end if params.key?(:name) || params.key?(:password) user = User.find_by_name(params[:name]) if user && user.authenticate(params[:password]) # パスワードがユーザーがポストした値と一致しているのか判断 session[:user_name] = params[:name] # sessionを設定 else session[:user_name] = nil # sessionを削除 end end end end cookiesについて cookiesとは、クライアント側のブラウザにあるデータ保存領域のこと。 Railsではデフォルトではsessionを管理するためにブラウザのcookieを利用する。 そもそもcookieは、Webアプリがブラウザを通してクライアントにデータを保持させる機能のことで、要するにユーザー側に持たせている情報と思えばいい。クライアントが情報を持っているので、ブラウザを閉じても破棄されないし、暗号化しないと簡単に読み取られて情報を奪われてしまいます。 実際にはDBにもハッシュ化した情報を保存し、次回訪問時にCookieの内容と突き合わせてログイン状態を参照するという方法を取ることが多いです。 cookiesの使い方 cookieの保存 # cookies[:cookie名] = { key: cookie情報 } cookies[:user_name] = "david" #=> cookieに保存 cookies[:lat_lon] = [47.68, -122.37] #=> cookieに配列を保存 cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } #=> cookieにハッシュを保存 オプション 説明 デフォルト値 :value cookieの値 - :path cookieが有効なパス - :domain cookieが有効なドメイン 現在のホスト :expires cookieの有効期限 / :secure 暗号化通信でのみcookieを送信 false :httponly HTTPcookieを有効 false 永続化cookie(有効期間が20年に設定されたクッキー)を設定 # cookies.permanent[cookie名] = 値 cookies.permanent[:user_name] = "Jamie" #=> [:user_name]に"Jamie"を永続化cookieとして保存 署名付きcookie(クライアント側の改ざん防止可能)を設定 # cookies.signed[クッキー名] = 値 cookies.signed[:user_id] = 45 #=> 署名付きcookieを設定 cookieの削除 # cookies.delete(:クッキー名 [, 対象のドメイン、またはパス]) cookies.delete :user_name #=> :user_nameで保存されているcookieを削除 終わりに 使い方自体は意外と簡単なものですね! これをどのように使うのが難しい所ですが、、、、 大体は、ログイン機能や検索条件の保存などが多いと思うので、その場面に直面した際はこの記事で培った知識を発揮してください!!! 参考 【Rails】Sessionの使い方について 【Rails入門】sessionの使い方まとめ Rails ドキュメント クッキー・キャッシュ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 Devise導入 Bootstrap対応/日本語対応/名前登録まで

自作のサービスを作る際、環境構築同様毎度同じことをやるのでまとめておく。 事前準備 rails sでページ表示済み Deviseインストールする。 Gemfileに追加する gem "devise" bundle install bin/rails g devise:install ↑このコマンドを実行すると4つのセットアップ方法が表示されるので従う。 config/environments/development.rb config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 本番用もこの時点で記述しておく config/environments/production.rb # 本番環境ではhost名を指定 config.action_mailer.default_url_options = { host: 'xxxx.com' } ルーティングの設定を行う config/routes.rb root to: "home#index" 次にトップページ用のコントローラーを生成 今回はDeivseの指定に合わせてhome indexで用意。対応させていれば自由に変更OK bin/rails g controller home index 2行のflashメッセージを追加する。 自分はbootstrapのcontainerクラス内かつ、パーシャル化させる。 app/views/layouts/application.html.erb <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> Deviseのビューファイルをコピー bin/rails g devise:views Userモデルを作成 bin/rails g devise User usersテーブルを作成 bin/rails db:migrate ↑要らぬ情報だけど、この時なぜか(この記事内では関係なかったため割愛)usersテーブルが既にありますというエラーが出たためrails db:migrate:resetで対処した。 この時点で、ログインしないとアクセスできないようにする。 もしこれをしない場合は、rails sで確認する際に/users/sign_inで確認することを忘れないよう注意。 作りたいサービスによるが、ログインしていなくてもトップページ一覧は閲覧できるようにしたい場合は、application_controller.rbではなく、トップページ(今回はhome)のControllerに記載+indexのみ除外などの工夫をすべき? app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :authenticate_user! end フォームが正しく表示されているか確認 bin/rails s ↑要らぬ情報2だけど、自分はDocker使用していて、一旦downして再度upしないとルーティングエラーになった。要は色々生成しているから更新しようということ。 これでお馴染みのフォームが生成されていればOK DeviseにBootstrapを適用 必要なものをインストール(バージョンは指定しなくても。) $ yarn add bootstrap@4.4.1 jquery@3.5.1 popper.js@1.16.1 config/webpack/environment.jsに追記 config/webpack/environment.js const { environment } = require('@rails/webpacker') const webpack = require('webpack') environment.plugins.append('Provide', new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', Popper: ['popper.js', 'default'] })) module.exports = environment app/javascript/packs/application.jsに追記 app/javascript/packs/application.js require("bootstrap/dist/js/bootstrap") application.cssの拡張子cssをscssに変更 最後にapp/assets/stylesheets/application.scssに追記 app/assets/stylesheets/application.scss /* *= require_tree . *= require_self */ @import "bootstrap/scss/bootstrap"; Viewを編集 レスポンシブ対応のviewportを忘れずに記載しておく app/views/layouts/application.html.erb .. <%= csrf_meta_tags %> <%= csp_meta_tag %> <meta name="viewport" content="width=device-width,initial-scale=1"> .. <body> <header> <nav class="navbar navbar-expand navbar-light"> <%= link_to "サンプル", root_path, class: 'navbar-brand' %> <div id="Navber"> <ul class="navbar-nav"> <% if user_signed_in? %> <li class="nav-item active"> <%= link_to 'アカウント編集', edit_user_registration_path, class: 'nav-link' %> </li> <li class="nav-item active"> <%= link_to 'ログアウト', destroy_user_session_path, method: :delete, class: 'nav-link' %> </li> <% else %> <li class="nav-item active"> <%= link_to "新規登録", new_user_registration_path, class: 'nav-link' %> </li> <li class="nav-item active"> <%= link_to "ログイン", new_user_session_path, class: 'nav-link' %> </li> <% end %> </ul> </div> </nav> </header> <div class="container"> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> .. devise用のbootstrapのGemを追加する (後の日本語化のGemもこの時点で追加しておきます。) .. # 日本語化 gem 'rails-i18n', '~> 6.0' gem 'devise-i18n' # Bootstrap gem 'devise-bootstrap-views', '~> 1.0' 既にdeviseのviewsをinstallしている場合は、下記コマンドでbootstrap用のテンプレートに変更する (このコマンドを使用すると、devise関連のviewにbootstrap用のclassが追加される) (このコマンドをしないと、ページはbootstrap適用されるが、devise関連が適用されない) **このコマンドを打つと、既にあるviewファイルとコンフリクトを起こすがa (all, overwrite this and all others)で上書きすれば大丈夫 bin/rails g devise:views:bootstrap_templates http://localhost:3000 で確認してみましょう このようになっていれば無事適用 Deviseに名前も登録する まず、usersテーブルにnameを追加する。 emailに適用されているユニークインデックスをnameに適用 rails g migration add_name_to_users name:string:uniq db/migrate/~~~~_add_name_to_users.rb class AddNameToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :name, :string add_index :users, :name, unique: true end end bin/rails db:migrate nameにバリデーション設定 app/models/user.rb .. validates :username, uniqueness: true, presence: true 設定ファイルで認証キーを変更 config/initializers/devise.rb - # config.authentication_keys = [:email] + config.authentication_keys = [:username] viewファイルを変更 まずは新規登録画面 nameを一番上にする場合は、デフォルトではemailに設定されているautofocusを変更する app/views/devise/registrations/new.html.erb .. <%= bootstrap_devise_error_messages! %> <!-- nameの入力欄を追加 --> <div class="form-group"> <%= f.label :name %> <%= f.text_field :name, autofocus: true, autocomplete: 'name', class: 'form-control' %> </div> <div class="form-group"> <%= f.label :email %> <%= f.email_field :email, autocomplete: 'email', class: 'form-control' %> </div> .. StrongParameterにname追加 app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters added_attrs = [ :name, :email, :password, :password_confirmation ] devise_parameter_sanitizer.permit :sign_up, keys: added_attrs devise_parameter_sanitizer.permit :account_update, keys: added_attrs devise_parameter_sanitizer.permit :sign_in, keys: added_attrs end end ログインも変更 app/views/devise/sessions/new.html.erb .. <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <div class="form-group"> <%= f.label :name %> <%= f.text_field :name, autofocus: true, autocomplete: 'name', class: 'form-control' %> </div> <div class="form-group"> <%= f.label :email %> <%= f.email_field :email, autocomplete: 'email', class: 'form-control' %> </div> .. ここまでで、新規登録/ログインともに名前込みで出来るようになったと思います deviseを日本語に config/application.rb .. config.load_defaults 6.0 # 日本語 config.i18n.default_locale = :ja # タイムゾーン変更 config.time_zone = 'Asia/Tokyo' .. 一度サーバーを落ちしてから確認してみてください。 http://localhost:3000 日本語の変更 下記のコマンドで生成されるdevise.views.ja.yml内を編集することで変更出来る bin/rails g devise:i18n:locale ja アカウント登録⇨新規登録 パスワードを忘れましたか?⇨パスワードの再設定 の方が自然に感じると思います。 参考記事 Devise入門64のレシピ その1 [Rails] deviseの使い方(rails6版) Deviseでログイン機能を追加・日本語化・Bootstrap4適用まで deviseで作成したUserモデルにusernameカラムを追加してDBへ登録できるようにする 【Rails】この記事を「devise 名前 ログイン」で調べたあなたへ贈る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails db:migrate をしたらMysql2::Error: Table 'テーブル名' doesn't exist と怒られる問題

何が起こったのか? migrationファイルを追加していつものように rails db:migrate を叩いたところ、以下のように怒られた。 StandardError: An error has occurred, all later migrations canceled: Mysql2::Error: Table 'Mysql内のテーブル名' doesn't exist <略> Caused by: ActiveRecord::StatementInvalid: Mysql2::Error: Table 'Mysql内のテーブル名' doesn't exist ActiveRecord::StatementInvalidとは? 簡潔に言えば「記述が違うぞ?」ということ。 ※Mysql内のテーブル名は % rails db を叩いてMysqlに入り、SHOW TABLES; で確認できます。 mysql> SHOW TABLES; 確認すべきところ ・ migrationファイルのテーブル名 → タイポしていないかどうか(ex:スペルミスや大文字になっていないかなど) ちなみに自分の場合はsorcery_coreのmigrationファイル内の「create_table :users」が大文字になっていたことが原因でした。 変更前 class SorceryCore < ActiveRecord::Migration[6.0] def change create_table :Users do |t| t.string :email, null: false t.string :crypted_password t.string :salt t.timestamps null: false end add_index :users, :email, unique: true end end 変更後 class SorceryCore < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :email, null: false t.string :crypted_password t.string :salt t.timestamps null: false end add_index :users, :email, unique: true end end タイポに気をつけて 初歩的ではあるものの、意外と気付き難いが故に根深い問題の1つではないでしょうか? こまめに確認して、どこに問題があるのかを探しやすくすることが大切だと痛感しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Elasticsearch-railsのサンプルを触ってみた

環境構築 Rails6/Docker/MySQL + Elasticsearch 環境構築 が完了した時点からのスタート Gem追加 gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git' gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git' bin/bundle install model作成 # コンテナ内に入る docker exec -it railsのコンテナ名 bash bin/rails g model author name:string bin/rails g model publisher name:string bin/rails g model category name:string bon/rails g model manga author:references publisher:references category:references title:string description:text bi/rails db:migrate seedにサンプルデータを投入 seeds.rb # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # # Examples: # # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) # category ct1 = Category.create(name: 'バトル・アクション') ct2 = Category.create(name: 'ギャグ・コメディ') ct3 = Category.create(name: 'ファンタジー') ct4 = Category.create(name: 'スポーツ') ct5 = Category.create(name: 'ラブコメ') ct6 = Category.create(name: '恋愛') ct7 = Category.create(name: '異世界') ct8 = Category.create(name: '日常系') ct9 = Category.create(name: 'グルメ') ct10 = Category.create(name: 'ミステリー・サスペンス') ct11 = Category.create(name: 'ホラー') ct12 = Category.create(name: 'SF') ct13 = Category.create(name: 'ロボット') ct14 = Category.create(name: '歴史') ct15 = Category.create(name: '少女漫画') ct16 = Category.create(name: '戦争') ct17 = Category.create(name: '職業・ビジネス') ct18 = Category.create(name: 'お色気') ct19 = Category.create(name: '学園もの') # 出版社 pb1 = Publisher.create(name: '集英社') pb2 = Publisher.create(name: '講談社') pb3 = Publisher.create(name: '小学館') pb4 = Publisher.create(name: '芳文社') pb5 = Publisher.create(name: '双葉社') # 作者 at1 = Author.create(name: '原泰久') at2 = Author.create(name: '堀越耕平') at3 = Author.create(name: '清水茜') at4 = Author.create(name: '井上雄彦') at5 = Author.create(name: '吉田秋生') at6 = Author.create(name: '野田サトル') at7 = Author.create(name: 'あfろ') at8 = Author.create(name: '神尾葉子') at9 = Author.create(name: '冨樫義博') at10 = Author.create(name: '川上秦樹') at11 = Author.create(name: 'こうの史代') at12 = Author.create(name: '古舘春一') at13 = Author.create(name: '三田紀房') at14 = Author.create(name: '藤沢とおる') # 漫画 Manga.create(title: "キングダム", publisher: pb1, author: at1, category: ct14, description: "時は紀元前―。いまだ一度も統一されたことのない中国大陸は、500年の大戦争時代。苛烈な戦乱の世に生きる少年・信は、自らの腕で天下に名を成すことを目指す!!") Manga.create(title: "僕のヒーローアカデミア", publisher: pb1, author: at3, category: ct1, description: "多くの人間が“個性という力を持つ。だが、それは必ずしも正義の為の力ではない。しかし、避けられぬ悪が存在する様に、そこには必ず我らヒーローがいる! ん? 私が誰かって? HA‐HA‐HA‐HA‐HA! さぁ、始まるぞ少年! 君だけの夢に突き進め! “Plus Ultra!!") Manga.create(title: "はたらく細胞", publisher: pb2, author: at3, category: ct1, description: "人間1人あたりの細胞の数、およそ60兆個! そこには細胞の数だけ仕事(ドラマ)がある! ウイルスや細菌が体内に侵入した時、アレルギー反応が起こった時、ケガをした時などなど、白血球と赤血球を中心とした体内細胞の人知れぬ活躍を描いた「細胞擬人化漫画」の話題作、ついに登場!!肺炎球菌! スギ花粉症! インフルエンザ! すり傷! 次々とこの世界(体)を襲う脅威。その時、体の中ではどんな攻防が繰り広げられているのか!? 白血球、赤血球、血小板、B細胞、T細胞...etc.彼らは働く、24時間365日休みなく!") Manga.create(title: "スラムダンク SLAM DUNK 新装再編版", publisher: pb1, author: at4, category: ct4, description: '中学時代、50人の女の子にフラれた桜木花道。そんな男が、進学した湘北高校で赤木晴子に一目惚れ! 「バスケットは…お好きですか?」。この一言が、ワルで名高い花道の高校生活を変えることに!!') Manga.create(title: "BANANA FISH バナナフィッシュ 復刻版全巻BOX", publisher: pb3, author: at5, category: ct15, description: 'フラワーコミックスの黄色いカバーを完全再現!!吉田秋生の不朽の名作が復刻版BOXとなって登場しました。フラワーコミックスの黄色いカバーを完全再現したコミックスと、特典ポストカードをセットにした完全保存版。ポストカードはファン垂涎の、アッシュ・英二のイラストをセレクトしたここでしか手に入らないオリジナルです。') Manga.create(title: "ゴールデンカムイ", publisher: pb1, author: at6, category: ct1, description: '『不死身の杉元』日露戦争での鬼神の如き武功から、そう謳われた兵士は、ある目的の為に大金を欲し、かつてゴールドラッシュに沸いた北海道へ足を踏み入れる。そこにはアイヌが隠した莫大な埋蔵金への手掛かりが!? 立ち塞がる圧倒的な大自然と凶悪な死刑囚。そして、アイヌの少女、エゾ狼との出逢い。『黄金を巡る生存競争』開幕ッ!!!!') Manga.create(title: "ゆるキャン△", publisher: pb4, author: at7, category: ct8, description: '富士山が見える湖畔で、一人キャンプをする女の子、リン。一人自転車に乗り、富士山を見にきた女の子、なでしこ。二人でカップラーメンを食べて見た景色は…。読めばキャンプに行きたくなる。行かなくても行った気分になる。そんな新感覚キャンプマンガの登場です!') Manga.create(title: "花のち晴れ〜花男 Next Season〜", publisher: pb1, author: at8, category: ct6, description: '英徳学園からF4が卒業して2年…。F4のリーダー・道明寺司に憧れる神楽木晴は、「コレクト5」を結成し、学園の品格を保つため“庶民狩りを始めた!! 隠れ庶民として学園に通う江戸川音はバイト中に晴と遭遇し!?') Manga.create(title: "HUNTER×HUNTER ハンター×ハンター", publisher: pb1, author: at9, category: ct1, description: '父と同じハンターになるため、そして父に会うため、ゴンの旅が始まった。同じようにハンターになるため試験を受ける、レオリオ・クラピカ・キルアと共に、次々と難関を突破していくが…!?') Manga.create(title: "転生したらスライムだった件", publisher: pb2, author: at10, category: ct7, description: '通り魔に刺されて死んだと思ったら、異世界でスライムに転生しちゃってた!?相手の能力を奪う「捕食者」と世界の理を知る「大賢者」、2つのユニークスキルを武器に、スライムの大冒険が今始まる!異世界転生モノの名作を、原作者完全監修でコミカライズ!') Manga.create(title: "この世界の片隅に", publisher: pb5, author: at11, category: ct16, description: '平成の名作・ロングセラー「夕凪の街 桜の国」の第2弾ともいうべき本作。戦中の広島県の軍都、呉を舞台にした家族ドラマ。主人公、すずは広島市から呉へ嫁ぎ、新しい家族、新しい街、新しい世界に戸惑う。しかし、一日一日を確かに健気に生きていく…。') Manga.create(title: "スラムダンク SLAM DUNK", publisher: pb1, author: at4, category: ct4, description: '中学3年間で50人もの女性にフラれた高校1年の不良少年・桜木花道は背の高さと身体能力からバスケットボール部の主将の妹、赤木晴子にバスケット部への入部を薦められる。彼女に一目惚れした「初心者」花道は彼女目当てに入部するも、練習・試合を通じて徐々にバスケットの面白さに目覚めていき、才能を開花させながら、全国制覇を目指していくのであったが……。') Manga.create(title: "ハイキュー!!", publisher: pb1, author: at12, category: ct4, description: 'おれは飛べる!! バレーボールに魅せられ、中学最初で最後の公式戦に臨んだ日向翔陽。だが、「コート上の王様」と異名を取る天才選手・影山に惨敗してしまう。リベンジを誓い烏野高校バレー部の門を叩く日向だが!?') Manga.create(title: "インベスターZ", publisher: pb2, author: at13, category: ct17, description: '創立130年の超進学校・道塾学園に、トップで合格した財前孝史。入学式翌日に、財前に明かされた学園の秘密。各学年成績トップ6人のみが参加する「投資部」が存在するのだ。彼らの使命は3000億を運用し、年8%以上の利回りを生み出すこと。それゆえ日本最高基準の教育設備を誇る道塾学園は学費が無料だった!「この世で一番エキサイティングなゲーム、人間の血が最も沸き返る究極の勝負……それは金……投資だよ!」') Manga.create(title: "GTO", publisher: pb2, author: at14, category: ct19, description: "かつて最強の不良「鬼爆」の一人として湘南に君臨した鬼塚英吉は、辻堂高校を中退後、優羅志亜(ユーラシア)大学に替え玉試験で入学した。彼は持ち前の体力と度胸、純粋な一途さと若干の不純な動機で、教師を目指した。無茶苦茶だが、目先の理屈よりも「ものの道理」を通そうとする鬼塚の行為に東京吉祥学苑理事長の桜井良子が目を付け、ある事情を隠して中等部の教員として採用する。学園内に蔓延する不正義や生徒内に淀むイジメの問題、そして何より体面や体裁に振り回され、臭いものに蓋をして見て見ぬ振りをしてしまう大人たち、それを信じられなくなって屈折してしまった子どもたち。この学園には様々な問題が山積していたのである。桜井は、鬼塚が問題に真っ向からぶつかり、豪快な力技で解決してくれることに一縷の望みを託すようになる。") bin/rails db:seed Controller, View, Routing を追加 bin/rails g controller Mangas index app/controllers/mangas_controller.rb class MangasController < ApplicationController def index @mangas = Manga.all end end config/route.rb Rails.application.routes.draw do root 'mangas#index' resources :mangas, only: %i(index) end app/views/mangas/index.html.erb <h1>Mangas</h1> <table> <thead> <tr> <th>Aauthor</th> <th>Publisher</th> <th>Category</th> <th>Author</th> <th>Title</th> <th>Description</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @mangas.each do |manga| %> <tr> <td><%= manga.author.name %></td> <td><%= manga.publisher.name %></td> <td><%= manga.category.name %></td> <td><%= manga.author.name %></td> <td><%= manga.title %></td> <td><%= manga.description %></td> </tr> <% end %> </tbody> </table> この時点でページを確認しようと、dockerを再度upしたところ、Gemfileが変更されていることによって?、railsのコンテナが起動しなかったため、buildしなおした。 docker-compose build もしくは、bundle install するなら、 docker-compose run web bundle install ここで一旦ページにseedで登録した内容が表示されているか確認しましょう ここからelasticsearchの部分に入っていきます configの設定 config/initializers/elasticsearch.rb # 「elasticsearch」はdocker-composeのservicesに設定した名前に合わせる config = { host: ENV['ELASTICSEARCH_HOST'] || "elasticsearch:9200/", } Elasticsearch::Model.client = Elasticsearch::Client.new(config) concernsの設定 app/models/manga.rb class Manga < ApplicationRecord include MangaSearchable belongs_to :author belongs_to :publisher belongs_to :category end app/models/concerns/manga_searchable.rb module MangaSearchable extend ActiveSupport::Concern included do include Elasticsearch::Model # Callbacksを指定すると、テーブルの更新時にelasticsearchも更新される include Elasticsearch::Model::Callbacks # ①index名 index_name "es_manga_#{Rails.env}" # ②マッピング情報 settings do mappings dynamic: 'false' do indexes :id, type: 'integer' indexes :publisher, type: 'keyword' indexes :author, type: 'keyword' indexes :category, type: 'text', analyzer: 'kuromoji' indexes :title, type: 'text', analyzer: 'kuromoji' indexes :description, type: 'text', analyzer: 'kuromoji' end end # ③mappingの定義に合わせてindexするドキュメントの情報を生成する def as_indexed_json(*) attributes .symbolize_keys .slice(:id, :title, :description) .merge(publisher: publisher_name, author: author_name, category: category_name) end end def publisher_name publisher.name end def author_name author.name end def category_name category.name end class_methods do # ④indexを作成するメソッド def create_index! client = __elasticsearch__.client # すでにindexを作成済みの場合は削除する client.indices.delete index: self.index_name rescue nil # indexを作成する client.indices.create(index: self.index_name, body: { settings: self.settings.to_hash, mappings: self.mappings.to_hash }) end end end このファイル内の動きはこの後説明(自身もまず動くものを確認したいので後ほど) 動作確認 bin/rails c elasticsearchの接続確認 pry(main)> Manga.__elasticsearch__.client.cluster.health => {"cluster_name"=>"docker-cluster", "status"=>"green", "timed_out"=>false, "number_of_nodes"=>1, "number_of_data_nodes"=>1, "active_primary_shards"=>0, "active_shards"=>0, "relocating_shards"=>0, "initializing_shards"=>0, "unassigned_shards"=>0, "delayed_unassigned_shards"=>0, "number_of_pending_tasks"=>0, "number_of_in_flight_fetch"=>0, "task_max_waiting_in_queue_millis"=>0, "active_shards_percent_as_number"=>100.0} indexの作成 pry(main)> Manga.create_index! => {"acknowledged"=>true, "shards_acknowledged"=>true, "index"=>"es_manga_development"} データの登録 Manga.__elasticsearch__.import (1.8ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 Manga Load (1.0ms) SELECT `mangas`.* FROM `mangas` ORDER BY `mangas`.`id` ASC LIMIT 1000 Publisher Load (1.4ms) SELECT `publishers`.* FROM `publishers` WHERE `publishers`.`id` = 1 LIMIT 1 . . 検索機能の追加 ここで自分は詰まったのだが、integer型のidを含めると、数字は検索可能なのだが、テキストはエラーになる。(型の違いによるエラー) multi_match type: 'cross_fields'を指定することで複数typeも検索可能だと思っていたのだが、上手く動作しなかったため、今回idは削除。 config/initializers/elasticsearch.rb class_methods do # ... # indexes :id, type: 'integer' # ... def es_search(query) __elasticsearch__.search({ query: { multi_match: { fields: %w(publisher author category title description), type: 'cross_fields', query: query, operator: 'and' # 検索fieldsによって重み付けしたい時(例:titleでヒットした場合は2倍) # fields: ["title^2", "descritption"] } } }) end end end Controller修正 app/controllers/mangas_controller.rb class MangasController < ApplicationController def index @mangas = if search_word.present? Manga.es_search(search_word).records else Manga.all end end private def search_word @search_word ||= params[:search_word] end end Viewの修正 app/views/mangas/index.html.erb // ... // 検索フォームを追加 <div> <%= form_tag(mangas_path, method: :get) do %> <div> <%= text_field_tag :search_word, @search_word, placeholder: "漫画を検索する" %> </div> <div> <%= submit_tag "検索" %> </div> <% end %> </div> <div> <table> // ... 動作確認 ここまでで一旦動作します。 自分の場合は、なかなか動かず時間がかかりました。 参考記事 RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成 Rails6でElasticsearchのキーワード検索実装ハンズオン elastisearch-railsを使ってRailsでElasticsearchを動かす【初心者向け】 Elastic SearchでString型のカラムが取り込めないエラー Query DSL
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】puma.rbの気になるあれこれ

背景 Railsを起動する時に出てくる「puma」。気になったところを設定ファイルの「puma.rb」を中心に調べてみました。 ❯ rails s #railsを起動した時に目にする画面。Booting Pumaと書いてある。 => Booting Puma => Rails 6.0.3.5 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 3.12.6 (ruby 2.6.5-p114), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://localhost:3000 Use Ctrl-C to stop ^C- Gracefully stopping, waiting for requests to finish === puma shutdown: 2021-04-17 07:49:47 +0900 === - Goodbye! ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 そもそもPumaって何? Railsを動かすアプリケーションサーバーの一つ。他にもMongrel、Unicorn、Thin、Rainbowsなど様々な種類がある。どれもアプリケーションを動かすために必要であり、書いたコードを読み込んで処理を行います。あくまでもアプリケーション内での動きに関わるらしく、Web上でアプリを起動させるためには、webサーバーが必要になります。 puma.rbの中身 max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count port ENV.fetch("PORT") { 3000 } environment ENV.fetch("RAILS_ENV") { "development" } pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } plugin :tmp_restart threads(スレッド)? プログラムの処理の実行単位。これの数が増えると同時に実行できる処理の数が増えるみたいです。 max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count 「max_threads_count」と「min_threads_count」に環境変数の値を代入し、「threads min_threads_count, max_threads_count」と書かれている記述によってpuma実行時に最大のスレッド数と最小のスレッド数を指定することができる。 port? pumaを起動した時にlocal環境のport番号を指定する。最初に「3000」と書いてありますが、この番号を変更するとport番号をrailsを実行する時にport番号を変えられます。 #port番号を3000から3001に変更 port ENV.fetch("PORT") { 3001 } port番号3001にrailsが起動しています。 environment? Pumaが起動する実行環境を指定します。 environment ENV.fetch("RAILS_ENV") { "development" } 「ENV.fetch("RAILS_ENV"){"development"}」の記述で「RAILS_ENV」と言う名前の環境変数を読み込むかそれがない場合には「development」という値が指定されます。 「development」は開発環境を意味し、他に「test」、「production」などがあります。それぞれテスト環境と本番環境を意味します。 pidfile? pid(Process ID)のことで、ここではpumaが起動した際の処理番号です。 pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 環境変数の「PIDFILE」の値を探し、なければ"tmp/pids/server.pid"の値が指定されます。場所は「railsアプリ名/tmp/pids/」です。server.pidをvscodeなどのエディターで開いてみる以下のように番号が書いてありました。 3157 コマンドラインで「ps -ef | grep puma」と入力し、pidを確認してみました。 ❯ ps -ef | grep puma 501 3157 2422 0 8:37AM ttys000 1:04.38 puma 3.12.6 (tcp://localhost:3001) [アプリ名] 501 3700 3321 0 8:54AM ttys002 0:00.01 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox puma 先ほどのserver.pidファイルに書かれていた「3157」のところをみてみると、「8時37分にlocalhost:3001でアプリケーションが実行されている記録が確認できます。 railsをstopし、再度確認してみると、 ❯ ps -ef | grep puma 501 3792 3321 0 8:55AM ttys002 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox puma pid「3157」という番号はありませんでした。どうやらプロセス(pumaを起動)がstopするとpidも削除されるみたいです。念のため、「railsアプリ名/tmp/pids/」のフォルダーをみてみたら、server.pidファイルが削除されていることが分かりました。 plugin pumaに機能を追加する時に使うらしいです。最初から「tmp_restart」というプラグインが記述してあります。「tmp/restart.txtをtouchするとリスタートする」とか「rails restart」コマンドでpumaを再起動できるようにするプラグイン。実際にどういう使い方をするのかについてはあまり知られていません。(というか見つかりませんでした) 他には「puma-heroku」というheroku用の設定を用意してくれるプラグインもあります。 # Gemfile gem 'puma-heroku' # gemfileをインストール bundle install # config/puma.rb plugin :heroku 参考サイト・記事 https://qiita.com/jnchito/items/3884f9a2ccc057f8f3a3 https://qiita.com/Orangina1050/items/9d816017217614bc3ce7 https://wa3.i-3-i.info/word12453.html https://normalse.hatenablog.jp/entry/2019/04/09/083022 https://site-builder.wiki/posts/13878 https://github.com/puma/puma/blob/v3.12.0/lib/puma/plugin/tmp_restart.rb http://nekorails.hatenablog.com/entry/2018/10/12/101011
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Js・vue.js】chart.jsでのデータ加工のやり方

こんにちは! 個人的に開発しているvuejs × rails apiのアプリにてグラフを使ったデータ表示を実装した時に躓いたポイントがあったので記事にしたいと思います。 環境 rails 5.2.3 rails 5.2.3 vue.js 2.6.12 参考 【Javascript】配列(オブジェクト)の操作【map/filter/some/reduce】 ActiveRecordで日付ごとにgroup byしたい Railsガイド 躓いたポイント chart.jsの使い方は参考記事がたくさんあったのですが、「じゃあapiで取得したデータをどうやってグラフに渡すの?」ってなり、なかなか参考記事が見つからずに苦労したので... 完成 これがchart.jsを使って表示している棒グラフです やりたい事 1週間の学習時間をDBから取得 1日に複数の学習時間を登録している場合は合計して1つの連想配列にまとめる 学習時間を登録していない日は学習時間を0とする 最終的に取得したデータをchart.jsに渡して棒グラフを表示する はじめにデータを取得 欲しいデータは以下のようなデータです [ {"id":9,"time":1.5,"user_id":1,"created_at":"2021-01-25T12:00:00.000+09:00","day_of_week":1}, {"id":11,"time":2.0,"user_id":1,"created_at":"2021-01-27T09:39:30.000+09:00","day_of_week":3}, {"id":14,"time":0.5,"user_id":1,"created_at":"2021-01-28T07:52:24.000+09:00","day_of_week":4} ] railsのactiveRecordを使って取得します まず今週の日〜土で期間指定します # models/study.rb def self.get_week_chart_data @this_day = Time.now @range = @this_day.all_week(:sunday) self.where(created_at: @range) end これでcreated_atが今週2021/01/24~2021/01/30で期間が指定できます 次にフロントで使うデータを指定して取得します def self.get_week_chart_data @this_day = Time.now @range = @this_day.all_week(:sunday) self.where(created_at: @range) .select(:id, "sum(time) as time", :user_id, :created_at, "dayofweek(created_at) - 1 as day_of_week") end 不要なデータも取ってきてますが気にしない笑 ポイントは、 - sum(time) as timeとして1日に複数回の学習時間(time)を登録している場合もあるので、その合計値を取得する - dayofweek(created_at) -1 as day_of_weekを使って曜日(この場合は曜日の添字である0~6)を取得する mysqlだとdayofweekで取得できる添字が1~7になるので注意 JsではgetDay()を使うと0~6が取得できる なんか揃ってないのが気になるので-1して揃えているだけ 最後に日付ごとにまとめる def self.get_week_chart_data @this_day = Time.now @range = @this_day.all_week(:sunday) self.where(created_at: @range) .select(:id, "sum(time) as time", :user_id, :created_at, "dayofweek(created_at) - 1 as day_of_week") .group("date(created_at)") end これで曜日ごとにグループ化できたので当初取得したかったデータが取得できる このモデルメソッドをコントローラに渡す. ついでにルーティングも設定 # controllers/studies_controller.rb def history histories = current_user.studies.get_week_chart_data // current_userの説明ははしょります. すいません! render json: histories end # config/routes.rb namespace :api, {format: 'json'} do namespace :v1 do get 'histories', controller: :studies, action: :history // RESTfulじゃないのは許して end end 本題 これで下準備が完了. chart.jsにデータを渡す // components/Chart.vue // これは公式の書き方のまま <script> import { Bar, mixins } from 'vue-chartjs'; const { reactiveProp } = mixins export default { extends: Bar, mixins: [reactiveProp], // reactiveProp使わないとデータ更新できないので注意 props: ['options'], mounted () { this.renderChart(this.chartData, this.options) } } </script> // History.vue <template> <div> <Chart class="chart" v-if="loaded" :chartData="chartData" :options="options"/> </div> </template> <script> import Chart from '../components/Chart'; export default { components: { Chart }, data() { histories: [], loaded: false, chartData: { labels: [], datasets: [] }, // これがchart.jsのデータの定義 options: { // ここの書き方はググるとたくさん出てくるので省略 } }, mounted () { this.$http.secured.get('/api/v1/histories') .then(response => { this.histories = response.data this.fillData() // エラー処理は省略 }, methods: { // ここでchartにデータを渡す処理書きます fillData () { this.loaded = false var data = this.arrayData() // ここでデータを加工するメソッドを呼び出しています this.chartData = { labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], datasets: [{ data: data }] // 配列の形でデータを渡す必要があります } this.loaded = true } } } </script> これで先程apiで取得したデータをchartに渡せるようになります ただ、まだjsonデータを加工してないのでarrayData()メソッドを書いていきます datasets:[{ data: }]に渡す時は配列にしてデータを渡します なので、jsonでは連想配列を配列にまとめた形でデータを受け取っているのでmapを使って配列に加工します ~省略~ methods: { arrayData () { var getData = this.histories; const times = getData.map(item => item.time) return times } ~省略~ } mapは連想配列のキーを指定する事でvalueだけ取り出してループ処理し配列を作ってくれます const times = getData.map(item => item.time) console.log(times) // => [1.5, 2.0, 0.5] あれ? 本来のデータ通りだと 1/25(月) => 1.5 1/27(水)=> 2.0 1/28(木)=> 0.5 にならなければいけないのに、mapで作ったデータでは 1/24(日) => 1.5 1/25(月) => 2.0 1/26(火) => 0.5 になっちゃってます 確かに配列データを作れたのでdatasets:[{ data: }]に渡せるデータにできましたが、1週間は7日なので、これだと日・月・火のデータになってしまっています... 現状の状態をまとめると、 [0, 1.5, 2.0, 0.5, 0, 0, 0]という配列を作らなくてはいけないのに、[1.5, 2.0, 0.5]という配列ができている状態です mapで作った配列はindex[0]から値を埋めていくので、3つの連想配列から取り出したプロパティはindex[0]~[2]に詰めてセットされてしまっています この問題を解消する為にやりたい事の3つ目学習時間を登録していない日は学習時間を0とする処理をarrayData()メソッドに書きます 考え方 (A) jsonデータのday_of_weekを使って比較用の配列を作成する (B) 1週間は7日なので比較対象の配列を用意する (A)と(B)を比較して(A)が持っていない数字(曜日)を求める 持っていない数字(曜日)番目に{ time: 0 }を突っ込んでやる(無理やり感...) 果たしてこれが良いやり方なのかは甚だ疑問だが、やってみよう データの成形 ~省略~ methods: { arrayData () { var getData = this.histories; // getDataの連想配列数が7つ未満の場合 if(getData.length < 7) { // (A)比較用の配列を作成する // 各連想配列created_atの曜日(添字)を抽出して配列を作成 const arrayDayOfWeek = getData.map(item => item.day_of_week) // => [1, 3, 4] // (B)比較対象の配列を用意する // 1週間7日分の曜日(添字)の数値を格納した配列を作成 const arraySeven = [0, 1, 2, 3, 4, 5, 6] // (A)と(B)を比較して差分を求める // filterメソッドを使って、arrayDay0fWeekが持っていない値を抽出 var resultArray = arraySeven.filter(i => arrayDayOfWeek.indexOf(i) == -1) // => [0, 2, 5, 6]がresultArrayに格納 // このケースだとresultArrayには4つデータが格納されているので4回ループされる // spliceメソッドを使ってgetDataの配列に格納されている[0, 2, 5, 6]番目に{time: 0}を追加 for(var i = 0; i < resultArray.length; i++) { getData.splice(resultArray[i], 0, {time: 0}) }; ↓ 結果 ↓ // 配列index番号の0, 2, 5, 6番目に{time: 0}が追加された(プロパティの数が違うのは気にしない) // getData = [ {time: 0} {id: 9, time: 1.5, user_id: 1, created_at: "2021-01-25T12:00:00.000+09:00", day_of_week: 1} {time: 0} {id: 11, time: 2, user_id: 1, created_at: "2021-01-27T09:39:30.000+09:00", day_of_week: 3} {id: 14, time: 0.5, user_id: 1, created_at: "2021-01-28T07:52:24.000+09:00", day_of_week: 4} {time: 0} {time: 0} ] // } const times = getData.map(item => item.time) return times // chartに渡したい配列を作成できた // => [0, 1.5, 0, 0.5, 0, 0, 0] } ~省略~ } for(var i = 0; i < resultArray.length; i++) { getData.splice(resultArray[i], 0, {time: 0}) }; この処理では、 resultArray[0] // => 配列0番目に格納されている値は[0] getData0番目にspliceメソッドによって{time: 0}が追加 resultArray[1] // => 配列1番目に格納されている値は[2] getData2番目にspliceメソッドによって{time: 0}が追加 をループ処理でresultArray.length分、繰り返します これで目的のデータの成形が完了したのでchartに渡せます chart.jsに成形したデータを渡す // 必要箇所だけ <template> <div> <Chart class="chart" v-if="loaded" :chartData="chartData" :options="options"/> // => chartDataにfillDataメソッドで生成されたデータが渡される // => optionsにdata() {}内で定義したoptionsデータを渡せる(この記事では省略してます) </div> </template> <script> export default { data() { histories: [], loaded: false, chartData: { labels: [], datasets: [] }, }, mounted() { this.$http.secured.get('/api/v1/histories') .then(response => { this.histories = response.data //jsonデータを受け取る this.fillData() // fillDataメソッドを呼び出す }) }, methods: { arrayData () { ~省略~ return times // 返り値 => [0, 1.5, 0, 0.5, 0, 0, 0] }, fillData () { this.loaded = false var data = this.arrayData() // [0, 1.5, 0, 0.5, 0, 0, 0]をdataに格納 this.chartData = { labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], datasets: [ { data: data } ] } // data() {}内で定義したchartData { labels: [], datasets: [] }に生成したデータが格納され, templateタグ内の<Chart ~~/>へマウントされる this.loaded = true } }, } </script> これで記事の最初に貼ったキャプチャの棒グラフが完成しました 最後に もっと効率的な書き方あれば教えて下さい! ちなみに、dayofweek()はmysqlで用意されているメソッドなのでpostgresqlだとエラーします... herokuでデプロイしたらエラーで萎えました... おわり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリに非同期通信のコメント機能を実装

はじめに Railsアプリに非同期通信のコメント機能を実装しました。 非同期通信は一言で言えば、画面遷移をせずにページを更新する技術です。 備忘録として手順をまとめたいと思います。 実装イメージ 開発環境 macOS Catalina Ruby 2.6.5 Ruby on Rails 5.2 目次 1.モデルとアソシエーション 2.ルーティング 3.コントローラー 4.view 1. モデルとアソシエーション モデル構成とアソシエーションはよくある構成です。 前提としてuserとpostモデルは作成済みとします。 commentモデル作成 $ rails g model comment user:references post:references content:string $ rails g migrate アソシエーション user.rb has_many :posts, dependent: :destroy has_many :comments, dependent: :destroy post.rb belongs_to :user has_many :comments, dependent: :destroy comment.rb belongs_to :user belongs_to :post # バリデーション(カラの入力を無効に)  validates :content, presence: true 2. ルーティング コメントは投稿に紐付いているのでルーティングはネストさせて記述します。 ネストさせることでアソシエーション先のレコード(今回で言えば、投稿に紐づくコメント)のidをparamsに追加してコントローラーにわたすことができるようになります。 routes.rb resources :posts, only: [:index, :new, :create, :show, :destroy] do resources :comments, only: [:create, :destroy] end 3. コントローラー まずはcommentのコントローラーを作成します。 $ rails g controller comments 次にcreateとdestroyアクションを定義します。 comments_controller.rb comments_controller.rb class CommentsController < ApplicationController def create @comment = Comment.create(comment_params) respond_to do |format| if @comment.save format.html { redirect_back(fallback_location: root_path) } # 前のページに遷移 format.js # create.js.erbが呼び出される else format.html { redirect_back(fallback_location: root_path) } # 前のページに遷移 end end end def destroy @post = Post.find(params[:post_id]) @comment = current_user.comments.find_by(post_id: @post.id) @comment.destroy redirect_back(fallback_location: root_path) end private def comment_params params.require(:comment).permit(:content).merge(user_id: current_user.id, post_id: params[:post_id]) end end Comment.create(comment_params) ・comment_paramsではmergeメソッドでuser_idとpost_idをcommentテーブルのレコードに格納します。 respond_to do |format| ・処理の結果をHTML形式で返すかJS形式で返すかを分岐させます。 ・今回はコメントが保存されたらJS形式で返すように設定します。format.jsと記述することでcreate.js.erbというファイルを返します。 redirect_back(fallback_location: root_path) ・もしJS形式で返せなかった場合は同期通信でコメントを作成します。その際redirect_backでコメント作成ページにとどまることができます。fallback_location: root_pathはエラーが起きた際にroot_pathに遷移するという記述です。 posts_controller.rb viewで反映させるためにposts_controller.rbを編集します。 posts_controller.rb def show @post = Post.find(params[:id]) @comment = Comment.new @comments = @post.comments.all end 4. View 非同期通信をさせるためにcreate.js.erbと_comment.html.erbという部分テンプレートを作成します。 一例ですがディレクトリ構成は以下のようになります。 views |-posts | |-show.html.erb |-comments |-_comment.html.erb |-create.js.erb 投稿詳細ページは以下のようになります。 posts/show.html.erb <% if @comments %> <div class="commentOutline"> <%= render partial: "modules/comment", locals: { comments: @comments }%> </div> <% else %> <p>コメントはまだありません</p> <% end %> <div class="bottomInput"> <%= form_with(model: [@post, @comment], id: "new-comment") do |f| %> <%= f.text_field :content, class: "inputComment" %> <%= f.submit "コメントする", class: "submitComment" %> <% end %> </div> renderメソッドで_comment.html.erbを呼び出します。更にlocalsオプションでposts_controller.rbで定義した@commnetsの変数をcommentsとして部分テンプレート内で使用できるようにします。 form_withはデフォルトでremote: tureになっているのでこれでJS形式でレスポンスすることができます。 部分テンプレートは以下の通りです。 comments/_comment.html.erb <% comments.each do |comment| %> <li> <div class="topPosition"> <%= link_to user_path(comment.user.id), class: "commentUserLink" do %> <% if comment.user.icon? %> <%= image_tag comment.user.icon.url, class: "commentUserIcon"%> <% else %> <i class="fas fa-user-circle"></i> <% end %> <p class="commentUserName"><%= comment.user.nickname %></p> <% end%> <% if comment.user_id == current_user.id %> <%= link_to post_comment_path(comment.post_id, comment.id), method: :delete, class: "deleteCommentLink" do %> <i class="fas fa-trash"></i> <% end %> <% end %> </div> <div class="bottomPosition"> <p class="commentContent"><%= comment.content %></p> <p class="commentDatetime"><%= comment.created_at.strftime('%Y/%m/%d') %></p> </div> </li> <% end %> 最後にcreate.js.erbを編集します。 comments/create.js.erb $(".commentsArea").html("<%= j(render 'comments/comment', { comments: @comment.post.comments }) %>") $(".inputComment").val(''); 1行目でコメントが作成されたらcommentsAreaに部分テンプレートの内容を更新するという記述をしています。 2行目はコメントを入力するinputエリアの値をリセットする記述です。なお記述を簡略にするためにjQueryで記述しています。 以上で非同期でのコメント機能が実装できていると思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む