MSSQL

MSSQL #

Microsoft SQL Server (MSSQL) adalah database enterprise yang dominan di ekosistem Microsoft dan banyak perusahaan besar. Mengaksesnya dari Rust memerlukan perhatian khusus pada beberapa hal yang berbeda dari MySQL atau PostgreSQL: sintaks T-SQL yang berbeda (parameter binding @P1 bukan ?), tipe data yang unik (NVARCHAR, UNIQUEIDENTIFIER, DATETIME2), cara mendapat ID setelah INSERT (OUTPUT INSERTED.id), dan pilihan driver yang lebih sedikit. Ada dua pilihan utama: sqlx dengan fitur MSSQL, atau crate tiberius yang merupakan driver native Rust untuk MSSQL. Artikel ini membahas keduanya dengan penekanan pada perbedaan penting dari MySQL.

Pilihan Driver #

flowchart TD
    Q{Kebutuhan?}
    Q --> S["sqlx + mssql feature\nAPI konsisten dengan MySQL/PostgreSQL\nCompile-time query check\nRekomendasi untuk proyek baru"]
    Q --> T["tiberius\nDriver native Rust\nKontrol lebih rendah\nBerguna jika sqlx tidak support fitur tertentu"]
    Q --> O["tiberius via connection pool\n(bb8 atau deadpool)"]
Aspeksqlx (mssql)tiberius langsung
APIKonsisten dengan database lainSQL Server spesifik
Query checkCompile-timeRuntime
Connection poolBuilt-inButuh bb8/deadpool
KemudahanTinggiSedang
KontrolSedangPenuh

Instalasi dengan sqlx #

[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "mssql", "macros", "chrono"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
chrono = "0.4"

String Koneksi MSSQL #

Format connection string MSSQL berbeda dari MySQL:

# Autentikasi SQL Server
mssql://username:password@host:1433/nama_database

# Autentikasi Windows (domain)
mssql://domain%5Cusername:password@host/nama_database

# Dengan TrustServerCertificate untuk dev/testing
mssql://sa:Password123!@localhost:1433/master?trustServerCertificate=true

# Azure SQL Database
mssql://username@server:[email protected]:1433/database

Koneksi dan Connection Pool #

use sqlx::mssql::MssqlPoolOptions;
use sqlx::MssqlPool;

async fn buat_pool(url: &str) -> Result<MssqlPool, sqlx::Error> {
    MssqlPoolOptions::new()
        .max_connections(10)
        .min_connections(2)
        .acquire_timeout(std::time::Duration::from_secs(10))
        .connect(url)
        .await
}

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    // Untuk pengembangan lokal, sering perlu trustServerCertificate
    let url = std::env::var("MSSQL_URL")
        .unwrap_or_else(|_| {
            "mssql://sa:Password123!@localhost:1433/master?trustServerCertificate=true"
                .to_string()
        });

    let pool = buat_pool(&url).await?;

    // Verifikasi koneksi dengan query sederhana
    let versi: String = sqlx::query_scalar("SELECT @@VERSION")
        .fetch_one(&pool)
        .await?;

    println!("Terhubung ke: {}", &versi[..50]);
    Ok(())
}

Perbedaan T-SQL dari MySQL #

MSSQL menggunakan T-SQL (Transact-SQL) yang punya beberapa perbedaan sintaks penting:

-- MySQL                          -- MSSQL (T-SQL)

-- Limit dan offset
SELECT * FROM t LIMIT 10          SELECT TOP 10 * FROM t
OFFSET 20 LIMIT 10                SELECT * FROM t ORDER BY id
                                  OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY

-- Auto-increment
id INT AUTO_INCREMENT             id INT IDENTITY(1,1)

-- String function
CONCAT(a, b)                      a + b  atau  CONCAT(a, b)
IFNULL(a, b)                      ISNULL(a, b)
GROUP_CONCAT(col)                 STRING_AGG(col, ',')

