delμ ID λ°°μ΄λ‘ μ¬λ¬ λ μ½λλ₯Ό ν λ²μ μμ νλ λ©μλμ
λλ€. νΈλμμ
λ΄μμ μμ νκ² μ€νλλ©°, κΈ°λ³Έμ μΌλ‘ κ΄λ¦¬μ κΆνμ΄ νμν©λλ€.
delμ BaseModelClassμ μ μλμ΄ μμ§ μμ΅λλ€. Entityλ₯Ό μμ±νλ©΄ Syncerκ° κ° Model ν΄λμ€μ μλμΌλ‘ μμ±νλ νμ€ ν¨ν΄μ
λλ€.
νμ
μκ·Έλμ²
async del(ids: number[]): Promise<number>
μλ μμ± μ½λ
Sonamuλ Entityλ₯Ό κΈ°λ°μΌλ‘ λ€μ μ½λλ₯Ό μλμΌλ‘ μμ±ν©λλ€:
// src/application/user/user.model.ts (μλ μμ±)
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"], guards: ["admin"] })
async del(ids: number[]): Promise<number> {
const wdb = this.getPuri("w");
// νΈλμμ
λ΄μμ μμ
await wdb.transaction(async (trx) => {
return trx.table("users").whereIn("users.id", ids).delete();
});
return ids.length;
}
}
λ΄λΆ λμ:
- getPuri(βwβ): BaseModelClass λ©μλλ‘ μ°κΈ°μ© Puri νλ
- transaction(): νΈλμμ
μμ
- table().whereIn().delete(): Knex λ©μλλ‘ μμ μ€ν
- ids.length λ°ν: μμ μμ²ν ID κ°μ λ°ν
λ§€κ°λ³μ
μμ ν λ μ½λμ ID λ°°μ΄μ
λλ€.
νμ
: number[]
// λ¨μΌ λ μ½λ μμ
await UserModel.del([123]);
// μ¬λ¬ λ μ½λ μμ
await UserModel.del([1, 2, 3, 4, 5]);
// λΉ λ°°μ΄ (μ무κ²λ μμ νμ§ μμ)
await UserModel.del([]);
λ°νκ°
νμ
: Promise<number>
μμ λ λ μ½λμ κ°μλ₯Ό λ°νν©λλ€.
// 3κ° μμ
const count = await UserModel.del([1, 2, 3]);
console.log(`${count} users deleted`); // "3 users deleted"
// μ‘΄μ¬νμ§ μλ ID ν¬ν¨
const count = await UserModel.del([1, 999, 1000]);
console.log(`${count} users deleted`); // "1 users deleted" (1λ²λ§ μ‘΄μ¬)
μ‘΄μ¬νμ§ μλ IDλ 무μλλ©° μλ¬λ₯Ό λ°μμν€μ§ μμ΅λλ€.
κΈ°λ³Έ μ¬μ©λ²
λ¨μΌ λ μ½λ μμ
import { UserModel } from "./user/user.model";
class UserService {
async deleteUser(userId: number) {
const count = await UserModel.del([userId]);
if (count === 0) {
throw new Error("User not found");
}
return { success: true };
}
}
μ¬λ¬ λ μ½λ μμ
async deleteUsers(userIds: number[]) {
const count = await UserModel.del(userIds);
return {
requested: userIds.length,
deleted: count
};
}
μ‘°κ±΄λΆ μμ
async deleteInactiveUsers() {
// λΉνμ± μ¬μ©μ μ‘°ν
const { rows } = await UserModel.findMany("A", {
status: "inactive",
num: 0
});
// ID μΆμΆνμ¬ μμ
const ids = rows.map(user => user.id);
const count = await UserModel.del(ids);
return { deleted: count };
}
νΈλμμ
delμ μλμΌλ‘ νΈλμμ
λ΄μμ μ€νλ©λλ€.
async del(ids: number[]): Promise<number> {
const wdb = this.getPuri("w");
// νΈλμμ
await wdb.transaction(async (trx) => {
return trx.table("users")
.whereIn("users.id", ids)
.delete();
});
return ids.length;
}
@transactionalκ³Ό ν¨κ»
import { api, transactional } from "sonamu";
class UserFrame {
@api({ httpMethod: "POST" })
@transactional()
async deleteUserAndRelated(userId: number) {
// κ΄λ ¨ λ°μ΄ν° μμ
await PostModel.del(
(await PostModel.findMany("A", {
user_id: userId,
num: 0
})).rows.map(p => p.id)
);
// μ¬μ©μ μμ
await UserModel.del([userId]);
// λͺ¨λ μ±κ³΅νκ±°λ λͺ¨λ μ€ν¨
return { success: true };
}
}
κΆν νμΈ
κΈ°λ³Έμ μΌλ‘ delμ κ΄λ¦¬μ κΆνμ΄ νμν©λλ€.
@api({
httpMethod: "POST",
clients: ["axios", "tanstack-mutation"],
guards: ["admin"] // κΈ°λ³Έ μ€μ
})
async del(ids: number[]): Promise<number> {
// ...
}
λ³ΈμΈ λ°μ΄ν° μμ
import { Sonamu, api, UnauthorizedException } from "sonamu";
class PostFrame {
@api({ httpMethod: "POST" })
async deleteMyPost(postId: number) {
const { user } = Sonamu.getContext();
// κ²μλ¬Ό μμ μ νμΈ
const post = await PostModel.findById("A", postId);
if (post.user_id !== user.id) {
throw new UnauthorizedException("λ³ΈμΈμ κ²μλ¬Όλ§ μμ ν μ μμ΅λλ€");
}
await PostModel.del([postId]);
return { success: true };
}
}
μ€μ μμ
νμ νν΄
κ²μλ¬Ό μμ
λλ μμ
μννΈ μμ
import { UserModel, PostModel, CommentModel } from "./models";
import { Sonamu, api, transactional } from "sonamu";
class UserFrame {
@api({ httpMethod: "POST" })
@transactional()
async deleteAccount() {
const { user } = Sonamu.getContext();
// μ¬μ©μμ λͺ¨λ λκΈ μμ
const { rows: comments } = await CommentModel.findMany("A", {
user_id: user.id,
num: 0
});
if (comments.length > 0) {
await CommentModel.del(comments.map(c => c.id));
}
// μ¬μ©μμ λͺ¨λ κ²μλ¬Ό μμ
const { rows: posts } = await PostModel.findMany("A", {
user_id: user.id,
num: 0
});
if (posts.length > 0) {
await PostModel.del(posts.map(p => p.id));
}
// μ¬μ©μ μμ
await UserModel.del([user.id]);
// μΈμ
μ’
λ£
Sonamu.getContext().passport.logout();
return { success: true };
}
}
import { PostModel, CommentModel, LikeModel } from "./models";
import { Sonamu, api, transactional, UnauthorizedException } from "sonamu";
class PostFrame {
@api({ httpMethod: "POST" })
@transactional()
async deletePost(postId: number) {
const { user } = Sonamu.getContext();
// κ²μλ¬Ό μμ μ νμΈ
const post = await PostModel.findById("A", postId);
if (post.user_id !== user.id) {
throw new UnauthorizedException("κΆνμ΄ μμ΅λλ€");
}
// κ²μλ¬Όμ λͺ¨λ μ’μμ μμ
const { rows: likes } = await LikeModel.findMany("A", {
post_id: postId,
num: 0
});
if (likes.length > 0) {
await LikeModel.del(likes.map(l => l.id));
}
// κ²μλ¬Όμ λͺ¨λ λκΈ μμ
const { rows: comments } = await CommentModel.findMany("A", {
post_id: postId,
num: 0
});
if (comments.length > 0) {
await CommentModel.del(comments.map(c => c.id));
}
// κ²μλ¬Ό μμ
await PostModel.del([postId]);
return { success: true };
}
}
import { UserModel } from "./user/user.model";
import { api, transactional } from "sonamu";
class AdminFrame {
@api({ httpMethod: "POST", guards: ["admin"] })
@transactional()
async bulkDeleteUsers(params: {
status?: string;
inactive_days?: number;
}) {
// μμ λμ μ‘°ν
let conditions: any = {};
if (params.status) {
conditions.status = params.status;
}
if (params.inactive_days) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - params.inactive_days);
conditions.last_login_before = cutoffDate.toISOString();
}
const { rows } = await UserModel.findMany("A", {
...conditions,
num: 0
});
if (rows.length === 0) {
return { deleted: 0 };
}
// ID μΆμΆ
const ids = rows.map(user => user.id);
// μμ (500κ°μ© λ°°μΉ μ²λ¦¬)
let totalDeleted = 0;
for (let i = 0; i < ids.length; i += 500) {
const batch = ids.slice(i, i + 500);
const count = await UserModel.del(batch);
totalDeleted += count;
}
return {
requested: ids.length,
deleted: totalDeleted
};
}
}
import { UserModel } from "./user/user.model";
import { api } from "sonamu";
class UserFrame {
// del λμ statusλ₯Ό λ³κ²½νλ μννΈ μμ
@api({ httpMethod: "POST" })
async softDeleteUser(userId: number) {
// statusλ₯Ό 'deleted'λ‘ λ³κ²½
await UserModel.save([
{
id: userId,
status: "deleted",
deleted_at: new Date()
}
]);
return { success: true };
}
// μꡬ μμ (κ΄λ¦¬μλ§)
@api({ httpMethod: "POST", guards: ["admin"] })
async hardDeleteUser(userId: number) {
// μ€μ λ‘ DBμμ μμ
await UserModel.del([userId]);
return { success: true };
}
// 볡ꡬ
@api({ httpMethod: "POST", guards: ["admin"] })
async restoreUser(userId: number) {
await UserModel.save([
{
id: userId,
status: "active",
deleted_at: null
}
]);
return { success: true };
}
}
APIμμ μ¬μ©
μλ μμ±λ del API
// Model ν΄λμ€
class UserModelClass extends BaseModelClass {
@api({
httpMethod: "POST",
clients: ["axios", "tanstack-mutation"],
guards: ["admin"]
})
async del(ids: number[]): Promise<number> {
// μλ μμ±λ μ½λ
}
}
ν΄λΌμ΄μΈνΈ μ½λ
import { UserService } from "@/services/UserService";
// μ¬μ©μ μμ
const count = await UserService.del([1, 2, 3]);
console.log(`${count} users deleted`);
React (TanStack Query)
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/UserService";
function DeleteUserButton({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const deleteUser = useMutation({
mutationFn: (id: number) => UserService.del([id]),
onSuccess: () => {
// μΊμ 무ν¨ν
queryClient.invalidateQueries({ queryKey: ["users"] });
}
});
const handleDelete = () => {
if (confirm("μ λ§ μμ νμκ² μ΅λκΉ?")) {
deleteUser.mutate(userId);
}
};
return (
<button
onClick={handleDelete}
disabled={deleteUser.isPending}
>
{deleteUser.isPending ? "Deleting..." : "Delete"}
</button>
);
}
μΈλ ν€ μ μ½μ‘°κ±΄
CASCADE μ€μ
-- λΆλͺ¨ λ μ½λ μμ μ μμ λ μ½λ μλ μμ
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (post_id)
REFERENCES posts(id)
ON DELETE CASCADE
);
// κ²μλ¬Ό μμ μ λκΈ μλ μμ λ¨
await PostModel.del([1]);
RESTRICT μ€μ
-- μμ λ μ½λκ° μμΌλ©΄ λΆλͺ¨ λ μ½λ μμ λΆκ°
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
);
// κ²μλ¬Όμ΄ μλ μ¬μ©μ μμ μ μλ¬
try {
await UserModel.del([1]);
} catch (error) {
// Foreign key violation
console.error("Cannot delete user with posts");
}
μλ μ²λ¦¬
@transactional()
async deleteUserWithPosts(userId: number) {
// λ¨Όμ κ²μλ¬Ό μμ
const { rows: posts } = await PostModel.findMany("A", {
user_id: userId,
num: 0
});
if (posts.length > 0) {
await PostModel.del(posts.map(p => p.id));
}
// κ·Έ λ€μ μ¬μ©μ μμ
await UserModel.del([userId]);
return { success: true };
}
μμ νμΈ
μ‘΄μ¬ μ¬λΆ νμΈ
async deleteUserSafely(userId: number) {
// μ¬μ©μ μ‘΄μ¬ νμΈ
try {
await UserModel.findById("A", userId);
} catch (error) {
if (error instanceof NotFoundException) {
throw new BadRequestException("μ¬μ©μλ₯Ό μ°Ύμ μ μμ΅λλ€");
}
throw error;
}
// μμ
const count = await UserModel.del([userId]);
return { success: count > 0 };
}
μμ ν νμΈ
async deleteAndVerify(userId: number) {
// μμ
const count = await UserModel.del([userId]);
// μ€μ λ‘ μμ λμλμ§ νμΈ
const deleted = await UserModel.findOne("A", { id: userId });
return {
deleteCount: count,
verified: deleted === null
};
}
μ±λ₯ μ΅μ ν
λ°°μΉ μμ
λλ μμ μ λ°°μΉλ‘ λλ μ μ²λ¦¬νμΈμ.
async deleteManyUsers(ids: number[]) {
const batchSize = 500;
let totalDeleted = 0;
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const count = await UserModel.del(batch);
totalDeleted += count;
}
return { total: totalDeleted };
}
μΈλ±μ€ νμ©
μμ£Ό μμ νλ μΈλ ν€μ μΈλ±μ€λ₯Ό μΆκ°νμΈμ.
-- user_idλ‘ μμ£Ό μμ νλ κ²½μ°
CREATE INDEX idx_posts_user_id ON posts(user_id);
μ£Όμμ¬ν
1. λ°°μ΄λ‘ μ λ¬
delμ λ°λμ λ°°μ΄μ λ°μ΅λλ€.
// β μλͺ»λ¨
await UserModel.del(123);
// β
μ¬λ°λ¦
await UserModel.del([123]);
2. λ°νκ°μ κ°μ
const count = await UserModel.del([1, 2, 3]);
console.log(count); // 3 (λλ μ€μ μμ λ κ°μ)
3. CASCADE μ£Όμ
CASCADE μ€μ μ΄ μμΌλ©΄ μλμΉ μμ λ°μ΄ν°κΉμ§ μμ λ μ μμ΅λλ€.
// β μν: κ²μλ¬Όμ λͺ¨λ λκΈλ μμ λ¨
await PostModel.del([1, 2, 3]);
4. νΈλμμ
κΆμ₯
κ΄λ ¨λ μ¬λ¬ ν
μ΄λΈμ μμ ν λλ νΈλμμ
μ μ¬μ©νμΈμ.
// β
μμ : λͺ¨λ μ±κ³΅νκ±°λ λͺ¨λ μ€ν¨
@transactional()
async deleteUserAndRelated(userId: number) {
await CommentModel.del([...]);
await PostModel.del([...]);
await UserModel.del([userId]);
}
5. κΆν νμΈ
κΈ°λ³Έ guards: ["admin"] μ€μ μ λ³κ²½ν λλ μ μ€νμΈμ.
// β οΈ μ£Όμ: λꡬλ μμ κ°λ₯
@api({ guards: [] })
async del(ids: number[]) {
// μν!
}
μννΈ μμ vs νλ μμ
νλ μμ (del)
// μꡬμ μΌλ‘ DBμμ μ κ±°
await UserModel.del([1]);
μ₯μ :
- λμ€ν¬ κ³΅κ° μ μ½
- λ¨μν ꡬ쑰
λ¨μ :
- 볡ꡬ λΆκ°λ₯
- νμ€ν 리 μμ€
μννΈ μμ (save)
// status λλ deleted_at μ»¬λΌ μ¬μ©
await UserModel.save([
{
id: 1,
status: "deleted",
deleted_at: new Date()
}
]);
μ₯μ :
- 볡ꡬ κ°λ₯
- νμ€ν 리 μ μ§
- κ°μ¬ μΆμ
λ¨μ :
- λμ€ν¬ κ³΅κ° μ¦κ°
- 쿼리 볡μ‘λ μ¦κ°
λ€μ λ¨κ³