10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HRBrainAdvent Calendar 2024

Day 8

権限やパフォーマンスを考慮して差分だけ更新するのが良さそう

Last updated at Posted at 2024-12-07

こんにちは:sunny:

株式会社HRBrainでフロントエンジニア(とバックエンドエンジニア?)をしているみつです!

先日、バックエンドの実装でUpdateの処理を書いた際に、DeleteAllしてからInsertAllの実装でプルリクエストを作ったところ、差分だけUpdateする方が良いとレビューをもらいました。

DeleteAllしてからInsertAllはコードの見通しは良くなるかもしれませんが、権限やパフォーマンスを考慮する場合には、この実装しかないのかぁ。と思い、改めて記事にしながら整理しました。

前提

  • 管理者とユーザーが存在します
  • 管理者はコンテンツに紐づくユーザーを登録する操作を行います
    • 例)管理者AはユーザーA, ユーザーBをコンテンツAに割り当てることで、各ユーザーはコンテンツAにアクセスすることができるようになります
  • 管理者は、ユーザー操作可能な範囲が権限で制限されている場合があります
    • 例)管理者Aは、一般ユーザーA, B, Cを操作できるが、管理者Bは一般ユーザーB, Cのみしか操作できません
  • データベースの設計上、論理削除を想定した記事となっていますが、物理削除の場合でも検討すべきだと考えています

実現したいこと

  • 管理画面から登録操作を行い、特定のユーザーをコンテンツAに割り当てたい
  • 特定の管理者が権限足らずで実は見えていないユーザーが消してしまわないようにしたい

最初の実装

特定のコンテンツIDに紐づくユーザーレコードをすべて消してから、新たに追加したいユーザーIDsをフロントから受け取り、一括でInsertするようにしました。

しかし、これだとsqlを実行する時、companyIDIDに一致するレコードはすべて消えてしまいます

user.go
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しなくても差分だけで操作することができる
user.go
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が埋まっている)状態とします

image.png

現在のデータベースの状態

  • [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文化を一緒に作っていきましょう!

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?