ワーパパエンジニアの学び手帳

ワーパパエンジニアの業務外での学びとかガジェットネタとか

予約サイトから空き情報を定期的に取得しslackに通知する

再開してからだいぶ間が開いてしまいました。     

今回の内容は、railsのジョブをheroku上で稼働させて、とある予約サイトから情報を取得してその内容をslackに通知するというものです。     

slackは仕事のチームで使っているとかではなく、個人でRSSリーダーやこういった通知のために利用しています。仕事でも使いたい。。    

f:id:us_key:20180809175027p:plain

目次

動作環境

  • Ruby 2.3.0p0
  • Rails 5.0.7
  • slack-notifier 2.3.2

railsでタスクを作る

ひとまずrails newRailsアプリケーションを生成した後、ジョブ実行するためのrakeファイルを作成します。

$ rails new schedulerSample

$ rails g task scheduler

slack通知する

slack-notifier

railsからslackにメッセージを送るためのgemslack-notifierというのがあるため、gemfileに追加してbundle installしておきます。

# Gemfile
gem 'slack-notifier'

slackの設定

以下にアクセスしてslackにAPPを作成します。 Slack API: Applications | Slack

開いた画面で「Create New App」を押します。 f:id:us_key:20180809122009p:plain

アプリケーションの名前を適当に決めて入力し、ワークスペースを選択して「Create App」を押します。 f:id:us_key:20180809122103p:plain

「Incoming Webhooks」を押します。 f:id:us_key:20180809122106p:plain

「Activate Incoming Webhooks」を「On」にします。 f:id:us_key:20180809122111p:plain

すると下の方に説明やら何やらが出てくるので、「Add New Webhook to Workspace」を押します。 f:id:us_key:20180809122114p:plain

確認画面が出てきます。投稿先をChannelやDirect Message先の中から選択し、「許可する」を押します。 f:id:us_key:20180809122119p:plain

リダイレクトされた画面の下の方に説明と合わせてWebhook用のURLが表示されます。これを後で使います。 f:id:us_key:20180809123505p:plain

rakeタスクの実装

今回空き情報を取得する予約サイトは、とあるパスに配置されているjsonを取得してその内容を表示しているというものです。
なのでそのjsonを取得し、rails上でslackに通知する内容に編集します。

先ほど作ったrakeタスクを編集します。

# /lib/tasks/scheduler.rake
namespace :scheduler do
end

生成された状態はこんな感じ。namespaceというのはタスクをまとめる単位ってところです。
この中にタスクを書いてあげます。

# /lib/tasks/scheduler.rake
namespace :scheduler do
  task :test do
    require 'net/http'
    require 'uri'
    require 'json'

    notifyMsg = ""

    notifier = Slack::Notifier.new("https://hooks.slack.com/services/hogehoge") #hogehogeの部分は生成されたWebhook URLに合わせて変えてください

    uri = URI.parse('https://hogehoge.json') #取得するjsonのURLを指定

    msg = ""

    json = Net::HTTP.get(uri)
    result = JSON.parse(json)

    #jsonの取得結果を編集してmsgに詰める…

    notifier.ping(msg)
  end
end

ここまでできたらローカルで動作確認してみます。

$ rake scheduler:test

想定通りにslackに通知が飛べばOK。

heroku schedulerからタスクを起動する

herokuへのデプロイ

ローカルでherokuコマンドが使用できるようになっている前提で。。
作成したrailsアプリをgit管理したうえで、heroku createでheroku上にアプリを作成します。

$ git init
$ git add .
$ git commit -m "first commit"

$ heroku create schedulersample

そして、herokuのリポジトリにpushしてアプリをデプロイします。

$ git push heroku master

と、ここでエラーが発生。

remote:  !
remote:  !     Failed to install gems via Bundler.
remote:  !     Detected sqlite3 gem which is not supported on Heroku:
remote:  !     https://devcenter.heroku.com/articles/sqlite3
remote:  !

herokuでサポートされていないsqlite3がgemfileに含まれていたからでした。
今回はデータベースを使わないので、gemfileの該当部分をコメントアウトします。
(herokuでデータベースを使う場合は、productionでは別のデータベースのgemを指定します)

bundle updateしたうえで再度herokuにデプロイします。

$ bundle update
$ git add .
$ git commit -m "gemfile revised"
$ git push heroku master

さて、デプロイはできましたがこれだけではまだ定期実行してくれません。 herokuでスケジューラの設定をします。

heroku schedulerの設定

herokuに作成したアプリケーションにaddonを追加します。

$ heroku addons:add scheduler:standard

$ heroku addons:open scheduler

開いた画面で「Add new job」を押します。
f:id:us_key:20180809173025p:plain

