はじめに
Part 1ではアプリケーションの概要と基本設計、Part 2ではユーザー認証システムの実装について解説しました。今回のPart 3では、Ballonの核となる悩み投稿機能の実装とそれに関連するデータベース設計の詳細に焦点を当てます。悩みのCRUD操作(作成、読み取り、更新、削除)の実装方法や、効率的なデータベースクエリの書き方などを詳しく説明します。また、中高生の悩みに対する適切な対応やセンシティブな内容のモデレーションについても触れていきます。
データベース設計
まず、悩み投稿に関連するテーブルの設計を見てみましょう。
class CreateWorries < ActiveRecord::Migration[5.2]
def change
create_table :worries do |t|
t.references :user, foreign_key: true
t.string :title, null: false
t.text :content, null: false
t.string :category
t.boolean :anonymous, default: false
t.boolean :is_public, default: true
t.datetime :resolved_at
t.timestamps
end
end
end
このマイグレーションファイルで、以下のような構造のworriesテーブルを作成します:
-
user_id
: 悩みの投稿者を示す外部キー -
title
: 悩みのタイトル(null不可) -
content
: 悩みの詳細内容(null不可) -
category
: 悩みのカテゴリ(例:学業、友人関係、家族など) -
anonymous
: 匿名投稿かどうかを示すフラグ -
is_public
: 公開/非公開を示すフラグ -
resolved_at
: 悩みが解決された日時 -
created_at
とupdated_at
: レコードの作成日時と更新日時(timestampsマクロで自動追加)
この設計により、悩みに関する必要な情報をすべて保存でき、かつユーザーとの関連付けも可能になります。
モデルの実装
次に、Worryモデルを実装します。
class Worry < ActiveRecord::Base
belongs_to :user
has_many :answers, dependent: :destroy
validates :title, presence: true, length: { maximum: 100 }
validates :content, presence: true, length: { maximum: 1000 }
validates :category, presence: true
scope :public_worries, -> { where(is_public: true) }
scope :unresolved, -> { where(resolved_at: nil) }
scope :resolved, -> { where.not(resolved_at: nil) }
before_save :sanitize_content
def resolve!
update(resolved_at: Time.current)
end
def posted_by?(user)
self.user_id == user.id
end
private
def sanitize_content
self.content = Sanitize.fragment(self.content)
end
end
このWorryモデルの各部分について説明します:
-
アソシエーション:
-
belongs_to :user
: 各悩みは1人のユーザーに属することを示します。 -
has_many :answers, dependent: :destroy
: 悩みは複数の回答を持ち、悩みが削除されると関連する回答も削除されます。
-
-
バリデーション:
-
validates :title, presence: true, length: { maximum: 100 }
: タイトルは必須で、最大100文字まで。 -
validates :content, presence: true, length: { maximum: 1000 }
: 内容は必須で、最大1000文字まで。 -
validates :category, presence: true
: カテゴリは必須項目です。
-
-
スコープ:
-
scope :public_worries
: 公開されている悩みのみを取得します。 -
scope :unresolved
: 未解決の悩みを取得します。 -
scope :resolved
: 解決済みの悩みを取得します。
-
-
コールバック:
-
before_save :sanitize_content
: 保存前に内容をサニタイズし、XSS攻撃を防ぎます。
-
-
インスタンスメソッド:
-
resolve!
: 悩みを解決済みにマークします。 -
posted_by?
: 指定されたユーザーが悩みの投稿者かどうかを確認します。
-
これらのスコープとメソッドにより、悩みの取得や状態の確認が容易になります。
悩み管理機能の実装
では、悩みのCRUD操作を実装していきましょう。
# 悩み一覧表示
get '/worries' do
@worries = Worry.public_worries.order(created_at: :desc)
erb :worries_index
end
# 新規悩み投稿フォーム
get '/worries/new' do
authenticate!
@worry = Worry.new
erb :worry_form
end
# 悩み投稿
post '/worries' do
authenticate!
@worry = current_user.worries.new(worry_params)
if @worry.save
redirect '/worries'
else
erb :worry_form
end
end
# 悩み詳細表示
get '/worries/:id' do
@worry = Worry.find(params[:id])
erb :worry_show
end
# 悩み編集フォーム
get '/worries/:id/edit' do
authenticate!
@worry = current_user.worries.find(params[:id])
erb :worry_form
end
# 悩み更新
put '/worries/:id' do
authenticate!
@worry = current_user.worries.find(params[:id])
if @worry.update(worry_params)
redirect "/worries/#{@worry.id}"
else
erb :worry_form
end
end
# 悩み削除
delete '/worries/:id' do
authenticate!
worry = current_user.worries.find(params[:id])
worry.destroy
redirect '/worries'
end
# 悩み解決
post '/worries/:id/resolve' do
authenticate!
@worry = current_user.worries.find(params[:id])
@worry.resolve!
redirect "/worries/#{@worry.id}"
end
private
def worry_params
params.slice('title', 'content', 'category', 'anonymous', 'is_public')
end
def authenticate!
redirect '/signin' unless current_user
end
各ルーティングについて詳しく説明します:
-
悩み一覧表示(GET /worries):
-
Worry.public_worries
で公開されている悩みのみを取得します。 -
order(created_at: :desc)
で最新の悩みが先に表示されるようにソートします。
-
-
新規悩み投稿(GET /worries/new, POST /worries):
- GETリクエストでフォームを表示し、POSTリクエストで新しい悩みを作成します。
-
current_user.worries.new
を使用して、現在のユーザーに紐づいた悩みを作成します。
-
悩み詳細表示(GET /worries/:id):
-
Worry.find(params[:id])
で特定の悩みを取得します。
-
-
悩み編集・更新(GET /worries/:id/edit, PUT /worries/:id):
- 編集フォームの表示と更新処理を行います。
-
current_user.worries.find(params[:id])
を使用して、現在のユーザーの悩みのみを編集可能にします。
-
悩み削除(DELETE /worries/:id):
-
destroy
メソッドで悩みを削除します。
-
-
悩み解決(POST /worries/:id/resolve):
-
resolve!
メソッドを呼び出して悩みを解決済みにマークします。
-
-
authenticate!
メソッド:- ユーザーがログインしていない場合、サインインページにリダイレクトします。
-
worry_params
メソッド:- パラメータを安全に取り出すためのプライベートメソッドです。
- マスアサインメント脆弱性を防ぐために重要です。
ビューの実装
悩み一覧表示のビューを例として示します。
<h2>悩み相談掲示板</h2>
<% if current_user %>
<a href="/worries/new">新しい悩みを投稿する</a>
<% end %>
<h3>未解決の悩み</h3>
<ul>
<% @worries.unresolved.each do |worry| %>
<li>
<h4><a href="/worries/<%= worry.id %>"><%= h(worry.title) %></a></h4>
<p>カテゴリ: <%= h(worry.category) %></p>
<p>
投稿者:
<% if worry.anonymous %>
匿名
<% else %>
<%= h(worry.user.username) %>
<% end %>
</p>
<p>投稿日時: <%= worry.created_at.strftime('%Y-%m-%d %H:%M') %></p>
</li>
<% end %>
</ul>
<h3>解決済みの悩み</h3>
<ul>
<% @worries.resolved.each do |worry| %>
<li>
<h4><a href="/worries/<%= worry.id %>"><%= h(worry.title) %></a></h4>
<p>カテゴリ: <%= h(worry.category) %></p>
<p>解決日時: <%= worry.resolved_at.strftime('%Y-%m-%d %H:%M') %></p>
</li>
<% end %>
</ul>
このビューでは、以下の点に注目してください:
- 未解決の悩みと解決済みの悩みを分けて表示しています。
- 匿名投稿の場合は投稿者名を「匿名」と表示しています。
- XSS対策として、ユーザー入力を表示する際に
h
メソッド(HTMLエスケープ)を使用しています。 - ログインユーザーのみに新規投稿リンクを表示しています。
センシティブな内容への対応
中高生向けのサービスであるため、センシティブな内容への対応が重要です。以下のような対策を実装することを検討しましょう:
- キーワードフィルタリング:
特定のキーワードを含む投稿を自動的にフラグ付けまたはモデレーションキューに入れます。
class Worry < ActiveRecord::Base
# 既存のコードに追加
before_save :check_sensitive_content
private
def check_sensitive_content
sensitive_words = ['自殺', '薬物', '虐待', # その他センシティブな単語]
if sensitive_words.any? { |word| self.content.include?(word) }
self.needs_moderation = true
end
end
end
- モデレーションシステム:
フラグ付けされた投稿を管理者が確認できるようにします。
# 管理者用:モデレーションが必要な悩みの一覧
get '/admin/moderation' do
authenticate_admin!
@worries_for_moderation = Worry.where(needs_moderation: true)
erb :admin_moderation
end
# 管理者用:悩みの承認または削除
post '/admin/moderation/:id' do
authenticate_admin!
worry = Worry.find(params[:id])
if params[:approve]
worry.update(needs_moderation: false)
elsif params[:delete]
worry.destroy
end
redirect '/admin/moderation'
end
- 専門家への紹介:
深刻な悩みの場合、専門家への相談を促すメッセージを表示します。
<% if @worry.needs_expert_advice? %>
<div class="alert">
この悩みについては、専門家に相談することをおすすめします。
<a href="/resources">相談窓口一覧はこちら</a>
</div>
<% end %>
効率的なデータベースクエリ
悩み管理において、効率的なデータベースクエリは重要です。以下にいくつかの例を示します:
-
N+1問題の回避:
@worries = Worry.includes(:user).public_worries.order(created_at: :desc)
これにより、悩みを取得する際に関連するユーザー情報も一緒に取得し、データベースへのクエリ回数を減らすことができます。
-
カウンターキャッシュの利用:
悩みに対する回答の数を
2. カウンターキャッシュの利用:
悩みに対する回答の数を頻繁に参照する場合、answers_count
カラムをworriesテーブルに追加し、カウンターキャッシュを利用することで、パフォーマンスを向上させることができます。db/migrate/YYYYMMDDHHMMSS_add_answers_count_to_worries.rbclass AddAnswersCountToWorries < ActiveRecord::Migration[5.2] def change add_column :worries, :answers_count, :integer, default: 0 end end
そして、Answerモデルに以下を追加します:
models.rbclass Answer < ActiveRecord::Base belongs_to :worry, counter_cache: true end
-
インデックスの活用:
頻繁に検索や並べ替えに使用されるカラムにはインデックスを追加します。例えば:db/migrate/YYYYMMDDHHMMSS_add_indexes_to_worries.rbclass AddIndexesToWorries < ActiveRecord::Migration[5.2] def change add_index :worries, :user_id add_index :worries, :category add_index :worries, :created_at add_index :worries, :is_public end end
-
複雑なクエリの最適化:
例えば、カテゴリ別の未解決の悩み数を取得する場合:app.rbget '/worry_stats' do @category_stats = Worry.unresolved.group(:category).count erb :worry_stats end
パフォーマンス最適化
-
ページネーション:
大量の悩みデータを扱う場合、ページネーションを実装することで、パフォーマンスを向上させることができます。will_paginate
gemを使用する例:app.rbrequire 'will_paginate' require 'will_paginate/active_record' get '/worries' do @worries = Worry.public_worries.paginate(page: params[:page], per_page: 20) erb :worries_index end
views/worries_index.erb<!-- 既存のコードの後に追加 --> <%= will_paginate @worries %>
-
キャッシング:
頻繁にアクセスされるが、更新頻度の低いデータ(例:カテゴリ一覧)をキャッシュすることで、パフォーマンスを向上させることができます。app.rbrequire 'sinatra/cache' set :cache_enabled, true set :cache_output_dir, Proc.new { File.join(root, 'public', 'cache') } get '/categories' do cache do @categories = Worry.distinct.pluck(:category) erb :categories end end
中高生向けサービスならではの配慮事項
-
適切な言葉遣いと表現:
中高生に適した言葉遣いや表現を使用することが重要です。システムメッセージやエラーメッセージなども、親しみやすく、理解しやすい表現を心がけましょう。helpers.rbhelpers do def user_friendly_error_message(error) case error when :not_found "お探しの内容が見つかりませんでした。別のキーワードで試してみてください。" when :unauthorized "この操作を行うには、ログインが必要です。ログインしてみませんか?" else "エラーが発生しました。もう一度試してみてください。" end end end
-
利用時間の制限:
中高生の健全な生活リズムを守るため、深夜帯の利用を制限することも検討しましょう。app.rbbefore do if Time.now.hour >= 22 || Time.now.hour < 6 @late_night = true end end
views/layout.erb<% if @late_night %> <div class="alert"> 夜遅くまでの利用は健康に影響を与える可能性があります。十分な睡眠をとることをおすすめします。 </div> <% end %>
-
ポジティブな環境作り:
悩みを共有し合うコミュニティであるため、ポジティブな雰囲気作りが重要です。励ましのメッセージや、解決に向けたアドバイスを促す機能を実装しましょう。models.rbclass Answer < ActiveRecord::Base # 既存のコードに追加 validates :content, presence: true, positivity: true end class PositivityValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) negative_words = ['馬鹿', 'ダメ', '無理', # その他ネガティブな単語] if negative_words.any? { |word| value.include?(word) } record.errors.add(attribute, 'はもう少し前向きな表現に変えてみましょう') end end end
-
教育的要素の組み込み:
悩みの解決過程を通じて、問題解決スキルや感情管理スキルを学べるような仕組みを取り入れましょう。app.rbget '/worries/:id/solve' do @worry = Worry.find(params[:id]) @steps = [ "問題を明確にする", "可能な解決策を考える", "それぞれの解決策のメリット・デメリットを考える", "最適な解決策を選ぶ", "実行する", "結果を評価する" ] erb :problem_solving_steps end
まとめ
Part 3では、中高生向け悩み相談Webサービス「Ballon」の核となる悩み投稿機能の実装とデータベース設計について詳しく解説しました。CRUD操作の実装、効率的なデータベースクエリの書き方、そしてセンシティブな内容への対応など、実践的な内容を多く含んでいます。
また、中高生向けサービスならではの配慮事項についても触れ、適切な言葉遣いや表現、利用時間の制限、ポジティブな環境作り、教育的要素の組み込みなど、ユーザーの年齢層に合わせた機能や工夫について説明しました。
次回のPart 4では、回答機能と「いいね」機能の実装、そしてより高度なセキュリティ対策について焦点を当てます。また、中高生のメンタルヘルスに配慮したコンテンツモデレーションの手法についても触れる予定です。
ご質問やフィードバックがありましたら、コメント欄にてお待ちしています!