save๋ ๋ ์ฝ๋๋ฅผ ์ ์ฅํ๊ฑฐ๋ ์์ ํ๋ ๋ฉ์๋์
๋๋ค. UpsertBuilder๋ฅผ ์ฌ์ฉํ์ฌ INSERT/UPDATE๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํ๋ฉฐ, ํธ๋์ญ์
๋ด์์ ์์ ํ๊ฒ ์คํ๋ฉ๋๋ค.
save๋ BaseModelClass์ ์ ์๋์ด ์์ง ์์ต๋๋ค. Entity๋ฅผ ์์ฑํ๋ฉด Syncer๊ฐ ๊ฐ Model ํด๋์ค์ ์๋์ผ๋ก ์์ฑํ๋ ํ์ค ํจํด์
๋๋ค.
ํ์
์๊ทธ๋์ฒ
async save(
saveParams: SaveParams[]
): Promise<number[]>
์๋ ์์ฑ ์ฝ๋
Sonamu๋ Entity๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ค์ ์ฝ๋๋ฅผ ์๋์ผ๋ก ์์ฑํฉ๋๋ค:
// src/application/user/user.model.ts (์๋ ์์ฑ)
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
async save(spa: UserSaveParams[]): Promise<number[]> {
const wdb = this.getPuri("w");
// 1. UpsertBuilder์ ๋ ์ฝ๋ ๋ฑ๋ก
spa.forEach((sp) => {
wdb.ubRegister("users", sp);
});
// 2. ํธ๋์ญ์
๋ด์์ Upsert ์คํ
return wdb.transaction(async (trx) => {
const ids = await trx.ubUpsert("users");
return ids;
});
}
}
๋ด๋ถ ๋์:
- getPuri(โwโ): BaseModelClass ๋ฉ์๋๋ก ์ฐ๊ธฐ์ฉ Puri ํ๋
- ubRegister(): UpsertBuilder์ ๋ ์ฝ๋ ๋ฑ๋ก
- transaction(): ํธ๋์ญ์
์์
- ubUpsert(): INSERT/UPDATE ์๋ ์ฒ๋ฆฌ ๋ฐ ID ๋ฐํ
๋งค๊ฐ๋ณ์
saveParams
์ ์ฅํ ๋ ์ฝ๋ ๋ฐ์ดํฐ ๋ฐฐ์ด์
๋๋ค.
ํ์
: SaveParams[]
type UserSaveParams = {
id?: number; // ์์ผ๋ฉด UPDATE, ์์ผ๋ฉด INSERT
email: string;
name: string;
status?: string;
// ... ๊ธฐํ ํ๋
}
// ๋จ์ผ ๋ ์ฝ๋ ์ ์ฅ
await UserModel.save([
{ email: "[email protected]", name: "John" }
]);
// ์ฌ๋ฌ ๋ ์ฝ๋ ์ ์ฅ
await UserModel.save([
{ email: "[email protected]", name: "John" },
{ email: "[email protected]", name: "Jane" }
]);
id์ ์ญํ
- id๊ฐ ์์ผ๋ฉด: ํด๋น ID์ ๋ ์ฝ๋๋ฅผ UPDATE
- id๊ฐ ์์ผ๋ฉด: ์ ๋ ์ฝ๋๋ฅผ INSERT
// INSERT (id ์์)
await UserModel.save([
{ email: "[email protected]", name: "New User" }
]);
// UPDATE (id ์์)
await UserModel.save([
{ id: 1, email: "[email protected]", name: "Updated Name" }
]);
๋ฐํ๊ฐ
ํ์
: Promise<number[]>
์ ์ฅ๋ ๋ ์ฝ๋์ ID ๋ฐฐ์ด์ ๋ฐํํฉ๋๋ค.
// INSERT
const [id] = await UserModel.save([
{ email: "[email protected]", name: "John" }
]);
console.log("Created ID:", id); // 123
// UPDATE
const [id] = await UserModel.save([
{ id: 123, name: "John Smith" }
]);
console.log("Updated ID:", id); // 123
// ์ฌ๋ฌ ๋ ์ฝ๋
const ids = await UserModel.save([
{ email: "[email protected]", name: "A" },
{ email: "[email protected]", name: "B" }
]);
console.log("Created IDs:", ids); // [124, 125]
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
์ ๋ ์ฝ๋ ์์ฑ (INSERT)
import { UserModel } from "./user/user.model";
class UserService {
async createUser(email: string, name: string) {
const [id] = await UserModel.save([
{
email,
name,
status: "active",
created_at: new Date()
}
]);
return { id };
}
}
๋ ์ฝ๋ ์์ (UPDATE)
async updateUser(userId: number, name: string) {
const [id] = await UserModel.save([
{
id: userId,
name,
updated_at: new Date()
}
]);
return { id };
}
Upsert (INSERT or UPDATE)
async upsertUser(userData: { id?: number; email: string; name: string }) {
const [id] = await UserModel.save([userData]);
return { id };
}
UpsertBuilder ๋์ ์๋ฆฌ
1. ๋ ์ฝ๋ ๋ฑ๋ก (ubRegister)
async save(spa: UserSaveParams[]): Promise<number[]> {
const wdb = this.getPuri("w");
// ๊ฐ ๋ ์ฝ๋๋ฅผ UpsertBuilder์ ๋ฑ๋ก
spa.forEach((sp) => {
wdb.ubRegister("users", sp);
});
// ...
}
2. ํธ๋์ญ์
์คํ
// ํธ๋์ญ์
๋ด์์ upsert ์คํ
return wdb.transaction(async (trx) => {
const ids = await trx.ubUpsert("users");
return ids;
});
3. INSERT/UPDATE ์๋ ์ฒ๋ฆฌ
UpsertBuilder๋ id ํ๋์ ์ ๋ฌด์ ๋ฐ๋ผ ์๋์ผ๋ก INSERT ๋๋ UPDATE๋ฅผ ์ํํฉ๋๋ค:
-- id ์์: INSERT
INSERT INTO users (email, name, status)
VALUES ('[email protected]', 'John', 'active')
RETURNING id;
-- id ์์: UPDATE
INSERT INTO users (id, email, name)
VALUES (123, '[email protected]', 'John Smith')
ON CONFLICT (id)
DO UPDATE SET
email = EXCLUDED.email,
name = EXCLUDED.name
RETURNING id;
๋ฐฐ์น ์ ์ฅ
์ฌ๋ฌ ๋ ์ฝ๋๋ฅผ ํ ๋ฒ์ ์ ์ฅํ ์ ์์ต๋๋ค.
async bulkCreateUsers(users: { email: string; name: string }[]) {
const ids = await UserModel.save(
users.map(user => ({
email: user.email,
name: user.name,
status: "active",
created_at: new Date()
}))
);
return { count: ids.length, ids };
}
๊ด๊ณ ๋ฐ์ดํฐ ์ ์ฅ
1:N ๊ด๊ณ
// ๊ฒ์๋ฌผ + ๋๊ธ ์ ์ฅ
const postId = await PostModel.save([
{
title: "My Post",
content: "Content here"
}
]);
await CommentModel.save([
{
post_id: postId[0],
content: "First comment"
},
{
post_id: postId[0],
content: "Second comment"
}
]);
N:M ๊ด๊ณ
// ์ฌ์ฉ์ + ์ญํ ํ ๋น
const userId = await UserModel.save([
{
email: "[email protected]",
name: "Admin User"
}
]);
await UserRoleModel.save([
{
user_id: userId[0],
role_id: 1 // Admin
},
{
user_id: userId[0],
role_id: 2 // Editor
}
]);
์ค์ ์์
ํ์๊ฐ์
ํ๋กํ ์์
๊ฒ์๋ฌผ ์์ฑ
์ฃผ๋ฌธ ์์ฑ
import { UserModel } from "./user/user.model";
import { api, BadRequestException } from "sonamu";
import bcrypt from "bcrypt";
class AuthFrame {
@api({ httpMethod: "POST" })
async signup(params: {
email: string;
password: string;
name: string;
}) {
// ์ด๋ฉ์ผ ์ค๋ณต ํ์ธ
const existing = await UserModel.findOne("A", {
email: params.email
});
if (existing) {
throw new BadRequestException("์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ด๋ฉ์ผ์
๋๋ค");
}
// ๋น๋ฐ๋ฒํธ ํด์ฑ
const hashedPassword = await bcrypt.hash(params.password, 10);
// ์ฌ์ฉ์ ์์ฑ
const [id] = await UserModel.save([
{
email: params.email,
password: hashedPassword,
name: params.name,
status: "active",
email_verified: false,
created_at: new Date()
}
]);
return {
id,
email: params.email,
name: params.name
};
}
}
import { UserModel } from "./user/user.model";
import { Sonamu, api, UnauthorizedException } from "sonamu";
class UserFrame {
@api({ httpMethod: "POST" })
async updateProfile(params: {
name?: string;
bio?: string;
avatar_url?: string;
}) {
const { user } = Sonamu.getContext();
if (!user) {
throw new UnauthorizedException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค");
}
// ํ๋กํ ์
๋ฐ์ดํธ
const [id] = await UserModel.save([
{
id: user.id,
...params,
updated_at: new Date()
}
]);
// ์
๋ฐ์ดํธ๋ ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
const updated = await UserModel.findById("A", id);
return updated;
}
}
import { PostModel } from "./post/post.model";
import { Sonamu, api } from "sonamu";
class PostFrame {
@api({ httpMethod: "POST" })
async createPost(params: {
title: string;
content: string;
tags?: string[];
}) {
const { user } = Sonamu.getContext();
// ๊ฒ์๋ฌผ ์์ฑ
const [postId] = await PostModel.save([
{
user_id: user.id,
title: params.title,
content: params.content,
status: "draft",
created_at: new Date()
}
]);
// ํ๊ทธ ์ ์ฅ
if (params.tags && params.tags.length > 0) {
await TagModel.save(
params.tags.map(tag => ({
post_id: postId,
name: tag
}))
);
}
return {
id: postId,
title: params.title
};
}
}
import { OrderModel, OrderItemModel } from "./models";
import { Sonamu, api, transactional } from "sonamu";
class OrderFrame {
@api({ httpMethod: "POST" })
@transactional()
async createOrder(params: {
items: Array<{
product_id: number;
quantity: number;
price: number;
}>;
shipping_address: string;
}) {
const { user } = Sonamu.getContext();
// ์ด์ก ๊ณ์ฐ
const total = params.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// ์ฃผ๋ฌธ ์์ฑ
const [orderId] = await OrderModel.save([
{
user_id: user.id,
total_amount: total,
status: "pending",
shipping_address: params.shipping_address,
created_at: new Date()
}
]);
// ์ฃผ๋ฌธ ํญ๋ชฉ ์์ฑ
const itemIds = await OrderItemModel.save(
params.items.map(item => ({
order_id: orderId,
product_id: item.product_id,
quantity: item.quantity,
price: item.price
}))
);
return {
orderId,
itemCount: itemIds.length,
total
};
}
}
๋ถ๋ถ ์
๋ฐ์ดํธ
์ง์ ํ ํ๋๋ง ์
๋ฐ์ดํธํ ์ ์์ต๋๋ค.
// ์ด๋ฆ๋ง ์
๋ฐ์ดํธ
await UserModel.save([
{
id: 123,
name: "New Name"
// ๋ค๋ฅธ ํ๋๋ ๊ทธ๋๋ก ์ ์ง
}
]);
// ์ฌ๋ฌ ํ๋ ์
๋ฐ์ดํธ
await UserModel.save([
{
id: 123,
name: "New Name",
email: "[email protected]",
updated_at: new Date()
}
]);
์ง์ ํ์ง ์์ ํ๋๋ ๋ณ๊ฒฝ๋์ง ์์ต๋๋ค. NULL๋ก ์ค์ ํ๋ ค๋ฉด ๋ช
์์ ์ผ๋ก null์ ์ ๋ฌํ์ธ์.
ํธ๋์ญ์
save๋ ์๋์ผ๋ก ํธ๋์ญ์
๋ด์์ ์คํ๋ฉ๋๋ค.
// ์๋ ํธ๋์ญ์
return wdb.transaction(async (trx) => {
const ids = await trx.ubUpsert("users");
return ids;
});
@transactional๊ณผ ํจ๊ป
import { api, transactional } from "sonamu";
class UserFrame {
@api({ httpMethod: "POST" })
@transactional()
async createUserWithProfile(params: {
email: string;
name: string;
bio: string;
}) {
// User ์์ฑ
const [userId] = await UserModel.save([
{
email: params.email,
name: params.name
}
]);
// Profile ์์ฑ
await ProfileModel.save([
{
user_id: userId,
bio: params.bio
}
]);
// ๋ ๋ค ์ฑ๊ณตํ๊ฑฐ๋ ๋ ๋ค ์คํจ (์์์ฑ)
return { userId };
}
}
1. Zod ์๋ ๊ฒ์ฆ
SaveParams๋ Entity ์ ์์์ Zod ์คํค๋ง๋ก ์๋ ์์ฑ๋ฉ๋๋ค.
// ์๋์ผ๋ก ๊ฒ์ฆ๋จ
await UserModel.save([
{
email: "invalid-email", // โ ์ด๋ฉ์ผ ํ์ ๊ฒ์ฆ ์คํจ
name: "John"
}
]);
2. ์ปค์คํ
๊ฒ์ฆ
async createUser(params: UserSaveParams) {
// ์ด๋ฉ์ผ ์ค๋ณต ํ์ธ
const existing = await UserModel.findOne("A", {
email: params.email
});
if (existing) {
throw new BadRequestException("์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ด๋ฉ์ผ์
๋๋ค");
}
// ๋น๋ฐ๋ฒํธ ๊ฐ๋ ํ์ธ
if (params.password.length < 8) {
throw new BadRequestException("๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค");
}
return await UserModel.save([params]);
}
API์์ ์ฌ์ฉ
์๋ ์์ฑ๋ save API
// Model ํด๋์ค
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
async save(spa: UserSaveParams[]): Promise<number[]> {
// ์๋ ์์ฑ๋ ์ฝ๋
}
}
ํด๋ผ์ด์ธํธ ์ฝ๋
import { UserService } from "@/services/UserService";
// ์ฌ์ฉ์ ์์ฑ
const ids = await UserService.save([
{
email: "[email protected]",
name: "John"
}
]);
React (TanStack Query)
import { useMutation } from "@tanstack/react-query";
import { UserService } from "@/services/UserService";
function CreateUserForm() {
const createUser = useMutation({
mutationFn: (params: { email: string; name: string }) =>
UserService.save([params])
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createUser.mutate({
email: formData.get("email") as string,
name: formData.get("name") as string
});
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="name" required />
<button type="submit" disabled={createUser.isPending}>
{createUser.isPending ? "Creating..." : "Create User"}
</button>
{createUser.isSuccess && (
<p>User created with ID: {createUser.data[0]}</p>
)}
{createUser.isError && (
<p>Error: {createUser.error.message}</p>
)}
</form>
);
}
๊ณ ๊ธ ๊ธฐ๋ฅ
Unique ์ ์ฝ์กฐ๊ฑด ์ฒ๋ฆฌ
UpsertBuilder๋ Unique ์ ์ฝ์กฐ๊ฑด์ ์๋์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
// email์ด unique์ธ ๊ฒฝ์ฐ
await UserModel.save([
{ email: "[email protected]", name: "John" } // INSERT
]);
await UserModel.save([
{ email: "[email protected]", name: "John Smith" } // UPDATE (๋์ผ ์ด๋ฉ์ผ)
]);
JSON ์ปฌ๋ผ
await UserModel.save([
{
id: 1,
metadata: {
preferences: {
theme: "dark",
language: "ko"
},
tags: ["vip", "premium"]
}
}
]);
๋ฐฐ์ด ์ปฌ๋ผ (PostgreSQL)
await PostModel.save([
{
id: 1,
tags: ["typescript", "node", "database"]
}
]);
์ฑ๋ฅ ์ต์ ํ
๋ฐฐ์น ํฌ๊ธฐ ์ ํ
// ๋๋ ๋ฐ์ดํฐ ์ ์ฅ ์ ์ฒญํฌ ๋จ์๋ก ๋๋๊ธฐ
const users = [/* 10000๊ฐ */];
// 500๊ฐ์ฉ ๋๋ ์ ์ ์ฅ
for (let i = 0; i < users.length; i += 500) {
const chunk = users.slice(i, i + 500);
await UserModel.save(chunk);
}
ํธ๋์ญ์
์ฌ์ฌ์ฉ
@api({ httpMethod: "POST" })
@transactional()
async bulkOperation(data: LargeDataSet) {
// ๊ฐ์ ํธ๋์ญ์
๋ด์์ ์ฌ๋ฌ save ์คํ
await UserModel.save(data.users);
await PostModel.save(data.posts);
await CommentModel.save(data.comments);
// ๋ชจ๋ ์ฑ๊ณตํ๊ฑฐ๋ ๋ชจ๋ ์คํจ
}
์ฃผ์์ฌํญ
1. ๋ฐฐ์ด๋ก ์ ๋ฌ
save๋ ๋ฐ๋์ ๋ฐฐ์ด์ ๋ฐ์ต๋๋ค.
2. ๋ฐํ๊ฐ์ ID ๋ฐฐ์ด
// ๋จ์ผ ๋ ์ฝ๋
const [id] = await UserModel.save([{ ... }]);
// ์ฌ๋ฌ ๋ ์ฝ๋
const [id1, id2, id3] = await UserModel.save([{ ... }, { ... }, { ... }]);
3. ๋ถ๋ถ ์
๋ฐ์ดํธ ์ ์ฃผ์
// โ ์ด๋ฉ์ผ์ด null๋ก ๋ณ๊ฒฝ๋จ
await UserModel.save([
{
id: 1,
name: "New Name"
// email ํ๋ ์์ โ null๋ก ๋ณ๊ฒฝ๋์ง ์์ (๊ธฐ์กด ๊ฐ ์ ์ง)
}
]);
// โ
๋ช
์์ ์ผ๋ก null ์ค์
await UserModel.save([
{
id: 1,
email: null // ๋ช
์์ null
}
]);
4. ๊ด๊ณ ๋ฐ์ดํฐ ์์
๋ถ๋ชจ ๋ ์ฝ๋๋ฅผ ๋จผ์ ์ ์ฅํ ํ ์์ ๋ ์ฝ๋๋ฅผ ์ ์ฅํ์ธ์.
// โ
์ฌ๋ฐ๋ฅธ ์์
const [postId] = await PostModel.save([{ ... }]);
await CommentModel.save([{ post_id: postId, ... }]);
// โ ์๋ชป๋ ์์ (post_id๊ฐ ์์)
await CommentModel.save([{ post_id: undefined, ... }]);
const [postId] = await PostModel.save([{ ... }]);
๋ค์ ๋จ๊ณ