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)"]| Aspek | sqlx (mssql) | tiberius langsung |
|---|---|---|
| API | Konsisten dengan database lain | SQL Server spesifik |
| Query check | Compile-time | Runtime |
| Connection pool | Built-in | Butuh bb8/deadpool |
| Kemudahan | Tinggi | Sedang |
| Kontrol | Sedang | Penuh |
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 MSSQL | Tipe Rust |
|---|---|
TINYINT | u8 |
SMALLINT | i16 |
INT | i32 |
BIGINT | i64 |
REAL | f32 |
FLOAT | f64 |
BIT | bool |
NVARCHAR(n) | String |
NVARCHAR(MAX) | String |
DATETIME2 | chrono::NaiveDateTime |
DATETIMEOFFSET | chrono::DateTime<Utc> |
UNIQUEIDENTIFIER | uuid::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.sqlxmenangani ini secara otomatis — kamu tetap menulis parameter sebagai argumen biasa.OUTPUT INSERTED.iduntuk mendapat ID setelah INSERT — MSSQL tidak punyaLAST_INSERT_ID(). GunakanOUTPUT INSERTED.kolomuntuk mendapat nilai dari baris yang baru diinsert.- Tipe
i64untuk BIGINT IDENTITY — berbeda dari MySQL yang biasanya menggunakanu64. MSSQL BIGINT adalah signed, sehingga petakan kei64.NVARCHARbukanVARCHARuntuk string Unicode — semua kolom teks sebaiknyaNVARCHARdi MSSQL agar mendukung karakter non-ASCII tanpa konfigurasi tambahan.- Pagination dengan
OFFSET-FETCH—ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLYbukanLIMIT 10 OFFSET 20.ORDER BYwajib ada untukOFFSET-FETCH.SYSDATETIME()lebih presisi dariGETDATE()— gunakan untuk kolomDATETIME2yang 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.tiberiusuntuk fitur MSSQL yang tidak didukung sqlx — Bulk Insert, Change Data Capture, dan fitur T-SQL lanjutan mungkin butuh driver tingkat rendah ini.trustServerCertificate=truehanya untuk development — di produksi, konfigurasi sertifikat SSL yang valid untuk koneksi aman.