MSSQL

MSSQL #

Microsoft SQL Server (MSSQL) adalah database relasional enterprise yang banyak dipakai di lingkungan korporat — terutama yang berjalan di atas infrastruktur Windows dan Microsoft Azure. Berbeda dari MySQL yang open-source, SQL Server hadir dengan fitur enterprise seperti Always On Availability Groups, columnstore index, dan integrasi mendalam dengan ekosistem Microsoft. Di Java, koneksi ke SQL Server menggunakan driver resmi Microsoft JDBC Driver for SQL Server (mssql-jdbc). Artikel ini membahas cara koneksi dengan JDBC, perbedaan penting sintaks SQL Server dari MySQL, autentikasi Windows, stored procedure dengan parameter output, transaksi dengan isolation level, hingga integrasi Spring Boot. Jika kamu sudah membaca artikel MySQL, banyak konsep JDBC yang sama — fokus artikel ini adalah pada hal-hal yang berbeda di SQL Server.

Perbedaan Utama SQL Server dari MySQL #

Sebelum masuk ke kode, penting untuk memahami perbedaan sintaks dan perilaku SQL Server yang sering menyebabkan kebingungan saat migrasi atau bekerja dengan kedua database sekaligus.

AspekMySQLSQL Server
Auto-incrementAUTO_INCREMENTIDENTITY(1,1)
Ambil ID baruLAST_INSERT_ID()SCOPE_IDENTITY() atau OUTPUT INSERTED.id
Limit barisLIMIT 10TOP 10 atau OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
String concatCONCAT() atau `
Karakter escape\'' (tanda kutip ganda)
BooleanBOOLEAN / TINYINT(1)BIT (0/1)
Tipe teks panjangTEXT, LONGTEXTVARCHAR(MAX), NVARCHAR(MAX)
Tipe tanggalDATETIMEDATETIME, DATETIME2, DATETIMEOFFSET
Case-sensitiveBergantung collationBergantung collation
Skema defaultDatabase langsungDatabase → Skema (default dbo)
Driver classcom.mysql.cj.jdbc.Drivercom.microsoft.sqlserver.jdbc.SQLServerDriver
flowchart TB
    A["Aplikasi Java"] --> B["Spring Data JPA\n(@Entity, Repository)"]
    A --> C["JDBC Murni / JdbcTemplate"]
    B --> D["Hibernate\n(SQL Server Dialect)"]
    C --> E["HikariCP\n(Connection Pool)"]
    D --> E
    E --> F["mssql-jdbc Driver"]
    F --> G[("SQL Server\n(Windows / Linux / Azure)")]

Persiapan — Driver dan Database #

Dependensi #

<!-- Maven -->
<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
    <version>12.6.1.jre11</version>
    <!-- Pilih versi JRE sesuai Java yang dipakai:
         jre8  → Java 8
         jre11 → Java 11, 17
         jre17 → Java 17+ (terbaru, fitur paling lengkap) -->
</dependency>

<!-- HikariCP -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>
// Gradle
implementation 'com.microsoft.sqlserver:mssql-jdbc:12.6.1.jre11'
implementation 'com.zaxxer:HikariCP:5.1.0'

Menyiapkan Database #

-- Jalankan di SQL Server Management Studio (SSMS) atau Azure Data Studio
CREATE DATABASE tokodb
    COLLATE Latin1_General_CI_AI;  -- CI = Case Insensitive, AI = Accent Insensitive
GO

USE tokodb;
GO

-- SQL Server menggunakan IDENTITY, NVARCHAR, BIT, DECIMAL
CREATE TABLE produk (
    id          BIGINT          IDENTITY(1,1) PRIMARY KEY,
    nama        NVARCHAR(255)   NOT NULL,      -- N = Unicode (mendukung karakter non-Latin)
    harga       DECIMAL(15,2)   NOT NULL,
    stok        INT             NOT NULL DEFAULT 0,
    kategori    NVARCHAR(100),
    aktif       BIT             NOT NULL DEFAULT 1,  -- BIT: 1=true, 0=false
    dibuat_pada DATETIME2       DEFAULT GETDATE(),   -- DATETIME2 lebih presisi dari DATETIME
    diubah_pada DATETIME2       DEFAULT GETDATE()
);
GO

-- Trigger untuk update otomatis diubah_pada
CREATE TRIGGER trg_produk_update
ON produk
AFTER UPDATE
AS
BEGIN
    UPDATE produk
    SET diubah_pada = GETDATE()
    FROM produk
    INNER JOIN inserted ON produk.id = inserted.id;
END;
GO

INSERT INTO produk (nama, harga, stok, kategori) VALUES
    (N'Laptop ProBook', 12000000.00, 5, N'Elektronik'),
    (N'Mouse Wireless', 150000.00, 20, N'Aksesori'),
    (N'Keyboard Mekanikal', 450000.00, 15, N'Aksesori'),
    (N'Monitor 27"', 3500000.00, 8, N'Elektronik');
GO

Koneksi JDBC #

Format URL Koneksi #

SQL Server mendukung beberapa format URL koneksi dengan berbagai opsi autentikasi:

// SQL Server Authentication (username + password)
String urlSQLAuth = "jdbc:sqlserver://localhost:1433;"
    + "databaseName=tokodb;"
    + "user=sa;"
    + "password=RahasiaKuat123!;"
    + "encrypt=true;"
    + "trustServerCertificate=true;"    // development: abaikan validasi sertifikat
    + "loginTimeout=30;";

// Windows Authentication (autentikasi OS, tanpa username/password Java)
// Perlu tambahkan sqljdbc_auth.dll ke PATH (hanya di Windows)
String urlWinAuth = "jdbc:sqlserver://localhost:1433;"
    + "databaseName=tokodb;"
    + "integratedSecurity=true;"
    + "encrypt=true;"
    + "trustServerCertificate=true;";

// Azure SQL Database
String urlAzure = "jdbc:sqlserver://namaserver.database.windows.net:1433;"
    + "databaseName=tokodb;"
    + "user=admin@namaserver;"
    + "password=RahasiaKuat123!;"
    + "encrypt=true;"
    + "trustServerCertificate=false;"   // produksi: validasi sertifikat server
    + "hostNameInCertificate=*.database.windows.net;"
    + "loginTimeout=30;";

// Named instance (SQL Server Express, dsb.)
String urlNamed = "jdbc:sqlserver://localhost\\SQLEXPRESS:1433;"
    + "databaseName=tokodb;"
    + "integratedSecurity=true;"
    + "encrypt=true;"
    + "trustServerCertificate=true;";

Koneksi dan Query Dasar #

import java.sql.*;

String url = "jdbc:sqlserver://localhost:1433;"
    + "databaseName=tokodb;encrypt=true;trustServerCertificate=true;";

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!")) {
    System.out.println("Terhubung ke SQL Server: "
        + conn.getMetaData().getDatabaseProductVersion());

    // SQL Server: gunakan TOP untuk membatasi baris (bukan LIMIT)
    String sql = "SELECT TOP 5 id, nama, harga, stok FROM produk "
               + "WHERE kategori = ? AND aktif = 1 ORDER BY harga DESC";

    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, "Elektronik");

        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                System.out.printf("%-5d %-25s Rp%,.2f (%d unit)%n",
                    rs.getLong("id"),
                    rs.getString("nama"),
                    rs.getDouble("harga"),
                    rs.getInt("stok"));
            }
        }
    }
} catch (SQLException e) {
    System.err.println("Error: " + e.getMessage());
    System.err.println("SQL State: " + e.getSQLState());
}

INSERT dengan SCOPE_IDENTITY dan OUTPUT #

SQL Server punya dua cara untuk mendapatkan ID yang baru digenerate:

// Cara 1: SCOPE_IDENTITY() — lebih sederhana, tapi perlu query terpisah
String sql1 = "INSERT INTO produk (nama, harga, stok, kategori) VALUES (?, ?, ?, ?); "
            + "SELECT SCOPE_IDENTITY() AS id;";

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!");
     PreparedStatement ps = conn.prepareStatement(sql1)) {

    ps.setString(1, "SSD 1TB");
    ps.setBigDecimal(2, new java.math.BigDecimal("750000.00"));
    ps.setInt(3, 30);
    ps.setString(4, "Penyimpanan");

    // execute() menjalankan batch INSERT + SELECT
    ps.execute();

    // Pindah ke ResultSet kedua (SELECT SCOPE_IDENTITY())
    try (ResultSet rs = ps.getResultSet()) {
        if (rs != null && rs.next()) {
            long idBaru = rs.getLong("id");
            System.out.println("ID baru: " + idBaru);
        }
    }
}

// Cara 2: OUTPUT clause — lebih modern, satu statement
String sql2 = "INSERT INTO produk (nama, harga, stok, kategori) "
            + "OUTPUT INSERTED.id, INSERTED.dibuat_pada "
            + "VALUES (?, ?, ?, ?)";

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!");
     PreparedStatement ps = conn.prepareStatement(sql2)) {

    ps.setString(1, "RAM 32GB");
    ps.setBigDecimal(2, new java.math.BigDecimal("1200000.00"));
    ps.setInt(3, 12);
    ps.setString(4, "Komponen");

    // OUTPUT diterima sebagai ResultSet biasa
    try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
            System.out.println("ID: " + rs.getLong("id"));
            System.out.println("Dibuat: " + rs.getTimestamp("dibuat_pada"));
        }
    }
}

Paginasi — OFFSET FETCH #

SQL Server tidak mendukung LIMIT. Gunakan OFFSET ... FETCH NEXT ... ROWS ONLY (SQL Server 2012+):

// Paginasi: halaman ke-N, ukuran M baris
int halaman   = 2;  // halaman ke-2 (0-indexed)
int ukuran    = 10; // 10 baris per halaman

String sql = """
    SELECT id, nama, harga, stok
    FROM produk
    WHERE aktif = 1
    ORDER BY id
    OFFSET ? ROWS           -- lewati N baris pertama
    FETCH NEXT ? ROWS ONLY  -- ambil M baris berikutnya
    """;

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!");
     PreparedStatement ps = conn.prepareStatement(sql)) {

    ps.setInt(1, halaman * ukuran); // OFFSET
    ps.setInt(2, ukuran);           // FETCH NEXT

    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("nama"));
        }
    }
}

Stored Procedure #

SQL Server sangat kuat dalam stored procedure — logika kompleks bisa diletakkan di database dan dipanggil dari Java. Ini berbeda dari MySQL yang stored procedure-nya lebih terbatas.

Stored Procedure Dasar #

-- Buat stored procedure di SQL Server
CREATE PROCEDURE sp_GetProdukByKategori
    @kategori   NVARCHAR(100),
    @minHarga   DECIMAL(15,2) = 0      -- parameter dengan nilai default
AS
BEGIN
    SET NOCOUNT ON;  -- supres pesan "rows affected"

    SELECT id, nama, harga, stok
    FROM produk
    WHERE kategori = @kategori
      AND harga >= @minHarga
      AND aktif = 1
    ORDER BY harga DESC;
END;
GO
// Panggil stored procedure dari Java
String call = "{call sp_GetProdukByKategori(?, ?)}";

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!");
     CallableStatement cs = conn.prepareCall(call)) {

    cs.setString(1, "Elektronik");
    cs.setBigDecimal(2, new java.math.BigDecimal("1000000.00"));

    try (ResultSet rs = cs.executeQuery()) {
        while (rs.next()) {
            System.out.printf("%s: Rp%,.2f%n",
                rs.getString("nama"), rs.getDouble("harga"));
        }
    }
}

Stored Procedure dengan Parameter Output #

Parameter output memungkinkan stored procedure mengembalikan nilai tambahan selain ResultSet — berguna untuk status, jumlah baris yang terpengaruh, atau nilai yang dihitung.

CREATE PROCEDURE sp_BuatProduk
    @nama       NVARCHAR(255),
    @harga      DECIMAL(15,2),
    @stok       INT,
    @kategori   NVARCHAR(100),
    @id_baru    BIGINT OUTPUT,      -- OUTPUT: kembalikan ID yang dibuat
    @pesan      NVARCHAR(500) OUTPUT -- OUTPUT: pesan sukses/gagal
AS
BEGIN
    SET NOCOUNT ON;

    -- Cek apakah nama sudah ada
    IF EXISTS (SELECT 1 FROM produk WHERE nama = @nama AND aktif = 1)
    BEGIN
        SET @id_baru = -1;
        SET @pesan = 'Produk dengan nama ini sudah ada';
        RETURN;
    END

    INSERT INTO produk (nama, harga, stok, kategori)
    VALUES (@nama, @harga, @stok, @kategori);

    SET @id_baru = SCOPE_IDENTITY();
    SET @pesan = 'Produk berhasil dibuat dengan ID ' + CAST(@id_baru AS NVARCHAR);
END;
GO
// Panggil stored procedure dengan parameter output
String call = "{call sp_BuatProduk(?, ?, ?, ?, ?, ?)}";

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!");
     CallableStatement cs = conn.prepareCall(call)) {

    // Parameter INPUT
    cs.setString(1, "Webcam HD");
    cs.setBigDecimal(2, new java.math.BigDecimal("350000.00"));
    cs.setInt(3, 25);
    cs.setString(4, "Aksesori");

    // Daftarkan parameter OUTPUT
    cs.registerOutParameter(5, Types.BIGINT);    // @id_baru
    cs.registerOutParameter(6, Types.NVARCHAR);  // @pesan

    cs.execute();

    // Baca nilai parameter OUTPUT setelah eksekusi
    long idBaru = cs.getLong(5);
    String pesan = cs.getString(6);

    System.out.println("ID baru: " + idBaru);
    System.out.println("Pesan: " + pesan);

    if (idBaru == -1) {
        System.err.println("Gagal membuat produk: " + pesan);
    }
}

Stored Procedure yang Mengembalikan Banyak ResultSet #

SQL Server memungkinkan satu stored procedure mengembalikan beberapa ResultSet sekaligus:

CREATE PROCEDURE sp_Dashboard
AS
BEGIN
    SET NOCOUNT ON;

    -- ResultSet 1: statistik umum
    SELECT COUNT(*) AS total_produk,
           SUM(stok) AS total_stok,
           AVG(CAST(harga AS FLOAT)) AS rata_harga
    FROM produk WHERE aktif = 1;

    -- ResultSet 2: top 5 produk termahal
    SELECT TOP 5 nama, harga FROM produk
    WHERE aktif = 1 ORDER BY harga DESC;

    -- ResultSet 3: jumlah per kategori
    SELECT kategori, COUNT(*) AS jumlah
    FROM produk WHERE aktif = 1
    GROUP BY kategori ORDER BY jumlah DESC;
END;
GO
try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!");
     CallableStatement cs = conn.prepareCall("{call sp_Dashboard()}")) {

    boolean adaResultSet = cs.execute();

    // ResultSet 1: statistik
    if (adaResultSet) {
        try (ResultSet rs = cs.getResultSet()) {
            if (rs.next()) {
                System.out.println("Total produk: " + rs.getInt("total_produk"));
                System.out.println("Total stok: " + rs.getInt("total_stok"));
                System.out.printf("Rata harga: Rp%,.2f%n", rs.getDouble("rata_harga"));
            }
        }
    }

    // Pindah ke ResultSet 2
    if (cs.getMoreResults()) {
        try (ResultSet rs = cs.getResultSet()) {
            System.out.println("\n--- TOP 5 TERMAHAL ---");
            while (rs.next()) {
                System.out.printf("%-25s Rp%,.2f%n",
                    rs.getString("nama"), rs.getDouble("harga"));
            }
        }
    }

    // Pindah ke ResultSet 3
    if (cs.getMoreResults()) {
        try (ResultSet rs = cs.getResultSet()) {
            System.out.println("\n--- PER KATEGORI ---");
            while (rs.next()) {
                System.out.println(rs.getString("kategori") + ": " + rs.getInt("jumlah"));
            }
        }
    }
}

Transaksi dan Isolation Level #

SQL Server mendukung semua isolation level standar SQL, plus satu tambahan yang khas: SNAPSHOT isolation yang menggunakan versioning baris untuk menghindari locking pembaca.

Isolation Level #

import java.sql.Connection;

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!")) {
    conn.setAutoCommit(false);

    // SET isolation level sebelum memulai transaksi
    conn.setTransactionIsolation(
        Connection.TRANSACTION_READ_COMMITTED    // default SQL Server
        // Connection.TRANSACTION_READ_UNCOMMITTED // bisa baca data belum di-commit (dirty read)
        // Connection.TRANSACTION_REPEATABLE_READ  // cegah non-repeatable read
        // Connection.TRANSACTION_SERIALIZABLE     // level tertinggi, paling aman, paling lambat
    );

    // SQL Server juga mendukung SNAPSHOT melalui SQL langsung
    // Aktifkan di database: ALTER DATABASE tokodb SET ALLOW_SNAPSHOT_ISOLATION ON;
    // Lalu di Java: jalankan SET TRANSACTION ISOLATION LEVEL SNAPSHOT

    try (Statement stmt = conn.createStatement()) {
        stmt.execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT");
    }

    try {
        // ... operasi database

        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}

Savepoint — Rollback Parsial #

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!")) {
    conn.setAutoCommit(false);

    try {
        // Operasi 1 — berhasil
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO produk (nama, harga, stok) VALUES (?, ?, ?)")) {
            ps.setString(1, "Produk A");
            ps.setBigDecimal(2, new java.math.BigDecimal("100000"));
            ps.setInt(3, 5);
            ps.executeUpdate();
        }

        // Buat savepoint setelah operasi yang berhasil
        Savepoint sp = conn.setSavepoint("setelah_produk_a");

        try {
            // Operasi 2 — mungkin gagal
            try (PreparedStatement ps = conn.prepareStatement(
                    "INSERT INTO produk (nama, harga, stok) VALUES (?, ?, ?)")) {
                ps.setString(1, "Produk B");
                ps.setBigDecimal(2, new java.math.BigDecimal("-1")); // harga negatif
                ps.setInt(3, 3);
                ps.executeUpdate();
            }
        } catch (SQLException e) {
            // Rollback hanya ke savepoint — Produk A tetap tersimpan
            conn.rollback(sp);
            System.err.println("Produk B gagal, rollback ke savepoint: " + e.getMessage());
        }

        conn.commit(); // commit Produk A
        System.out.println("Produk A berhasil disimpan.");

    } catch (SQLException e) {
        conn.rollback(); // rollback seluruh transaksi
        throw e;
    }
}

HikariCP untuk SQL Server #

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class SQLServerPool {

    private static final HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();

        config.setJdbcUrl("jdbc:sqlserver://localhost:1433;"
            + "databaseName=tokodb;"
            + "encrypt=true;"
            + "trustServerCertificate=true;");
        config.setUsername("sa");
        config.setPassword("RahasiaKuat123!");
        config.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

        // Pool sizing
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(2);
        config.setConnectionTimeout(30_000);
        config.setIdleTimeout(600_000);
        config.setMaxLifetime(1_800_000);

        // SQL Server: query validasi koneksi
        config.setConnectionTestQuery("SELECT 1");

        // SQL Server-specific properties
        config.addDataSourceProperty("applicationName", "AplikasiFE"); // tampil di Activity Monitor
        config.addDataSourceProperty("sendStringParametersAsUnicode", "true");

        config.setPoolName("MSSQL-Pool");

        dataSource = new HikariDataSource(config);
    }

    public static java.sql.Connection getConnection() throws java.sql.SQLException {
        return dataSource.getConnection();
    }
}

Spring Boot + Spring Data JPA #

Dependensi #

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
    <scope>runtime</scope>
</dependency>

Konfigurasi application.yml #

spring:
  datasource:
    url: jdbc:sqlserver://localhost:1433;databaseName=tokodb;encrypt=true;trustServerCertificate=true;
    username: sa
    password: RahasiaKuat123!
    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 30000
      pool-name: MSSQL-Pool
      data-source-properties:
        applicationName: SpringBootApp
        sendStringParametersAsUnicode: true

  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    open-in-view: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.SQLServerDialect
        format_sql: true
        jdbc:
          batch_size: 50

Entity Khusus SQL Server #

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "produk",
       indexes = {
           @Index(name = "idx_produk_kategori", columnList = "kategori"),
           @Index(name = "idx_produk_aktif", columnList = "aktif")
       })
public class Produk {

    @Id
    // SQL Server: gunakan IDENTITY, bukan AUTO_INCREMENT
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // NVARCHAR di SQL Server untuk Unicode
    @Column(nullable = false, length = 255, columnDefinition = "NVARCHAR(255)")
    private String nama;

    @Column(nullable = false, precision = 15, scale = 2)
    private BigDecimal harga;

    @Column(nullable = false)
    private Integer stok = 0;

    @Column(length = 100, columnDefinition = "NVARCHAR(100)")
    private String kategori;

    // BIT di SQL Server untuk boolean
    @Column(nullable = false, columnDefinition = "BIT DEFAULT 1")
    private Boolean aktif = true;

    // DATETIME2 lebih presisi dari DATETIME
    @Column(name = "dibuat_pada", updatable = false,
            columnDefinition = "DATETIME2 DEFAULT GETDATE()")
    private LocalDateTime dibuatPada;

    @Column(name = "diubah_pada",
            columnDefinition = "DATETIME2 DEFAULT GETDATE()")
    private LocalDateTime diubahPada;

    @PrePersist
    protected void onCreate() {
        dibuatPada = LocalDateTime.now();
        diubahPada = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        diubahPada = LocalDateTime.now();
    }

    // Konstruktor, getter, setter
    public Produk() {}
    public Produk(String nama, BigDecimal harga, int stok, String kategori) {
        this.nama = nama; this.harga = harga; this.stok = stok; this.kategori = kategori;
    }
    // ... getter dan setter
}

Repository dengan Query Spesifik SQL Server #

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

@Repository
public interface ProdukRepository extends JpaRepository<Produk, Long> {

    // Method name convention — sama seperti MySQL
    List<Produk> findByAktifTrue();
    List<Produk> findByKategoriAndAktifTrue(String kategori);

    // JPQL — bekerja sama di semua database
    @Query("SELECT p FROM Produk p WHERE p.aktif = true ORDER BY p.harga DESC")
    List<Produk> findSemuaUrutHarga();

    // Native query SQL Server — gunakan TOP, NVARCHAR, dan sintaks SQL Server
    @Query(value = """
        SELECT TOP(:limit) id, nama, harga, stok
        FROM produk
        WHERE aktif = 1
          AND LOWER(nama) LIKE LOWER(CONCAT('%', :kata, '%'))
        ORDER BY harga DESC
        """, nativeQuery = true)
    List<Object[]> cariProdukNative(@Param("kata") String kata, @Param("limit") int limit);

    // Panggil stored procedure via @Query nativeQuery
    @Query(value = "EXEC sp_GetProdukByKategori :kategori, :minHarga", nativeQuery = true)
    List<Object[]> panggilStoredProcedure(@Param("kategori") String kategori,
                                           @Param("minHarga") BigDecimal minHarga);

    // Full-text search SQL Server (perlu Full-Text Index aktif)
    @Query(value = """
        SELECT id, nama, harga, stok
        FROM produk
        WHERE CONTAINS(nama, :kata) AND aktif = 1
        """, nativeQuery = true)
    List<Object[]> fullTextSearch(@Param("kata") String kata);

    // Paginasi dengan OFFSET FETCH
    @Query(value = """
        SELECT id, nama, harga, stok
        FROM produk
        WHERE aktif = 1
        ORDER BY id
        OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY
        """, nativeQuery = true)
    List<Object[]> findWithPagination(@Param("offset") int offset, @Param("limit") int limit);
}

Fitur Khusus SQL Server #

Bulk Copy — Insert Massal Sangat Cepat #

SQL Server punya SQLServerBulkCopy yang jauh lebih cepat dari batch JDBC biasa untuk data massal:

import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy;
import com.microsoft.sqlserver.jdbc.SQLServerBulkCopyOptions;

// Siapkan data sumber (bisa dari ResultSet lain, CSV, dll.)
// Di sini kita simulasikan dengan ResultSet in-memory
String sqlSumber = "SELECT nama, harga, stok, kategori FROM staging_produk";

try (Connection connSumber = DriverManager.getConnection(urlStaging, "sa", "pass");
     Connection connTujuan = DriverManager.getConnection(url, "sa", "RahasiaKuat123!")) {

    // Opsi bulk copy
    SQLServerBulkCopyOptions options = new SQLServerBulkCopyOptions();
    options.setBatchSize(1000);          // proses 1000 baris per batch
    options.setBulkCopyTimeout(600);     // timeout 10 menit
    options.setCheckConstraints(true);   // validasi constraints
    options.setFireTriggers(true);       // aktifkan trigger

    try (SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connTujuan)) {
        bulkCopy.setBulkCopyOptions(options);
        bulkCopy.setDestinationTableName("produk");

        // Mapping kolom sumber → tujuan (jika nama berbeda)
        bulkCopy.addColumnMapping("nama", "nama");
        bulkCopy.addColumnMapping("harga", "harga");
        bulkCopy.addColumnMapping("stok", "stok");
        bulkCopy.addColumnMapping("kategori", "kategori");

        // Jalankan bulk copy
        try (PreparedStatement ps = connSumber.prepareStatement(sqlSumber);
             ResultSet rs = ps.executeQuery()) {
            bulkCopy.writeToServer(rs);
        }

        System.out.println("Bulk copy selesai.");
    }
}

Table-Valued Parameter (TVP) — Kirim Banyak Baris sebagai Parameter #

TVP memungkinkan kamu mengirim tabel sebagai parameter ke stored procedure:

-- Buat tipe tabel di SQL Server
CREATE TYPE ProdukTableType AS TABLE (
    nama      NVARCHAR(255),
    harga     DECIMAL(15,2),
    stok      INT,
    kategori  NVARCHAR(100)
);
GO

-- Stored procedure yang menerima TVP
CREATE PROCEDURE sp_BulkInsertProduk
    @produkList ProdukTableType READONLY
AS
BEGIN
    INSERT INTO produk (nama, harga, stok, kategori)
    SELECT nama, harga, stok, kategori FROM @produkList;

    SELECT @@ROWCOUNT AS jumlah_insert;
END;
GO
import com.microsoft.sqlserver.jdbc.SQLServerDataTable;
import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement;
import java.sql.Types;

// Buat data table untuk TVP
SQLServerDataTable tvp = new SQLServerDataTable();
tvp.addColumnMetadata("nama",     java.sql.Types.NVARCHAR);
tvp.addColumnMetadata("harga",    java.sql.Types.DECIMAL);
tvp.addColumnMetadata("stok",     java.sql.Types.INTEGER);
tvp.addColumnMetadata("kategori", java.sql.Types.NVARCHAR);

// Isi data
tvp.addRow("Produk X", new java.math.BigDecimal("100000"), 5, "Umum");
tvp.addRow("Produk Y", new java.math.BigDecimal("200000"), 3, "Premium");
tvp.addRow("Produk Z", new java.math.BigDecimal("300000"), 8, "Umum");

try (Connection conn = DriverManager.getConnection(url, "sa", "RahasiaKuat123!")) {
    String call = "{call sp_BulkInsertProduk(?)}";

    try (SQLServerPreparedStatement ps =
            (SQLServerPreparedStatement) conn.prepareStatement(call)) {

        // Set TVP sebagai parameter
        ps.setStructured(1, "ProdukTableType", tvp);

        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                System.out.println("Berhasil insert: " + rs.getInt("jumlah_insert") + " baris");
            }
        }
    }
}

Kapan Menggunakan SQL Server vs MySQL #

Pilih SQL SERVER jika:
  ✓ Infrastruktur berbasis Windows / Microsoft ecosystem
  ✓ Perlu integrasi dengan Azure cloud services
  ✓ Tim sudah familiar dengan T-SQL dan SQL Server tools (SSMS)
  ✓ Butuh fitur enterprise: Always On, columnstore index, SSRS
  ✓ Stored procedure kompleks dengan parameter output dan TVP
  ✓ Bulk copy performa tinggi dengan SQLServerBulkCopy

Pilih MYSQL jika:
  ✓ Infrastruktur berbasis Linux / open-source stack
  ✓ Budget terbatas (MySQL community edition gratis)
  ✓ Aplikasi web umum tanpa kebutuhan enterprise spesifik
  ✓ Tim lebih familiar dengan ekosistem LAMP/LEMP

Perbedaan yang perlu diperhatikan saat migrasi MySQL → SQL Server:
  ✗ Ganti LIMIT → TOP atau OFFSET FETCH
  ✗ Ganti AUTO_INCREMENT → IDENTITY(1,1)
  ✗ Ganti BOOLEAN → BIT
  ✗ Ganti TEXT/LONGTEXT → VARCHAR(MAX) / NVARCHAR(MAX)
  ✗ Ganti NOW() → GETDATE() atau SYSDATETIME()
  ✗ Ganti IFNULL() → ISNULL() atau COALESCE()
  ✗ Ganti GROUP_CONCAT() → STRING_AGG()

Ringkasan #

  • Driver mssql-jdbc adalah pilihan resmi — unduh dari Maven Central (com.microsoft.sqlserver:mssql-jdbc). Pilih versi JRE yang sesuai: jre11 untuk Java 11-16, jre17 untuk Java 17+.
  • Format URL berbeda dari MySQL — gunakan ; sebagai separator parameter, bukan ? dan &. Parameter penting: encrypt=true, trustServerCertificate=true (development), databaseName=, integratedSecurity=true (Windows Auth).
  • TOP bukan LIMITSELECT TOP 10 ... untuk membatasi baris. Untuk paginasi, gunakan OFFSET n ROWS FETCH NEXT m ROWS ONLY (butuh ORDER BY).
  • OUTPUT clause untuk ID baruINSERT INTO tabel OUTPUT INSERTED.id VALUES (...) adalah cara modern mendapatkan ID yang digenerate, lebih bersih dari SCOPE_IDENTITY().
  • CallableStatement untuk stored procedure — gunakan registerOutParameter() untuk parameter OUTPUT sebelum execute(), lalu baca nilainya setelah eksekusi.
  • Snapshot isolation — SQL Server mendukung SNAPSHOT isolation level yang mencegah lock pembaca-penulis. Aktifkan di database dengan ALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON.
  • SQLServerBulkCopy untuk data massal — jauh lebih cepat dari batch JDBC biasa untuk insert jutaan baris. Mirip BULK INSERT T-SQL tapi dari Java.
  • TVP untuk batch parameter — Table-Valued Parameter memungkinkan pengiriman tabel data sebagai satu parameter ke stored procedure, menggantikan loop insert satu per satu.
  • Dialect JPA — gunakan org.hibernate.dialect.SQLServerDialect di konfigurasi Spring Boot. Hibernate otomatis menyesuaikan sintaks SQL (TOP, IDENTITY, dsb.) untuk SQL Server.

← Sebelumnya: MySQL   Berikutnya: Oracle →

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