実行するタスクの内容(ローカルで実行したのと同じコマンド)、FREQUENCY(Daily/Hourly/every 10 minutesから選択)、次回の実行時間を指定し、「save」を押します。 f:id:us_key:20180809173409p:plain

ジョブの稼働時間が課金対象になりますが、簡単な処理であれば複数稼働していても無料範囲(クレジット登録ありで月1000時間)を超えることはないと思います。
今回のような内容であれば1回の稼働での処理は数秒で終わるので、1時間おきに稼働させたとして1日あたり数分、1か月で数時間ってところかなと。
(もちろん処理内容によるのでログを確認するなどしてください。)

まとめ

ということで、heroku schedulerを使うと簡単にrailsでジョブスケジューラが組めるよという内容でした。
もちろん複雑なスケジュールを組むには別の方法を採る必要があるかもしれませんが、個人で実装するような簡単な内容ならこれで十分かなと感じます。
クローリングなんかにも使えそうですね。

参考

qiita.com

qiita.com

こちらも1年以上ぶり

子育てブログの方も1年放置してましたが、こちらも1年ぶりの投稿をば。

1年更新してなかったわけですが、なぜか1年前より最近の方がアクセス数は増えていました。
なぜかなと思いアクセス解析を見てみると、最もアクセス数を稼いでいた記事がこちら。

us-key-tech.hatenablog.com

うーん、Web系のプログラミングで躓いたとことかの記録で書き始めたブログなのですが、最もアクセス数が多いのがザ・SIergrepに関する記事という。。複雑な心境。。
直近のアクセスの65%がこの記事でした。そしてその結果として、アクセスが平日に集中しており土日はほとんどアクセスがないっていう。業務中にググってもらってるわけですね。 確かに「サクラエディタ 拡張子 除外」で検索するとトップに出てきました。ありがたや?
そんなわけで、業務外の勉強内容を中心に書いていきつつ、業務で役立ちそうなネタもちょいちょい書いていけたらと思います。
ま、1年放置したくらいなので、マイペースでぼちぼち書いていこうかなと。

てな感じでこれからもよろしくお願いします。

【Ruby on Rails】acts-as-taggable-onでタグ付けしたレコードのcount取得に苦戦

今現在Railsの勉強をしてまして、Railsチュートリアルを一通り終えて作ったものをベースにちょっと改良を加えています。

今やろうとしているのは、投稿した内容にタグ付けし、タグで投稿を検索できるようにするというもの。

f:id:us_key:20170413223426g:plain
投稿のフォームはこんな感じ。
タグ機能acts-as-taggable-onというgem、タグ表示のUIはBootstrap Tags InputというjQueryプラグインを使用しています。

目次

動作環境

  • Ruby 2.3.0p0
  • Rails 5.0.0.1
  • acts-as-taggable-on 4.0.0

使用方法

こちらのブログ記事を参考にしました。

ruby-rails.hatenadiary.com 動作環境のバージョンは異なりますが問題なく使えました。

実装

こんな感じの検索フォームを用意しました。
f:id:us_key:20170413225603g:plain
見た目はいまいちですが。。
検索条件でタグを指定するとそのタグを使用している投稿が表示されます。

参考記事にも書かれていますが、タグでのレコード抽出方法にはいくつかあります。

検索条件に指定したタグのいずれかを含むレコードを検索

#Model(post.rb)
class Post < ApplicationRecord
  acts_as_taggable_on :labels
  acts_as_taggable #この2行の指定でタグ関連テーブルと紐付けられる

###中略###

  scope :find_by_tag, -> tags {
    #タグが入力されている場合のみタグを条件に抽出
    if tags.present?
      tagged_with(tags, any: true) #タグ付けレコードの取得方法を指定
    end
  }

end

#Controller(posts_controller.rb)
class PostsController < ApplicationController

###中略###

  def search
    @tags = params[:tags]
    @posts = current_user.posts.find_by_tag(@tags) #current_userはログインユーザー取得のヘルパーメソッド
    respond_to do |format|
      format.js
    end
  end

end
/* js.erb(search.js.erb) */
var searchResultHtml = "";
var count = 0;
// 検索結果が存在する場合のみカウントを取得
<%if @posts.exists? %>
  searchResultHtml = '<%=j render @posts%>';
  count = <%=@posts.count%>
<%end%>
$('#post-search-result-table').html(searchResultHtml);
$('span#search-result-count').html(count.toString());

タグと関連付けたModelに対しtagged_with(tags, any: true)というメソッドを使うことで、検索条件で指定したタグのいずれかを含むレコードが検索できます。
今回の実装ではajaxで検索結果を表示するようviewというかformで指定しているため、controllerでインスタンス変数にセットした検索結果をjs.erbで画面に反映しています。

