MSSQL #
Microsoft SQL Server adalah database enterprise yang sangat umum di lingkungan korporat dan aplikasi berbasis stack Microsoft. Mengintegrasikannya dengan TypeScript menggunakan library mssql memberi kamu akses ke fitur-fitur SQL Server seperti stored procedure, table-valued parameters, dan output parameters — semuanya dengan type safety yang solid. Ada perbedaan penting dalam cara mssql bekerja dibanding mysql2: parameter tidak menggunakan ? melainkan named parameter dengan prefix @, dan tipe data kolom harus dideklarasikan secara eksplisit saat menggunakan Request. Memahami perbedaan ini sejak awal akan menghindarkan kamu dari banyak kebingungan saat pertama kali bermigrasi dari MySQL atau PostgreSQL ke SQL Server.
Instalasi dan Setup #
Library utama yang digunakan adalah mssql beserta type definitions-nya yang sudah disertakan dalam paket yang sama.
# Instalasi paket
npm install mssql
# Type definitions sudah termasuk dalam mssql
# Tidak perlu @types/mssql terpisah
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
Struktur project yang direkomendasikan:
src/
├── db/
│ ├── connection.ts -- konfigurasi pool dan singleton
│ └── types.ts -- tipe SQL Server custom
├── models/
│ ├── user.model.ts
│ └── produk.model.ts
├── repositories/
│ ├── base.repository.ts
│ └── user.repository.ts
└── index.ts
Konfigurasi Koneksi dan Connection Pool #
mssql mengelola koneksi melalui ConnectionPool. Berbeda dengan mysql2, pool di mssql perlu di-connect secara eksplisit sebelum bisa digunakan.
import sql, { config as SqlConfig, ConnectionPool } from 'mssql';
// Konfigurasi koneksi
const dbConfig: SqlConfig = {
server: process.env.DB_HOST ?? 'localhost',
port: Number(process.env.DB_PORT ?? 1433),
database: process.env.DB_NAME ?? 'toko_online',
user: process.env.DB_USER ?? 'sa',
password: process.env.DB_PASSWORD ?? '',
options: {
encrypt: true, // wajib untuk Azure SQL
trustServerCertificate: true, // untuk development lokal
enableArithAbort: true, // direkomendasikan SQL Server 2017+
},
pool: {
max: 10, // maksimum koneksi bersamaan
min: 0, // minimum koneksi idle
idleTimeoutMillis: 30000, // tutup koneksi idle setelah 30 detik
},
requestTimeout: 30000, // timeout per query dalam ms
connectionTimeout: 15000, // timeout saat membuka koneksi
};
// Singleton pool
let poolInstance: ConnectionPool | null = null;
export async function getPool(): Promise<ConnectionPool> {
if (!poolInstance) {
poolInstance = new sql.ConnectionPool(dbConfig);
await poolInstance.connect();
// Handle error pada pool
poolInstance.on('error', (err) => {
console.error('SQL Pool error:', err);
poolInstance = null; // reset agar koneksi bisa dibuat ulang
});
}
return poolInstance;
}
export async function closePool(): Promise<void> {
if (poolInstance) {
await poolInstance.close();
poolInstance = null;
}
}
// Graceful shutdown
process.on('SIGTERM', closePool);
process.on('SIGINT', closePool);
flowchart TD
A[Aplikasi start] --> B[new ConnectionPool\ncreate config]
B --> C[pool.connect]
C --> D{Koneksi\nberhasil?}
D -- Ya --> E[Pool siap\nterima request]
D -- Tidak --> F[Throw error\nkoneksi gagal]
E --> G[Request masuk]
G --> H[pool.request]
H --> I[Eksekusi query]
I --> J[Koneksi kembali\nke pool]
J --> Emssqlmemerlukan pemanggilanpool.connect()secara eksplisit sebelum pool bisa digunakan. Jika kamu langsung memanggilpool.request()tanpaconnect(), kamu akan mendapat error"Connection not yet open". Pola singleton yang memanggilconnect()saat inisialisasi pertama kali adalah cara paling aman untuk menghindari masalah ini.
Mendefinisikan Tipe untuk Row Database #
Berbeda dari mysql2 yang menggunakan RowDataPacket, tipe result di mssql lebih sederhana — kamu cukup mendefinisikan interface biasa dan menggunakannya sebagai type parameter.
import sql from 'mssql';
// Tipe untuk tabel users
interface User {
id: number;
nama: string;
email: string;
passwordHash: string;
role: 'admin' | 'user' | 'moderator';
aktif: boolean;
dibuatPada: Date;
diperbarui_pada: Date | null;
}
interface Produk {
id: number;
nama: string;
deskripsi: string | null;
harga: number; // DECIMAL di SQL Server — hati-hati presisi
stok: number;
kategoriId: number;
gambarUrl: string | null;
dibuatPada: Date;
}
// Pemetaan tipe TypeScript ke tipe SQL Server
// Penting untuk parameter input
const SQL_TYPES = {
INT: sql.Int,
BIGINT: sql.BigInt,
VARCHAR: (n: number) => sql.VarChar(n),
NVARCHAR: (n: number) => sql.NVarChar(n),
BIT: sql.Bit, // boolean
DECIMAL: (p: number, s: number) => sql.Decimal(p, s),
FLOAT: sql.Float,
DATETIME2: sql.DateTime2,
UNIQUEIDENTIFIER: sql.UniqueIdentifier,
} as const;
Tipe data SQL Server memiliki padanan TypeScript yang perlu kamu perhatikan:
| SQL Server Type | TypeScript Type | Catatan |
|---|---|---|
INT, SMALLINT | number | Integer 32-bit |
BIGINT | string | number | Bisa overflow JS number |
VARCHAR(n), NVARCHAR(n) | string | N prefix = Unicode |
BIT | boolean | 0/1 otomatis dikonversi |
DECIMAL(p,s), NUMERIC | number | Hati-hati presisi floating point |
DATETIME2, DATETIME | Date | Konversi otomatis oleh driver |
UNIQUEIDENTIFIER | string | GUID dalam format string |
VARBINARY | Buffer | Data biner |
Query Dasar — SELECT #
Cara kerja parameterized query di mssql berbeda signifikan dari MySQL. Parameter menggunakan named syntax @namaParam dan harus dideklarasikan dengan tipe data SQL yang eksplisit.
import { getPool } from './db/connection';
import sql from 'mssql';
// ── SELECT sederhana tanpa parameter
async function semuaUser(): Promise<User[]> {
const pool = await getPool();
const result = await pool.request()
.query<User>('SELECT id, nama, email, role FROM users WHERE aktif = 1');
return result.recordset; // array of rows
}
// ── SELECT dengan parameter — WAJIB deklarasikan tipe!
async function cariUserById(id: number): Promise<User | null> {
const pool = await getPool();
const result = await pool.request()
.input('id', sql.Int, id) // nama, tipe SQL, nilai
.query<User>('SELECT * FROM users WHERE id = @id AND aktif = 1');
return result.recordset[0] ?? null;
}
// ── SELECT dengan multiple parameter
async function produkByKategoriDanHarga(
kategoriId: number,
hargaMin: number,
hargaMax: number
): Promise<Produk[]> {
const pool = await getPool();
const result = await pool.request()
.input('kategoriId', sql.Int, kategoriId)
.input('hargaMin', sql.Decimal(18, 2), hargaMin)
.input('hargaMax', sql.Decimal(18, 2), hargaMax)
.query<Produk>(`
SELECT *
FROM produk
WHERE kategori_id = @kategoriId
AND harga BETWEEN @hargaMin AND @hargaMax
AND stok > 0
ORDER BY harga ASC
`);
return result.recordset;
}
// ── SELECT dengan LIKE
async function cariProdukByNama(keyword: string): Promise<Produk[]> {
const pool = await getPool();
// Escape karakter khusus LIKE di SQL Server: %, _, [, ^
const safeKeyword = keyword.replace(/[%_\[\^]/g, '[$&]');
const result = await pool.request()
.input('keyword', sql.NVarChar(200), `%${safeKeyword}%`)
.query<Produk>(`
SELECT TOP 20 * FROM produk
WHERE nama LIKE @keyword
ORDER BY nama
`);
return result.recordset;
}
// ANTI-PATTERN: string concatenation langsung
async function cariUserTidakAman(email: string): Promise<User[]> {
const pool = await getPool();
// Rentan SQL injection!
const result = await pool.request()
.query<User>(`SELECT * FROM users WHERE email = '${email}'`);
return result.recordset;
}
// BENAR: selalu deklarasikan parameter dengan .input()
async function cariUserAman(email: string): Promise<User | null> {
const pool = await getPool();
const result = await pool.request()
.input('email', sql.NVarChar(255), email)
.query<User>('SELECT * FROM users WHERE email = @email');
return result.recordset[0] ?? null;
}
Operasi INSERT #
SQL Server menyediakan klausa OUTPUT yang sangat berguna untuk mendapatkan kembali nilai kolom dari baris yang baru disisipkan — termasuk ID auto-increment dan kolom dengan default value.
import sql from 'mssql';
import { getPool } from './db/connection';
interface InputUser {
nama: string;
email: string;
passwordHash: string;
role?: 'admin' | 'user' | 'moderator';
}
// ── INSERT dengan OUTPUT untuk mendapat ID
async function buatUser(input: InputUser): Promise<number> {
const pool = await getPool();
const result = await pool.request()
.input('nama', sql.NVarChar(100), input.nama)
.input('email', sql.NVarChar(255), input.email)
.input('passwordHash', sql.VarChar(255), input.passwordHash)
.input('role', sql.VarChar(20), input.role ?? 'user')
.query<{ id: number }>(`
INSERT INTO users (nama, email, password_hash, role, aktif, dibuat_pada)
OUTPUT INSERTED.id
VALUES (@nama, @email, @passwordHash, @role, 1, GETDATE())
`);
return result.recordset[0].id;
}
// ── INSERT dan kembalikan seluruh baris yang baru dibuat
async function buatProduk(
input: Omit<Produk, 'id' | 'dibuatPada'>
): Promise<Produk> {
const pool = await getPool();
const result = await pool.request()
.input('nama', sql.NVarChar(200), input.nama)
.input('deskripsi', sql.NVarChar(sql.MAX), input.deskripsi)
.input('harga', sql.Decimal(18, 2), input.harga)
.input('stok', sql.Int, input.stok)
.input('kategoriId', sql.Int, input.kategoriId)
.query<Produk>(`
INSERT INTO produk (nama, deskripsi, harga, stok, kategori_id, dibuat_pada)
OUTPUT INSERTED.*
VALUES (@nama, @deskripsi, @harga, @stok, @kategoriId, GETDATE())
`);
return result.recordset[0];
}
// ── INSERT batch dengan Table-Valued Parameter (TVP)
// TVP adalah fitur SQL Server untuk memasukkan banyak baris sekaligus secara efisien
async function buatBanyakProduk(
produkList: Array<{ nama: string; harga: number; stok: number; kategoriId: number }>
): Promise<number> {
const pool = await getPool();
// Buat TVP table
const tvp = new sql.Table();
tvp.columns.add('nama', sql.NVarChar(200), { nullable: false });
tvp.columns.add('harga', sql.Decimal(18, 2), { nullable: false });
tvp.columns.add('stok', sql.Int, { nullable: false });
tvp.columns.add('kategori_id', sql.Int, { nullable: false });
for (const p of produkList) {
tvp.rows.add(p.nama, p.harga, p.stok, p.kategoriId);
}
const result = await pool.request()
.input('produkList', tvp)
.execute('sp_InsertBanyakProduk'); // stored procedure yang menerima TVP
return result.rowsAffected[0];
}
sequenceDiagram
participant App
participant Pool
participant SqlServer
App->>Pool: request().input('nama', type, val)
Note over Pool: Parameter ditambahkan ke request
App->>Pool: .query(INSERT...OUTPUT INSERTED.*)
Pool->>SqlServer: Kirim query dengan parameter
SqlServer-->>Pool: recordset dengan baris yang diinsert
Pool-->>App: result.recordset[0]
Note over App: Dapat seluruh data baris baruOperasi UPDATE dan DELETE #
UPDATE di SQL Server juga bisa memanfaatkan OUTPUT untuk mendapatkan nilai sebelum dan sesudah perubahan — berguna untuk audit log.
import sql from 'mssql';
import { getPool } from './db/connection';
// ── UPDATE dengan OUTPUT — tahu nilai sebelum dan sesudah
async function updateStokProduk(
id: number,
deltaStok: number
): Promise<{ stokLama: number; stokBaru: number } | null> {
const pool = await getPool();
const result = await pool.request()
.input('id', sql.Int, id)
.input('delta', sql.Int, deltaStok)
.query<{ stokLama: number; stokBaru: number }>(`
UPDATE produk
SET stok = stok + @delta
OUTPUT DELETED.stok AS stokLama,
INSERTED.stok AS stokBaru
WHERE id = @id AND (stok + @delta) >= 0
`);
return result.recordset[0] ?? null; // null jika stok tidak cukup
}
// ── UPDATE field tertentu
async function updateUser(
id: number,
data: Partial<Pick<User, 'nama' | 'email'>>
): Promise<boolean> {
const entries = Object.entries(data).filter(([, v]) => v !== undefined);
if (entries.length === 0) return false;
// Whitelist field yang boleh diupdate
const allowedFields: Record<string, [string, unknown]> = {
nama: ['nama', sql.NVarChar(100)],
email: ['email', sql.NVarChar(255)],
};
const pool = await getPool();
const request = pool.request().input('id', sql.Int, id);
const setClauses: string[] = [];
for (const [key, value] of entries) {
if (!allowedFields[key]) continue;
const [sqlType] = allowedFields[key];
request.input(`p_${key}`, sqlType as sql.ISqlType, value);
setClauses.push(`${key} = @p_${key}`);
}
if (setClauses.length === 0) return false;
const result = await request.query(`
UPDATE users
SET ${setClauses.join(', ')}, diperbarui_pada = GETDATE()
WHERE id = @id
`);
return result.rowsAffected[0] > 0;
}
// ── DELETE — hard delete
async function hapusProduk(id: number): Promise<boolean> {
const pool = await getPool();
const result = await pool.request()
.input('id', sql.Int, id)
.query('DELETE FROM produk WHERE id = @id');
return result.rowsAffected[0] > 0;
}
// ── Soft delete
async function softDeleteUser(id: number): Promise<boolean> {
const pool = await getPool();
const result = await pool.request()
.input('id', sql.Int, id)
.query(`
UPDATE users
SET aktif = 0, dihapus_pada = GETDATE()
WHERE id = @id AND aktif = 1
`);
return result.rowsAffected[0] > 0;
}
Transaction #
Transaction di mssql menggunakan objek Transaction yang harus di-commit atau di-rollback secara eksplisit. Setiap Request dalam transaksi harus dibuat dari objek Transaction, bukan langsung dari pool.
import sql, { Transaction, Request } from 'mssql';
import { getPool } from './db/connection';
interface ItemPesanan {
produkId: number;
jumlah: number;
hargaSatuan: number;
}
async function buatPesanan(
userId: number,
items: ItemPesanan[]
): Promise<number> {
const pool = await getPool();
const transaction = new sql.Transaction(pool);
try {
await transaction.begin();
const totalHarga = items.reduce(
(sum, item) => sum + item.hargaSatuan * item.jumlah, 0
);
// 1. Buat record pesanan
const r1 = await new sql.Request(transaction)
.input('userId', sql.Int, userId)
.input('total', sql.Decimal(18, 2), totalHarga)
.query<{ id: number }>(`
INSERT INTO pesanan (user_id, total_harga, status, dibuat_pada)
OUTPUT INSERTED.id
VALUES (@userId, @total, 'pending', GETDATE())
`);
const pesananId = r1.recordset[0].id;
// 2. Proses setiap item
for (const item of items) {
// Sisipkan item pesanan
await new sql.Request(transaction)
.input('pesananId', sql.Int, pesananId)
.input('produkId', sql.Int, item.produkId)
.input('jumlah', sql.Int, item.jumlah)
.input('harga', sql.Decimal(18, 2), item.hargaSatuan)
.query(`
INSERT INTO pesanan_item
(pesanan_id, produk_id, jumlah, harga_satuan)
VALUES (@pesananId, @produkId, @jumlah, @harga)
`);
// Kurangi stok — UPDATE gagal jika stok tidak cukup
const rStok = await new sql.Request(transaction)
.input('jumlah', sql.Int, item.jumlah)
.input('produkId', sql.Int, item.produkId)
.query(`
UPDATE produk
SET stok = stok - @jumlah
WHERE id = @produkId AND stok >= @jumlah
`);
if (rStok.rowsAffected[0] === 0) {
await transaction.rollback();
throw new Error(`Stok tidak cukup untuk produk ID ${item.produkId}`);
}
}
await transaction.commit();
return pesananId;
} catch (error) {
// Rollback jika belum di-rollback (misalnya error tak terduga)
try {
await transaction.rollback();
} catch {
// Abaikan error rollback jika transaksi sudah di-rollback
}
throw error;
}
}
Helper untuk menyederhanakan pola transaction:
async function withTransaction<T>(
callback: (transaction: Transaction) => Promise<T>
): Promise<T> {
const pool = await getPool();
const transaction = new sql.Transaction(pool);
await transaction.begin();
try {
const result = await callback(transaction);
await transaction.commit();
return result;
} catch (error) {
try { await transaction.rollback(); } catch { /* sudah rollback */ }
throw error;
}
}
// Penggunaan
const pesananId = await withTransaction(async (trx) => {
const r1 = await new sql.Request(trx)
.input('userId', sql.Int, userId)
.query<{ id: number }>(`
INSERT INTO pesanan (user_id) OUTPUT INSERTED.id VALUES (@userId)
`);
return r1.recordset[0].id;
});
flowchart TD
A[new sql.Transaction pool] --> B[transaction.begin]
B --> C[new sql.Request transaction]
C --> D[Eksekusi query]
D --> E{Error?}
E -- Tidak --> F{Masih ada\noperasi?}
F -- Ya --> C
F -- Tidak --> G[transaction.commit]
E -- Ya --> H[transaction.rollback]
G --> I[Selesai sukses]
H --> J[Re-throw error]Stored Procedure #
SQL Server banyak menggunakan stored procedure. mssql mendukung pemanggilan SP dengan parameter input, output, dan return value.
import sql from 'mssql';
import { getPool } from './db/connection';
// ── Memanggil stored procedure sederhana
async function spCariUser(email: string): Promise<User | null> {
const pool = await getPool();
const result = await pool.request()
.input('email', sql.NVarChar(255), email)
.execute<User>('sp_CariUserByEmail');
return result.recordset[0] ?? null;
}
// ── SP dengan OUTPUT parameter
async function spDaftarUser(
input: InputUser
): Promise<{ userId: number; pesan: string }> {
const pool = await getPool();
const result = await pool.request()
.input('nama', sql.NVarChar(100), input.nama)
.input('email', sql.NVarChar(255), input.email)
.input('passwordHash', sql.VarChar(255), input.passwordHash)
.output('userId', sql.Int) // deklarasikan OUTPUT
.output('pesan', sql.NVarChar(500))
.execute('sp_DaftarUser');
return {
userId: result.output.userId as number,
pesan: result.output.pesan as string,
};
}
// ── SP dengan return value (RETURN statement di SP)
async function spCekStok(produkId: number): Promise<number> {
const pool = await getPool();
const result = await pool.request()
.input('produkId', sql.Int, produkId)
.execute('sp_CekStok');
// Return value ada di result.returnValue
return result.returnValue as number;
}
Error Handling #
SQL Server memiliki kode error yang berbeda dari MySQL. Penting untuk memetakan error spesifik ke domain error yang bermakna.
import sql from 'mssql';
// Kode error SQL Server yang sering ditemui
const MSSQL_ERRORS = {
UNIQUE_VIOLATION: 2627, // UNIQUE constraint violation
PK_VIOLATION: 2627, // PRIMARY KEY violation (sama dengan unique)
FK_VIOLATION: 547, // FOREIGN KEY constraint violation
NULL_VIOLATION: 515, // Cannot insert NULL
VALUE_TOO_LONG: 8152, // String or binary data would be truncated
DEADLOCK: 1205, // Transaction deadlock
TIMEOUT: -2, // Query timeout
LOGIN_FAILED: 18456, // Login failed
OBJECT_NOT_FOUND: 208, // Invalid object name (tabel tidak ada)
} as const;
class DatabaseError extends Error {
constructor(
message: string,
public readonly code?: number
) {
super(message);
this.name = 'DatabaseError';
}
}
class DuplicateEntryError extends DatabaseError {
constructor(detail?: string) {
super(`Duplikat data${detail ? `: ${detail}` : ''}`);
this.name = 'DuplicateEntryError';
}
}
class ForeignKeyError extends DatabaseError {
constructor(message = 'Referensi data tidak valid') {
super(message);
this.name = 'ForeignKeyError';
}
}
class DeadlockError extends DatabaseError {
constructor() {
super('Deadlock terdeteksi — coba ulangi operasi');
this.name = 'DeadlockError';
}
}
function tanganiMssqlError(error: unknown): never {
if (error instanceof sql.RequestError) {
switch (error.number) {
case MSSQL_ERRORS.UNIQUE_VIOLATION:
case MSSQL_ERRORS.PK_VIOLATION:
throw new DuplicateEntryError(error.message);
case MSSQL_ERRORS.FK_VIOLATION:
throw new ForeignKeyError();
case MSSQL_ERRORS.NULL_VIOLATION:
throw new DatabaseError('Field wajib tidak boleh kosong', error.number);
case MSSQL_ERRORS.VALUE_TOO_LONG:
throw new DatabaseError('Data melebihi panjang maksimum kolom', error.number);
case MSSQL_ERRORS.DEADLOCK:
throw new DeadlockError();
}
}
throw new DatabaseError(`Error database: ${String(error)}`);
}
// Decorator retry untuk deadlock — deadlock bisa coba ulang
async function withDeadlockRetry<T>(
fn: () => Promise<T>,
maxRetry = 3,
delayMs = 100
): Promise<T> {
for (let attempt = 1; attempt <= maxRetry; attempt++) {
try {
return await fn();
} catch (error) {
if (error instanceof DeadlockError && attempt < maxRetry) {
await new Promise(res => setTimeout(res, delayMs * attempt));
continue;
}
throw error;
}
}
throw new Error('Unreachable');
}
// Penggunaan dalam repository
async function buatUserAman(input: InputUser): Promise<number> {
try {
return await buatUser(input);
} catch (error) {
tanganiMssqlError(error);
}
}
Paginasi dengan SQL Server #
SQL Server menggunakan sintaks OFFSET ... FETCH NEXT yang tersedia sejak SQL Server 2012. Untuk versi lama, gunakan ROW_NUMBER().
import sql from 'mssql';
import { getPool } from './db/connection';
type UserSortField = 'nama' | 'email' | 'dibuat_pada';
type SortOrder = 'ASC' | 'DESC';
const ALLOWED_SORT: Set<string> = new Set<UserSortField>([
'nama', 'email', 'dibuat_pada'
]);
interface PaginasiResult<T> {
data: T[];
total: number;
halaman: number;
perHalaman: number;
totalHalaman: number;
}
async function getUserPaginasi(
halaman: number,
perHalaman: number,
sortBy: UserSortField = 'dibuat_pada',
sortOrder: SortOrder = 'DESC'
): Promise<PaginasiResult<User>> {
// Validasi whitelist — WAJIB untuk kolom yang diinterpolasi
const safeSort = ALLOWED_SORT.has(sortBy) ? sortBy : 'dibuat_pada';
const safeOrder: SortOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC';
const offset = (halaman - 1) * perHalaman;
const pool = await getPool();
const result = await pool.request()
.input('offset', sql.Int, offset)
.input('fetch', sql.Int, perHalaman)
.query<User & { totalRows: number }>(`
SELECT
id, nama, email, role, aktif, dibuat_pada,
COUNT(*) OVER() AS totalRows
FROM users
WHERE aktif = 1
ORDER BY ${safeSort} ${safeOrder}
OFFSET @offset ROWS
FETCH NEXT @fetch ROWS ONLY
`);
const total = result.recordset[0]?.totalRows ?? 0;
return {
data: result.recordset.map(({ totalRows: _, ...user }) => user as User),
total,
halaman,
perHalaman,
totalHalaman: Math.ceil(total / perHalaman),
};
}
ORDER BYwajib ada saat menggunakanOFFSET ... FETCH NEXTdi SQL Server. Query tanpaORDER BYakan menghasilkan error"ORDER BY clause is required". Ini berbeda dari MySQL yang tidak memerlukan ORDER BY untuk LIMIT/OFFSET.
Kapan Beralih ke Pendekatan Lain #
Tetap gunakan mssql langsung jika:
✓ Aplikasi enterprise yang banyak menggunakan stored procedure
✓ Query kompleks yang butuh kontrol penuh atas T-SQL
✓ Perlu fitur SQL Server spesifik: TVP, OUTPUT, MERGE
✓ Tim sudah familiar dengan T-SQL dan tidak butuh abstraksi ORM
✓ Performa kritis dengan query yang sudah dioptimasi DBA
Pertimbangkan ORM / Query Builder jika:
✗ Banyak model dengan relasi kompleks — TypeORM dengan SQL Server driver
✗ Multi-database (MSSQL + PostgreSQL) — TypeORM atau Knex.js
✗ Rapid prototyping — Prisma (mendukung SQL Server sejak versi 2.10)
✗ Tim kurang familiar T-SQL — ORM membantu tapi bisa menyembunyikan masalah
✗ Butuh migrasi otomatis — TypeORM migrations atau Prisma migrate
| Aspek | mssql | TypeORM | Prisma |
|---|---|---|---|
| Kontrol SQL | Penuh | Sebagian | Terbatas |
| Type Safety | Manual | Dekorator | Otomatis dari schema |
| Stored Procedure | ✓ Native | ✓ | Terbatas |
| OUTPUT clause | ✓ | Tidak langsung | Tidak |
| Migrasi | Manual | Otomatis | Otomatis |
| Learning curve | Rendah | Sedang | Rendah |
Ringkasan #
pool.connect()wajib dipanggil sebelum request — berbeda darimysql2, poolmssqlperlu dikoneksikan secara eksplisit; gunakan pola singleton denganconnect()di inisialisasi pertama.- Parameter menggunakan
@namaParam, bukan?— deklarasikan setiap parameter dengan.input('nama', sql.TipeSQL, nilai)sebelum memanggil.query()atau.execute().- Tipe SQL harus dideklarasikan eksplisit —
sql.Int,sql.NVarChar(255),sql.Decimal(18,2); tanpa ini driver bisa salah memetakan tipe dan menyebabkan truncation atau error tipe data.- Gunakan
OUTPUT INSERTED.*untuk mendapatkan data baris yang baru diinsert — lebih idiomatis di SQL Server dibandingSELECT SCOPE_IDENTITY()terpisah.OUTPUT DELETED.*danINSERTED.*tersedia di UPDATE untuk audit log — fitur unik SQL Server yang tidak ada di MySQL.new sql.Request(transaction)untuk query dalam transaksi — request tidak dibuat dari pool langsung; selalu buat dari objekTransaction.ORDER BYwajib untukOFFSET...FETCH NEXT— SQL Server error jika tidak ada ORDER BY, berbeda dari MySQL yang toleran.- Validasi whitelist untuk nama kolom dinamis — nama kolom tidak bisa diparameterkan, wajib divalidasi dari Set sebelum diinterpolasi ke query.
- Retry untuk deadlock — deadlock
(error.number === 1205)adalah kondisi yang bisa di-recover; implementasikan logika retry dengan backoff untuk meningkatkan ketahanan aplikasi.sql.NVarCharuntuk teks Unicode — gunakanNVarChar(bukanVarChar) untuk string yang mungkin mengandung karakter non-ASCII seperti nama dalam bahasa Arab atau aksara lain.