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.
| Aspek | MySQL | SQL Server |
|---|---|---|
| Auto-increment | AUTO_INCREMENT | IDENTITY(1,1) |
| Ambil ID baru | LAST_INSERT_ID() | SCOPE_IDENTITY() atau OUTPUT INSERTED.id |
| Limit baris | LIMIT 10 | TOP 10 atau OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY |
| String concat | CONCAT() atau ` | |
| Karakter escape | \ | '' (tanda kutip ganda) |
| Boolean | BOOLEAN / TINYINT(1) | BIT (0/1) |
| Tipe teks panjang | TEXT, LONGTEXT | VARCHAR(MAX), NVARCHAR(MAX) |
| Tipe tanggal | DATETIME | DATETIME, DATETIME2, DATETIMEOFFSET |
| Case-sensitive | Bergantung collation | Bergantung collation |
| Skema default | Database langsung | Database → Skema (default dbo) |
| Driver class | com.mysql.cj.jdbc.Driver | com.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-jdbcadalah pilihan resmi — unduh dari Maven Central (com.microsoft.sqlserver:mssql-jdbc). Pilih versi JRE yang sesuai:jre11untuk Java 11-16,jre17untuk 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 LIMIT —
SELECT TOP 10 ...untuk membatasi baris. Untuk paginasi, gunakanOFFSET n ROWS FETCH NEXT m ROWS ONLY(butuhORDER BY).- OUTPUT clause untuk ID baru —
INSERT INTO tabel OUTPUT INSERTED.id VALUES (...)adalah cara modern mendapatkan ID yang digenerate, lebih bersih dariSCOPE_IDENTITY().CallableStatementuntuk stored procedure — gunakanregisterOutParameter()untuk parameter OUTPUT sebelumexecute(), lalu baca nilainya setelah eksekusi.- Snapshot isolation — SQL Server mendukung
SNAPSHOTisolation level yang mencegah lock pembaca-penulis. Aktifkan di database denganALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON.SQLServerBulkCopyuntuk 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.SQLServerDialectdi konfigurasi Spring Boot. Hibernate otomatis menyesuaikan sintaks SQL (TOP, IDENTITY, dsb.) untuk SQL Server.