- 投稿日:2021-01-29T23:20:20+09:00
いいね機能をLaravelとVue.jsで実装②
こちらの記事はパート2です。
前回の記事はこちらから↓↓
https://qiita.com/tanaka2020/items/8ec8ae0fdd3ad4409c74実装
4.ルートの設定
routesディレクトリ内のweb.phpを編集
Route::get('/posts/{post?}/firstcheck', 'LikeController@firstcheck')->name('like.firstcheck');・・・1 Route::get('/posts/{post?}/check', 'LikeController@check')->name('like.check');・・・21.Vue読み込み時にLikeControllerのfirstCheck()アクション呼び出し
2.いいねボタンクリック時に記事id別にLikeControllerのCheck()アクション呼び出し5.Vue Components作成
resouces
|ーjs
|ーcomponents
|ーLikeComponent.vue<template> <div> <button v-if="status == false" type="button" @click.prevent="like_check" class="btn btn-outline-warning">♡</button><a v-if="status == false" href="#">{{count}}</a> <button v-else type="button" @click.prevent="like_check" class="btn btn-warning">♥</button><a v-if="status == true" href="#">{{count}}</a> </div> </template> <script> export default { props: ['post_id'],・・・1 data() { return { status: false,・・・2 count: 0, } }, created() { this.first_check()・・・3 }, methods: { first_check() { const id = this.post_id const array = ["/posts/",id,"/firstcheck"]; const path = array.join('') axios.get(path).then(res => { if(res.data[0] == 1) { console.log(res) this.status = true this.count = res.data[1] } else { console.log(res) this.status = false this.count = res.data[1] } }).catch(function(err) { console.log(err) }) }, like_check() { const id = this.post_id const array = ["/posts/",id,"/check"]; const path = array.join('') axios.get(path).then(res => { if(res.data[0] == 1) { this.status = true this.count = res.data[1] } else { this.status = false this.count = res.data[1] } }).catch(function(err) { console.log(err) }) }, } } </script>1.blade側はより投稿記事のidが渡されます。
<like-component :post_id="{{$post->id}}"></like-component>2.statusがfalseだといいねされていない状態です。
3.Vue読み込み時にLikeControllerのfirstCheck()アクションが実行されます。まとめ
今回はAxiousで実装しており非同期でLikeContorollerのアクションを呼び出して、いいね機能を実装しています。もっと簡単で出来る方法や綺麗な記述ありましたらご教授お願いします。
- 投稿日:2021-01-29T17:56:35+09:00
関連エンティティがあるときのJPQLの書き方
環境
Spring boot
参考にしたサイト
Spring DATA JPAでデータ検索
JPQLの内部テーブル結合を試してみるエラーメッセージ
MySQL server version for the right syntax to use near '' at line 1
もうこし長かったけど、構文エラーがあるよってエラー。
原因
JPQLの書き方が間違っていた!というか、MySQLのNativeQueryとはちょっと書き方がちがうんだなぁ~くらいの認識でしたけど、結構違う感じでしたね。
特に
@OneToOne
とかの関連エンティティがある場合の扱い方が間違っていました。外部キーで結合するので、
LEFT JOIN
とか書いていたけど、それが原因でした。application.propertiesに次のように書いて、SQLを参考にしました。コンソールに大量に表示されるようになります。
application.propertiesspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect spring.jpa.show-sql=trueけど、MySQLのSQL文とはなんかが違うみたいですね。
Hibernateってなに?
wikiによると
Java のためのオブジェクト関係マッピング (ORM) ライブラリであり、オブジェクト指向のドメインモデルを関係データベースにマッピングするためのフレームワークを提供する。
初心者には難しいですけど、多分の解釈でいうと、
データベースから取得してきたデータを、オブジェクトとかインスタンスのフィールドに代入して、以降はオブジェクトとして扱えるようにするためにHibernateっていうのがよしなにやってくれる
って認識で良さそうです。
PHPの学習をしているときには、
1. DB接続
2. レコード取得
3. カラムを取得
4. 配列に格納
5. DB切断って言う流れをそのまま書いてましたね。オブジェクトとかクラスをまだ知らないときです。
Javaをやりだして、早速クラスを勉強して、便利さもわかってきたところでなんとデーブルをそのままクラスと対応させることができるということもできることを理解しだしたのが最近です。
JPQLってなに?
JPAでDB(正確にはエンティティ)からデータを取得するSQLをJPQLと言います。
RepositoryでJPAが出てきてたな。これでデータベースから取得してクラスに格納できるようになるんだなと、なんとなく理解。
Repositoryでも@Queryアノテーションを付けてfindBy~みたいなメソッドを定義したことあったけど、なんのこっちゃわかっていませんでした。
でも今日わかったことが。
// 普通のSQL文を自分で書く @Query(value="select * from なんちゃら~", nativeQuery = true)// これがJPQLで、テーブルと紐付けたエンティティでSQL文を書ける的な @Query("select エイリアス from オブジェクト エイリアス ~")確かにわかりやすい記述ができるから便利だな~
エンティティの中に別のエンティティがあったらどうするの?
spring.jpa.show-sql=true
としていると、
jpaで発行されたSQLがコンソールに出力されるので、それを見ると、結合しているテーブルがある場合はleft outer join
って書いてました。ちょっと複雑な条件の場合は生のSQL文を書いて、結合の部分は
left outer join
って書けばいいんだなとあいまいに考えていましたが、ちゃいましたね。@OneToOneや@OneToManyなどのアノテーションで関連を記述していたら、そのエンティティを含むオブジェクトをデータベースから取得する際にjpaが勝手に中のエンティティのデータも持ってきてくれる。
イメージ
Book.javapublic class Book { private Integer id; private String name; @OneToOne( fetch = FetchType.EAGER ,cascade = CascadeType.ALL) @JoinColumn(name="buyer_id") private Buyer buyer; }Buyer.javapublic class Buyer { private name; private address; }こんなイメージの関係性で、この場合は、
@Query("select b from Book b")とするだけで、Buyerもちゃんと取ってきてくれる。
Joinとか書いてたら永遠に構文エラーが発生する
@Query("select b from Book b JOIN Buyer")って書いてたら永遠に構文エラーになります。
しかもどこがおかしいっていうメッセージも、なんか場所がちがうんですよね。
near '' at line 1 `` ってどこ??`''`の中におかしいところがあるよってメッセージだけど、なんかちゃんと教えてくれませんでした。 ###結論! JPQLとSQLは違うよっていう明確な、強い意志と理念が必要!!
- 投稿日:2021-01-29T16:47:05+09:00
自分なりのN+1問題へのアプローチ
N+1問題とは
ループ処理の中で大量のSQLクエリが発行され、アプリにおけるパフォーマンス上の問題を引き起こす現象のこと。
バックエンドを主戦場とするエンジニアが最も神経を使う問題の1つである。自分なりのアプローチ
自分の作成したアプリ上でもN+1問題が起きていないかチェックしたいと考え、方法を調べたところ2通りの方法があった。
①Bullet Gemを使用する
このGemを使えば、N+1問題が発生しているページにアクセスするとブラウザ・ログなどに警告を出してくれる。②ログを見る
このアプリはDocker環境で開発しているため、docker-compose up
コマンドでコンテナを起動するとターミナルにログが出力される。ログの中に大量のSQLクエリが発行されていれば,
そのページではN+1問題が発生し、レスポンスが遅くなったり、最悪アプリがサーバーダウンする可能性があることがわかる。今回はこの2通りの方法で自分のアプリをチェックした。
Bullet Gemのセットアップ
まずは以下のドキュメントを参考にBullet Gemの設定を行う。
https://github.com/flyerhzm/bullet①Gemのインストール
Gemfileに以下の様に記述し、ビルドコマンドを実行するGemfilegem 'bullet', group: 'development'ターミナルdocker-compose build※開発環境にDockerを使用されていない方は
bundle install
のなどのコマンドでインストールする。次にBulletを使用可能するためのジェネレートコマンドを実行する。
ターミナルdocker-compose run web bundle exec rails g bullet:installこれでセットアップは完了!
前提
いざチェックをする前に自分がどのようなアプリを使うのか簡単に説明する。
このアプリでは、学校側がアプリ上でアンケート機能が付いたお知らせを作成し、保護者(User)
がお知らせ(Board)
にある『参加する』ボタンを押すことでアンケート(Join)
に返答し、その結果を集約することができる。またその集約の結果を見ることができる。
具体的な使用例を挙げると、まず学校が『除草作業のお知らせ』という題名のお知らせを作成し、保護者に公開する。次にその『除草作業』に参加したい保護者は、お知らせにある『参加する』ボタンを押す。すると同じお知らせにある『アンケート結果』をクリックし、アクセスすると『参加する』ボタンを押した保護者の名前が全て表示される。この一連の機能でアンケート集約を迅速に行うことができる。チェックの結果
全てのページにアクセスした結果、N+1問題、いわゆる大量のSQLクエリが発行されているページが1つだけあった。
そのページは、先ほど紹介した
お知らせ(Board)
のアンケート結果、つまり行事に参加する(Join)保護者(User)
の一覧がループ処理で表示されているページであった。
実際にそのページアクセスするために『アンケート結果』のリンクをクリックすると以下の様にBulletによる警告がブラウザ、ログに表示された。
ターミナル省略・・・ web_1 | User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 5 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 6 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 7 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 8 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 9 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 10 LIMIT 1 web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | CACHE User Load (0.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 11 LIMIT 1 [["id", 11], ["LIMIT", 1]] web_1 | ↳ app/views/joins/show.html.erb:10 web_1 | CACHE (0.0ms) SELECT COUNT(*) FROM `joins` WHERE `joins`.`board_id` = 1 web_1 | ↳ app/views/joins/show.html.erb:16 web_1 | Rendered joins/show.html.erb within layouts/application (78.9ms) web_1 | Completed 200 OK in 262ms (Views: 211.1ms | ActiveRecord: 6.7ms) web_1 | web_1 | web_1 | user: root web_1 | GET /boards/1/joins web_1 | USE eager loading detected web_1 | Join => [:user] web_1 | Add to your query: .includes([:user]) web_1 | Call stack web_1 | /app_name/app/views/joins/show.html.erb:10:in `block in _app_views_joins_show_html_erb___278131760983844940_69984784735660' web_1 | /app_name/app/views/joins/show.html.erb:8:in `_app_views_joins_show_html_erb___278131760983844940_69984784735660'
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1
といったユーザー情報を出力するSQLクエリが参加人数の10人分発行されていることが見てわかった。この原因を突き止めるべく、ソースコードを確認することにした。該当コードは以下の通りである。該当コードを見ると…
app/controllers/joins_controller.rbdef show @board = Board.find(params[:board_id]) ←① @joins = @board.joins.page(params[:page]) ←② endapp/views/joins/show.html.erb<div class="container"> <div class="card"> <div class="card-header"> 参加者一覧:参加人数 <%= @board.joins.count %> </div> <table class="table"> <% @joins.each do |join| %> ←③ <tr> <td><%= link_to join.user.name, join.user %></td> </tr> <% end %> </table> </div> <%= paginate @joins %> </div>①お知らせ(Board)のデータを取得し、変数@boardに代入
②Joinsテーブルにあるお知らせ(board_id)が格納されているカラムのデータを全て取得し、変数@joinsに代入
③eachを使った繰り返し処理でアンケートに返答した行事の参加者(join.user)の名前(name)を表示するといった処理の流れになっている。
問題は③の繰り返し処理の時に10回分Joinsテーブルに格納されているuser_idをもとにUsersテーブルからデータを取得するSQLが発行されるようなコードが記述されているところであるとわかった。これを日常生活で例えるなら、買い物をする時にカゴを使わず1個1個商品を売り場から持ってきてレジに通し、ヘトヘトになっている状況と同じである。なんとも非効率的…
改善策
ではどうすればこの状況を改善できるか。ヒントはBullet Gemが警告と共に教えてくれていた。
先程警告が出たログの最下部を見ると…user: root GET /boards/1/joins USE eager loading detected Join => [:user] Add to your query: .includes([:user])指示通りに実装してみた結果、コードは以下の通りになった。
app/controllers/joins_controller.rbdef show @board = Board.find(params[:board_id]) #includesメソッドを使ってuser_idをもとに参加者(user)のデータを取得する @joins = @board.joins.includes([:user]).page(params[:page]) endこれによりJoinsテーブルのデータと一緒にUsersテーブルのデータも取得できるようになった。もう一度警告された参加者一覧ページにアクセスした結果、以下のログが出力された。
ターミナル省略・・・ web_1 | User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11) web_1 | ↳ app/views/joins/show.html.erb:8 web_1 | CACHE (0.0ms) SELECT COUNT(*) FROM `joins` WHERE `joins`.`board_id` = 1 web_1 | ↳ app/views/joins/show.html.erb:16 web_1 | Rendered joins/show.html.erb within layouts/application (39.2ms) web_1 | Completed 200 OK in 215ms (Views: 182.1ms | ActiveRecord: 3.1ms)
User Load (0.6ms) SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
というログからUsersテーブルの情報がまとめられ、1回で出力されていることがわかる。警告も出なかったので、これでひとまずN+1問題は解決することができた。includesメソッドについて
今回大活躍してくれたincludesメソッドをもう少し深く調べてみると、実はモデルの関連状況によって
eager_load
メソッドとpreload
メソッドを使い分けていることがわかった。今回のケースでincludesメソッドは、SQLの発行状況からpreload
メソッドの挙動をしていたこともわかった。これからは、このincludesメソッド内での挙動の違いについて詳しく調べて行きたいと思う。最後までお読みいただきありがとうございました。
参考
bulletドキュメント
ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由
ActiveRecordのincludes, preload, eager_load の個人的な使い分け