> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# save

> Save and update records

`save` is a method that saves or updates records. It automatically handles INSERT/UPDATE using UpsertBuilder and executes safely within a transaction.

<Info>
  `save` is not defined in BaseModelClass. It's a **standard pattern** automatically generated by
  the Syncer in each Model class when you create an Entity.
</Info>

## Type Signature

```typescript theme={null}
async save(
  saveParams: SaveParams[]
): Promise<number[]>
```

## Auto-Generated Code

Sonamu automatically generates the following code based on your Entity:

```typescript theme={null}
// src/application/user/user.model.ts (auto-generated)
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
  async save(spa: UserSaveParams[]): Promise<number[]> {
    const wdb = this.getPuri("w");

    // 1. Register records with UpsertBuilder
    spa.forEach((sp) => {
      wdb.ubRegister("users", sp);
    });

    // 2. Execute Upsert within transaction
    return wdb.transaction(async (trx) => {
      const ids = await trx.ubUpsert("users");
      return ids;
    });
  }
}
```

**How it works:**

1. **getPuri("w")**: Gets write Puri from BaseModelClass method
2. **ubRegister()**: Registers records with UpsertBuilder
3. **transaction()**: Starts transaction
4. **ubUpsert()**: Automatically handles INSERT/UPDATE and returns IDs

## Parameters

### saveParams

Array of record data to save.

**Type:** `SaveParams[]`

```typescript theme={null}
type UserSaveParams = {
  id?: number; // If present: UPDATE, if absent: INSERT
  email: string;
  name: string;
  status?: string;
  // ... other fields
};

// Save single record
await UserModel.save([{ email: "john@example.com", name: "John" }]);

// Save multiple records
await UserModel.save([
  { email: "john@example.com", name: "John" },
  { email: "jane@example.com", name: "Jane" },
]);
```

### Role of id

* **If id exists:** **UPDATE** the record with that ID
* **If id is absent:** **INSERT** a new record

```typescript theme={null}
// INSERT (no id)
await UserModel.save([{ email: "new@example.com", name: "New User" }]);

// UPDATE (with id)
await UserModel.save([{ id: 1, email: "updated@example.com", name: "Updated Name" }]);
```

## Return Value

**Type:** `Promise<number[]>`

Returns an array of IDs for the saved records.

```typescript theme={null}
// 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

// Multiple records
const ids = await UserModel.save([
  { email: "a@example.com", name: "A" },
  { email: "b@example.com", name: "B" },
]);
console.log("Created IDs:", ids); // [124, 125]
```

## Basic Usage

### Create New Record (INSERT)

```typescript theme={null}
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 Record (UPDATE)

```typescript theme={null}
async updateUser(userId: number, name: string) {
  const [id] = await UserModel.save([
    {
      id: userId,
      name,
      updated_at: new Date()
    }
  ]);

  return { id };
}
```

### Upsert (INSERT or UPDATE)

```typescript theme={null}
async upsertUser(userData: { id?: number; email: string; name: string }) {
  const [id] = await UserModel.save([userData]);

  return { id };
}
```

## UpsertBuilder How It Works

### 1. Register Records (ubRegister)

```typescript theme={null}
async save(spa: UserSaveParams[]): Promise<number[]> {
  const wdb = this.getPuri("w");

  // Register each record with UpsertBuilder
  spa.forEach((sp) => {
    wdb.ubRegister("users", sp);
  });

  // ...
}
```

### 2. Execute Transaction

```typescript theme={null}
// Execute upsert within transaction
return wdb.transaction(async (trx) => {
  const ids = await trx.ubUpsert("users");
  return ids;
});
```

### 3. Automatic INSERT/UPDATE Handling

UpsertBuilder automatically performs INSERT or UPDATE based on the presence of the `id` field:

```sql theme={null}
-- No id: INSERT
INSERT INTO users (email, name, status)
VALUES ('john@example.com', 'John', 'active')
RETURNING id;

