- 投稿日:2021-08-15T20:46:45+09:00
#6 Rails × Vue.jsで動的なページをSPA化させる
前回の続き。 railsのデータをjson形式でvueに受け渡すことができたので、vueの方の記述をやっていこうというフェーズです。 投稿の詳細ページをvueで実装しようとしてるのですが、以前railsで実装したいいね機能や、コメント機能などをvueで新たに書き直す必要があるので、vueに疎い自分にとってはまぁまぁ難関ポイントですが しっかり頑張っていきたいと思います。 まずはVueでいいね機能を実装 drink(投稿)テーブルの方にlikes_countといったカラムがあり、 何個いいねがついてるかといったデータはそこのカラムに格納されます。 そのデータは既にjson形式でvueの方に受け渡しています。 なので、その投稿に何個いいねがついたか表示するといった機能は簡単にできそう。 問題は、どうやっていいねをつけるか。 app.vue import axios from 'axios'; と非同期通信を可能にしてくれるモジュールをインポートしてるのでそれを駆使したりしそうですね。 一旦railsがどうやっていいねを機能させていたかを振り返り いいね機能がどの様にできていたか分解すればvueの方のいいね機能の実装が簡単になる気がするので 一旦railsがどうやっていいねを機能させていたかを振り返ります。 _like.html.erb <div class="like" id="like-link-<%= drink.id %>"> <% if current_user.likes.find_by(drink_id: drink.id) %> <%= link_to unlike_path(drink.id), method: :delete, remote: true do %> <div class = "iine__button"> <i class="fas fa-heart"></i> <%= drink.likes.count %></div> <% end %> <% else %> <%= link_to like_path(drink.id), method: :post, remote: true do %> <div class = "iine__button"><i class="far fa-heart"></i><%= drink.likes.count %></div> <% end %> <% end %> </div> userオブジェクトの方に、likesメソッドがあってそこでいいねされていた場合は、リンクのHTTP動詞がdetele、いいねされていなかったらpostになっています。 likesメソッドとはなんぞやと思ったので見てみると、 users_controller.rb def likes @user = User.find(params[:id]) @pagy,@drinks = pagy(@user.like_drinks.order('created_at DESC').includes(:user,{image_attachment: :blob})) @title = "#{@user.nickname}がいいねした投稿" end んー、このメソッドは一言でまとめると、users/likes.html.erbを表示するメソッドに過ぎないぞ。。。 呼んでるメソッドはこれではなさそう。。。。 となったら、 binging.pryで処理を止めて、 [10] pry(#<#<Class:0x00007f9c2612b258>>)> current_user.likes => [#<Like:0x00007f9c262c00c8 id: 2, user_id: 7, drink_id: 8, created_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00>, #<Like:0x00007f9c262c7e90 id: 3, user_id: 7, drink_id: 5, created_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00>, #<Like:0x00007f9c262c7cb0 あー、なるほどね。 user.rbの has_many :likes has_many :like_drinks, through: :likes, source: :drink これですね。has_manyとはbelongs_toの本質はアソシエーションではなく、メソッドを作るメソッドということです。ってのをRails チュートリアルの安川さんが仰っていたのを思い出した。 てか、多対多のリレーションを組む時に、throughとかsourceとかあったわ。 調べてみよ。 まず、userは色々な投稿にいいねしていて、drinks(投稿)も色々なユーザーにいいねされている。 そこで、多対多のリレーションになる。 中間テーブルとして、likesテーブルがある。 user_id,drink_idカラムがあり、どのユーザーがどんなユーザーにいいねしたか分かるようになる。 多対多の関連がある時はthroughオプションをつけてあげると中間テーブルを経由して関連先のモデルを取得できるようになります。 throughをつけないと中間テーブルの先のレコードを取得するのが一手間必要になり微妙なので設定するのが吉です。 というか、中間テーブルを用いた多対多のリレーションの時に、はthroughにそもそも中間テーブルを指定してあげないといけない感じ。 もうそーゆーもんだと一旦割り切ろう(脳死) ただ、railsのアソシエーションを忘れていた。っていう課題が出てきたので、 後日またrailsチュートリアルを見直すしかない。 sourceオブションは、 sourceオプションを設定することでuser.like_drinksでuserがお気に入りした投稿を一気に取得することができます。 んー、とりあえずこの二つの合わせ技で、user.like_drinksとやったら、 ユーザーがいいねした投稿を取得できるって感じですかね。。 [11] pry(#<#<Class:0x00007f9c2612b258>>)> current_user.like_drinks => Drink Load (1.9ms) SELECT `drinks`.* FROM `drinks` INNER JOIN `likes` ON `drinks`.`id` = `likes`.`drink_id` WHERE `likes`.`user_id` = 7 ↳ app/views/likes/_like.html.erb:3 [#<Drink:0x00007f9c200b4258 id: 8, name: "aaaaa", price: 3000, explain: "#sssss", user_id: 7, created_at: Fri, 06 Aug 2021 05:33:18.586567000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:33:18.657514000 UTC +00:00, region_id: 3, body_id: 2, acidity_id: 3, processing_id: 2, likes_count: 1>, #<Drink:0x00007f9c200b4190 id: 5, name: "エチオピア", price: 1330, explain: " #CITRUS(シトラス) #DARK_COCOA(ダークココア) ダークチョコレート、ペッパーのようなスパイス、そしてスイートシトラスの風味が特徴の、やわらかでベルベットのような口あたりのコーヒーです。", user_id: 6, created_at: Sat, 12 Jun 2021 13:35:18.097644000 UTC +00:00, updated_at: Sat, 12 Jun 2021 13:35:18.153340000 UTC +00:00, region_id: 4, body_id: 3, acidity_id: 4, processing_id: 2, 確かにこんな感じで取得できた。 binding.pryのおかげで助かった。。。 んで本題に戻る。として、 current_user.likesで現在のユーザーが、 [10] pry(#<#<Class:0x00007f9c2612b258>>)> current_user.likes => [#<Like:0x00007f9c262c00c8 id: 2, user_id: 7, drink_id: 8, created_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00>, #<Like:0x00007f9c262c7e90 id: 3, user_id: 7, drink_id: 5, created_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00>, #<Like:0x00007f9c262c7cb0 どの投稿にいいねしてるかという情報が手に入る。 そこでまた、find_byをしてあげて、その投稿のidがuser.likesの中のdrink_idと一致したら その投稿にいいねを既にしているということなので、 <%= link_to unlike_path(drink.id), method: :delete, remote: true do %> このリンクが表示される。 このリンクを押すと、likes#unlikeメソッドが作動する。 その時にremote: trueなのでjava scriptのリクエストになる。 こうすることで一部分だけの表示を変えることができる。 likes_controller.rb before_action :set_variables def unlike like = current_user.likes.find_by(drink_id: @drink.id).destroy # binding.pry #=> like.js.erbに遷移する。 end def set_variables @drink = Drink.find(params[:drink_id]) @id_name = "#like-link-#{@drink.id}" end とあるので、パラメーターで受け取ったdrink_idと一致したcurrent_user.likes(ユーザーがいいねした投稿)のいいねを消去する。 そして like.js.erb $("<%= @id_name %>").html('<%= escape_javascript(render("likes/like", drink: @drink )) %>'); <%= @id_name %>この部分の表示だけ、renderで部分的な更新をします。 既に、 current_user.likes.find_by(drink_id: @drink.id).destroy でユーザーがいいねした投稿のいいねを消しているので、 likes/likeにレンダリングしたら今度は <% else %> <%= link_to like_path(drink.id), method: :post, remote: true do %> <div class = "iine__button"> <i class="far fa-heart"> </i><%= drink.likes.count %> </div> <% end %> <% end %> こっちの部分が表示されることになります。 まぁ、なんとなくは分かった。 改めて、Vue.jsでいいね機能を実装したい。 コントローラーの記述は既存の物を使って、like.json.jbuilderとかで json形式でデータをやり取りして vueの方で、いいねの表示の切り替えをする様なイメージですかね。。。 まぁ何はともあれ「Vue.js いいね機能」とかで検索。 https://qiita.com/TakeshiFukushima/items/a6c698fec648c11eee9a この様な記事がヒットしたので一旦この方の記事を拝見させていただきながら進めます。 rails g controller api/likes index create destroy --skip-template-engine --skip-test-framework --skip-assets --skip-helper railsでいいね機能を実装した時と同様にlikes_controllerをapiな感じで作成 api/likes_controller.rb class Api::LikesController < ApplicationController include SessionsHelper before_action :set_variables def like current_user.likes.new(drink_id: @drink.id).save # redirect_to drinks_path # jsを用いるので画面遷移は行わない # binding.pry #=> like.js.erbに遷移する。 end def unlike current_user.likes.find_by(drink_id: @drink.id).destroy # binding.pry end private def set_variables @drink = Drink.find(params[:drink_id]) end end drinks#showをそのままコピペした時と同様に、likes_controllerもそのまま既存の物を 一旦コピペしときます。 api*じゃない方の*likes_controllerは likes_controller.rb def set_variables @drink = Drink.find(params[:drink_id]) @id_name = "#like-link-#{@drink.id}" end この様なメソッドを定義していましたが、 今回は@id_nameの方はいらないでしょう。 vueの方でフロントの処理を書くので。 んで、drinks_controllerの方のAPIを作成した時は、 app/views/api/drinks/show.json.jbuilderにどんなデータをjson形式で送るかを定義したので、 今回、likes_controllerの方をjson形式でデータのやり取りをさせたいから app/views/api/likes/like.json.jbuilder app/views/api/likes/unlike.json.jbuilder と言ったjsonファイルを作成すればいい感じかも んで、そこでどんなjsonデータを定義すればいいか。 drinks#showの時は api/drinks_controller.rb class Api::DrinksController < ApplicationController def show @drink = Drink.find(params[:id]) #@user = @drink.user #@comment = Comment.new # @comments = @drink.comments.includes(:user).order('created_at DESC') end end app/views/api/drinks/show.json.jbuilder show.json.jbuilder json.(@drink, :name ,:price , :explain , :region_id , :body_id , :acidity_id , :processing_id , :likes_count, :user_id) こんな感じで定義してた。 となると一旦インスタンス変数を定義する必要があるので、 api/likes_controller.rb class Api::LikesController < ApplicationController include SessionsHelper before_action :set_variables def like @like = current_user.likes.new(drink_id: @drink.id) @like.save # redirect_to drinks_path # jsを用いるので画面遷移は行わない # binding.pry #=> like.js.erbに遷移する。 end def unlike @like = current_user.likes.find_by(drink_id: @drink.id) @like.destroy # binding.pry end private def set_variables @drink = Drink.find(params[:drink_id]) end end こんな感じで定義して、 likes/like.json.jbuilder json.jbuilder json.(@like, :user_id,:drink_id) 雰囲気こんな感じ? ただ今回は、drinks#showと違って、既にrailsにあるデータをjson形式で手に入れるのではなく、 post,destroyなど、railsの方のデータを書き換えたり、消去しなければならない。 となると、drinks#showのやり方を踏襲しても意味ない気がする。。。。 https://qiita.com/TakeshiFukushima/items/a6c698fec648c11eee9a この方の記事を拝見させていただくと、 controllerの方でjson形式の何かを書いているわけではない。。。 いいね一覧を取得するメソッドのときはrender: jsonとかやってるけど。。。 create,destroyの時にはそう言った物を書いていない。。。 なぜだ。。。 まぁ、とりあえず、一旦rails側で何か書くものはもう既にないって感じでいいのかな。。。。 あとはvueで色々書けばいいね機能が実装されるのか。。。 他にも検索してみよう。 んーー、多分なさそう。vue.jsのaxiosがrailsのapl/likesにpostリクエストを送って、api/likes_controllerが動くから非同期処理できる。って仮説を立ててvueの方の処理を書こう。 だけど、どのユーザーがどんな投稿にいいねしたかという情報は必要じゃないか?? post_idとかuser_idをどうやって付与してリクエストを送るのだろうか。。。。 Vue.jsを記述 app.vue <likeButton></likeButton> <script> import likeButton from './packs/components/like/likeButton.vue' export default { components: { likeButton }, を追記して、んで、コンポーネントを読み込み。 export default { // propsでrailsのviewからデータを受け取る props: ['userId', 'postId'], この方の記事に、propsでviewからデータを受け取る。と言った記述があった。 そんなことができるの?と思ったので、調べてみると https://qiita.com/kazutosato/items/66bb6604bd680421101d こんな記事が見つかった。 https://qiita.com/kazutosato/items/38caffdbd21508a5c126 この記事を参考にして、 _like.html.erb <script> var userId = <%= { userId: current_user.try(:id) }.to_json.html_safe %>; var drinkId = <%= { drinkId: drink.try(:id) }.to_json.html_safe %>; </script> この様な記述をして、 likeButton.vue <script> import axios from 'axios' console.log(userId) console.log(drinkId) export default { props: ['userId', 'drinkId'] } </script> にこんな感じで書いたら、useridに7が入っていて、drinkIdには10が入っていました。 とりあえず、erbからvue.jsにデータを受け渡すのは成功した?っぽい!! これで、axios使っていい感じにrailsにリクエストを 送れそう!! やった====!!!! 次回はその続きでvueでいいね機能の仕上げに入りたい
- 投稿日:2021-08-15T20:46:45+09:00
#6 Rails × Vue.jsで動的なページをSPA化させる【いいね機能】
前回の続き。 railsのデータをjson形式でvueに受け渡すことができたので、vueの方の記述をやっていこうというフェーズです。 投稿の詳細ページをvueで実装しようとしてるのですが、以前railsで実装したいいね機能や、コメント機能などをvueで新たに書き直す必要があるので、vueに疎い自分にとってはまぁまぁ難関ポイントですが しっかり頑張っていきたいと思います。 まずはVueでいいね機能を実装 drink(投稿)テーブルの方にlikes_countといったカラムがあり、 何個いいねがついてるかといったデータはそこのカラムに格納されます。 そのデータは既にjson形式でvueの方に受け渡しています。 なので、その投稿に何個いいねがついたか表示するといった機能は簡単にできそう。 問題は、どうやっていいねをつけるか。 app.vue import axios from 'axios'; と非同期通信を可能にしてくれるモジュールをインポートしてるのでそれを駆使したりしそうですね。 一旦railsがどうやっていいねを機能させていたかを振り返り いいね機能がどの様にできていたか分解すればvueの方のいいね機能の実装が簡単になる気がするので 一旦railsがどうやっていいねを機能させていたかを振り返ります。 _like.html.erb <div class="like" id="like-link-<%= drink.id %>"> <% if current_user.likes.find_by(drink_id: drink.id) %> <%= link_to unlike_path(drink.id), method: :delete, remote: true do %> <div class = "iine__button"> <i class="fas fa-heart"></i> <%= drink.likes.count %></div> <% end %> <% else %> <%= link_to like_path(drink.id), method: :post, remote: true do %> <div class = "iine__button"><i class="far fa-heart"></i><%= drink.likes.count %></div> <% end %> <% end %> </div> userオブジェクトの方に、likesメソッドがあってそこでいいねされていた場合は、リンクのHTTP動詞がdetele、いいねされていなかったらpostになっています。 likesメソッドとはなんぞやと思ったので見てみると、 users_controller.rb def likes @user = User.find(params[:id]) @pagy,@drinks = pagy(@user.like_drinks.order('created_at DESC').includes(:user,{image_attachment: :blob})) @title = "#{@user.nickname}がいいねした投稿" end んー、このメソッドは一言でまとめると、users/likes.html.erbを表示するメソッドに過ぎないぞ。。。 呼んでるメソッドはこれではなさそう。。。。 となったら、 binging.pryで処理を止めて、 [10] pry(#<#<Class:0x00007f9c2612b258>>)> current_user.likes => [#<Like:0x00007f9c262c00c8 id: 2, user_id: 7, drink_id: 8, created_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00>, #<Like:0x00007f9c262c7e90 id: 3, user_id: 7, drink_id: 5, created_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00>, #<Like:0x00007f9c262c7cb0 あー、なるほどね。 user.rbの has_many :likes has_many :like_drinks, through: :likes, source: :drink これですね。has_manyとはbelongs_toの本質はアソシエーションではなく、メソッドを作るメソッドということです。ってのをRails チュートリアルの安川さんが仰っていたのを思い出した。 てか、多対多のリレーションを組む時に、throughとかsourceとかあったわ。 調べてみよ。 まず、userは色々な投稿にいいねしていて、drinks(投稿)も色々なユーザーにいいねされている。 そこで、多対多のリレーションになる。 中間テーブルとして、likesテーブルがある。 user_id,drink_idカラムがあり、どのユーザーがどんなユーザーにいいねしたか分かるようになる。 多対多の関連がある時はthroughオプションをつけてあげると中間テーブルを経由して関連先のモデルを取得できるようになります。 throughをつけないと中間テーブルの先のレコードを取得するのが一手間必要になり微妙なので設定するのが吉です。 というか、中間テーブルを用いた多対多のリレーションの時に、はthroughにそもそも中間テーブルを指定してあげないといけない感じ。 もうそーゆーもんだと一旦割り切ろう(脳死) ただ、railsのアソシエーションを忘れていた。っていう課題が出てきたので、 後日またrailsチュートリアルを見直すしかない。 sourceオブションは、 sourceオプションを設定することでuser.like_drinksでuserがお気に入りした投稿を一気に取得することができます。 んー、とりあえずこの二つの合わせ技で、user.like_drinksとやったら、 ユーザーがいいねした投稿を取得できるって感じですかね。。 [11] pry(#<#<Class:0x00007f9c2612b258>>)> current_user.like_drinks => Drink Load (1.9ms) SELECT `drinks`.* FROM `drinks` INNER JOIN `likes` ON `drinks`.`id` = `likes`.`drink_id` WHERE `likes`.`user_id` = 7 ↳ app/views/likes/_like.html.erb:3 [#<Drink:0x00007f9c200b4258 id: 8, name: "aaaaa", price: 3000, explain: "#sssss", user_id: 7, created_at: Fri, 06 Aug 2021 05:33:18.586567000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:33:18.657514000 UTC +00:00, region_id: 3, body_id: 2, acidity_id: 3, processing_id: 2, likes_count: 1>, #<Drink:0x00007f9c200b4190 id: 5, name: "エチオピア", price: 1330, explain: " #CITRUS(シトラス) #DARK_COCOA(ダークココア) ダークチョコレート、ペッパーのようなスパイス、そしてスイートシトラスの風味が特徴の、やわらかでベルベットのような口あたりのコーヒーです。", user_id: 6, created_at: Sat, 12 Jun 2021 13:35:18.097644000 UTC +00:00, updated_at: Sat, 12 Jun 2021 13:35:18.153340000 UTC +00:00, region_id: 4, body_id: 3, acidity_id: 4, processing_id: 2, 確かにこんな感じで取得できた。 binding.pryのおかげで助かった。。。 んで本題に戻る。として、 current_user.likesで現在のユーザーが、 [10] pry(#<#<Class:0x00007f9c2612b258>>)> current_user.likes => [#<Like:0x00007f9c262c00c8 id: 2, user_id: 7, drink_id: 8, created_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:22.009804000 UTC +00:00>, #<Like:0x00007f9c262c7e90 id: 3, user_id: 7, drink_id: 5, created_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00, updated_at: Fri, 06 Aug 2021 05:41:54.834559000 UTC +00:00>, #<Like:0x00007f9c262c7cb0 どの投稿にいいねしてるかという情報が手に入る。 そこでまた、find_byをしてあげて、その投稿のidがuser.likesの中のdrink_idと一致したら その投稿にいいねを既にしているということなので、 <%= link_to unlike_path(drink.id), method: :delete, remote: true do %> このリンクが表示される。 このリンクを押すと、likes#unlikeメソッドが作動する。 その時にremote: trueなのでjava scriptのリクエストになる。 こうすることで一部分だけの表示を変えることができる。 likes_controller.rb before_action :set_variables def unlike like = current_user.likes.find_by(drink_id: @drink.id).destroy # binding.pry #=> like.js.erbに遷移する。 end def set_variables @drink = Drink.find(params[:drink_id]) @id_name = "#like-link-#{@drink.id}" end とあるので、パラメーターで受け取ったdrink_idと一致したcurrent_user.likes(ユーザーがいいねした投稿)のいいねを消去する。 そして like.js.erb $("<%= @id_name %>").html('<%= escape_javascript(render("likes/like", drink: @drink )) %>'); <%= @id_name %>この部分の表示だけ、renderで部分的な更新をします。 既に、 current_user.likes.find_by(drink_id: @drink.id).destroy でユーザーがいいねした投稿のいいねを消しているので、 likes/likeにレンダリングしたら今度は <% else %> <%= link_to like_path(drink.id), method: :post, remote: true do %> <div class = "iine__button"> <i class="far fa-heart"> </i><%= drink.likes.count %> </div> <% end %> <% end %> こっちの部分が表示されることになります。 まぁ、なんとなくは分かった。 改めて、Vue.jsでいいね機能を実装したい。 コントローラーの記述は既存の物を使って、like.json.jbuilderとかで json形式でデータをやり取りして vueの方で、いいねの表示の切り替えをする様なイメージですかね。。。 まぁ何はともあれ「Vue.js いいね機能」とかで検索。 https://qiita.com/TakeshiFukushima/items/a6c698fec648c11eee9a この様な記事がヒットしたので一旦この方の記事を拝見させていただきながら進めます。 rails g controller api/likes index create destroy --skip-template-engine --skip-test-framework --skip-assets --skip-helper railsでいいね機能を実装した時と同様にlikes_controllerをapiな感じで作成 api/likes_controller.rb class Api::LikesController < ApplicationController include SessionsHelper before_action :set_variables def like current_user.likes.new(drink_id: @drink.id).save # redirect_to drinks_path # jsを用いるので画面遷移は行わない # binding.pry #=> like.js.erbに遷移する。 end def unlike current_user.likes.find_by(drink_id: @drink.id).destroy # binding.pry end private def set_variables @drink = Drink.find(params[:drink_id]) end end drinks#showをそのままコピペした時と同様に、likes_controllerもそのまま既存の物を 一旦コピペしときます。 api*じゃない方の*likes_controllerは likes_controller.rb def set_variables @drink = Drink.find(params[:drink_id]) @id_name = "#like-link-#{@drink.id}" end この様なメソッドを定義していましたが、 今回は@id_nameの方はいらないでしょう。 vueの方でフロントの処理を書くので。 んで、drinks_controllerの方のAPIを作成した時は、 app/views/api/drinks/show.json.jbuilderにどんなデータをjson形式で送るかを定義したので、 今回、likes_controllerの方をjson形式でデータのやり取りをさせたいから app/views/api/likes/like.json.jbuilder app/views/api/likes/unlike.json.jbuilder と言ったjsonファイルを作成すればいい感じかも んで、そこでどんなjsonデータを定義すればいいか。 drinks#showの時は api/drinks_controller.rb class Api::DrinksController < ApplicationController def show @drink = Drink.find(params[:id]) #@user = @drink.user #@comment = Comment.new # @comments = @drink.comments.includes(:user).order('created_at DESC') end end app/views/api/drinks/show.json.jbuilder show.json.jbuilder json.(@drink, :name ,:price , :explain , :region_id , :body_id , :acidity_id , :processing_id , :likes_count, :user_id) こんな感じで定義してた。 となると一旦インスタンス変数を定義する必要があるので、 api/likes_controller.rb class Api::LikesController < ApplicationController include SessionsHelper before_action :set_variables def like @like = current_user.likes.new(drink_id: @drink.id) @like.save # redirect_to drinks_path # jsを用いるので画面遷移は行わない # binding.pry #=> like.js.erbに遷移する。 end def unlike @like = current_user.likes.find_by(drink_id: @drink.id) @like.destroy # binding.pry end private def set_variables @drink = Drink.find(params[:drink_id]) end end こんな感じで定義して、 likes/like.json.jbuilder json.jbuilder json.(@like, :user_id,:drink_id) 雰囲気こんな感じ? ただ今回は、drinks#showと違って、既にrailsにあるデータをjson形式で手に入れるのではなく、 post,destroyなど、railsの方のデータを書き換えたり、消去しなければならない。 となると、drinks#showのやり方を踏襲しても意味ない気がする。。。。 https://qiita.com/TakeshiFukushima/items/a6c698fec648c11eee9a この方の記事を拝見させていただくと、 controllerの方でjson形式の何かを書いているわけではない。。。 いいね一覧を取得するメソッドのときはrender: jsonとかやってるけど。。。 create,destroyの時にはそう言った物を書いていない。。。 なぜだ。。。 まぁ、とりあえず、一旦rails側で何か書くものはもう既にないって感じでいいのかな。。。。 あとはvueで色々書けばいいね機能が実装されるのか。。。 他にも検索してみよう。 んーー、多分なさそう。vue.jsのaxiosがrailsのapl/likesにpostリクエストを送って、api/likes_controllerが動くから非同期処理できる。って仮説を立ててvueの方の処理を書こう。 だけど、どのユーザーがどんな投稿にいいねしたかという情報は必要じゃないか?? post_idとかuser_idをどうやって付与してリクエストを送るのだろうか。。。。 Vue.jsを記述 app.vue <likeButton></likeButton> <script> import likeButton from './packs/components/like/likeButton.vue' export default { components: { likeButton }, を追記して、んで、コンポーネントを読み込み。 export default { // propsでrailsのviewからデータを受け取る props: ['userId', 'postId'], この方の記事に、propsでviewからデータを受け取る。と言った記述があった。 そんなことができるの?と思ったので、調べてみると https://qiita.com/kazutosato/items/66bb6604bd680421101d こんな記事が見つかった。 https://qiita.com/kazutosato/items/38caffdbd21508a5c126 この記事を参考にして、 _like.html.erb <script> var userId = <%= { userId: current_user.try(:id) }.to_json.html_safe %>; var drinkId = <%= { drinkId: drink.try(:id) }.to_json.html_safe %>; </script> この様な記述をして、 likeButton.vue <script> import axios from 'axios' console.log(userId) console.log(drinkId) export default { props: ['userId', 'drinkId'] } </script> にこんな感じで書いたら、useridに7が入っていて、drinkIdには10が入っていました。 とりあえず、erbからvue.jsにデータを受け渡すのは成功した?っぽい!! これで、axios使っていい感じにrailsにリクエストを 送れそう!! やった====!!!! 次回はその続きでvueでいいね機能の仕上げに入りたい
- 投稿日:2021-08-15T19:21:24+09:00
ActiveRecord のクエリ(group, having, select, count) まとめ
はじめに データ取得の際に、クエリ周りで悩むことが多かったのでこの際まとめて自分の思考を整理しようと思い記事を書くことにしました。初心者なので、間違えているとこなどあるかもしれません、その点ご留意ください。 この記事で扱う内容 タイトル通り、ActiveRecord のクエリ (group, having, select, count) についてまとめます。 ActiveRecord とは DB とアプリケーションをつなぐもの。SQLを意識せずにデータ取得が行えるという利点がある。一方で、意識していないと N+1問題のような、無駄にSQLを発行してしまう事態になることもあるので注意が必要。(こういうのをORマッパーというらしい) クエリとは DBにデータをとってきてください、とお願いする命令文のこと。基本的にDBは検索に最適化されているので、データの取得などはクエリに任せる方が高速化しやすい。 今回記事にまとめようと思ったのも、モデルをインスタンス化してからrubyの構文で検索を行なってしまっていたところを、「ActiveRecord で取得した方が早いかもね〜」 とレビューしてもらったのがきっかけ。 group group 化するメソッド。SQLのgroup_byに対応します。返り値はそのグループの一番IDの小さいレコードですが、裏ではグループ化されています。groupだけで使うことは基本的にないので返り値はあまり気にしなくていいかも? 発行されるSQLは以下の通り User.group(:sex) => SELECT 'users'.* FROM 'users' GROUP BY 'users'.'sex' having group 化した後に条件指定を行います。実行順序はグループ化の後であることに注意。 コード例と発行されるSQLは以下の通り。 (Railsガイド https://railsguides.jp/active_record_querying.html#having より) コード例) Order.select("date(created_at) as ordered_date, sum(price) as total_price"). group("date(created_at)").having("sum(price) > ?", 100) 発行されるSQL) SELECT date(created_at) as ordered_date, sum(price) as total_price FROM orders GROUP BY date(created_at) HAVING sum(price) > 100 ざっとした意味 はじめの date(created_at) as ordered_date で date カラムの抽出を行います。as はカラムに別名をつける表現。 group 以降が今回の主題。 group('カラム名').having('条件')で、グループ化→グループ化したカラムに条件を指定して抽出、ということができます。 今回の例だと、「日付毎にグループ化」→「日付とその日の注文合計を抽出」→「各日付での注文合計が100以上のものを選択」 という流れ。 SQLの抽出順序は、以下を覚えておくと書きやすそう。 from(テーブル) → where(条件指定) → group(グルーピング) → having(グルーピングに条件指定) →select(抽出) → order(抽出したデータの順番指定) ↑の手順全て踏んで作られたデータを抽出 select これはSQLのselectまんまです。 select('カラム名')で、カラムを抽出する。 count モデルそのものに使う場合と、グループ化した後に使う場合で挙動が異なります。 モデルに使うと User.count => 100 # レコード数が返ってくる グループ化して使うと User.group('target_date').count => {Sat, 03 Jul 2021=>1, Mon, 02 Aug 2021=>143926, Tue, 03 Aug 2021=>1, Wed, 04 Aug 2021=>4, Fri, 06 Aug 2021=>65043, Mon, 09 Aug 2021=>307} 日付毎でのカウントを返します。グループ化したカラムごとで返すようになる、という点に注意。 終わりに データ抽出は基本中の基本なので、意識しなくてもスラスラかけるようになりたいと思いました。機会があれば他のクエリについてもまとめるかも?
- 投稿日:2021-08-15T18:15:55+09:00
個人開発で『TATEKAE』アプリ開発。過去の反省を踏まえながら取り組んでみた。
はじめに 自己紹介 私は関西で主にフロントエンド開発に携わっておりますオオツカと申します。 最近はリモートワークが増えて自宅で一人寂しく仕事をしているので、もっと人と関わりたいと思いながらの毎日を送っている次第でございます。 WEBに関わるキャリアとしてはweb制作会社で1年弱、現在の開発会社で1年と少しになります。 現在のプロジェクトではNuxt.js(vue.js)で主にフロントエンドのUI周りの調整を行っています。 まだまだ得意とまでは言えるレベルではありませんが、JavaScript、Reactあたりはプロジェクトとしていくつか経験をさせていただきましたので他の技術よりかは分かるかな?というレベル感です。 最近はコロナの影響や、子供と遊んだりであまり外のイベントには出れていないのですが、少し前まではもくもくエキスパートというもくもく会を開催しておりました。コロナがもう少し落ち着いたらまた開催をしたいと思いますので、その際は皆様是非ご参加くださいませ? 今回の記事について 過去に自分がどのような失敗を行なったのか?そして今回の個人開発でどのように改善を行なったのか?というところを自分の備忘録として残すことが目的です。 なおこれからフロントエンドを触る方々に対しても多少なりお役に立てればという気持ちで書いてみました。 主にフロントエンドの事について記述させていただきました。 APIとして実装をしたRailsに関しては特に雰囲気で書いてしまっているため、説明は控えさせていただきます。 誤った認識、もしくは「もっとこうしたらどう?」みたいなところがございましたらコメントでご指摘くださいませ。 今回作った『TATEKAE』アプリについて アプリURL:https://www.tatekae.work/ ※ 私個人が使うために作ったものになるので、動作の確認等であればテストユーザーを使って見ていただければと思います。バグがあったらこっそりDMで教えてください?♂️大丈夫なはず。。 ※ スマホで使う時に一番使いやすいように画面実装を行なっております。 ↓アカウント https://twitter.com/keisei_otsuka なぜ作ったのか? 理由① 自分が使いたいアプリを作りたかったから 前提として私は奥さんから毎月決まった額のお小遣いをもらって生活をしています。 その中で私の住んでいる所は割と田舎でクレジットカードを取り扱っているところが少ないのですが、家庭での必要な買い物を私のお小遣いから『立て替え』をする機会が多く、そのお金の管理をもう少し楽に出来ないかなという事で、今回のアプリを開発しました。 このアプリはどのような事を解決するのか? 主に下記のような問題と解決をすることができると考えています。 買い物をした後のレシートが溜まってしまう。 アプリに記録をすることでレシートを溜めることがない 買い物をした時に毎回家族に連絡をするのがめんどくさい。 LINE NOTIFYと連携させる事によって、買い物の登録、指定した買い物をまとめた請求をした時にLINEで通知をすることができる。 過去の買い物、請求の記録を見たい時 アプリ内で記録を見直すことができる。 etc... 理由② 過去に参画をしたプロジェクトでの反省を活かす こちらが一番の理由になりますが、私が今まで参画をしたプロジェクトで自分の技術力不足や知識不足でうまく出来ていなかった箇所の改善をしたかった。個人開発であれば納期もないのでリファクタリングにたくさんの時間を避けるため。 主なサービスの構成について フロントエンド => React(Next.js)、TypeScript フロントエンドのデプロイ先 => Vercel バックエンド => Ruby on Rails バックエンドのデプロイ先 => Heroku 過去の失敗と改善について 問題① 定数の管理があまり出来ていなかった こちら基本的なことかと思うのですが、自分があまり出来て部分があったので明らかに同じものを使う場合は定数のファイルに配置をする事にしました。直越なんでも書いてしまうと後で変更をする時に変更忘れがあったりするのはやはり恐いですね。。 export const LABEL_NAME = { NAME: '名前', EMAIL: 'メールアドレス', PASSWORD: 'パスワード', PASSWORD_CONFIRMATION: 'パスワードの再確認', } as const; export const USER_FORM = { NAME: { LABEL: LABEL_NAME.NAME, ID: 'name', }, EMAIL: { LABEL: LABEL_NAME.EMAIL, ID: 'email', }, PASSWORD: { LABEL: LABEL_NAME.PASSWORD, ID: 'password', }, PASSWORD_CONFIRMATION: { LABEL: LABEL_NAME.PASSWORD_CONFIRMATION, ID: 'passwordConfirmation', }, } as const; あと個人的によかったなと思ったのがAPIとして使用をしているRailsのエンドポイントをオブジェクトとしてまとめたことはよかったかなと思いました。 const auth = 'auth'; const claims = 'claims'; const shops = 'shops'; const shoppings = 'shoppings'; const settings = 'settings'; export const END_POINT = { DEVISE_TOKEN_AUTH: { REGISTRATIONS: { CREATE: `/${auth}`, // ユーザー新規登録 NEW: `/${auth}/sign_in`, // ユーザーログイン }, }, SHOPS: { INDEX: `/${shops}`, // ショップ登録 CREATE: `/${shops}`, // ショップ登録 }, SHOPPINGS: { INDEX: `/${shoppings}`, // ショップ一覧 CREATE: `/${shoppings}`, // 買い物登録 SHOW: (id: string) => `/${shoppings}/${id}`, // 買い物詳細 EDIT: (id: string) => `/${shoppings}/${id}/edit`, // 買い物編集 UPDATE: (id: string) => `/${shoppings}/${id}`, // 買い物編集 DESTROY: (id: string) => `/${shoppings}/${id}`, // 買い物削除 }, CLAIMS: { INDEX: `/${claims}`, // 請求一覧 CREATE: `/${claims}`, // 請求登録 NEW: `/${claims}/new`, // 請求登録 UPDATE: (id: string) => `/${claims}/${id}`, // 請求受領 DESTROY: (id: string) => `/${claims}/${id}`, // 請求解除 SHOPPINGS: (claimId: string) => `/${claims}/${claimId}/${shoppings}`, // 請求内訳一覧 }, SETTINGS: { INDEX: `/${settings}`, // 設定情報表示 UPDATE: `/${settings}`, // 設定情報の更新 }, } as const; END_POINTというオブジェクトのkeyはRailsのコントローラー定義を表すようにして、オブジェクトの最終的なvalueに当たるのが対象のエンドポイントになるようにしてみました。 これでRailsがあまり分からない人でも直感的にルーティング周りが分かるように出来たのは個人的には良いやり方なのかなと思っています。 問題② コンポーネントの命名について 私がコンポーネントを作っていく時に『ボタン』を作成する時に命名として、下記のように作成していました。 ├── button │ ├── Button.tsx まんまですね。 ですが、こちらを上記のように定義をすると名前衝突という問題が起きる可能性があります。 例えば別でMaterialーuiのボタンのコンポーネントの名前もButtonというコンポーネント名になっており、 こちらのボタンの名前と被ってしまいます。 なお、でもこちらの命名についてVue.jsのスタイルガイドでも説明がされております。 なので今回は基本となるコンポーネントには接頭辞としてBaseという接頭辞をつけるようにしました。 ├── button │ ├── BaseButton.tsx 今回あまり派生のコンポーネントは作成していないのですが、そこから派生をさせる時は、~~~Buttonのように派生をさせていけば分かりやすいのかなと考えています。 問題③ コンポーネントのディレクトリー構造について 下記のコンポーネントはいくつか省略をしていますが、過去に大体がこのようなディレクトリーで構造で切っていました。(こちら命名が少し良くない箇所があるかと思いますがご容赦ください) ├── atoms │ ├── ALink.tsx │ ├── AuthUserIcon.tsx │ ├── BackButton.tsx │ ├── Button.tsx │ ├── CheckBox.tsx │ ├── CloseButton.tsx ├── molecules │ ├── CList.tsx │ ├── NoUserProgress.tsx │ ├── Progress.tsx │ ├── TermOfUseContent.tsx │ └── UserSelect.tsx ├── organisms │ ├── RegisterInfoList.tsx │ ├── ReserveModal.tsx │ ├── UserForm.tsx │ ├── UserInformation.tsx │ ├── common │ │ ├── ConfirmListModal.tsx │ │ ├── ConfirmModal.tsx │ ├── family │ │ └── FamilyListInfo.tsx │ ├── reservation │ │ ├── MonthCalendar.tsx │ │ ├── NoUserReservationConfirmMiniInfo.tsx │ │ └── ReservationListInfo.tsx │ └── user │ ├── FamilyEditForm.tsx │ ├── UserEditForm.tsx │ ├── UserEditListInfo.tsx │ ├── UserListInfo.tsx │ └── UserResisterForm.tsx └── layout ├── CommonWrapTemplate.tsx 私はフロントエンドで使う画面の部品を作っていく中でアトミックデザインのような考え方でコンポーネントを分割をしていました。(アトミックデザインについてはいろんな記事で紹介をされているのでここでは説明は割愛させていただきます) ここで上記のディレクトリー構造について私が感じた問題点については下記です。 新しいコンポーネントを作成する中でこれはatomsなの?それともmoleculesなの?みたいなことを考えることが多くなってしまった。 コンポーネントがpageに依存をしているものなのか、どこでも使って良いコンポーネントなのか分からない こちら色んな記事を参考にしたい自分で考えてみたりで下記のようになりました。 ├── common │ ├── layout │ │ └── CommonWrapTemplate.tsx │ ├── organisms │ │ ├── footer │ │ ├── header │ │ ├── index.ts │ │ └── sidebar │ └── uiParts │ ├── README.md │ ├── badge │ ├── button │ ├── card │ │ └── BaseCard.tsx │ ├── container │ │ └── BaseContainer.tsx │ ├── drawer │ │ └── BaseDrawer.tsx │ ├── error │ │ └── BaseErrorMessageWrapper.tsx │ ├── form │ │ ├── checkbox │ │ │ ├── BaseCheckBox.tsx │ │ │ └── LabelAndCheckBox.tsx │ │ ├── control │ │ │ ├── BaseFormControl.tsx │ │ │ └── BaseFormControlLabel.tsx │ │ ├── helperText │ │ │ ├── BaseHelperText.tsx │ │ │ └── IsUseLineHelper.tsx │ │ ├── label │ │ │ └── BaseLabel.tsx │ │ ├── molecules │ │ ├── required │ │ │ └── BaseRequired.tsx │ │ ├── select │ │ │ ├── BaseSelect.tsx │ │ │ └── LabelAndSelect.tsx │ │ ├── switch │ │ │ ├── BaseSwitch.tsx │ │ │ └── LabelAndSwitch.tsx │ │ ├── textarea │ │ │ ├── BaseTextArea.tsx │ │ │ └── LabelAndTextArea.tsx │ │ └── textfield │ │ ├── BaseTextField.tsx │ │ └── LabelAndTextField.tsx │ ├── index.ts │ ├── item │ ├── link │ ├── list │ ├── loading │ ├── modal │ ├── text │ ├── title │ └── toast └── pages ├── common │ ├── card │ │ ├── claim │ │ │ ├── ClaimCardLinkGroup.tsx │ │ │ └── ClaimCardWrapper.tsx │ │ └── shopping │ │ ├── ShoppingCardLinkGroup.tsx │ │ └── ShoppingCardWrapper.tsx │ ├── index.ts │ └── modal │ ├── claim │ │ ├── ConfirmDeleteClaimModal.tsx │ │ └── ConfirmReceiptClaimModal.tsx │ └── shopping │ └── ConfirmDeleteShoppingModal.tsx └── index └── layout └── TopPageTemplate.tsx 上記についても一部コンポーネントを省略していますが、問題点についてどのように解決をできたのかを振り返ってみたいと思います。 新しいコンポーネントを作成する中でこれはatomsなの?それともmoleculesなの?みたいなことを考えることが多くなってしまった こちらについてはatoms、moleculesのディレクトリーで作る事をまず分ける事をやめ、どのようなパーツなのかというところで分けるように変更をしました。このように変更をする事によって、対象の部品(button,card,modal)など、どの部品の中にいるのかを探せば良いようになりました。 organismsについてはパーツを大きくまとめる時に必要かなと思って作っています。 コンポーネントがpageに依存をしているものなのか、どこでも使って良いコンポーネントなのか分からない まずは下記のようにしてcommon(共通)のものなのか?それともpage(ページ)に依存しているものなのかを分ける事にしました。 ├── common └── pages その中で特定のページに依存はしているけども、各ページで同じコンポーネントを使用するパターンではpages配下のcommonに配置するようにしてみました。 なお、pages配下にindexというディレクトリーがあるのですが、こちらは/のページで使っているコンポーネントになります。 例えば、/shoppingsというページに依存するコンポーネントを作成をする時にはpages配下にshoppingsというディレクトリーを切るイメージです。 自分的にはこの考え方自体がとてもスッキリしているのですが、もっといい考え方があればご教授ください。 └── pages ├── common │ ├── card │ │ ├── claim │ │ │ ├── ClaimCardLinkGroup.tsx │ │ │ └── ClaimCardWrapper.tsx │ │ └── shopping │ │ ├── ShoppingCardLinkGroup.tsx │ │ └── ShoppingCardWrapper.tsx │ ├── index.ts │ └── modal │ ├── claim │ │ ├── ConfirmDeleteClaimModal.tsx │ │ └── ConfirmReceiptClaimModal.tsx │ └── shopping │ └── ConfirmDeleteShoppingModal.tsx └── index └── layout └── TopPageTemplate.tsx 前よりは個人的に管理がしやすくなってよかったなと感じております。 問題④ localStorageを色んな箇所で使用してカオスへ localStorageはとても便利です。色んなところで呼び出してデータを記憶をさせて、そのデータをいろんなところで呼び出したりと。。 ただページを跨いでlocalStorageを管理する時に何がどのように使用をされているのかが分からなくなってしまうように感じた事、どこでも使えるからこそ制約をつけながら使うのが大事だなと感じました。 過去に色んなところで無闇に使いすぎて少し辛味にハマった記憶こともあったり、、 なのでここでは何がlocalStorageで扱われているのかを管理するために専用のクラスを作成してみました。 LocalStorage.ts export const noticeStorageKeys = { // ページ遷移をした後のtoastの表示に使用。ページ遷移前にセット、遷移後に削除 pageMoveNotice: 'pageMoveNotice', } as const; export const authStorageKeys = { // ログインをした後のtokenの管理等に使用。全ページで使用。ログアウト時に削除される。 logined: 'logined', } as const; export const storageKeys = { ...authStorageKeys, ...noticeStorageKeys, } as const; /* NOTE `TStorageKey` ここにlocalStorageで使うkeyを定義する事。何がlocalStorageで使われているかを管理するため。 */ type TStorageKey = keyof typeof storageKeys; export type TLocalStorage = { getStorageItem: (itemKey: TStorageKey) => string | null | undefined; setStorageItem: (itemKey: TStorageKey, value: any) => void; removeStorageItem: (itemKey: TStorageKey) => void; }; class LocalStorage implements TLocalStorage { private readonly localStorage; constructor() { if (process.browser && window.localStorage) { this.localStorage = window.localStorage; } } public getStorageItem = (itemKey: TStorageKey) => { if (this.localStorage !== undefined) { return this.localStorage.getItem(itemKey); } }; public setStorageItem = (itemKey: TStorageKey, value: any) => { if (this.localStorage !== undefined) { this.localStorage.setItem(itemKey, value); } }; public removeStorageItem = (itemKey: TStorageKey) => { if (this.localStorage !== undefined) { return this.localStorage.removeItem(itemKey); } }; } export default LocalStorage; こちらのクラスでやっていることは元々のlocalstorageで扱うメソッドと同様です。ただ今回こちらの専用のファイルを作成する事によって下記のメリットが生まれたと思います。 localStorageでセットできるkeyの型をTypeScriptで縛る事によってどのようなkeyがセットされているかを把握することができる。 type TStorageKey = keyof typeof storageKeys; // "pageMoveNotice" | "logined" localstorageのkeyをこちらのファイルに配置し、コメントを残すなどする事でどのような目的でlocalStorageを使用をしているかがこのファイルを見たら大体わかるようになリました。 export const noticeStorageKeys = { // ページ遷移をした後のtoastの表示に使用。ページ遷移前にセット、遷移後に削除させること pageMoveNotice: 'pageMoveNotice', } as const; export const authStorageKeys = { // ログインをした後のtokenの管理等に使用。全ページで使用。ログアウト時に削除させること。 logined: 'logined', } as const; 実際にローカルストレージを扱うロジック部分までは考慮出来ていないのですが、通常のlocalStorageを呼び出すよりは制約を設けてよくすることができたかなと思っています。 問題⑤ TypeScriptの型が基本的なことしかできずに冗長に書いてしまっていた 前の参画した案件でTypeScriptを扱いましたが正直あまり良い書き方はできていなかったなと思う次第です。。 今でもまだまだ分からないことがありますが、以前より少しだけ扱いがわかるようになったので個人開発で改善をした箇所を少しだけご紹介をしたいと思います。 まず冗長に書くとこうなる。 // 大元になるschemaを定義 export type TShopping = { id: number; price: number; date: Date; description: string; isLineNotice: boolean; isLineNoticed: boolean; shopId: number; claimId: number; createdAt: Date; updatedAt: Date; }; // フォームに使用する export type TShoppingForm = { price: number; date: Date; description: string; isLineNotice: boolean; shopId: number; }; // totalPriceを追加したものを使う export type TShoppingList = { id: number; price: number; date: Date; description: string; isLineNotice: boolean; isLineNoticed: boolean; shopId: number; claimId: number; createdAt: Date; updatedAt: Date; totalPrice: number; // 追加 }; // フォームのエラーメッセージに対応をさせて全てstringにする export type TShoppingFormError = { price: string; date: string; description: string; isLineNotice: string; shopId: string; }; 同じ事を何度も書いています。 最初はまずこんな感じで書いてしまっていました。反省です。 上記をTypeScriptにあるUtilityクラス等を扱うって少しリファクタリングしました。 type TShopping = { id: number; price: number; date: Date; description: string; isLineNotice: boolean; isLineNoticed: boolean; shopId: number; claimId: number; createdAt: Date; updatedAt: Date; }; // フォームに使用する type TShoppingForm = Pick< TShopping, 'price' | 'date' | 'description' | 'isLineNotice' | 'shopId' >; // totalPriceを追加したものを使う type TShoppingAndTotalPrice = TShopping & { totalPrice: number }; // フォームのエラーメッセージに対応をさせて全てstringにする type TShoppingFormError = Record<keyof TShoppingForm, string>; 大分スッキリしました。ただスッキリしただけでなく、きちんとTShoppingという大元になるschemaから新しい型が生成されていることが重要だと考えています。 半年前の自分が見た時には、パッと見た時に何しているんですか?となっていると思います。ここから部分的な説明をしますと、 下記についてはフォームで使用をするキーをTShoppingから指定をして新たにTShoppingFormという型を生成しています。 type TShoppingForm = Pick< TShopping, 'price' | 'date' | 'description' | 'isLineNotice' | 'shopId' >; 下記についてはtotalPriceという型を新たに追加しております。これは直感的に分かりやすいのではないかと思います。 // totalPriceを追加したものを使う type TShoppingAndTotalPrice = TShopping & { totalPrice: number }; 最後にこちらについて // フォームのエラーメッセージに対応をさせて全てstringにする type TShoppingFormError = Record<keyof TShoppingForm, string>; 少しだけややこしく見えてしまうかなと思うのですが、やっている事を日本語にすると TShoppingFormにあるkeyを取得して、それを全てstringにしたTShoppingFormErrorという新しい型を作る という事になると思います。実際にエディターでhoverをしながら確認をしてみますと下記のようになります。 TypeScriptで上記のような型を作成をする時は元の型を拡張したりする事によって誤った型を定義した時にtslintでエラーを出して誤った記述をさせないようにすることが大事なんだなと改めて思った次第です。 下記はdescriptionをタイポした事によってTSLintでしっかりエラーを出してくれていますね。 このような恩恵を授かる意味でもきちんと元の型から型を拡張することの重要性が理解できるかと思います。 TypeScriptについてはまだまだ分かっていないこともたくさんあります。。 時間がある時に型職人になれるように勉強した次第です。 最後に 他にも色々と反省点はあるのですが、追加機能やAPI側で実装をすべきことがまだ残っているので、これからまた実装を進めていき、気づきがあればまた記事としてアウトプットできれば良いなと考えております。 こちら少し長くなりましたがお読みいただき、ありがとうございました!
- 投稿日:2021-08-15T13:29:58+09:00
PG::ConnectionBad (could not translate host name "db" to address: Name or service not knownが出た際の対処
事象 PG::ConnectionBad (could not translate host name "db" to address: Name or service not known docker-compose build時に上記エラー 対処 dockerのimageがたくさん立ち上がったままでデバイスの容量を圧迫していたのが原因っぽい。 上記の $ docker image prune でbuildし直したら治った
- 投稿日:2021-08-15T12:26:33+09:00
【就活】階層分析で最高の就職先を算出するアプリをつくりました【転職】
はじめに 複数の就職先候補で迷っている就活生・転職希望者の方、たくさんいらっしゃると思います。 そうした方々に向けて、選択肢の中から最高の就職先を算出するアプリをつくりました。 後悔なく意思決定するための少しでも足しになればと。 JobHunter's Choice(ジョブハンターズチョイス) https://jobhunters-choice.com サービス概要 AHP(階層分析法)という分析法を用いて複数の就職先の選択肢の中からベストなものを算出する、という手法を採用しました。 階層分析とは 複数ある選択肢のうちから最良のものを選択するための意思決定法です。 概要 例えばお部屋探しという目標に対して、評価基準(どの条件で選ぶか)と代替案(候補物件)があるとします。それらについて、 評価基準同士の重要性を比較する(ex:家賃と間取りのどちらをどの程度重視するか) 評価基準ごとに代替案同士を比較する(ex:家賃に関して物件Aと物件Bのどちらがどの程度すぐれているか) 以上をすべての組み合わせで行い、いろいろ計算すると、その人がどの評価基準をどの程度重視するかを加味した上で、それらの基準を最も満たす代替案を算出することができるわけです。 実績 工場の建設地の選定 大学教授の選抜 ソフトウェアシステムの品質の数量化 etc.. 企業活動、公共事業における幅広い分野で利用されているようです サービスの目的 で、この作業には前述の通りいろいろな計算が伴うので、表計算ソフトとかでやると相当面倒です。そもそも分析のノウハウどころか階層分析の存在すら知らない人のほうが多いですよね。 というわけで、 就職先選びにおいて、定量的なデータを提供し、かつその手間を最小限まで削減する これを目的として、サービス作成に至りました。 使い方 トップページのSTARTボタンを押すと分析開始。 STEP1 選択肢の記入 就職先の選択肢を入力します。 入力欄はデフォルトで3つですが、必要に応じて増やすことが可能。 STEP2 評価基準の選択 就職先選びの上で考慮する条件にチェックを付けます。 基準はデフォルトで出ているものの他に追加することが可能。 STEP3 評価基準の重要度評価 STEP2で選んだ基準がそれぞれペアになっており、全ての組み合わせが表示されています。 7段階評価のボタンが用意されているので、以下の基準でどちらを重視するか選びます。 1 = 左側の基準を重視する 4 = 両方同じくらい 7 = 右側の基準を重視する STEP4 選択肢の評価 評価基準ごとに、STEP1入力した選択肢がそれぞれペアになって表示されています。 その基準においてどちらの選択肢が優れている(と思う)かをSTEP4と同様に7段階評価ボタンで評価します。 結果 全て評価し終えてボタンを押すと、結果が表示されました! 階層分析によって企業ごとの評点が算出されており、総合評点が最も高い企業がベストチョイスになります。 各企業に各評価基準での評点が加算されていますが、重視する基準ほど多めに加算されているのがわかりますね。 その他の機能 分析結果はTwitterでシェア可能。 また、アカウント作成しログイン状態で使うと分析結果を保存したり過去の入力データを再利用してより作業を簡略化できます。 ↓ログイン時。過去の入力値がボタンで表示されます。 使用技術 Backend - Ruby on Rails 6.0.3 Frontend - Vue.js 2.6.12 UI - Vuetify 2.5.6, vue-chartjs 3.5.1, bootstrap 5.0.2 Infra' - AWS(EC2, RDS, Route53) WebServer - nginx 1.20.0 AppServer - unicorn 6.0.0 DB - PostgreSQL 13.x 段階評価ボタン Vuetifyのv-btn-toggleで作成。 評点は$emitで上位のコンポーネントに渡し、表示順通りに配列に格納する形で集計。 [Vue.js+Vuetify]段階評価ボタン ~評価項目の数と評価対象の数に応じて動的に表示する~ ボタンのレスポンシブ対応 [Vuetify]v-btn-toggle内のボタン幅をレスポンシブ対応 自動スクロール window.scrollByでボタン間のy座標の差分だけ自動スクロールさせます。 ボタン間の距離が固定値でなかったので、両者のy座標を毎回取得するようにしました。 // ボタンを押したら次のボタンへと自動スクロール autoScroll() { const cur = event.currentTarget.getBoundingClientRect().top // そのボタンの上端のy座標 const nxtItem = document.getElementById(`${次のボタンのID}`) // 自身の次のボタン(ターゲット) const nxt = nxtItem.getBoundingClientRect().top // ターゲットの上端のy座標 window.scrollBy(0, nxt-cur) //ターゲットと自身のy座標の差分だけスクロール } フォームの追加 フォームでバインドしているalternativesの初期値を長さ3のnullの配列にしているためデフォルトでフォームが3つイテレートされています。 この配列に新たにnullを加えるとフォームが追加され、過去の入力値を再利用するときはこの配列にその入力値を追加しています。 <template> ... <input v-for="(item, index) in alternatives" :key="index" v-model="alternatives[index]" > ... </template> <script> ... data() { return { alternatives: [null, null, null], ... </script> チェックボックスの追加 criteriaの要素がデフォルトで表示されている項目で、チェックされた要素はselectedCriteriaに格納されます。 項目を追加したり、過去のチェック項目を再利用するときは、criteriaとselectedCriteriaの両方にその項目が追加されることでチェックが入った状態で項目が追加されます。 <template> ... <v-checkbox v-for="(item, index) in criteria" :key="item" v-model="selectedCriteria" :value="item" :label="item" /> ... </template> <script> ... data() { return { criteria: [ '労働時間', '通勤時間', '雇用の安定', '仕事の裁量権', '社会への貢献度', ... ], selectedCriteria: [], ... おわりに できる限りユーザーの手数を減らすことを意識しました。このアプリでかなり時短になるはずです(当社比) まあ就職先の優先順位つけるだけっちゃつけるだけですが、わりと楽しくポチポチできるんじゃないでしょうか。 ジョブハンターの皆さんぜひ使ってみてください! 引用文献 [Vue.js+Vuetify]段階評価ボタン ~評価項目の数と評価対象の数に応じて動的に表示する~ [Vuetify]v-btn-toggle内のボタン幅をレスポンシブ対応 階層分析法
- 投稿日:2021-08-15T11:48:51+09:00
満経過月数を計算するロジック(Ruby)
経過月数(満月)を計算するロジックについて解説します。(あまり記事がなかった) 具体的に言うと以下のようなロジック。 start:2021年01月15日 end:2021年02月14日 →経過月数0を返す start:2021年01月15日 end:2021年02月15日 →経過月数1を返す start:2021年01月15日 end:2022年03月15日 →経過月数14を返す コード IN:開始日(start),終了日(end) :data型 OUT:満経過月数 def calculation_diff_months(start,end) (end.year - start.year) * 12 + end.month - start.month - (end.day >= start.day ? 0 : 1) end コードの解説 ■年の計算 (end.year - start.year) * 12 → 年の差分を求めて月変換 ■月の計算 end.month - start.month → そのまま月の差分を求める ここまではシンプル。 ■日付の計算 (end.day >= start.day ? 0 : 1) 終了の日付>=開始の日付の場合はそのまま月数を出したいので0で計算 終了の日付<開始の日付の場合は月数をマイナス-1にしたいので1で計算 閏年のこととか考えて最初、戸惑いましたが以外とシンプルに実装できました。
- 投稿日:2021-08-15T11:25:53+09:00
has_one型のデータを表示させたくて
こんにちは!! ユーザー登録が完了したら、ライブ情報を 投稿できるアプリを作成中です。 今回はhas_one型のデータの表示方法について 自分のメモ用としてアウトプットしていきたいです。 アソシエーション 作成中のアプリには3つのエンティティが存在します。 deviseを使って実装したユーザー登録をするadmin_usersテーブル、 ユーザーの詳細なプロフィールを保存するadmin_profilesテーブル、 ライブ情報を投稿できるeventsテーブルが存在します。 3つのテーブルのアソシエーションは以下の通りです。 Admin_user has_one admin_profiles. Admin_user has_many events. Admin_profile belongs_to admin_user. Event belongs_to admin_user. ユーザー登録はウィザード形式で保存する為、 AdminProfileモデルに belongs_to :admin_user, optional: true has_one_attached :admin_image と記述しました。 また、ライブハウスの画像を保存したかったので、 Active Strageを利用してadmin_imageとしました。 実装したいこと 訪れたユーザーが、ライブハウスの詳細ページ(admin_users/show)にいくと ユーザーの詳細な情報(admin_profilesテーブルに保存された情報)を 見ることができるように試みました。 admin_users_controllerには以下のように記述 class AdminUsersController < ApplicationController def show admin_user = AdminUser.find(params[:id]) @store_name = admin_user.store_name @admin_profile = admin_user.admin_profile end end NoMethodError ウキウキでブラウザを更新してみると以下の表示が。 ふざけやがって。 ナンジャそりゃ。 NoMethodErrorですって。 悩みすぎてrubyが嫌いになったのでやめてしまおうかと考えてました。 optional trueによる弊害 今回のエラーはウィザード形式でbelongs_toの外部キーのnilを許可する optional: trueを記述したことで起こったようです。 ターミナルでコンソールを立ち上げて pluckを確かめてみると [1] pry(main)> AdminUser.pluck(:id) (0.3ms) SELECT `admin_users`.`id` FROM `admin_users` => [5, 16, 7, 14, 12, 15, 6, 8, 13, 1, 11, 9, 4, 18, 3, 17, 2, 10] [2] pry(main)> AdminProfile.pluck(:admin_user_id) (0.3ms) SELECT `admin_profiles`.`admin_user_id` FROM `admin_profiles` => [4, 6, 7, 13, 14, 15, 17, 18] admin_userとadmin_profileで紐付けられていないidが いますねえ。 ぼっちを使う もう今更ユーザー登録をウィザード形式で実装してたのを やめて実装し直すとかめんどくさいんでやりたくなかったので ボッチ演算子を使用することにしました。 <div class="profile-contents"> <p class="shop-name"><%= @store_name %></p> <p class="postal-code"> 〒 <%= @admin_profile&.postal_code %> </p> <p class="prefecture-text"> <%= @admin_profile&.prefecture&.name %><%= @admin_profile&.municipality %> </p> <p class="address-text"> <%= @admin_profile&.address %><%= @admin_profile&.building_name %> </p> <%= link_to "Google mapで見る", "#", class:"map-link" %> <p class="tel"> TEL: <%= link_to @admin_profile&.phone_number, "#", class:"phone-number" %> </p> <p class="admin-profile"> <%= @admin_profile&.profile %> </p> </div> レシーバーであるオブジェクトに対してあるメソッドを実行する時にエラーを出さずにメソッドを 表示することができるぼっち演算子を使うことで解消できました。 rubyおもしろい。
- 投稿日:2021-08-15T08:27:03+09:00
【Ruby on Rails】Time.currentメソッドを使って簡単に時間表示をする方法
対象者 時間の表示方法を知りたい方 目的 Time.currentメソッドを使って時間を表示する 実際の手順と実例 1.結論 Time.currentメソッドを使えば簡単に時間が表示できます。 rails cで確認すると $ rails c Running via Spring preloader in process 6425 Loading development environment (Rails 5.2.6) [1] pry(main)> Time.current => Sat, 14 Aug 2021 23:20:51 UTC +00:00 [2] pry(main)> 上記のように今日の時間が取得できます。 2.応用編 Time.current.all_month #今月 Time.current.all_week #今週 Time.current.all_day #今日 参照 時刻や日付を扱うメソッドの基本情報まとめ【Ruby】【Rails】 投稿者コメント 時間どのように表示すればいいんだろうって前に悩んでたんですけど、これで一発なんですね。。。勉強になりました。。。 My Profile プログラミング学習歴3ヶ月目のアカウントです! プログラミングスクールで学んだ内容や自分が躓いた箇所等のアウトプットの為に発信しています。 また、プログラミング初学者の方にわかりやすく、簡潔にまとめて情報共有できればと考えています。 もし、投稿した記事の中に誤り等ございましたら、コメント欄でご教授いただけると幸いです。
- 投稿日:2021-08-15T01:34:48+09:00
[Rails]redirect_toとrenderの違いについて
はじめに 本記事では、redirectとrenderの違いについて記述しています。 私自身何気なく、コントローラーなどに記述しておりましたが、 改めて振り返りアウトプットしたいと思います。 redirectとrenderの違い redirect_to ↓ ルーティング ↓ コントローラー ↓ ビュー render ↓ ビュー 随分あっさり書きましたが、このような違いです。 redirect_to redirect_toは新しくリクエストが送られてきた時と同じ流れになります。 そのため、同様にルーティング→コントローラー→ビューの流れになります。 render renderは、特にリクエストをされることがなく、そのまま直接ビューにいきます。 redirect_toはコントローラーへ! renderはビューへ!ということですね。 例え class MessagesController < ApplicationController . . 省略 . . def create @room = Room.find(params[:room_id]) @message = @room.messages.new(message_params) if @message.save redirect_to room_messages_path(@room) ←ここ else @messages = @room.messages.includes(:user) render :index ←ここ end end . . "省略" . . end redirect_toは、 データを送信する時(例えば、記事を投稿できた時、記事を編集できた時) の場合などに使用されています。 今回の場合ですと、 メッセージが送れたら、ルーティング→コントローラー→ビューroom_messages_path(@room)ですね。 コントローラーへいくことでとモデルとも連携ができ、メッセージを情報として保存することもできます。(メッセージのテーブルがあれば) renderは、 データの送信に失敗した時(例えば、なんらかの項目が空白でログインができなかった時、記事が投稿できない時) の場合などに使用されています。 今回の場合ですと、 送信に失敗したら、ビューへindexですね。 メッセージ送信に失敗しているので、 イジワルな言い方をするともっかいやり直し〜(ビューへ)という感じですね? 終わりに インプットの段階では、???でしたが、 簡単なアプリケーションで試してみると理解は深まると思い、コードを記述してみました。 他に、図を用いて理解を深めてらっしゃる記事もありましたので、 こちらを参考にしながらだと、なお理解は深まると思います。 分かりやすい図がある記事 RailsGuide 明日は日曜日ですが、コツコツがんばります!!
- 投稿日:2021-08-15T00:15:31+09:00
rubyをバージョンアップしてrailsコマンドが使えなくなった時の対処法
rubyをバージョンアップしてrailsコマンドが使えなくなった時の対処法 railsコマンドを実行して can't find gem railties (>= 0.a) with executable rails (Gem::GemNotFoundException) 上のエラーが出る時 gem install rails を実行すると解決する。