INSERT/SELECTしかしない掲示板の設計
Qiitaにも投げた
神速さんの記事を読んで最近自分も似た感じの設計をしたなあと思い出したので覚えているうちに書く。
掲示板を作る
誰しもが
一度は作る
掲示板
Railsアプリを作る
便利そうなので --edge を付けておく
code:console
% rbenv shell 2.7.1
% gem install --pre rails
% rails new --edge --database=postgresql --skip-bundle --skip-webpack-install kesenai_tsumi
% cd kesenai_tsumi
いい感じに docker-compose.yml
code:docker-compose.yml
version: "3.8"
services:
app: &rails
build: .
depends_on:
- db
environment:
DATABASE_URL: postgres://postgres:hi@db:5432
RAILS_ENV: development
WEBPACKER_DEV_SERVER_HOST: webpacker
ports:
- "3000:3000"
volumes:
- .:/app
- ./home:/home/app
- bundle:/app/.bundle
- node_modules:/app/node_modules
webpacker:
<<: *rails
command: webpack-dev-server
environment:
WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
ports:
- "3035:3035"
db:
image: postgres:12.3-alpine
environment:
POSTGRES_PASSWORD: hi
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
bundle:
node_modules:
pgdata:
と Dockerfile
code:Dockerfile
FROM rubylang/ruby:2.7.1-bionic
RUN apt-get update -qq && \
apt-get install -y curl gnupg && \
apt-get update -qq && \
apt-get install -y nodejs postgresql-client build-essential libpq-dev yarn && \
groupadd --non-unique --gid 1000 app && \
useradd --system --non-unique --create-home --uid 1000 --gid 1000 app && \
mkdir -p /app/.bundle && \
mkdir -p /app/node_modules && chown -R app:app /app
COPY entrypoint.sh /usr/bin
RUN chmod +x /usr/bin/entrypoint.sh
USER app
WORKDIR /app
ENV PATH /app/bin:$PATH
EXPOSE 3000
と entrypoint.sh
code:entrypoint.sh
set -e
rm -f /app/tmp/pids/server.pid
exec "$@"
そして bin/setupを書き換え
code:bin/setup.diff
diff --git a/bin/setup b/bin/setup
index 5853b5e..5f32464 100755
--- a/bin/setup
+++ b/bin/setup
@@ -15,10 +15,10 @@ FileUtils.chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
- system('bundle check') || system!('bundle install')
+ system('bundle check') || system!('bundle install --path=.bundle')
# Install JavaScript dependencies
- # system('bin/yarn')
+ system('bin/yarn')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
データベースつくって立ち上げて http://localhost:3000 でアクセスできるか確認する
code:console
% alias dc=docker-compose
% dc build
% dc run --rm app rails db:create
% dc run -e RAILS_ENV=test --rm app rails db:create
% dc run --rm app setup
% dc up
掲示板を作る
掲示板には名前がある
code:console
% dc run --rm app rails g scaffold Board title:string
名前がNULLの掲示板をやめたい
生成されたマイグレーションを見るとtitle カラムがNULL可になっている
そのため名前がない掲示板が作れる。しかし名前は必須としたい
code:db/migrate/20200606170056_create_boards.rb
class CreateBoards < ActiveRecord::Migration6.0 def change
create_table :boards do |t|
t.string :title
t.timestamps
end
end
end
NULL可を生まれる前に消し去るのがよさそう
NULLオプションを付け忘れても大丈夫なようにいつも null: false をつけるパッチを書きます
code:config/initializers/always_prefer_not_null.rb
ActiveRecord::ConnectionAdapters::TableDefinition.prepend(Module.new {
def column(name, type, index: nil, **options)
super(name, type, index: index, **options.merge(null: false))
end
})
NULLの考慮が必要なくなりました
便利
でもマイグレーションにちゃんと null: false と書いておきたいので null に false を設定しておきます
code:db/migrate/20200606170056_create_boards.rb
class CreateBoards < ActiveRecord::Migration6.0 def change
create_table :boards do |t|
t.string :title, null: false
t.timestamps
end
end
end
投稿を作る
掲示板には投稿があり以下の3つを持つものとします、なお投稿者はUserモデルを作るのが面倒なので文字列です
どこの掲示板への投稿か
投稿者
本文
code:console
% dc run --rm app rails g model Post board:references poster:string body:text
マイグレーションに null: false を追加
code:db/migrate/20200607031441_create_posts.rb
class CreatePosts < ActiveRecord::Migration6.0 def change
create_table :posts do |t|
t.references :board, null: false, foreign_key: true
t.string :poster, null: false
t.text :body, null: false
t.timestamps
end
end
end
バリデーションを追加しテストを書く
Boardのtitleが空でないことを確認
Boardに紐づく投稿はBoardを削除する際に合わせて削除する
Postのposterが空でないことを確認
Postのbodyが空でないことを確認
code:app/models/board.rb
class Board < ApplicationRecord
has_many :posts, dependent: :destroy
validates :title, presence: true
end
code:test/models/board_test.rb
require 'test_helper'
class BoardTest < ActiveSupport::TestCase
test "title validation" do
assert Board.new(title: 'board').valid?
assert Board.new(title: '').invalid?
assert Board.new(title: nil).invalid?
end
test "dependent destroy" do
assert_difference('Post.count', -1) do
boards(:one).destroy
end
end
end
code:app/models/post.rb
class Post < ApplicationRecord
belongs_to :board
validates :poster, :body, presence: true
end
code:test/models/post_test.rb
require 'test_helper'
class PostTest < ActiveSupport::TestCase
setup do
@board = Board.new(title: 'board')
end
test "poster validation" do
assert Post.new(board: @board, poster: 'john.doe', body: '4423').valid?
assert Post.new(board: @board, poster: '', body: '4423').invalid?
assert Post.new(board: @board, poster: nil, body: '4423').invalid?
end
test "body validation" do
assert Post.new(board: @board, poster: 'john.doe', body: '4423').valid?
assert Post.new(board: @board, poster: 'john.doe', body: '').invalid?
assert Post.new(board: @board, poster: 'john.doe', body: nil).invalid?
end
end
掲示板に投稿できるようにする
routesを生やし
code:config/routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
resources :posts, only: :create
end
end
PostsController を作る
code:app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_board
# POST /boards/:board_id/posts
# POST /boards/:board_id/posts.json
def create
@new_post = @board.posts.new(post_params)
respond_to do |format|
if @new_post.save
format.html { redirect_to @board, notice: 'Post was successfully created.' }
format.json { render :show, status: :created, location: @board }
else
format.html { render 'boards/show' }
format.json { render json: @new_post.errors, status: :unprocessable_entity }
end
end
end
private
def set_board
end
def post_params
params.require(:post).permit(:poster, :body)
end
end
BoardsController#show を書き換え
code:rb
class BoardsController < ApplicationController
# GET /boards/1
# GET /boards/1.json
def show
@new_post = Board.find(@board.id).posts.new
end
いい感じに投稿を表示・作成できるようにviewを書き換える
code:app/views/boards/show.html.erb
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @board.title %>
</p>
<%= render @board.posts %>
<%= render 'posts/form', board: @board, post: @new_post %>
<%= link_to 'Edit', edit_board_path(@board) %> |
<%= link_to 'Back', boards_path %>
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.body %>
</p>
code:app/views/posts/_form.html.erb
<%= form_with(model: board, post, local: true) do |form| %> <% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :poster %>
<%= form.text_field :poster %>
</div>
<div class="field">
<%= form.label :body %>
<%= form.text_field :body %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
投稿を削除したい: 真の削除の場合
DELETE でレコードを消す世界観、べんり
code:config/routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
end
end
code:app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_board
before_action :set_post, only: :destroy
# DELETE /boards/:board_id/posts/1
# DELETE /boards/:board_id/posts/1.json
def destroy
@post.destroy
respond_to do |format|
format.html { redirect_to @board, notice: 'Post was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_board
end
def set_post
@post = @board.posts.find(params:id) end
end
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.body %>
<%= link_to 'Destroy', post.board, post, method: :delete, data: { confirm: 'Are you sure?' } %> </p> 投稿を削除したい: 投稿内容を削除したい編
真の削除をすると投稿があったかどうかが分からない
真の削除をすると投稿自体がなかったことになる
たとえば掲示板への総投稿数を @board.posts.countした数値も減る
真の削除はせず投稿自体は残したまま投稿内容を「投稿は削除されました」としたい
投稿内容を上書きする場合はこう
code:config/routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
end
end
code:app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_board
# PATCH/PUT /boards/:board_id/posts/1
# PATCH/PUT /boards/:board_id/posts/1.json
def update
respond_to do |format|
if @post.update(update_post_params)
format.html { redirect_to @board, notice: 'Post was successfully updated.' }
format.json { render :show, status: :ok, location: @post }
else
format.html { render :edit }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
private
def set_board
end
def set_post
@post = @board.posts.find(params:id) end
def update_post_params
params.require(:post).permit(:body)
end
end
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.body %>
<%= link_to 'Destroy', post.board, post, method: :delete, data: { confirm: 'Are you sure?' } %> <%= link_to 'Update the body', post.board, post, remote: true, method: :patch, data: { confirm: 'Are you sure?', params: { post: { body: 'The body is deleted.' } }.to_param } %> </p>
投稿を削除したい: 元の投稿内容は保存しておき投稿内容を取り消したい編
投稿内容を上書きするとどういう投稿があったかが後から調べられない
真の削除はせず投稿自体はのこしたまま投稿を取り消し「投稿は削除されました」とするが投稿した内容自体は残したい
cancel を実装しましょう
code:config/routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
post :cancel, on: :member
end
end
end
code:app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_board
# POST /boards/:board_id/posts/1/cancel
# POST /boards/:board_id/posts/1/cancel.json
def cancel
respond_to do |format|
if @post.cancel
format.html { redirect_to @board, notice: 'Post was successfully cancelled.' }
format.json { render :show, status: :ok, location: @post }
else
format.html { render :edit }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
end
code:app/models/post.rb
class Post < ApplicationRecord
def cancel(at: Time.zone.now)
self.cancelled_at = at
save
end
def cancelled?
!cancelled_at.nil?
end
def display_body
cancelled? ? 'This post is deleted.' : body
end
end
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.display_body %>
<% unless post.cancelled? %>
<%= link_to 'Destroy', post.board, post, method: :delete, data: { confirm: 'Are you sure?' } %> <%= link_to 'Update the body', post.board, post, remote: true, method: :patch, data: { confirm: 'Are you sure?', params: { post: { body: 'The body is deleted.' } }.to_param } %> <%= link_to 'Cancel', cancel_board_post_path(post.board, post), remote: true, method: :post, data: { confirm: 'Are you sure?' } %>
<% end %>
</p>
投稿を削除したい: 元の投稿内容は保存しておき投稿内容は取り消したいがそれを更に取り消したい
投稿内容を上書きするとどういう投稿があったかが後から調べられない
真の削除はせず投稿自体はのこしたまま投稿を取り消し「投稿は削除されました」とするが投稿した内容自体は残したい
しかし間違えて取り消しちゃったのでやっぱり元に戻したい
そんなときがあると思います
uncancel を実装しましょう
code:config/routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
member do
post :cancel
post :uncancel
end
end
end
end
code:app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_board
# POST /boards/:board_id/posts/1/uncancel
# POST /boards/:board_id/posts/1/uncancel.json
def uncancel
respond_to do |format|
if @post.uncancel
format.html { redirect_to @board, notice: 'Post was successfully uncancelled.' }
format.json { render :show, status: :ok, location: @post }
else
format.html { render :edit }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
end
code:app/models/post.rb
class Post < ApplicationRecord
def uncancel
self.cancelled_at = nil
save
end
end
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.display_body %>
<% if post.cancelled? %>
<%= link_to 'Uncancel', uncancel_board_post_path(post.board, post), remote: true, method: :post, data: { confirm: 'Are you sure?' } %>
<% else %>
<%= link_to 'Destroy', post.board, post, method: :delete, data: { confirm: 'Are you sure?' } %> <%= link_to 'Update the body', post.board, post, remote: true, method: :patch, data: { confirm: 'Are you sure?', params: { post: { body: 'The body is deleted.' } }.to_param } %> <%= link_to 'Cancel', cancel_board_post_path(post.board, post), remote: true, method: :post, data: { confirm: 'Are you sure?' } %>
<% end %>
</p>
投稿を削除したい: 元の投稿内容は保存しておき投稿内容は取り消したいがそれを更に取り消したい feat. NOT NULL
投稿内容を上書きするとどういう投稿があったかが後から調べられない
真の削除はせず投稿自体はのこしたまま投稿を取り消し「投稿は削除されました」とするが投稿した内容自体は残したい
しかし間違えて取り消しちゃったのでやっぱり元に戻したい
そういうときに cancelled_at カラムを追加して cancel メソッドを呼び出し投稿を取り消し uncancel メソッドで投稿の取り消しを更に取り消す実装をしました
しかし新しい投稿は取り消されているわけではないので cancelled_at には nil が入ります。NOT NULLの制約を掛けられません
Post#cancel を実装する代わりに cancel の動詞を名詞の cancellation に変更し投稿内容を取り消す際は cancellation リソースを作成することにします
こうすると cancellation のモデルを別に分けることができます
cancellation のモデルが作成された日時を投稿内容が取り消された日時としましょう
NOT NULLがつけられます、やったね
先程 cancelled_at を追加した際はカラムにNOT NULL制約がかかりませんでした
NULLオプションを付け忘れても大丈夫なようにいつも null: false をつけるパッチがカラム追加でも動作するようにします
code:config/initializers/always_prefer_not_null.rb
# NOTE: PostgreSQLAdapter is not loaded before connecion resolved
require "active_record/connection_adapters/postgresql_adapter"
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(Module.new {
def add_column(table_name, column_name, type, **options)
super(table_name, column_name, type, **options.merge(null: false))
end
})
posts テーブルから cancelled_at カラムを削除し post_cancellations テーブルを追加します
code:db/migrate/20200607164016_remove_cancelled_at_from_posts.rb
class RemoveCancelledAtFromPosts < ActiveRecord::Migration6.0 def change
remove_column :posts, :cancelled_at
end
end
code:db/migrate/20200607164139_create_post_cancellations.rb
class CreatePostCancellations < ActiveRecord::Migration6.0 def change
create_table :post_cancellations do |t|
t.references :post, null: false, foreign_key: true
t.timestamps
end
end
end
member アクションが2つ消えて cancellation リソースが増えました
code:config/routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
resource :cancellation, only: %Icreate destroy, controller: 'post_cancellations' end
end
end
PostsController の役割は減り
code:app/controllers/posts_controller.rb
class PostsController < ApplicationController
# cancel / uncancelを削除
end
新たに PostCancellationsController が投稿の取り消しを扱うようになりました
code:app/controllers/post_cancellations_controller.rb
class PostCancellationsController < ApplicationController
before_action :set_board
before_action :set_post
# POST /boards/:board_id/posts/1/cancellation
# POST /boards/:board_id/posts/1/cacellation.json
def create
respond_to do |format|
if @post.create_cancellation
format.html { redirect_to @board, notice: 'PostCancellation was successfully created.' }
format.json { render :show, status: :created, location: @board }
else
format.html { render 'boards/show' }
format.json { render json: @post.cancellation.errors, status: :unprocessable_entity }
end
end
end
# DELETE /boards/:board_id/posts/1/cancellation
# DELETE /boards/:board_id/posts/1/cancellation.json
def destroy
@post.cancellation.destroy
respond_to do |format|
format.html { redirect_to @board, notice: 'PostCancellation was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_board
end
def set_post
@post = @board.posts.find(params:post_id) end
end
投稿取り消しは投稿自体とは別のモデルで持つようになり
code:app/models/post.rb
class Post < ApplicationRecord
has_one :cancellation, class_name: 'PostCancellation', dependent: :destroy
def cancelled?
!cancellation.nil?
end
end
code:app/models/post_cancellation.rb
class PostCancellation < ApplicationRecord
belongs_to :post, inverse_of: :cancellation
end
投稿取り消しリソースへのPOST/DELETEへとリンクが変更
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.display_body %>
<% if post.cancelled? %>
<%= link_to 'Uncancel', board_post_cancellation_path(post.board, post), remote: true, method: :delete, data: { confirm: 'Are you sure?' } %>
<% else %>
<%= link_to 'Destroy', post.board, post, method: :delete, data: { confirm: 'Are you sure?' } %> <%= link_to 'Update the body', post.board, post, remote: true, method: :patch, data: { confirm: 'Are you sure?', params: { post: { body: 'The body is deleted.' } }.to_param } %> <%= link_to 'Cancel', board_post_cancellation_path(post.board, post), remote: true, method: :post, data: { confirm: 'Are you sure?' } %>
<% end %>
</p>
投稿を削除したい: 元の投稿内容は保存しておき投稿内容は取り消したい関連した操作履歴を消したくない
投稿内容を上書きするとどういう投稿があったかが後から調べられない
真の削除はせず投稿自体はのこしたまま投稿を取り消し「投稿は削除されました」とするが投稿した内容自体は残したい
しかし間違えて取り消しちゃったのでやっぱり元に戻したい
しかし誰が投稿の取り消し・取り消しの取り消しをしたのか確認したい
PostCancellation と Post の間に中間モデル PostPostCancellation をおき有効な取り消し操作を示すようにします
PostPostCancellation を削除してもPostCancellation 自体は消えないようにしておきます
有効な取り消し履歴は1つのみとするためUNIQUE制約をかけます
code:db/migrate/20200607170618_create_post_post_cancellations.rb
class CreatePostPostCancellations < ActiveRecord::Migration6.0 def change
create_table :post_post_cancellations do |t|
t.references :post, null: false, foreign_key: true
t.references :post_cancellation, null: false, foreign_key: true
t.timestamps
end
end
end
いい感じにモデルの定義を更新して
code:app/models/post.rb
class Post < ApplicationRecord
has_one :post_post_cancellation, dependent: :destroy
has_one :cancellation, class_name: 'PostCancellation', through: :post_post_cancellation, source: :post_cancellation
has_many :post_cancellations, dependent: :destroy
end
code:app/models/post_cancellation.rb
class PostCancellation < ApplicationRecord
belongs_to :post
has_one :post_post_cancellation, dependent: :destroy
end
code:app/models/post_post_cancellation.rb
class PostPostCancellation < ApplicationRecord
belongs_to :post
belongs_to :post_cancellation
end
いい感じに @post.post_post_cancellation を操作するようにします
code:app/controllers/post_cancellations_controller.rb
class PostCancellationsController < ApplicationController
before_action :set_board
before_action :set_post
# POST /boards/:board_id/posts
# POST /boards/:board_id/posts.json
def create
respond_to do |format|
if @post.create_post_post_cancellation(post_cancellation: @post.post_cancellations.build)
format.html { redirect_to @board, notice: 'PostCancellation was successfully created.' }
format.json { render :show, status: :created, location: @board }
else
format.html { render 'boards/show' }
format.json { render json: @post.cancellation.errors, status: :unprocessable_entity }
end
end
end
# DELETE /boards/:board_id/posts/1/cancellation
# DELETE /boards/:board_id/posts/1/cancellation.json
def destroy
@post.post_post_cancellation.destroy
respond_to do |format|
format.html { redirect_to @board, notice: 'PostCancellation was successfully destroyed.' }
format.json { head :no_content }
end
end
end
ここの destroy で誰がどの post_cancellation に対する post_post_cancellation を削除したのか post_cancellation_deletion を作れば履歴が残る
投稿を削除したい: 元の投稿内容は保存しておき投稿内容は取り消したい関連した操作履歴を消したくない feat. SELECT, INSERT
投稿内容を上書きするとどういう投稿があったかが後から調べられない
真の削除はせず投稿自体はのこしたまま投稿を取り消し「投稿は削除されました」とするが投稿した内容自体は残したい
しかし間違えて取り消しちゃったのでやっぱり元に戻したい
しかし誰が投稿の取り消し・取り消しの取り消しをしたのか確認したい
そしてDELETEを絶対に発行したくない場合
missing を使うため 6-0-stable を master に書き換え bundle update します
code:Gemfile
# gem 'rails', github: "rails/rails", branch: "6-0-stable"
gem 'rails', github: "rails/rails", branch: "master"
PostPostCancellation を消して付け替えるのではなく PostCancellationInvalidation を作成しましょう
code:db/migrate/20200608172902_drop_post_post_cancellations.rb
class DropPostPostCancellations < ActiveRecord::Migration6.0 def change
drop_table :post_post_cancellations
end
end
code:db/migrate/20200608173626_create_post_cancellation_invalidations.rb
class CreatePostCancellationInvalidations < ActiveRecord::Migration6.0 def change
create_table :post_cancellation_invalidations do |t|
t.references :post_cancellation, null: false, foreign_key: true
t.timestamps
end
end
end
code:routes.rb
Rails.application.routes.draw do
root to: 'boards#index'
resources :boards do
resource :cancellation, only: :create, controller: 'post_cancellations' do
resource :invalidation, only: :create, controller: 'post_cancellation_invalidations'
end
end
end
end
code:app/controllers/post_cancellation_invalidations_controller.rb
class PostCancellationInvalidationsController < ApplicationController
before_action :set_board
before_action :set_post
before_action :set_cancellation
# POST /boards/:board_id/posts/:post_id/cancellation/invalidation
# POST /boards/:board_id/posts/:post_id/cancellation/invalidation.json
def create
respond_to do |format|
if @cancellation.create_invalidation
format.html { redirect_to @board, notice: 'PostCancellationInvalidation was successfully created.' }
format.json { render :show, status: :created, location: @board }
else
format.html { render 'boards/show' }
format.json { render json: @cancellation.invalidation.errors, status: :unprocessable_entity }
end
end
end
private
def set_board
end
def set_cancellation
@cancellation = @post.cancellation
end
def set_post
@post = @board.posts.find(params:post_id) end
end
code:app/models/post.rb
class Post < ApplicationRecord
belongs_to :board
has_one :cancellation, -> { where.missing(:invalidation) }, class_name: 'PostCancellation'
has_many :post_cancellations, dependent: :destroy
validates :poster, :body, presence: true
def cancelled?
!cancellation.nil?
end
def display_body
cancelled? ? 'This post is deleted.' : body
end
end
code:app/models/post_cancellation.rb
class PostCancellation < ApplicationRecord
belongs_to :post
has_one :invalidation, class_name: 'PostCancellationInvalidation'
end
code:app/models/post_cancellation_invalidation.rb
class PostCancellationInvalidation < ApplicationRecord
belongs_to :post_cancellation
end
code:app/views/posts/_post.html.erb
<p>
<%= post.poster %>:<%= post.display_body %>
<% if post.cancelled? %>
<%= link_to 'Uncancel', board_post_cancellation_invalidation_path(post.board, post), remote: true, method: :post, data: { confirm: 'Are you sure?' } %>
<% else %>
<%= link_to 'Destroy', post.board, post, method: :delete, data: { confirm: 'Are you sure?' } %> <%= link_to 'Update the body', post.board, post, remote: true, method: :patch, data: { confirm: 'Are you sure?', params: { post: { body: 'The body is deleted.' } }.to_param } %> <%= link_to 'Cancel', board_post_cancellation_path(post.board, post), remote: true, method: :post, data: { confirm: 'Are you sure?' } %>
<% end %>
</p>
絶対に削除できないようにActiveRecordのモデルを削除・更新できない用のユーザーを作っておく
code:db/migrate/20200610173406_create_rails_app_role.rb
class CreateRailsAppRole < ActiveRecord::Migration6.1 def up
execute(<<~SQL)
revoke all on all tables in schema public from rails_app;
revoke all on all sequences in schema public from rails_app;
revoke all on database kesenai_tsumi_development from rails_app;
drop role rails_app;
create role rails_app login password 'kesenai';
grant connect on database kesenai_tsumi_development to rails_app;
grant select, insert on all tables in schema public to rails_app;
grant update on all sequences in schema public to rails_app;
SQL
end
def down
execute(<<~SQL)
revoke all on all tables in schema public from rails_app;
revoke all on all sequences in schema public from rails_app;
revoke all on database kesenai_tsumi_development from rails_app;
drop role rails_app;
SQL
end
end
普段Railsアプリを実行する際は削除する権限がないユーザーを使って接続し、マイグレーションを行うときはマイグレーションを行う権限があるユーザーで行う
code:docker-compose.yml
version: "3.8"
# 一部略
services:
app: &rails
environment:
DATABASE_URL: postgres://rails_app:kesenai@db:5432
migration:
<<: *rails
environment:
DATABASE_URL: postgres://postgres:hi@db:5432
まとめ
以下のような形で投稿内容の削除を実装してみました
真の削除
投稿内容を削除
元の投稿内容は保存しておき投稿内容を取り消し
元の投稿内容は保存しておき投稿内容は取り消したいがそれを更に取り消したい
元の投稿内容は保存しておき投稿内容は取り消したいがそれを更に取り消したい feat. NOT NULL
元の投稿内容は保存しておき投稿内容は取り消したい関連した操作履歴を消したくない
元の投稿内容は保存しておき投稿内容は取り消したい関連した操作履歴を消したくない feat. SELECT, INSERT
いかがでしたか?
真の削除
postリソースのみ扱う
ActiveRecordのdestroyメソッドを使うだけなので単純
削除された投稿内容は確認できない
誰が投稿したかも確認できない
削除をなかったことにできない
削除に関連する操作の履歴はわからない
投稿内容を削除
postリソースのみ扱う
ActiveRecordのupdateメソッドを使うだけなので単純
削除された投稿内容は確認できない
誰が投稿したかは確認できる
投稿があったことはわかる
削除をなかったことにできない
削除に関連する操作の履歴はわからない
元の投稿内容は保存しておき投稿内容を取り消し
postリソースのみ扱う
postリソースに対して独自のmemberアクションcancelを1つ追加
Postモデルにnull可なカラムが増えPostがキャンセルされたかどうかの状態を持つようになった
削除された投稿内容を確認できる
誰が投稿したかはわかる
投稿があったことはわかる
削除をなかったことにできない
削除に関連する操作の履歴はわからない
元の投稿内容は保存しておき投稿内容は取り消したいがそれを更に取り消したい
postリソースのみ扱う
postリソースに対して独自のmemberアクションcancel/uncancelの2つを追加
Postモデルにnull可なカラムが増えPostがキャンセルされたかどうかの状態を持つようになった
削除された投稿内容を確認できる
誰が投稿したかはわかる
投稿があったことはわかる
削除をなかったことにできる
削除に関連する操作の履歴はわからない
元の投稿内容は保存しておき投稿内容は取り消したいがそれを更に取り消したい feat. NOT NULL
postリソースに加え子リソースのcancellationを扱う
cancellationでは標準のアクションcreate/destroyの2つを実装
Postモデルにnull可能なカラムはないのでPostが変化する状態を持たなくなった
PostCancellationモデルが増えた
削除された投稿内容を確認できる
誰が投稿したかはわかる
投稿があったことはわかる
削除をなかったことにできる
削除に関連する操作の履歴はわからない
元の投稿内容は保存しておき投稿内容は取り消したい関連した操作履歴を消したくない
postリソースに加え子リソースのcancellationを扱う
cancellationでは標準のアクションcreate/destroyの2つを実装
Postモデルにnull可能なカラムはないのでPostが変化する状態を持たなくなった
PostCancellationモデルが増えた
PostPostCancellationモデルも増えた
削除された投稿内容を確認できる
誰が投稿したかはわかる
投稿があったことはわかる
削除をなかったことにできる
削除に関連する操作の履歴がわかる
投稿を削除したい: 元の投稿内容は保存しておき投稿内容は取り消したい関連した操作履歴を消したくない feat. SELECT, INSERT
postリソースに加え子リソースのcancellationを扱う
cancellationでは標準のアクションcreate/destroyの2つを実装
Postモデルにnull可能なカラムはないのでPostが変化する状態を持たなくなった
PostCancellationモデルが増えた
PostPostCancellationモデルも増えた
削除された投稿内容を確認できる
誰が投稿したかはわかる
投稿があったことはわかる
削除をなかったことにできる
削除に関連する操作の履歴がわかる
DELETEせず常にINSERT
所感です
単純に削除できるならそれが一番簡単
何を削除するのか考えたい
モデルの状態を操作すると複雑さが増す
NULL / NOT NULL
Rails標準ではないアクションの定義
削除する操作に名前を付けてリソースとして扱うと削除対象のモデルに状態をもたせたり削除対象のモデルを操作しなくて済み削除する操作自体のリソースを残すことができ、便利な場面がある。削除対象が不変なら削除を取り消すのも簡単にできる。一方実装は膨らみがち。
要はバランス (完)