検索条件に指定したタグすべてを含むレコードを検索

上記の実装でtagged_with(tags, any: true)としていた個所をtagged_with(tags, match_all: true)とします(“any"を"match_all"に変えている)。

詰まったところ

どちらの取得方法でも件数を取得するとこで詰まりました。。

検索条件に指定したタグのいずれかを含むレコードを検索した場合の件数取得

上記の実装でタグを条件に含む検索を実行すると、見事にエラー。こんなログ出ました(見やすいように改行を入れています)。

ActionView::Template::Error (SQLite3::SQLException: near "*": syntax error: 
SELECT COUNT( posts.*) FROM "posts" WHERE "posts"."user_id" = ? 
AND (EXISTS (SELECT 1 FROM taggings posts_taggings_7e240de 
WHERE posts_taggings_7e240de.taggable_id = posts.id 
AND posts_taggings_7e240de.taggable_type = 'Post' 
AND posts_taggings_7e240de.tag_id in (2)
))):

おいおいまさかのSyntax error…
ぱっと見何がダメなのか分からなかったのですが、よくよく見てみると。。。最後のカッコが1つ多いような。。。

GitHubリポジトリ見てみたら、issueに挙がってました。

.tagged_with([], any: true).count throws MySQL error · Issue #649 · mbleigh/acts-as-taggable-on · GitHub

issueはopenのままなんですが、解決方法を書いてくれてる人が。
どうやら.count.count(:all)とすれば解消するようです。。

/* 修正後のsearch.js.erb */
var searchResultHtml = "";
var count = 0;
<%if @posts.exists? %>
  searchResultHtml = '<%=j render @posts%>';
  count = <%=@posts.count(:all)%> // .count⇒.count(:all)
<%end%>
$('#post-search-result-table').html(searchResultHtml);
$('span#search-result-count').html(count.toString());

修正後の内容で発行されるクエリはこんな感じになりました。カッコの数が正しくなってます。あとcountの取り方が変わってますね(ここだけ見ても結果は同じですが)。

SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ?
AND (EXISTS (SELECT 1 FROM taggings posts_taggings_a94a8fe
 WHERE posts_taggings_a94a8fe.taggable_id = posts.id
 AND posts_taggings_a94a8fe.taggable_type = 'Post'
 AND posts_taggings_a94a8fe.tag_id in (1)
))

検索条件に指定したタグすべてを含むレコードを検索した場合の件数取得

こちらも件数がちゃんと取得できておらず。
こちらはログに出力されたクエリがこんな感じになってました。

/* タグあり、match_all指定 */
SELECT COUNT(*) AS count_all,
       posts.id AS posts_id
FROM "posts"
JOIN taggings posts_taggings_a94a8fe
ON  posts_taggings_a94a8fe.taggable_id = posts.id
AND posts_taggings_a94a8fe.taggable_type = 'Post'
AND posts_taggings_a94a8fe.tag_id = 1
LEFT OUTER JOIN taggings posts_taggings_group
ON  posts_taggings_group.taggable_id = posts.id
AND posts_taggings_group.taggable_type = 'Post'
WHERE "posts"."user_id" = ?
GROUP BY posts.id
HAVING (COUNT(posts_taggings_group.taggable_id) = 1)
ORDER BY "posts"."created_at" DESC

どうもcountだけじゃなくpostのIDも取得されてしまってるようです。
結果はHashとして、{“1” => “1”,“1” => “2”}(件数 => ID)のような形で返却されます。
postのIDごとにgroup byしてるけど、件数はすべて1件ずつになる気がするので.count.lengthでHashの要素数取れば合計の件数が取れるかなぁ。
しかしタグを指定していないと、こんな感じでcountだけを取得するクエリになっています。

/* タグなし */
SELECT COUNT(*)
FROM "posts"
WHERE "posts"."user_id" = ?

タグが指定されてるかどうかで件数の取り方を変えなあかん、てことでこんな実装になりました。汚い。

/* match_all指定時のjs.erb実装 */
var searchResultHtml = "";
var count = "0";
<%if @posts.exists? %>
  searchResultHtml = '<%=j render @posts%>';
  // タグを検索条件に含めるとModel#countの結果がHashとなってしまうため
  // hashの長さを取得(件数分hashの要素が作られる想定)
  <%if @tags.present?%>
    count = <%=@posts.count.length%>;
  <%elsif%>
    count = <%=@posts.count%>
  <%end%>
<%end%>
$('#post-search-result-table').html(searchResultHtml);
$('span#search-result-count').html(count.toString());

うーん詰まりまくったけどひとまずこんな感じでOKかなぁ。

railsというかrubyというか、まだまだ色々分かってないので詰まるとなかなか大変です。

長くなってしまいましたが今回はこの辺で。