-- Tanggal
NOW()                             GETDATE()  atau  SYSDATETIME()
DATE_FORMAT(d, '%Y-%m-%d')        FORMAT(d, 'yyyy-MM-dd')
DATE_ADD(d, INTERVAL 1 DAY)       DATEADD(day, 1, d)

-- Tipe string
VARCHAR(255) utf8mb4              NVARCHAR(255)  -- unicode built-in
TEXT                              NVARCHAR(MAX)

-- Backtick untuk identifier
`nama_tabel`                      [nama_tabel]  atau  "nama_tabel"

Membuat Tabel #

-- migrations/001_buat_tabel.sql untuk MSSQL

CREATE TABLE pengguna (
    id          BIGINT IDENTITY(1,1) PRIMARY KEY,
    nama        NVARCHAR(100)   NOT NULL,
    email       NVARCHAR(255)   NOT NULL,
    password    NVARCHAR(255)   NOT NULL,
    peran       NVARCHAR(20)    NOT NULL DEFAULT 'user',
    aktif       BIT             NOT NULL DEFAULT 1,
    dibuat_pada DATETIME2       NOT NULL DEFAULT SYSDATETIME(),
    diperbarui  DATETIME2       NOT NULL DEFAULT SYSDATETIME(),
    CONSTRAINT UQ_pengguna_email UNIQUE (email),
    CONSTRAINT CK_pengguna_peran CHECK (peran IN ('user', 'admin', 'moderator'))
);

CREATE TABLE artikel (
    id              BIGINT IDENTITY(1,1) PRIMARY KEY,
    pengguna_id     BIGINT          NOT NULL,
    judul           NVARCHAR(255)   NOT NULL,
    konten          NVARCHAR(MAX)   NOT NULL,
    diterbitkan     BIT             NOT NULL DEFAULT 0,
    dibuat_pada     DATETIME2       NOT NULL DEFAULT SYSDATETIME(),
    CONSTRAINT FK_artikel_pengguna FOREIGN KEY (pengguna_id)
        REFERENCES pengguna(id) ON DELETE CASCADE
);

CREATE NONCLUSTERED INDEX IX_artikel_pengguna ON artikel(pengguna_id);
CREATE NONCLUSTERED INDEX IX_artikel_diterbitkan ON artikel(diterbitkan) INCLUDE (dibuat_pada, judul);

Struct dan Tipe Data MSSQL #

MSSQL punya beberapa tipe data yang unik — petakan dengan hati-hati ke tipe Rust:

use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;

#[derive(Debug, FromRow, Serialize, Deserialize, Clone)]
struct Pengguna {
    pub id: i64,               // BIGINT IDENTITY → i64 (bukan u64 seperti MySQL)
    pub nama: String,          // NVARCHAR → String
    pub email: String,         // NVARCHAR → String
    #[serde(skip_serializing)]
    pub password: String,
    pub peran: String,         // NVARCHAR → String
    pub aktif: bool,           // BIT → bool
    pub dibuat_pada: NaiveDateTime,  // DATETIME2 → NaiveDateTime
    pub diperbarui: NaiveDateTime,
}

#[derive(Debug, FromRow, Serialize)]
struct Artikel {
    pub id: i64,
    pub pengguna_id: i64,
    pub judul: String,
    pub konten: String,
    pub diterbitkan: bool,
    pub dibuat_pada: NaiveDateTime,
}
Tipe MSSQLTipe Rust
TINYINTu8
SMALLINTi16
INTi32
BIGINTi64
REALf32
FLOATf64
BITbool
NVARCHAR(n)String
NVARCHAR(MAX)String
DATETIME2chrono::NaiveDateTime
DATETIMEOFFSETchrono::DateTime<Utc>
UNIQUEIDENTIFIERuuid::Uuid
VARBINARY(MAX)Vec<u8>
DECIMAL(p,s)rust_decimal::Decimal

Query dan Fetch #

