そうだ、Batch処理を使おう！【Rails】【タグ機能】【Whenever】【cron】


# はじめに
こんにちは！<font color="red"><b>DMM WEBCAMP Advent Calendar 2021 :christmas_tree: 19日目</b></font>を担当させていただく@smasa1112です！

今回で初のqiita投稿ですのでお見苦しいところ多々あるかと思います
どうか温かい目で見守っていただけますと幸いです…

本日は初学者の方が中級者になるときに使うであろうBatch処理とそれを有効に使えるであろうタグ機能に関連することを書いていきます！


## 目次
1. [Batch処理とは](#1-batch処理とは)
1. [目標](#2-目標)
    1. [完成形](#21-完成形)
    1. [ER図](#22-er図)
1. [実装](#3-実装)
    1. [使用環境](#31-使用環境)  
    1. [タグ機能の処理](#32-タグ機能の処理)
    1. [Batch処理](#33-batch処理)
1. [最後に](#4-最後に)
1. [参考記事](#5-参考記事)


# 1. Batch処理とは
調べてみると…
 > バッチ（Batch）は「ひと束」「一群」「1回分にまとめる」という意味で、バッチ処理はあらかじめ登録した一連の処理を自動的に実行する処理方式を指す。(大塚商会 IT用語辞典)
>https://www.otsuka-shokai.co.jp/words/batch-processing.html

つまり
__単純な作業をまとめてする__
処理のことです！

今回バッチ処理する部分はタグ機能で投稿に結びついていない不要なタグを削除する部分です！

<a id="anchor2"></a>
# 2. 目標

##### 今回はこの機能を活かして以下の目的を達成します！

- 投稿にタグ付けできる
- 使わないタグを自動で削除できる

それではやっていきましょう！

<a id="anchor2-1"></a>
## 2.1 完成形
まず完成形を示します！
こちらのようにタグ付け投稿を行いまた一定時間で不要なタグを削除するようにします

### 投稿部分

![投稿.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286061/a666cfee-2331-44bc-67a8-c319945134cd.gif)

### 削除部分

![cron.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286061/6eb89225-fdde-0b38-1b8a-d0f283cb362e.gif)



<a id="anchor2-2"></a>
## 2.2 ER図
この機能を実現するためのER図を示します！
今回は機能実現だけが目標なので最低限です!
![ER図.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286061/3e47eff4-a6d5-ee83-bb23-a13015a7304f.png)

<a id="anchor3"></a>
# 3. 実装
実装は参考記事にも記載しますが以下の記事を多いに参考にしました。こちらの記事をベースに書いていきます。
https://qiita.com/you8/items/b2394104c6f9865f5d46

また作成したコードは[私のgithubレポジトリ](https://github.com/smasa1112/qiita_tag)に挙げてあるので参考にしたい方は見てみてください!

[タグを簡単に作成できるGem(参考記事)](https://qiita.com/ddd555/items/e1caa8b73d118822a0a2)もあるかと思いますが今回は勉強のため、そちらを使用せずに作成します！

<a id="anchor3-1"></a>
## 3.1 使用環境
主に以下の通りです。Batch処理にはUNIX系OSのcronを使用します
特になんの変哲もありません

- Ruby 2.6.3
- Rails 6.1.4.4
- whenever 1.0

<a id="anchor3-1"></a>
## 3.2 タグ機能の処理
まずBatch処理をする土台のプログラムを作っていきます！
コードは長くなるため重要な部分だけ示します！他の部分どうなってるか気になる方は是非[Github](https://github.com/smasa1112/qiita_tag)を見るか[Twitter](https://twitter.com/gami_gamigamiHU)に直接ご連絡ください！

### モデルの作成
大事な部分はpost.rb内の以下の部分です。中間テーブルであるtag_mapテーブルを介してPostレコードから直接Tagレコードが取得できるようにします。

```post.rb
has_many :tags, through: :tag_maps
```
<details><summary>Modelソースコード</summary><div>

```post.rb
class Post < ApplicationRecord
  has_many :tag_maps, dependent: :destroy
  has_many :tags, through: :tag_maps
  validates :context, presence: true

  def update_and_create_tags(post_tags)
    # その投稿が持っているタグを列挙
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    # 変更があった場合に削除されるアソシエーションを列挙
    # 配列同士で減算できる！すごい！！！
    old_tags = current_tags - post_tags
    # 今回保存されたものと現在の差を新しいタグとする。新しいタグは保存
    new_tags = post_tags - current_tags

    # Destroy old taggings
    old_tags.each do |old_name|
      #self.tags.delete Tag.find_by(name:old_name)
      #railsっぽい書き方に直すなら…
      tag=self.tags.find_by(name:old_name)
      self.tag_maps.find_by(tag_id: tag.id).destroy
    end

    # Create new taggings
    new_tags.each do |new_name|
      post_tag = Tag.find_or_create_by(name:new_name)
      # 配列に保存
      # self.tags << post_tag
      # railsっぽい書き方に直すなら…
      TagMap.create(tag_id: post_tag.id, post_id: self.id)
    end
  end
end

```

```tag.rb
class Post < ApplicationRecord
  has_many :tag_maps, dependent: :destroy
  has_many :tags, through: :tag_maps
  validates :context, presence: true

  def update_and_create_tags(post_tags)
    # その投稿が持っているタグを列挙
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    # 変更があった場合に削除されるアソシエーションを列挙
    # 配列同士で減算できる！すごい！！！
    old_tags = current_tags - post_tags
    # 今回保存されたものと現在の差を新しいタグとする。新しいタグは保存
    new_tags = post_tags - current_tags

    # Destroy old taggings
    old_tags.each do |old_name|
      #self.tags.delete Tag.find_by(name:old_name)
      #railsっぽい書き方に直すなら…
      tag=self.tags.find_by(name:old_name)
      self.tag_maps.find_by(tag_id: tag.id).destroy
    end

    # Create new taggings
    new_tags.each do |new_name|
      post_tag = Tag.find_or_create_by(name:new_name)
      # 配列に保存
      # self.tags << post_tag
      # railsっぽい書き方に直すなら…
      TagMap.create(tag_id: post_tag.id, post_id: self.id)
    end
  end
end

```

```tag_map.rb
class TagMap < ApplicationRecord
  belongs_to :post
  belongs_to :tag
  validates :post_id,presence:true
  validates :tag_id,presence:true
end
```

</div></details>

### コントローラーの作成
重要な点としてはアソシエーションしたレコードに対して、preloadを使用してレコード取得時にSQLクエリが何度も発行されることを防ぎました。N+1問題に関しては参考記事を見てください。

```posts_controller.rb
  def index
    #N+1問題解決のためにpreload
    @posts = Post.preload(:tags).all
    @post = Post.new
  end
```

<details><summary>Controllerソースコード</summary><div>

```posts_controller.rb
class PostsController < ApplicationController
  def index
    #N+1問題解決のためにpreload
    @posts = Post.preload(:tags).all
    @post = Post.new
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      #送られてきたParamを,で分割
      tag_list = params[:post][:tag_strs].split(",")
      @post.update_and_create_tags(tag_list)
      redirect_to posts_path
    else
      @posts=Post.all
      render :index
    end
  end


  def update
    @post= Post.preload(:tags).find(params[:id])
    if @post.update(post_params)
      tag_list = params[:post][:tag_strs].split(",")
      @post.update_and_create_tags(tag_list)
      redirect_to posts_path
    else
      @tag_list= @post.tags.pluck(:name).join(",") unless @post.tags.nil?
      render :edit
    end
  end

  def destroy
    post= Post.preload(:tags).find(params[:id])
    post.destroy
    redirect_to posts_path
  end

  def edit
    @post= Post.preload(:tags).find(params[:id])
    @tag_list= @post.tags.pluck(:name).join(",") unless @post.tags.nil?
  end

  private

  def post_params
    params.require(:post).permit(:context)
  end
end

```

```tags_controller
class TagsController < ApplicationController
  def index
    @tags = Tag.preload(:posts).all
  end
end
```

</div></details>

### ビューの作成
工夫した点は特になく雑に作成しただけです。

<details><summary>Viewソースコード</summary><div>

```posts/index.html.erb
<h1>投稿一覧</h1>
<table>
  <thead>
    <tr>
      <th style="width:15%;">ID</th>
      <th style="width:25%;">投稿内容</th>
      <th style="width:15%;">付属タグ一覧</th>
      <th style="width:15%;"></th>
      <th style="width:15%;"></th>
    </tr>
  </thead>
  <tbody>
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.id %></td>
        <td><%= post.context %></td>
        <td>
          <% post.tags.each do |tag| %>
            <%= tag.name %><br>
          <% end %>
        </td>
        <td><%= link_to "編集", edit_post_path(post.id) %></td>
        <td><%= link_to "削除", post_path(post.id), method: :delete %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<h1>新規投稿</h1>
<%= form_with model:@post, url: posts_path, local: true do |f| %>
  <%= f.label :context, "投稿内容" %><br>
  <%= f.text_field :context %><br>
  <%= f.label :tag_strs, "付属したいタグ" %><br>
  <%= f.text_field :tag_strs %><br>
  <p><b>タグはカンマ(,)区切りで入力してください！</b></p>
  <%= f.submit "投稿作成" %>
<% end %>
```

```posts/edit.html.erb
<h1>編集</h1>
<%= form_with model:@post, url: post_path(@post), local: true do |f| %>
  <%= f.label :context, "投稿内容" %><br>
  <%= f.text_field :context %><br>

  <%= f.label :tag_strs, "付属したいタグ" %><br>
  <%= f.text_field :tag_strs,value: @tag_list %><br>

  <p><b>タグはカンマ(,)区切りで入力してください！</b></p>
  <%= f.submit "投稿作成" %>
<% end %>
```


```tags/index.html.erb
<h1> タグ一覧 </h1>
<table>
  <thead>
    <tr>
      <th>タグ名</th>
      <th>結びついた投稿数</th>
    </tr>
  </thead>
  <tbody>
    <% @tags.each do |tag| %>
      <tr>
        <td><%= tag.name %></td>
        <td><%= tag.posts.size %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```

</div></details>

### ここまででの完成形

![投稿.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286061/a666cfee-2331-44bc-67a8-c319945134cd.gif)


<a id="anchor3-3"></a>
## 3.3 Batch処理
最後にBatch処理に移っていきます。
作業としては

- 結びつくPostがないTagを全抽出
- それらを一定時間で削除

といったことをやっていきます！Batch処理にはWheneverというgemを使います！

### gemの導入・初期化

```PHP:Gemfile
# cron
gem 'whenever', require: false
```

CLIでインストールと有効化を行いましょう
そうするとconfig内にschedule.rbが作成されます

```txt:CLI
$ bundle install --path vendor/bundle
$ bundle exec wheneverize .
```

### wheneverの設定
まずBatch処理用のファイルを読み込めるようにconfig/application.rbを作成しましょう

```config/application.rb
require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module TagBatch
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    ### 追記部分 ###
    # lib内のBatchファイルを読めるようにする
    config.paths.add 'lib', eager_load: true
    ###############

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
  end
end

```

使用するBatch処理を作成しましょう
抽出の部分が納得いっていないのでSQLの勉強をちゃんとしないといけないと思いました…

```lib/batch/remove_unuse_tag.rb
class Batch::RemoveUnuseTag
  def self.remove_unuse_tag
    # 付属するPostがないタグを抽出
    tag_count_hash=Tag.joins(:posts).group(:id).count
    Tag.all.each do |tag|
      unless tag_count_hash[tag.id]
        tag.destroy
      end
    end
    p "結びつきのないタグを全て削除しました"
  end
end
```

Batchファイルを作成したらwheneverの設定を行いましょう

``` config/schedule.rb
# root_path認識に必要
require File.expand_path(File.dirname(__FILE__) + "/environment")
# cronを実行する環境変数
rails_env = Rails.env.to_sym
# 環境変数の設定
set :environment, rails_env
# ログの出力先の設定
set :output, 'log/cron.log'

#どれくらいの期間で何をするか決める
#1週間とかにしたい場合には
#every 1.week do
every 3.minute do
  begin
    runner "Batch::RemoveUnuseTag.remove_unuse_tag"
  rescue => e
    Rails.logger.error("aborted rails runner")
    raise e
  end
end

```

最後にCronを有効化しましょう！！

```txt:CLI
$ bundle exec whenever
$ bundle exec whenever --update-crontab
$ sudo systemctl start crond

```

完成です！！

cronを停止したい場合には以下のコマンドを打ちましょう

```txt:CLI
$ sudo systemctl stop crond
```

<a id="anchor4"></a>
# 4. 終わりに
最後まで見ていただき本当にありがとうございました！
初めての記事で分かりづらい部分もあったかと思いますが、皆様の理解の一助となりましたら幸いです :bow_tone1:

#### Let's Batch処理！！

明日は@harikaさんの記事となります！お楽しみに！

[昨日の記事](https://qiita.com/hidemitsuaoki/items/5dc595ab6d6e6fbcb0e0) 執筆者:@hidemitsuaoki


<a id="anchor5"></a>
# 5. 参考記事
https://qiita.com/you8/items/b2394104c6f9865f5d46

https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58

https://qiita.com/Esfahan/items/e7a924f7078faf3294f2