こんにちは
株式会社HRBrainでフロントエンジニア(とバックエンドエンジニア?)をしているみつです!
先日、バックエンドの実装でUpdateの処理を書いた際に、DeleteAll
してからInsertAll
の実装でプルリクエストを作ったところ、差分だけUpdateする方が良いとレビューをもらいました。
DeleteAll
してからInsertAll
はコードの見通しは良くなるかもしれませんが、権限やパフォーマンスを考慮する場合には、この実装しかないのかぁ。と思い、改めて記事にしながら整理しました。
前提
- 管理者とユーザーが存在します
-
管理者はコンテンツに紐づくユーザーを登録する操作を行います
- 例)管理者AはユーザーA, ユーザーBをコンテンツAに割り当てることで、各ユーザーはコンテンツAにアクセスすることができるようになります
-
管理者は、ユーザー操作可能な範囲が権限で制限されている場合があります
- 例)管理者Aは、一般ユーザーA, B, Cを操作できるが、管理者Bは一般ユーザーB, Cのみしか操作できません
- データベースの設計上、論理削除を想定した記事となっていますが、物理削除の場合でも検討すべきだと考えています
実現したいこと
- 管理画面から登録操作を行い、特定のユーザーをコンテンツAに割り当てたい
- 特定の管理者が権限足らずで実は見えていないユーザーが消してしまわないようにしたい
最初の実装
特定のコンテンツIDに紐づくユーザーレコードをすべて消してから、新たに追加したいユーザーIDsをフロントから受け取り、一括でInsertするようにしました。
しかし、これだとsqlを実行する時、companyID
とID
に一致するレコードはすべて消えてしまいます。
func (r userRepository) UpdateUsers(
ctx context.Context,
companyID uint64,
contentID domain.contentID,
userIDs domain.UserIDs,
) error {
err = r.DeleteAll(ctx, companyID, contentID)
if err != nil {
return errorの処理
}
err = r.InsertAll(ctx, companyID, contentID, userIDs)
if err != nil {
return errorの処理
}
return nil
}
レビューをもらった後の実装
フロントからuserIDs
を受け取るところは変わりません。
ただ、usecase層からrepository層に渡す際、新たにeditableUserIDs
というその管理者が操作可能な範囲のユーザーIDを持ってくるようにします。
そうすることで、
- 新規で追加したいユーザー
- 一度削除したが、今回の操作で復元したいユーザー
- 削除したいユーザー
を分類することができ、下記のような良さがあります。
- 管理者が操作可能な範囲でレコードを操作することができる
- 追加や削除が頻繁に行われるかつ、大量にユーザーがいる場合に毎度InsertAllしなくても差分だけで操作することができる
func (r userRepository) UpdateUsers(
ctx context.Context,
companyID uint64,
contentID domain.contentID,
newUserIDs domain.UserIDs,
editableUserIDs domain.UserIDs,
) error {
currentUserIDs, err := r.getCurrentUserIDs(ctx, companyID, contentID)
if err != nil {
return errorの処理
}
deletedUserIDs, err := r.getDeletedUserIDs(ctx, companyID, contentID)
if err != nil {
return errorの処理
}
userIDsToAdd, userIDsToRecover, userIDsToDelete := r.filterUserIDsToUpdate(
newUserIDs,
editableUserIDs,
currentUserIDs,
deletedUserIDs,
)
err = r.CreateUsers(ctx, companyID, contentID, userIDsToAdd)
if err != nil {
return errorの処理
}
err = r.recoverUsers(ctx, companyID, contentID, userIDsToRecover)
if err != nil {
return errorの処理
}
err = r.deleteUsers(ctx, companyID, contentID, userIDsToDelete)
if err != nil {
return errorの処理
}
return nil
}
具体的な処理の流れ
具体的な例を下記に記載します。
今回、実際行われるのは、
[D]
を新しくInsertする[C]
を復元(deletedAtをNULLにする)
になります。
前提条件
- 登場するユーザーは、
[A, B, C, D]
の4人とします - 今回の管理者は、
[B]
に対する閲覧権限を持っていないものとします [C]
は、今回の操作より前のタイミングで削除された(deletedAtが埋まっている)状態とします
現在のデータベースの状態
-
[A, B, ~C~]
(削除済み)
処理前
- newUserIDs ▶ フロントから渡ってきたユーザー
[A, C, D]
[D]
が新しく登場(添付図参照)
- editableUserIDs ▶ 今回の管理者が操作可能なユーザー
[A, C, D]
[B]
は操作できない(添付図参照)
- currentUserIDs ▶ 登録されているユーザー
[A, B]
- deletedUserID ▶ 削除されたユーザー
[~C~]
処理後
- memberIDsToAdd ▶ 新しくレコード追加するユーザー
[D]
- memberIDsToRecover ▶ 復元するユーザー
[C]
- memberIDsToDelete ▶ 削除するユーザー
- 本来
[B]
だが、editableUserIDsに含まれないので、管理者は操作ができない
- 本来
まとめ
DeleteAllしてからInsertAllするのは可読性の観点では◎かもしれません。
しかし、権限やパフォーマンスを考慮する時、1つのアプローチとして差分だけを操作する方法があることを知りました。
この実装だけが最適ではないのだろうと思いますが、1つのアプローチとして今後の実装に活かしたいです。
おわり。
PR
株式会社HRBrainでは、一緒に働く仲間を募集しています!
興味を持っていただいた方はぜひ弊社の採用ページをご確認ください!
HRBrain文化を一緒に作っていきましょう!