Parameter binding di MSSQL menggunakan @P1, @P2, dst — berbeda dari ? di MySQL:

use sqlx::MssqlPool;

async fn ambil_semua_pengguna(pool: &MssqlPool) -> Result<Vec<Pengguna>, sqlx::Error> {
    // sqlx menangani parameter binding secara otomatis
    sqlx::query_as!(
        Pengguna,
        "SELECT id, nama, email, password, peran, aktif, dibuat_pada, diperbarui
         FROM pengguna
         ORDER BY dibuat_pada DESC"
    )
    .fetch_all(pool)
    .await
}

async fn cari_pengguna_by_id(pool: &MssqlPool, id: i64) -> Result<Option<Pengguna>, sqlx::Error> {
    sqlx::query_as!(
        Pengguna,
        "SELECT id, nama, email, password, peran, aktif, dibuat_pada, diperbarui
         FROM pengguna
         WHERE id = @P1",  // MSSQL menggunakan @P1, @P2, dst
        id
    )
    .fetch_optional(pool)
    .await
}

// Pagination dengan OFFSET-FETCH (MSSQL 2012+)
async fn daftar_pengguna_halaman(
    pool: &MssqlPool,
    halaman: i64,
    per_halaman: i64,
) -> Result<Vec<Pengguna>, sqlx::Error> {
    let offset = (halaman.saturating_sub(1)) * per_halaman;

    sqlx::query_as!(
        Pengguna,
        "SELECT id, nama, email, password, peran, aktif, dibuat_pada, diperbarui
         FROM pengguna
         ORDER BY id DESC
         OFFSET @P1 ROWS FETCH NEXT @P2 ROWS ONLY",
        offset,
        per_halaman
    )
    .fetch_all(pool)
    .await
}

INSERT dengan OUTPUT INSERTED #

MSSQL tidak memiliki LAST_INSERT_ID(). Cara idiomatis untuk mendapat ID setelah INSERT adalah menggunakan OUTPUT INSERTED:

async fn buat_pengguna(
    pool: &MssqlPool,
    nama: &str,
    email: &str,
    password_hash: &str,
) -> Result<i64, sqlx::Error> {
    // OUTPUT INSERTED.id mengembalikan ID yang baru dibuat sebagai result set
    let row = sqlx::query!(
        "INSERT INTO pengguna (nama, email, password)
         OUTPUT INSERTED.id
         VALUES (@P1, @P2, @P3)",
        nama,
        email,
        password_hash
    )
    .fetch_one(pool)
    .await?;

    Ok(row.id)
}

// Alternatif: INSERT dan SELECT terpisah dalam transaksi
async fn buat_pengguna_alt(
    pool: &MssqlPool,
    nama: &str,
    email: &str,
    password_hash: &str,
) -> Result<i64, sqlx::Error> {
    let mut tx = pool.begin().await?;

    sqlx::query!(
        "INSERT INTO pengguna (nama, email, password) VALUES (@P1, @P2, @P3)",
        nama, email, password_hash
    )
    .execute(&mut *tx)
    .await?;

    let id: i64 = sqlx::query_scalar("SELECT SCOPE_IDENTITY()")
        .fetch_one(&mut *tx)
        .await?;

    tx.commit().await?;
    Ok(id)
}

UPDATE dan DELETE #

async fn perbarui_pengguna(
    pool: &MssqlPool,
    id: i64,
    nama: &str,
    email: &str,
) -> Result<bool, sqlx::Error> {
    let hasil = sqlx::query!(
        "UPDATE pengguna
         SET nama = @P1, email = @P2, diperbarui = SYSDATETIME()
         WHERE id = @P3",
        nama, email, id
    )
    .execute(pool)
    .await?;

    Ok(hasil.rows_affected() > 0)
}

