はじめに
こんにちは!DMM WEBCAMP Advent Calendar 2021 19日目を担当させていただく@smasa1112です!
今回で初のqiita投稿ですのでお見苦しいところ多々あるかと思います
どうか温かい目で見守っていただけますと幸いです…
本日は初学者の方が中級者になるときに使うであろうBatch処理とそれを有効に使えるであろうタグ機能に関連することを書いていきます!
目次
1. Batch処理とは
調べてみると…
バッチ(Batch)は「ひと束」「一群」「1回分にまとめる」という意味で、バッチ処理はあらかじめ登録した一連の処理を自動的に実行する処理方式を指す。(大塚商会 IT用語辞典)
https://www.otsuka-shokai.co.jp/words/batch-processing.html
つまり
単純な作業をまとめてする
処理のことです!
今回バッチ処理する部分はタグ機能で投稿に結びついていない不要なタグを削除する部分です!
2. 目標
今回はこの機能を活かして以下の目的を達成します!
- 投稿にタグ付けできる
- 使わないタグを自動で削除できる
それではやっていきましょう!
2.1 完成形
まず完成形を示します!
こちらのようにタグ付け投稿を行いまた一定時間で不要なタグを削除するようにします
投稿部分
削除部分
2.2 ER図
この機能を実現するためのER図を示します!
今回は機能実現だけが目標なので最低限です!
3. 実装
実装は参考記事にも記載しますが以下の記事を多いに参考にしました。こちらの記事をベースに書いていきます。
https://qiita.com/you8/items/b2394104c6f9865f5d46
また作成したコードは私のgithubレポジトリに挙げてあるので参考にしたい方は見てみてください!
タグを簡単に作成できるGem(参考記事)もあるかと思いますが今回は勉強のため、そちらを使用せずに作成します!
3.1 使用環境
主に以下の通りです。Batch処理にはUNIX系OSのcronを使用します
特になんの変哲もありません
- Ruby 2.6.3
- Rails 6.1.4.4
- whenever 1.0
3.2 タグ機能の処理
まずBatch処理をする土台のプログラムを作っていきます!
コードは長くなるため重要な部分だけ示します!他の部分どうなってるか気になる方は是非Githubを見るかTwitterに直接ご連絡ください!
モデルの作成
大事な部分はpost.rb内の以下の部分です。中間テーブルであるtag_mapテーブルを介してPostレコードから直接Tagレコードが取得できるようにします。
has_many :tags, through: :tag_maps
Modelソースコード
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
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
class TagMap < ApplicationRecord
belongs_to :post
belongs_to :tag
validates :post_id,presence:true
validates :tag_id,presence:true
end
コントローラーの作成
重要な点としてはアソシエーションしたレコードに対して、preloadを使用してレコード取得時にSQLクエリが何度も発行されることを防ぎました。N+1問題に関しては参考記事を見てください。
def index
#N+1問題解決のためにpreload
@posts = Post.preload(:tags).all
@post = Post.new
end
Controllerソースコード
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
class TagsController < ApplicationController
def index
@tags = Tag.preload(:posts).all
end
end
ビューの作成
工夫した点は特になく雑に作成しただけです。
Viewソースコード
<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 %>
<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 %>
<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>
ここまででの完成形
3.3 Batch処理
最後にBatch処理に移っていきます。
作業としては
- 結びつくPostがないTagを全抽出
- それらを一定時間で削除
といったことをやっていきます!Batch処理にはWheneverというgemを使います!
gemの導入・初期化
# cron
gem 'whenever', require: false
CLIでインストールと有効化を行いましょう
そうするとconfig内にschedule.rbが作成されます
$ bundle install --path vendor/bundle
$ bundle exec wheneverize .
wheneverの設定
まずBatch処理用のファイルを読み込めるように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の勉強をちゃんとしないといけないと思いました…
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の設定を行いましょう
# 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を有効化しましょう!!
$ bundle exec whenever
$ bundle exec whenever --update-crontab
$ sudo systemctl start crond
完成です!!
cronを停止したい場合には以下のコマンドを打ちましょう
$ sudo systemctl stop crond
4. 終わりに
最後まで見ていただき本当にありがとうございました!
初めての記事で分かりづらい部分もあったかと思いますが、皆様の理解の一助となりましたら幸いです
Let's Batch処理!!
明日は@harikaさんの記事となります!お楽しみに!
昨日の記事 執筆者:@hidemitsuaoki
5. 参考記事