-- With 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;
```

## Batch Save

You can save multiple records at once.

```typescript theme={null}
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 };
}
```

## Saving Relationship Data

### 1:N Relationship

```typescript theme={null}
// Save post + comments
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 Relationship

```typescript theme={null}
// Save user + assign roles
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
  },
]);
```

## Practical Examples

<Tabs>
  <Tab title="User Signup" icon="user-plus">
    ```typescript theme={null}
    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;
      }) {
        // Check email duplicate
        const existing = await UserModel.findOne("A", {
          email: params.email
        });

        if (existing) {
          throw new BadRequestException("Email already in use");
        }

        // Hash password
        const hashedPassword = await bcrypt.hash(params.password, 10);

        // Create user
        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
        };
      }
    }
    ```
  </Tab>

  <Tab title="Update Profile" icon="pen-to-square">
    ```typescript theme={null}
    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("Login required");
        }

        // Update profile
        const [id] = await UserModel.save([
          {
            id: user.id,
            ...params,
            updated_at: new Date()
          }
        ]);

        // Fetch updated user info
        const updated = await UserModel.findById("A", id);

        return updated;
      }
    }
    ```
  </Tab>

  <Tab title="Create Post" icon="file-pen">
    ```typescript theme={null}
    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();

        // Create post
        const [postId] = await PostModel.save([
          {
            user_id: user.id,
            title: params.title,
            content: params.content,
            status: "draft",
            created_at: new Date()
          }
        ]);

        // Save tags
        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
        };
      }
    }
    ```
  </Tab>

  <Tab title="Create Order" icon="cart-shopping">
    ```typescript theme={null}
    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();

        // Calculate total
        const total = params.items.reduce(
          (sum, item) => sum + (item.price * item.quantity),
          0
        );

        // Create order
        const [orderId] = await OrderModel.save([
          {
            user_id: user.id,
            total_amount: total,
            status: "pending",
            shipping_address: params.shipping_address,
            created_at: new Date()
          }
        ]);

        // Create order items
        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
        };
      }
    }
    ```
  </Tab>
</Tabs>

## Partial Update

You can update only specified fields.

```typescript theme={null}
// Update only name
await UserModel.save([
  {
    id: 123,
    name: "New Name",
    // Other fields remain unchanged
  },
]);

// Update multiple fields
await UserModel.save([
  {
    id: 123,
    name: "New Name",
    email: "new@example.com",
    updated_at: new Date(),
  },
]);
```

<Info>Fields not specified are not changed. To set a field to NULL, explicitly pass `null`.</Info>

## Transactions

`save` automatically executes within a transaction.

```typescript theme={null}
// Automatic transaction
return wdb.transaction(async (trx) => {
  const ids = await trx.ubUpsert("users");
  return ids;
});
```

### With @transactional

```typescript theme={null}
import { api, transactional } from "sonamu";

class UserFrame {
  @api({ httpMethod: "POST" })
  @transactional()
  async createUserWithProfile(params: { email: string; name: string; bio: string }) {
    // Create user
    const [userId] = await UserModel.save([
      {
        email: params.email,
        name: params.name,
      },
    ]);

    // Create profile
    await ProfileModel.save([
      {
        user_id: userId,
        bio: params.bio,
      },
    ]);

    // Both succeed or both fail (atomicity)
    return { userId };
  }
}
```

## Validation

### 1. Automatic Zod Validation

SaveParams are automatically generated as Zod schemas from Entity definitions.

```typescript theme={null}
// Automatically validated
await UserModel.save([
  {
    email: "invalid-email", // ❌ Email format validation fails
    name: "John",
  },
]);
```

### 2. Custom Validation

```typescript theme={null}
async createUser(params: UserSaveParams) {
  // Check email duplicate
  const existing = await UserModel.findOne("A", {
    email: params.email
  });

  if (existing) {
    throw new BadRequestException("Email already in use");
  }

  // Check password strength
  if (params.password.length < 8) {
    throw new BadRequestException("Password must be at least 8 characters");
  }

  return await UserModel.save([params]);
}
```