// UPDATE dengan OUTPUT — mendapat data sebelum dan sesudah update
async fn toggle_aktif(
    pool: &MssqlPool,
    id: i64,
) -> Result<Option<bool>, sqlx::Error> {
    // MSSQL bisa return INSERTED (nilai baru) atau DELETED (nilai lama)
    let row = sqlx::query!(
        "UPDATE pengguna
         SET aktif = CASE WHEN aktif = 1 THEN 0 ELSE 1 END
         OUTPUT INSERTED.aktif
         WHERE id = @P1",
        id
    )
    .fetch_optional(pool)
    .await?;

    Ok(row.map(|r| r.aktif))
}

async fn hapus_pengguna(pool: &MssqlPool, id: i64) -> Result<bool, sqlx::Error> {
    let hasil = sqlx::query!(
        "DELETE FROM pengguna WHERE id = @P1",
        id
    )
    .execute(pool)
    .await?;

    Ok(hasil.rows_affected() > 0)
}

Transaksi #

async fn pindahkan_artikel(
    pool: &MssqlPool,
    artikel_id: i64,
    penulis_lama_id: i64,
    penulis_baru_id: i64,
) -> Result<(), sqlx::Error> {
    let mut tx = pool.begin().await?;

    // Verifikasi artikel dimiliki oleh penulis lama
    let artikel: Option<i64> = sqlx::query_scalar!(
        "SELECT id FROM artikel WHERE id = @P1 AND pengguna_id = @P2",
        artikel_id, penulis_lama_id
    )
    .fetch_optional(&mut *tx)
    .await?;

    if artikel.is_none() {
        // Rollback otomatis saat tx di-drop
        return Err(sqlx::Error::RowNotFound);
    }

    // Verifikasi penulis baru ada
    let penulis_baru: Option<i64> = sqlx::query_scalar!(
        "SELECT id FROM pengguna WHERE id = @P1 AND aktif = 1",
        penulis_baru_id
    )
    .fetch_optional(&mut *tx)
    .await?;

    if penulis_baru.is_none() {
        return Err(sqlx::Error::RowNotFound);
    }

    // Pindahkan artikel
    sqlx::query!(
        "UPDATE artikel SET pengguna_id = @P1 WHERE id = @P2",
        penulis_baru_id, artikel_id
    )
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;
    println!("Artikel {} dipindahkan ke pengguna {}", artikel_id, penulis_baru_id);
    Ok(())
}

Stored Procedure #

MSSQL sangat banyak menggunakan stored procedure. Cara memanggilnya dari Rust:

-- Stored procedure di SQL Server
CREATE PROCEDURE sp_buat_pengguna
    @Nama       NVARCHAR(100),
    @Email      NVARCHAR(255),
    @Password   NVARCHAR(255),
    @NamaPeran  NVARCHAR(20) = 'user'
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (SELECT 1 FROM pengguna WHERE email = @Email)
    BEGIN
        RAISERROR('Email sudah terdaftar', 16, 1);
        RETURN;
    END

    INSERT INTO pengguna (nama, email, password, peran)
    VALUES (@Nama, @Email, @Password, @NamaPeran);

    SELECT SCOPE_IDENTITY() AS id_baru;
END;
async fn panggil_sp_buat_pengguna(
    pool: &MssqlPool,
    nama: &str,
    email: &str,
    password: &str,
) -> Result<i64, sqlx::Error> {
    // Panggil stored procedure dengan EXEC
    let row = sqlx::query!(
        "EXEC sp_buat_pengguna @Nama = @P1, @Email = @P2, @Password = @P3",
        nama, email, password
    )
    .fetch_one(pool)
    .await?;

    Ok(row.id_baru.unwrap_or(0))
}

Menggunakan tiberius Langsung #

Untuk kasus di mana sqlx tidak mendukung fitur MSSQL tertentu (misalnya Bulk Insert atau fitur CDC), gunakan tiberius:

