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: "john@example.com", name: "John" }
]);
// μ¬λ¬ λ μ½λ μ μ₯
await UserModel.save([
{ email: "john@example.com", name: "John" },
{ email: "jane@example.com", name: "Jane" }
]);
idμ μν
- idκ° μμΌλ©΄: ν΄λΉ IDμ λ μ½λλ₯Ό UPDATE
- idκ° μμΌλ©΄: μ λ μ½λλ₯Ό INSERT
// INSERT (id μμ)
await UserModel.save([
{ email: "new@example.com", name: "New User" }
]);
// UPDATE (id μμ)
await UserModel.save([
{ id: 1, email: "updated@example.com", name: "Updated Name" }
]);
λ°νκ°
νμ
: Promise<number[]>
μ μ₯λ λ μ½λμ ID λ°°μ΄μ λ°νν©λλ€.
// INSERT
const [id] = await UserModel.save([
{ email: "john@example.com", 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: "a@example.com", name: "A" },
{ email: "b@example.com", 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 ('john@example.com', 'John', 'active')
RETURNING id;
-- id μμ: UPDATE
INSERT INTO users (id, email, name)
VALUES (123, 'john@example.com', '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: "admin@example.com",
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: "new@example.com",
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: "john@example.com",
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: "john@example.com", name: "John" } // INSERT
]);
await UserModel.save([
{ email: "john@example.com", 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λ λ°λμ λ°°μ΄μ λ°μ΅λλ€.
// β μλͺ»λ¨
await UserModel.save({ email: "john@example.com" });
// β
μ¬λ°λ¦
await UserModel.save([{ email: "john@example.com" }]);
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([{ ... }]);
λ€μ λ¨κ³