20210129のMySQLに関する記事は3件です。

いいね機能を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');・・・2

1.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">&#9825;</button><a v-if="status == false" href="#">{{count}}</a>
  <button v-else type="button" @click.prevent="like_check" class="btn btn-warning">&#9829;</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()アクションが実行されます。

まとめ

status = falseスクリーンショット 2021-01-29 23.16.28.pngstaatus = trueスクリーンショット 2021-01-29 23.16.38.png

今回はAxiousで実装しており非同期でLikeContorollerのアクションを呼び出して、いいね機能を実装しています。もっと簡単で出来る方法や綺麗な記述ありましたらご教授お願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

関連エンティティがあるときの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.properties
spring.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.java
public class Book {
private Integer id;
private String name;

@OneToOne( fetch = FetchType.EAGER ,cascade = CascadeType.ALL)
@JoinColumn(name="buyer_id")
private Buyer buyer;

}

Buyer.java
public class Buyer {
private name;
private address;
}

image.png

こんなイメージの関係性で、この場合は、

@Query("select b from Book b")

とするだけで、Buyerもちゃんと取ってきてくれる。

Joinとか書いてたら永遠に構文エラーが発生する

@Query("select b from Book b JOIN Buyer")

って書いてたら永遠に構文エラーになります。

しかもどこがおかしいっていうメッセージも、なんか場所がちがうんですよね。

near '' at line 1
``

ってどこ??`''`の中におかしいところがあるよってメッセージだけど、なんかちゃんと教えてくれませんでした。


###結論!

JPQLとSQLは違うよっていう明確な、強い意志と理念が必要!!



  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分なりの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に以下の様に記述し、ビルドコマンドを実行する

Gemfile
gem 'bullet', group: 'development'
ターミナル
docker-compose build

※開発環境にDockerを使用されていない方はbundle installのなどのコマンドでインストールする。

次にBulletを使用可能するためのジェネレートコマンドを実行する。

ターミナル
docker-compose run web bundle exec rails g bullet:install

これでセットアップは完了!

前提

いざチェックをする前に自分がどのようなアプリを使うのか簡単に説明する。
このアプリでは、学校側がアプリ上でアンケート機能が付いたお知らせを作成し、保護者(User)お知らせ(Board)にある『参加する』ボタンを押すことでアンケート(Join)に返答し、その結果を集約することができる。またその集約の結果を見ることができる。
具体的な使用例を挙げると、まず学校が『除草作業のお知らせ』という題名のお知らせを作成し、保護者に公開する。次にその『除草作業』に参加したい保護者は、お知らせにある『参加する』ボタンを押す。すると同じお知らせにある『アンケート結果』をクリックし、アクセスすると『参加する』ボタンを押した保護者の名前が全て表示される。この一連の機能でアンケート集約を迅速に行うことができる。

アプリには以下のテーブルが作成されている。
2021-01-29 11.49のイメージ.jpg

チェックの結果

全てのページにアクセスした結果、N+1問題、いわゆる大量のSQLクエリが発行されているページが1つだけあった。

そのページは、先ほど紹介したお知らせ(Board)のアンケート結果、つまり行事に参加する(Join)保護者(User)の一覧がループ処理で表示されているページであった。
2021-01-29 12.06のイメージ.jpg

実際にそのページアクセスするために『アンケート結果』のリンクをクリックすると以下の様にBulletによる警告がブラウザ、ログに表示された。
CA5478FB-FE4C-4136-BD65-A06B4EE572A3_1_105_c.jpeg

ターミナル
省略・・・
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.rb
def show
  @board = Board.find(params[:board_id])    ←①
  @joins = @board.joins.page(params[:page]) ←②
end
app/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.rb
def 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 の個人的な使い分け

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む