[dependencies]
tiberius = { version = "0.12", features = ["rustls", "chrono"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
use tiberius::{AuthMethod, Client, Config, Query};
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = Config::new();
    config.host("localhost");
    config.port(1433);
    config.authentication(AuthMethod::sql_server("sa", "Password123!"));
    config.database("contoh");
    config.trust_cert();  // untuk development — jangan di produksi

    let tcp = TcpStream::connect(config.get_addr()).await?;
    tcp.set_nodelay(true)?;

    let mut klien = Client::connect(config, tcp.compat_write()).await?;

    // Query sederhana
    let query = Query::new("SELECT id, nama FROM pengguna WHERE aktif = @P1");
    // Tidak bisa gunakan query! macro — runtime binding
    let mut hasil = klien.query("SELECT id, nama FROM pengguna WHERE aktif = @P1",
        &[&1i32]).await?;

    while let Some(baris) = hasil.try_next().await? {
        let id: i32 = baris.get(0).unwrap_or(0);
        let nama: &str = baris.get(1).unwrap_or("");
        println!("ID: {}, Nama: {}", id, nama);
    }

    Ok(())
}

// Untuk menggunakan try_next
use futures::TryStreamExt;

Perbedaan Penting dari MySQL #

MySQL                           MSSQL (T-SQL)

Parameter binding
  ?                             @P1, @P2, @P3, ...

Mendapat ID setelah INSERT
  LAST_INSERT_ID()              OUTPUT INSERTED.id
                                atau SCOPE_IDENTITY()

Pagination
  LIMIT 10 OFFSET 20            ORDER BY id
                                OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
  (atau TOP 10 tanpa offset)    SELECT TOP 10 * (tanpa offset)

Waktu sekarang
  NOW()                         GETDATE() atau SYSDATETIME()

Cek nilai NULL
  IFNULL(a, b)                  ISNULL(a, b) atau COALESCE(a, b)

String unicode
  VARCHAR charset utf8mb4       NVARCHAR (selalu unicode)

Auto increment
  AUTO_INCREMENT                IDENTITY(1,1)

Kondisional
  IF(kondisi, a, b)             CASE WHEN kondisi THEN a ELSE b END
  IFNULL                        ISNULL

Identifier quoting
  `nama`                        [nama] atau "nama"

Ringkasan #

  • Parameter binding di MSSQL menggunakan @P1, @P2 — berbeda dari ? di MySQL. sqlx menangani ini secara otomatis — kamu tetap menulis parameter sebagai argumen biasa.
  • OUTPUT INSERTED.id untuk mendapat ID setelah INSERT — MSSQL tidak punya LAST_INSERT_ID(). Gunakan OUTPUT INSERTED.kolom untuk mendapat nilai dari baris yang baru diinsert.
  • Tipe i64 untuk BIGINT IDENTITY — berbeda dari MySQL yang biasanya menggunakan u64. MSSQL BIGINT adalah signed, sehingga petakan ke i64.
  • NVARCHAR bukan VARCHAR untuk string Unicode — semua kolom teks sebaiknya NVARCHAR di MSSQL agar mendukung karakter non-ASCII tanpa konfigurasi tambahan.
  • Pagination dengan OFFSET-FETCHORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY bukan LIMIT 10 OFFSET 20. ORDER BY wajib ada untuk OFFSET-FETCH.
  • SYSDATETIME() lebih presisi dari GETDATE() — gunakan untuk kolom DATETIME2 yang butuh presisi nanodetik.
  • Stored procedure umum di lingkungan enterprise — panggil dengan EXEC nama_sp @Param = @P1. Pastikan stored procedure mengembalikan result set jika butuh data kembali.
  • tiberius untuk fitur MSSQL yang tidak didukung sqlx — Bulk Insert, Change Data Capture, dan fitur T-SQL lanjutan mungkin butuh driver tingkat rendah ini.
  • trustServerCertificate=true hanya untuk development — di produksi, konfigurasi sertifikat SSL yang valid untuk koneksi aman.

← Sebelumnya: MySQL   Berikutnya: Oracle →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact