VideoとCommentを投稿できるUserをそれぞれの投稿への一定期間中のLike数の合計で並べ替え
前提
- UserはVideoとCommentを持っている。
- それらに付いたLikeをwhere('created_at > ?', Time.now - 1.days)とかで一定期間に制限してカウントして足し合わせて、その合計が多い順にUserを並べたい。
- PostgreSQL
以下、例示のために必要な部分だけ抜き出した。
/app/models/user.rb
has_many :videos, dependent: :destroy has many :comments, dependent: :destroy
- /app/models/video.rb
has_many :video_likes, dependent: :destroy belongs_to :user
- /app/models/video_like.rb
belongs_to :video
- /app/models/comment.rb
has_many :comment_likes, dependent: :destroy belongs_to :user
- /app/models/comment_like.rb
belongs_to :comment
実装
# tは「Time.now - 1.days」とか「Time.now- 1.month」とか User.joins(%| left join ( select users.id as user_id, coalesce(sum(videos.video_likes_count), 0) as video_likes_count from users left join ( select videos.id, videos.user_id, count(video_likes.id) as video_likes_count from videos left join ( select id, video_id from video_likes where created_at > '#{t}' ) as video_likes on video_likes.video_id = videos.id group by videos.id ) as videos on videos.user_id = users.id group by users.id order by video_likes_count desc ) as user_video on user_video.user_id = users.id left join ( select users.id as user_id, coalesce(sum(comments.comment_likes_count), 0) as comment_likes_count from users left join ( select comments.id, comments.user_id, count(comment_likes.id) as comment_likes_count from comments left join ( select id, comment_id from comment_likes where created_at > '#{t}' ) as comment_likes on comment_likes.comment_id = comments.id group by comments.id ) as comments on comments.user_id = users.id group by users.id order by comment_likes_count desc ) as c on c.user_id = users.id |) .select('users.*', '(user_video.video_likes_count + c.comment_likes_count) as count') .order('count desc', created_at: :desc)
解説
- | video_id | video_likes_count |と| comment_id | comment_likes_count |を作って、それらをusersにleft joinし、video_likes_countとcomment_likes_countの和をcountとした。
- coalesceを使ってlikeされた事のないUserの*_likes_countがnilじゃなくて0になるようにした。
感想
- joinが多すぎるのでデータが増えたらレスポンスがかなり遅くなりそう。
- 大学でDBの講義を担当している教員に訊いてみたら「概ねこのやり方でいいと思う。ただしjoinの回数が多すぎるのでマテリアライズドビューを使って途中結果をキャッシュしたほうがいい。」と教えていただいた。
実装し終わってから以下の実装方法もありだと思った。
- 実は本当に欲しいcreated_atのレンジは「一日」「一週間」「一ヶ月」「制限無し」の4パターンだったので、これらを保存しておくテーブルを作り、6時間毎くらいで更新する。
他の実装方法あったら教えて欲しいです。
参考
ActiveModelSerializersのCollectionSerializerを使うときはネストの深さをどこで指定する?
A. renderで渡す
@res = Hoge.where(name: hoge) @items = ActiveModel::Serializer::CollectionSerializer.new( @res, serializer: HogeSerializer, some_values: user_signed_in? && current_user.some_values ) render json: { items: @items.page(@page), length: @items.size, }, include: ['hoge_child_a.hoge_child_child', 'hoge_child_b', 'hoge_child_c'], status: :ok
YouTube埋め込みプレーヤーのあるページでhistoryに同一URLがpushされる
何が起こった?
- YouTube埋め込みプレーヤーを使っているページで同一のURLがhistoryにpushされていた。つまりページAからYouTube埋め込みプレーヤーのあるページBに遷移し動画の読み込みが完了した後に「前のページに戻る」操作をしてもそこは同じページBであり、二回戻る操作をしなければいけなくなった。
- React、React Router
なぜ起こった?
- 以下のようにiframeのsrcにyoutube_video_idを渡しているが、このyoutube_video_idはAPIを叩いて取得した値である。つまり初めはnullで、APIの結果が返ってくると実際の値が入る。
- 初めは
<iframe src={null}>
でAPIの結果が返ってくると<iframe src={"https://..."}>
という感じでiframe内のURLが変化している。この変化によって前述の問題が発生していた。
const PageB =() => { const { youtube_video_id } = useSelector(state => state.hoge); return ( <div> <iframe title={"youtube" +youtube_video_id} src={"https://www.youtube.com/embed/" + youtube_video_id} frameborder="0" allowfullscreen ></iframe> </div> ); }
どう直した?
- つまりは正しいyoutube_video_idが手に入るまではiframe自体を描画しなければいいので以下のようにした。
const PageB = () => { const { youtube_video_id } = useSelector(state => state.hoge); if (youtube_video_id == null) { return ( <div>読み込み中...</div> ); } return ( <div> <iframe title={"youtube" + youtube_video_id} src={"https://www.youtube.com/embed/" + youtube_video_id} frameborder="0" allowfullscreen ></iframe> </div> ); }
気をつける点
- これはiframeのsrcがnullからあるURLに変化したから起こったバグであるが、もしPageB -> 他のページ -> PageBという流れで移動したときに一回目のPageBのyoutube_video_idと二回目のPageBのyoutube_video_idが違う場合に注意が必要である。PageBがアンマウントされた時にStore内のyoutube_video_idをnullで初期化しておかないと今回の問題と全く同じ問題が発生する。
参考
RailsでItem has many Tagの時にTagに対する条件のANDをしたい
やりたいこと
# Item has_many :item_tag_relations, dependent: :destroy has_many :tags, through: :item_tag_relations, dependent: :destroy
# ItemTagRelation belongs_to :item belongs_to :tag
- Tag name: string
という状況で「"red"と"blue"と"yellow"全てのnameのTagを持ったItem」が欲しいという状況がある。 単純に考えて
names = ['red', 'blue', 'yellow'] tag_ids = Tag.where(name: names).select(:id) item_ids = ItemTagRelation.where(tag_id: tag_ids).select(:item_id) @items = Item.where(id: item_ids)
としても「"red"と"blue"と"yellow"のいずれかのnameのTagを持ったA」になってしまい、これは条件のORである。 これを「"red"と"blue"と"yellow"全てのnameのTagを持ったItem」という形で条件のANDにしたかった。
やりかた
names = ['red', 'blue', 'yellow'] item_ids = Item.joins(:tags) .where(tags: { name: names }) .group('items.id') .having('count(distinct tags.name) = ?', names.uniq.count) @items = Item.where(id: item_ids)
やってることは単純で、joinした後に条件をクリアしたTagのnameによる重複を排除した数と条件の数が等しいならそれは全ての条件をクリアしているということなのでそれを利用している。
Heroku, Rails, BigQueryを使って日付毎にログを保存して、APIエンドポイント毎のコール数を知りたい
なぜ?
WEBページのPVやAUはGoogle Analyticsを見ればわかるが、ページによってはタブの遷移などでURLが割り振られていない動作があり、それはAPIコールの回数を解析することでしか知ることができない。そして解析はSQLを使っていい感じにやりたい。
やりかた : サーバ
参考にさせていただいた以下の記事通りにやればほとんど出来るが一部変更点がある。
変更点
$ heroku drains:add https://my-fluentd-app.herokuapp.com/log -a myapp
→$ heroku drains:add https://my-fluentd-app.herokuapp.com/access_log -a myapp
td-agent.confの一部を以下のように変更した。(pathにクエリパラメータを含めないようにし、テーブル名を変えた)
format /\<.*\>\d (?<strtime>.+)\..* method=(?<method>.+) path=((?<path>[^\?]+)(\?.*)?) host=(?<host>.+) request_id=(?<request_id>.+) fwd=(?<fwd>.+) dyno=(?<dyno>.+) connect=(?<connect>.+) service=(?<service>.+) status=(?<status>.+) bytes=(?<bytes>.+) protocol=https$/
table api_call_%{time_slice}
field_string strtime,path,host,status,method,client,fwd,service,bytes,request_id,dyno,connect
やりかた:BigQuery
SELECT path, COUNT(*) AS count FROM `hogehoge.fugafuga.api_call_*` WHERE _TABLE_SUFFIX BETWEEN '20191001' AND '20191031' GROUP BY path LIMIT 1000
Rubyでgoogle-api-clientを使ってYouTube Data APIを呼ぶ
使い方例
準備
Gemfile
gem 'google-api-client', '~> 0.30.6'
/config/initializers/youtube.rb
require 'google/apis/youtube_v3' module YouTube class Client Service = Google::Apis::YoutubeV3::YouTubeService.new Service.key = ENV.fetch('DEVELOPER_KEY') end end
呼び出し
res = YouTube::Client::Service.list_channels( 'statistics', id: youtube_channel_id ) res.items.first.statistics.subscriber_count
res = YouTube::Client::Service.list_playlist_items( 'snippet', playlist_id: playlist_id )
topic_res = YouTube::Client::Service.list_videos( 'topicDetails', id: youtube_video_id )
- メソッドの命名規則はおそらく
(list | update | insert ...)_(playlist_items | videos | channels ...)
という感じになっていて、公式のドキュメント(https://developers.google.com/youtube/v3/docs?hl=ja)のメニューの第一階層(PlaylistItems, Videos, Channels ...)と第二階層(list, update, insert ...)に対応している。 - メソッドの第一引数はpart(APIのレスポンスに含める内容を決める値)で、カンマ区切りで複数指定できる。
- レスポンス内のキーはスネークケース
参考
google-api-ruby-client/service.rb at master · googleapis/google-api-ruby-client · GitHub
google-api-ruby-client/you_tube.rb at master · googleapis/google-api-ruby-client · GitHub
RailsでSerializerを使ってログインしたUserがLikeできるItem列をPaginationしたい
詰まった点
Serializer側で、「ユーザはログインしているか」と「ユーザは過去にLikeしたか」を判断して値を返さなければいけないがActiveModel::Serializer::CollectionSerializerを使ってどうやってやる?
解決策
ActiveModel::Serializer::CollectionSerializerの#initialize(resources, options = {}) のoptions[:item_likes]にログインしてないならfalseをわたし、ログインしているならそのユーザのitem_likesをわたす
モデルはこんな感じ
- User
- has_many: item_likes
- Item
- has_many: item_likes
- ItemLike
- belongs_to: user
- belongs_to: item
コントローラはこんな感じ
items_json = ActiveModel::Serializer::CollectionSerializer.new( Item.hogehoge.page(@page), serializer: ItemSerializer, item_likes: user_signed_in? && current_user.item_likes ).as_json render json: { items: items_json, length: length }, status: :ok
Serializerはこんな感じ
class ItemSerializer < ActiveModel::Serializer attributes %i[id is_liked] def initialize(object, options = {}) super @item_likes = options[:item_likes] end def is_liked @item_likes && @item_likes.exists?(item_id: object[:id]) end end