๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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;
    });
  }
}
๋‚ด๋ถ€ ๋™์ž‘:
  1. getPuri(โ€œwโ€): BaseModelClass ๋ฉ”์„œ๋“œ๋กœ ์“ฐ๊ธฐ์šฉ Puri ํš๋“
  2. ubRegister(): UpsertBuilder์— ๋ ˆ์ฝ”๋“œ ๋“ฑ๋ก
  3. transaction(): ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘
  4. 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
    };
  }
}

๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ

์ง€์ •ํ•œ ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// ์ด๋ฆ„๋งŒ ์—…๋ฐ์ดํŠธ
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๋Š” ๋ฐ˜๋“œ์‹œ ๋ฐฐ์—ด์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.
// โŒ ์ž˜๋ชป๋จ
await UserModel.save({ email: "[email protected]" });

// โœ… ์˜ฌ๋ฐ”๋ฆ„
await UserModel.save([{ email: "[email protected]" }]);

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([{ ... }]);

๋‹ค์Œ ๋‹จ๊ณ„