MSSQL #
Microsoft SQL Server (MSSQL) adalah sistem manajemen basis data relasional enterprise dari Microsoft yang banyak digunakan di lingkungan korporat — terutama di ekosistem Windows/.NET, tapi juga di aplikasi berbasis JVM/Kotlin. MSSQL punya beberapa perbedaan penting dari MySQL: sintaks T-SQL yang berbeda, autentikasi Windows terintegrasi, fitur enterprise seperti Always On, dan dukungan stored procedure yang kuat. Di Kotlin, kamu berinteraksi dengan MSSQL melalui JDBC driver dari Microsoft, HikariCP untuk connection pooling, dan ORM seperti Exposed. Artikel ini membahas semua aspek dari koneksi dasar, CRUD, fitur khas T-SQL, hingga penanganan stored procedure dan perbedaan yang perlu diperhatikan saat migrasi dari MySQL.
Setup dan Dependensi #
// build.gradle.kts
dependencies {
// Microsoft JDBC Driver untuk SQL Server
implementation("com.microsoft.sqlserver:mssql-jdbc:12.6.1.jre11")
// HikariCP connection pool
implementation("com.zaxxer:HikariCP:5.1.0")
// Exposed ORM (opsional)
implementation("org.jetbrains.exposed:exposed-core:0.49.0")
implementation("org.jetbrains.exposed:exposed-dao:0.49.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.49.0")
// Flyway untuk migrasi (SQL Server edition)
implementation("org.flywaydb:flyway-core:10.10.0")
implementation("org.flywaydb:flyway-sqlserver:10.10.0")
}
Format Connection String #
MSSQL memiliki format connection string yang berbeda dari MySQL:
// Format dasar dengan SQL Server Authentication
val urlDasar = "jdbc:sqlserver://localhost:1433;databaseName=myapp;encrypt=false"
// Format lengkap dengan semua opsi
val urlLengkap = buildString {
append("jdbc:sqlserver://")
append("localhost:1433") // host:port (default 1433)
append(";databaseName=myapp") // nama database
append(";encrypt=true") // enkripsi koneksi
append(";trustServerCertificate=true") // percayai sertifikat self-signed (dev)
append(";loginTimeout=15") // timeout login dalam detik
append(";sendStringParametersAsUnicode=true") // UTF-8 support
append(";applicationName=MyKotlinApp") // nama aplikasi untuk monitoring
}
// Windows Authentication (tanpa username/password)
val urlWindowsAuth = "jdbc:sqlserver://localhost:1433;databaseName=myapp;integratedSecurity=true;encrypt=false"
// Azure SQL Database
val urlAzure = buildString {
append("jdbc:sqlserver://myserver.database.windows.net:1433")
append(";databaseName=myapp")
append(";encrypt=true")
append(";trustServerCertificate=false")
append(";hostNameInCertificate=*.database.windows.net")
append(";loginTimeout=30")
append(";authentication=ActiveDirectoryPassword") // atau SqlPassword
}
Koneksi dengan HikariCP #
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
object DatabaseMssql {
private val dataSource: HikariDataSource by lazy {
val config = HikariConfig().apply {
jdbcUrl = buildString {
append("jdbc:sqlserver://")
append(System.getenv("DB_HOST") ?: "localhost:1433")
append(";databaseName=${System.getenv("DB_NAME") ?: "myapp"}")
append(";encrypt=${System.getenv("DB_ENCRYPT") ?: "false"}")
append(";trustServerCertificate=true")
append(";sendStringParametersAsUnicode=true")
}
driverClassName = "com.microsoft.sqlserver.jdbc.SQLServerDriver"
username = System.getenv("DB_USER") ?: "sa"
password = System.getenv("DB_PASSWORD") ?: "Password123!"
// Pool settings
minimumIdle = 2
maximumPoolSize = 10
idleTimeout = 300_000 // 5 menit
connectionTimeout = 30_000 // 30 detik
maxLifetime = 1_800_000 // 30 menit
poolName = "MSSQL-Pool"
// Test query untuk validasi koneksi
connectionTestQuery = "SELECT 1"
}
HikariDataSource(config)
}
fun <T> gunakan(blok: (java.sql.Connection) -> T): T {
return dataSource.connection.use(blok)
}
fun tutup() {
if (!dataSource.isClosed) dataSource.close()
}
}
fun main() {
DatabaseMssql.gunakan { koneksi ->
koneksi.createStatement().use { stmt ->
stmt.executeQuery("SELECT @@VERSION").use { rs ->
if (rs.next()) println("SQL Server: ${rs.getString(1).lines().first()}")
}
}
}
}
Perbedaan T-SQL vs MySQL SQL #
MSSQL menggunakan dialek T-SQL yang berbeda dari MySQL. Ini adalah perbedaan yang perlu kamu perhatikan saat menulis query:
-- AUTO INCREMENT
-- MySQL:
CREATE TABLE pengguna (id INT AUTO_INCREMENT PRIMARY KEY, ...);
-- MSSQL (T-SQL):
CREATE TABLE pengguna (id INT IDENTITY(1,1) PRIMARY KEY, ...);
-- LIMIT / PAGING
-- MySQL:
SELECT * FROM produk ORDER BY nama LIMIT 10 OFFSET 20;
-- MSSQL:
SELECT * FROM produk ORDER BY nama OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- String functions
-- MySQL: IFNULL(), GROUP_CONCAT()
-- MSSQL: ISNULL(), STRING_AGG()
-- Date functions
-- MySQL: NOW(), DATE_FORMAT()
-- MSSQL: GETDATE(), FORMAT()
-- Boolean
-- MySQL: BOOLEAN (alias untuk TINYINT(1)), nilai TRUE/FALSE
-- MSSQL: BIT (0 atau 1, tidak ada TRUE/FALSE literal)
-- Backtick vs Square bracket
-- MySQL: SELECT `nama` FROM `produk`
-- MSSQL: SELECT [nama] FROM [produk] atau SELECT "nama" FROM "produk"
Schema Tabel MSSQL #
CREATE TABLE dbo.produk (
id INT IDENTITY(1,1) PRIMARY KEY,
nama NVARCHAR(255) NOT NULL, -- N prefix untuk Unicode
deskripsi NVARCHAR(MAX), -- setara TEXT di MySQL
harga DECIMAL(15,2) NOT NULL,
stok INT NOT NULL DEFAULT 0,
kategori NVARCHAR(100),
aktif BIT NOT NULL DEFAULT 1, -- BIT bukan BOOLEAN
dibuat_pada DATETIME2 DEFAULT GETDATE(),
diperbarui_pada DATETIME2 DEFAULT GETDATE()
);
-- Index
CREATE INDEX IX_produk_kategori ON dbo.produk(kategori);
CREATE INDEX IX_produk_aktif ON dbo.produk(aktif);
GunakanNVARCHAR(bukanVARCHAR) untuk kolom teks yang menyimpan karakter non-ASCII seperti huruf Indonesia, Arab, atau karakter Unicode lainnya.NVARCHARmenyimpan dalam format UTF-16, sehingga panjang karakter dihitung berbeda —NVARCHAR(100)menyimpan 100 karakter Unicode.
CRUD dengan JDBC #
Model dan Repository #
import java.math.BigDecimal
import java.sql.ResultSet
import java.sql.Types
data class Produk(
val id: Int = 0,
val nama: String,
val deskripsi: String? = null,
val harga: BigDecimal,
val stok: Int = 0,
val kategori: String? = null,
val aktif: Boolean = true
)
class ProdukRepositoryMssql {
private fun ResultSet.toProduk() = Produk(
id = getInt("id"),
nama = getString("nama"),
deskripsi = getString("deskripsi"),
harga = getBigDecimal("harga"),
stok = getInt("stok"),
kategori = getString("kategori"),
aktif = getBoolean("aktif") // BIT otomatis dikonversi ke Boolean
)
// INSERT — MSSQL menggunakan OUTPUT INSERTED.id untuk mendapatkan ID yang di-generate
fun simpan(produk: Produk): Produk {
val sql = """
INSERT INTO dbo.produk (nama, deskripsi, harga, stok, kategori, aktif)
OUTPUT INSERTED.id
VALUES (?, ?, ?, ?, ?, ?)
""".trimIndent()
return DatabaseMssql.gunakan { koneksi ->
koneksi.prepareStatement(sql).use { stmt ->
stmt.setString(1, produk.nama)
// Null handling — setNull untuk kolom nullable
if (produk.deskripsi != null) stmt.setString(2, produk.deskripsi)
else stmt.setNull(2, Types.NVARCHAR)
stmt.setBigDecimal(3, produk.harga)
stmt.setInt(4, produk.stok)
if (produk.kategori != null) stmt.setString(5, produk.kategori)
else stmt.setNull(5, Types.NVARCHAR)
stmt.setBoolean(6, produk.aktif)
stmt.executeQuery().use { rs ->
if (rs.next()) produk.copy(id = rs.getInt(1))
else throw RuntimeException("Gagal mendapatkan ID yang di-generate")
}
}
}
}
// SELECT dengan paginasi MSSQL (OFFSET...FETCH)
fun cariSemua(
kategori: String? = null,
aktifSaja: Boolean = true,
halaman: Int = 1,
ukuran: Int = 20
): List<Produk> {
val offset = (halaman - 1) * ukuran
val kondisi = mutableListOf<String>()
val params = mutableListOf<Any?>()
if (aktifSaja) kondisi.add("aktif = 1")
if (kategori != null) {
kondisi.add("kategori = ?")
params.add(kategori)
}
val whereClause = if (kondisi.isNotEmpty()) "WHERE ${kondisi.joinToString(" AND ")}" else ""
val sql = """
SELECT *
FROM dbo.produk
$whereClause
ORDER BY nama
OFFSET ? ROWS
FETCH NEXT ? ROWS ONLY
""".trimIndent()
return DatabaseMssql.gunakan { koneksi ->
koneksi.prepareStatement(sql).use { stmt ->
var idx = 1
params.forEach { p ->
when (p) {
is String -> stmt.setString(idx++, p)
else -> stmt.setObject(idx++, p)
}
}
stmt.setInt(idx++, offset)
stmt.setInt(idx, ukuran)
stmt.executeQuery().use { rs ->
buildList { while (rs.next()) add(rs.toProduk()) }
}
}
}
}
// SELECT dengan MSSQL TOP (alternatif paginasi)
fun ambilTop(n: Int, aktifSaja: Boolean = true): List<Produk> {
val sql = """
SELECT TOP (?) *
FROM dbo.produk
${if (aktifSaja) "WHERE aktif = 1" else ""}
ORDER BY harga DESC
""".trimIndent()
return DatabaseMssql.gunakan { koneksi ->
koneksi.prepareStatement(sql).use { stmt ->
stmt.setInt(1, n)
stmt.executeQuery().use { rs ->
buildList { while (rs.next()) add(rs.toProduk()) }
}
}
}
}
// UPDATE
fun perbarui(produk: Produk): Boolean {
val sql = """
UPDATE dbo.produk
SET nama = ?, deskripsi = ?, harga = ?, stok = ?, kategori = ?
WHERE id = ?
""".trimIndent()
return DatabaseMssql.gunakan { koneksi ->
koneksi.prepareStatement(sql).use { stmt ->
stmt.setString(1, produk.nama)
if (produk.deskripsi != null) stmt.setString(2, produk.deskripsi)
else stmt.setNull(2, Types.NVARCHAR)
stmt.setBigDecimal(3, produk.harga)
stmt.setInt(4, produk.stok)
if (produk.kategori != null) stmt.setString(5, produk.kategori)
else stmt.setNull(5, Types.NVARCHAR)
stmt.setInt(6, produk.id)
stmt.executeUpdate() > 0
}
}
}
// MERGE (UPSERT) — fitur khas T-SQL
fun upsert(produk: Produk): Produk {
val sql = """
MERGE dbo.produk AS target
USING (SELECT ? AS nama, ? AS harga, ? AS stok, ? AS kategori) AS source
ON target.nama = source.nama
WHEN MATCHED THEN
UPDATE SET harga = source.harga, stok = source.stok
WHEN NOT MATCHED THEN
INSERT (nama, harga, stok, kategori)
VALUES (source.nama, source.harga, source.stok, source.kategori)
OUTPUT INSERTED.id;
""".trimIndent()
return DatabaseMssql.gunakan { koneksi ->
koneksi.prepareStatement(sql).use { stmt ->
stmt.setString(1, produk.nama)
stmt.setBigDecimal(2, produk.harga)
stmt.setInt(3, produk.stok)
if (produk.kategori != null) stmt.setString(4, produk.kategori)
else stmt.setNull(4, Types.NVARCHAR)
stmt.executeQuery().use { rs ->
if (rs.next()) produk.copy(id = rs.getInt(1))
else produk
}
}
}
}
}
Transaction di MSSQL #
fun transferStokMssql(dariId: Int, keId: Int, jumlah: Int) {
DatabaseMssql.gunakan { koneksi ->
koneksi.autoCommit = false
try {
// Gunakan WITH (UPDLOCK, ROWLOCK) untuk mencegah deadlock
val sqlCekStok = """
SELECT stok FROM dbo.produk WITH (UPDLOCK, ROWLOCK)
WHERE id = ?
""".trimIndent()
val stokSumber = koneksi.prepareStatement(sqlCekStok).use { stmt ->
stmt.setInt(1, dariId)
stmt.executeQuery().use { rs ->
if (!rs.next()) throw NoSuchElementException("Produk $dariId tidak ditemukan")
rs.getInt("stok")
}
}
if (stokSumber < jumlah) {
throw IllegalStateException("Stok tidak cukup: tersedia $stokSumber, diminta $jumlah")
}
koneksi.prepareStatement("UPDATE dbo.produk SET stok = stok - ? WHERE id = ?").use { stmt ->
stmt.setInt(1, jumlah)
stmt.setInt(2, dariId)
stmt.executeUpdate()
}
koneksi.prepareStatement("UPDATE dbo.produk SET stok = stok + ? WHERE id = ?").use { stmt ->
stmt.setInt(1, jumlah)
stmt.setInt(2, keId)
stmt.executeUpdate()
}
koneksi.commit()
println("Transfer berhasil: $jumlah unit dari produk $dariId ke $keId")
} catch (e: Exception) {
koneksi.rollback()
println("Transfer gagal, rollback: ${e.message}")
throw e
} finally {
koneksi.autoCommit = true
}
}
}
Stored Procedure #
Stored procedure adalah fitur kuat MSSQL yang sering digunakan di lingkungan enterprise. Kotlin bisa memanggil stored procedure melalui CallableStatement:
-- Definisi stored procedure di SQL Server
CREATE PROCEDURE dbo.sp_CariProduk
@Kategori NVARCHAR(100) = NULL,
@HargaMaks DECIMAL(15,2) = NULL,
@Limit INT = 20
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (@Limit) id, nama, harga, stok, kategori
FROM dbo.produk
WHERE aktif = 1
AND (@Kategori IS NULL OR kategori = @Kategori)
AND (@HargaMaks IS NULL OR harga <= @HargaMaks)
ORDER BY nama;
END;
GO
-- Stored procedure dengan output parameter
CREATE PROCEDURE dbo.sp_HitungStokTotal
@Kategori NVARCHAR(100),
@TotalStok INT OUTPUT,
@JumlahProduk INT OUTPUT
AS
BEGIN
SELECT
@TotalStok = SUM(stok),
@JumlahProduk = COUNT(*)
FROM dbo.produk
WHERE aktif = 1 AND kategori = @Kategori;
END;
GO
// Memanggil stored procedure
fun cariProdukViaStoredProcedure(
kategori: String? = null,
hargaMaks: BigDecimal? = null,
limit: Int = 20
): List<Produk> {
return DatabaseMssql.gunakan { koneksi ->
// {call NamaStoredProcedure(?, ?, ?)}
koneksi.prepareCall("{call dbo.sp_CariProduk(?, ?, ?)}").use { stmt ->
// Named parameter atau positional
if (kategori != null) stmt.setString(1, kategori)
else stmt.setNull(1, Types.NVARCHAR)
if (hargaMaks != null) stmt.setBigDecimal(2, hargaMaks)
else stmt.setNull(2, Types.DECIMAL)
stmt.setInt(3, limit)
stmt.executeQuery().use { rs ->
buildList {
while (rs.next()) {
add(Produk(
id = rs.getInt("id"),
nama = rs.getString("nama"),
harga = rs.getBigDecimal("harga"),
stok = rs.getInt("stok"),
kategori = rs.getString("kategori")
))
}
}
}
}
}
}
// Stored procedure dengan output parameter
fun hitungStokKategori(kategori: String): Pair<Int, Int> {
return DatabaseMssql.gunakan { koneksi ->
koneksi.prepareCall("{call dbo.sp_HitungStokTotal(?, ?, ?)}").use { stmt ->
stmt.setString(1, kategori)
// Daftarkan output parameter
stmt.registerOutParameter(2, Types.INTEGER) // @TotalStok
stmt.registerOutParameter(3, Types.INTEGER) // @JumlahProduk
stmt.execute()
val totalStok = stmt.getInt(2)
val jumlahProduk = stmt.getInt(3)
Pair(totalStok, jumlahProduk)
}
}
}
// Penggunaan
fun main() {
val produk = cariProdukViaStoredProcedure(kategori = "Elektronik", limit = 5)
produk.forEach { println("${it.nama}: Rp${it.harga}") }
val (totalStok, jumlahProduk) = hitungStokKategori("Elektronik")
println("Elektronik: $jumlahProduk produk, total stok $totalStok unit")
}
Exposed dengan MSSQL #
Exposed bisa digunakan dengan MSSQL dengan penyesuaian dialect:
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object TabelProdukMssql : Table("dbo.produk") {
val id = integer("id").autoIncrement()
val nama = varchar("nama", 255)
val deskripsi = text("deskripsi").nullable()
val harga = decimal("harga", 15, 2)
val stok = integer("stok").default(0)
val kategori = varchar("kategori", 100).nullable()
val aktif = bool("aktif").default(true)
override val primaryKey = PrimaryKey(id)
}
fun inisialisasiExposedMssql() {
// Gunakan DataSource dari HikariCP
Database.connect(DatabaseMssql.dataSource)
}
// Query dengan Exposed — sama seperti MySQL
fun cariProdukExposed(kategori: String? = null) = transaction {
TabelProdukMssql
.select { TabelProdukMssql.aktif eq true }
.apply {
if (kategori != null) {
andWhere { TabelProdukMssql.kategori eq kategori }
}
}
.orderBy(TabelProdukMssql.nama)
.map { baris ->
Produk(
id = baris[TabelProdukMssql.id],
nama = baris[TabelProdukMssql.nama],
harga = baris[TabelProdukMssql.harga],
stok = baris[TabelProdukMssql.stok],
kategori = baris[TabelProdukMssql.kategori],
aktif = baris[TabelProdukMssql.aktif]
)
}
}
Migrasi dengan Flyway untuk MSSQL #
import org.flywaydb.core.Flyway
fun jalankanMigrasiMssql() {
val flyway = Flyway.configure()
.dataSource(DatabaseMssql.dataSource)
.locations("classpath:db/migration/mssql")
.defaultSchema("dbo")
.baselineOnMigrate(true)
.validateOnMigrate(true)
.load()
val hasil = flyway.migrate()
println("MSSQL Migration: ${hasil.migrationsExecuted} migrasi dijalankan")
}
File migrasi untuk MSSQL harus menggunakan sintaks T-SQL:
-- src/main/resources/db/migration/mssql/V1__create_produk.sql
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'produk' AND schema_id = SCHEMA_ID('dbo'))
BEGIN
CREATE TABLE dbo.produk (
id INT IDENTITY(1,1) PRIMARY KEY,
nama NVARCHAR(255) NOT NULL,
deskripsi NVARCHAR(MAX),
harga DECIMAL(15,2) NOT NULL,
stok INT NOT NULL DEFAULT 0,
kategori NVARCHAR(100),
aktif BIT NOT NULL DEFAULT 1,
dibuat_pada DATETIME2 DEFAULT GETDATE()
);
CREATE INDEX IX_produk_kategori ON dbo.produk(kategori);
CREATE INDEX IX_produk_aktif ON dbo.produk(aktif);
END;
GO
Tips Khusus MSSQL #
Windows Authentication #
// Jika aplikasi berjalan di Windows dan perlu Windows Authentication
val urlWindowsAuth = buildString {
append("jdbc:sqlserver://localhost:1433")
append(";databaseName=myapp")
append(";integratedSecurity=true")
append(";encrypt=false")
// Tidak perlu username/password — menggunakan kredensial Windows pengguna saat ini
}
// Untuk Windows Auth, tambahkan DLL ke PATH atau dependency
// implementation("com.microsoft.sqlserver:mssql-jdbc_auth:12.6.1.x64") // Windows only
Menangani Deadlock #
MSSQL lebih agresif dalam mendeteksi deadlock dibanding MySQL. Tangani SQLServerException dengan kode error 1205:
import com.microsoft.sqlserver.jdbc.SQLServerException
fun eksekusiDenganRetry(blok: () -> Unit, maksPercobaan: Int = 3) {
var percobaan = 0
while (percobaan < maksPercobaan) {
try {
blok()
return
} catch (e: SQLServerException) {
if (e.errorCode == 1205 && percobaan < maksPercobaan - 1) {
// Error 1205 = deadlock victim
percobaan++
println("Deadlock terdeteksi, percobaan ke-$percobaan...")
Thread.sleep(100L * percobaan) // exponential backoff sederhana
} else {
throw e
}
}
}
}
Ringkasan #
- Driver Microsoft resmi — gunakan
com.microsoft.sqlserver:mssql-jdbc(bukan jtds yang sudah ketinggalan zaman). Driver Microsoft mendukung semua fitur SQL Server modern termasuk enkripsi dan AAD.- NVARCHAR untuk teks Unicode — selalu gunakan
NVARCHAR(bukanVARCHAR) untuk kolom yang menyimpan teks yang mungkin mengandung karakter non-ASCII. Di JDBC,setNString()atausetString()keduanya bekerja dengan NVARCHAR.- OUTPUT INSERTED untuk mendapat ID — di MSSQL,
INSERT ... OUTPUT INSERTED.idlebih andal darigetGeneratedKeys()untuk mendapatkan nilai IDENTITY yang baru di-generate. Ini juga bisa mengembalikan banyak kolom sekaligus.- OFFSET…FETCH untuk paginasi — gantikan
LIMIT...OFFSETMySQL denganOFFSET n ROWS FETCH NEXT m ROWS ONLYdi T-SQL. KlausaORDER BYwajib ada saat menggunakan OFFSET.- MERGE untuk upsert — T-SQL punya sintaks
MERGEyang powerful untuk operasi insert-or-update berdasarkan kondisi. Lebih eksplisit dan fleksibel dariINSERT ... ON DUPLICATE KEY UPDATEdi MySQL.- Stored procedure dengan
CallableStatement— di lingkungan enterprise MSSQL, stored procedure sering digunakan untuk logika bisnis di sisi database.CallableStatementdengan sintaks{call sp_nama(?, ?)}menangani parameter input dan output.WITH (UPDLOCK, ROWLOCK)untuk mencegah deadlock — saat membaca data yang akan segera diupdate dalam satu transaction, gunakan hint ini untuk mendapatkan kunci lebih awal dan menghindari deadlock.- Tangani deadlock dengan retry — MSSQL memilih salah satu transaksi sebagai “deadlock victim” (error 1205) dan rollback. Tangani ini dengan mekanisme retry dengan exponential backoff.