## API Usage

### Auto-Generated save API

```typescript theme={null}
// Model class
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
  async save(spa: UserSaveParams[]): Promise<number[]> {
    // Auto-generated code
  }
}
```

### Client Code

```typescript theme={null}
import { UserService } from "@/services/UserService";

// Create user
const ids = await UserService.save([
  {
    email: "john@example.com",
    name: "John",
  },
]);
```

### React (TanStack Query)

```typescript theme={null}
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>
  );
}
```

## Advanced Features

### Unique Constraint Handling

UpsertBuilder automatically handles Unique constraints.

```typescript theme={null}
// If email is unique
await UserModel.save([
  { email: "john@example.com", name: "John" }, // INSERT
]);

await UserModel.save([
  { email: "john@example.com", name: "John Smith" }, // UPDATE (same email)
]);
```

### JSON Columns

```typescript theme={null}
await UserModel.save([
  {
    id: 1,
    metadata: {
      preferences: {
        theme: "dark",
        language: "ko",
      },
      tags: ["vip", "premium"],
    },
  },
]);
```

### Array Columns (PostgreSQL)

```typescript theme={null}
await PostModel.save([
  {
    id: 1,
    tags: ["typescript", "node", "database"],
  },
]);
```

## Performance Optimization

### Batch Size Limit

```typescript theme={null}
// Split large data saves into chunks
const users = [
  /* 10000 items */
];

// Save in chunks of 500
for (let i = 0; i < users.length; i += 500) {
  const chunk = users.slice(i, i + 500);
  await UserModel.save(chunk);
}
```

### Transaction Reuse

```typescript theme={null}
@api({ httpMethod: "POST" })
@transactional()
async bulkOperation(data: LargeDataSet) {
  // Execute multiple saves in same transaction
  await UserModel.save(data.users);
  await PostModel.save(data.posts);
  await CommentModel.save(data.comments);

  // All succeed or all fail
}
```

## Cautions

### 1. Pass as Array

`save` **must receive an array**.

```typescript theme={null}
// ❌ Wrong
await UserModel.save({ email: "john@example.com" });

// ✅ Correct
await UserModel.save([{ email: "john@example.com" }]);
```

### 2. Return Value is ID Array

```typescript theme={null}
// Single record
const [id] = await UserModel.save([{ ... }]);

// Multiple records
const [id1, id2, id3] = await UserModel.save([{ ... }, { ... }, { ... }]);
```

### 3. Caution with Partial Updates

```typescript theme={null}
// ❌ Email NOT changed to null
await UserModel.save([
  {
    id: 1,
    name: "New Name",
    // No email field → keeps existing value, does NOT change to null
  },
]);

// ✅ Explicit null
await UserModel.save([
  {
    id: 1,
    email: null, // Explicit null
  },
]);
```

### 4. Relationship Data Order

Save parent records before child records.

```typescript theme={null}
// ✅ Correct order
const [postId] = await PostModel.save([{ ... }]);
await CommentModel.save([{ post_id: postId, ... }]);

// ❌ Wrong order (no post_id)
await CommentModel.save([{ post_id: undefined, ... }]);
const [postId] = await PostModel.save([{ ... }]);
```

## Next Steps

<CardGroup cols={2}>
  <Card title="findById" icon="magnifying-glass" href="/en/api-reference/base-model/find-by-id">
    Query saved records
  </Card>

  <Card title="del" icon="trash" href="/en/api-reference/base-model/del">
    Delete records
  </Card>

  <Card title="UpsertBuilder" icon="layer-group" href="/en/database/upsert-builder/basic-usage">
    UpsertBuilder detailed guide
  </Card>

  <Card title="@transactional" icon="arrows-rotate" href="/en/api-reference/decorators/transactional">
    Using transactions
  </Card>
</CardGroup>
