메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
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: "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
    };
  }
}

λΆ€λΆ„ μ—…λ°μ΄νŠΈ

μ§€μ •ν•œ ν•„λ“œλ§Œ μ—…λ°μ΄νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.
// μ΄λ¦„λ§Œ μ—…λ°μ΄νŠΈ
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([{ ... }]);

λ‹€μŒ 단계