MSSQL #
Microsoft SQL Server (MSSQL) adalah database enterprise yang banyak digunakan di lingkungan korporat, khususnya pada stack berbasis Windows dan .NET. Dart dapat terhubung ke MSSQL melalui package mssql_connection yang mengimplementasikan protokol TDS (Tabular Data Stream) — protokol native SQL Server — secara langsung tanpa membutuhkan driver ODBC atau library native. Artikel ini membahas koneksi, query, transaksi, stored procedure, dan perbedaan penting antara T-SQL (SQL Server) dengan MySQL yang perlu diperhatikan.
Setup Package #
dart pub add mssql_connection
# pubspec.yaml
dependencies:
mssql_connection: ^0.0.8
Package mssql_connection mengimplementasikan protokol TDS secara native Dart. Tidak perlu instalasi driver ODBC atau SQL Server Native Client di mesin Dart. Cukup pastikan SQL Server dapat diakses dari mesin tersebut (firewall, TCP/IP enabled).
Koneksi ke SQL Server #
import 'package:mssql_connection/mssql_connection.dart';
Future<void> main() async {
final conn = MssqlConnection.getInstance();
// Koneksi dengan SQL Server Authentication
final berhasil = await conn.connect(
ip: 'localhost', // atau nama server: 'SERVER\INSTANCE'
port: '1433', // port default SQL Server
databaseName: 'TokoDB',
username: 'sa',
password: 'P@ssw0rd!',
timeoutInSeconds: 15,
);
if (!berhasil) {
throw Exception('Gagal terhubung ke SQL Server');
}
print('Terhubung ke SQL Server');
// Tutup koneksi setelah selesai
await conn.disconnect();
}
Koneksi dengan Windows Authentication #
Untuk lingkungan domain Windows, gunakan Windows Authentication (Integrated Security):
// Windows Authentication — username dan password kosong
// Server harus dikonfigurasi untuk menerima Windows Auth
final berhasil = await conn.connect(
ip: 'sql-server.corp.example.com',
port: '1433',
databaseName: 'CorporateDB',
username: '', // kosong untuk Windows Auth
password: '', // kosong untuk Windows Auth
timeoutInSeconds: 30,
);
Koneksi ke Named Instance #
SQL Server sering dikonfigurasi dengan named instance (bukan default instance):
// Named instance — gunakan backslash
// Contoh: SERVER\SQLEXPRESS, SERVER\MSSQLSERVER2019
final berhasil = await conn.connect(
ip: r'SERVER\SQLEXPRESS', // raw string untuk backslash
port: '1433',
databaseName: 'MyDB',
username: 'dbuser',
password: 'password',
);
Query Dasar #
import 'package:mssql_connection/mssql_connection.dart';
import 'dart:convert';
Future<void> contohQuery(MssqlConnection conn) async {
// SELECT — mengembalikan JSON string
final jsonHasil = await conn.getData('SELECT TOP 10 * FROM Pengguna');
// Parse JSON ke List
final List<dynamic> baris = jsonDecode(jsonHasil);
for (final row in baris) {
print('ID: ${row['Id']}');
print('Nama: ${row['Nama']}');
print('Email: ${row['Email']}');
}
// getData dengan kondisi — SELALU gunakan parameterized!
// Lihat bagian parameterized query di bawah
}
MssqlConnection.getData()mengembalikanStringberformat JSON, bukan list objek langsung. Kamu perlujsonDecode()untuk mengkonversinya keList<dynamic>. Setiap elemen adalahMap<String, dynamic>dengan nama kolom sebagai kunci.
Parameterized Query — Wajib untuk Keamanan #
Package mssql_connection menggunakan sintaks @namaParam untuk parameter (berbeda dari MySQL yang menggunakan :namaParam):
import 'package:mssql_connection/mssql_connection.dart';
import 'dart:convert';
Future<void> contohParameterized(MssqlConnection conn) async {
// ANTI-PATTERN: string interpolation langsung — SQL injection!
final inputBerbahaya = "'; DROP TABLE Pengguna; --";
await conn.getData(
"SELECT * FROM Pengguna WHERE Nama = '$inputBerbahaya'",
); // ✗ sangat berbahaya!
// BENAR: parameterized query dengan @namaParam (sintaks T-SQL)
final namaInput = "'; DROP TABLE Pengguna; --";
final jsonHasil = await conn.getData(
'SELECT * FROM Pengguna WHERE Nama = @nama',
params: {'@nama': namaInput}, // aman — driver handle escaping
);
// Multiple parameter
final produk = await conn.getData(
'SELECT * FROM Produk WHERE Kategori = @kat AND Harga <= @maks AND Aktif = 1',
params: {'@kat': 'Elektronik', '@maks': 5000000},
);
// LIKE dengan parameter
final kata = 'laptop';
final cari = await conn.getData(
'SELECT * FROM Produk WHERE Nama LIKE @keyword',
params: {'@keyword': '%$kata%'},
);
final daftar = jsonDecode(produk) as List;
print('Ditemukan ${daftar.length} produk');
}
Sintaks T-SQL yang Berbeda dari MySQL #
SQL Server menggunakan T-SQL yang memiliki beberapa perbedaan penting dari MySQL:
-- MySQL | T-SQL (SQL Server)
-- --------------------------------|--------------------------------
-- LIMIT 10 | TOP 10 (di SELECT)
-- AUTO_INCREMENT | IDENTITY(1,1)
-- `backtick` | [bracket] atau "double quote"
-- IFNULL() | ISNULL() atau COALESCE()
-- NOW() | GETDATE() atau SYSDATETIME()
-- DATE_FORMAT() | FORMAT() atau CONVERT()
-- GROUP_CONCAT() | STRING_AGG()
-- INSERT IGNORE | INSERT ... WHERE NOT EXISTS
-- ON DUPLICATE KEY UPDATE | MERGE statement
-- SHOW TABLES | SELECT * FROM sys.tables
// Contoh query T-SQL yang berbeda dari MySQL
// Pagination dengan OFFSET-FETCH (SQL Server 2012+)
final halaman = 1;
final perHalaman = 20;
final offset = (halaman - 1) * perHalaman;
final paginasi = await conn.getData(
'SELECT Id, Nama, Harga FROM Produk '
'ORDER BY Nama '
'OFFSET @offset ROWS '
'FETCH NEXT @perHalaman ROWS ONLY',
params: {'@offset': offset, '@perHalaman': perHalaman},
);
// TOP N rows
final topProduk = await conn.getData(
'SELECT TOP 5 Id, Nama, TotalTerjual FROM Produk ORDER BY TotalTerjual DESC',
);
// Tanggal dengan GETDATE()
final hariIni = await conn.getData(
'SELECT * FROM Order WHERE CAST(TanggalOrder AS DATE) = CAST(GETDATE() AS DATE)',
);
// String aggregation (pengganti GROUP_CONCAT)
final tags = await conn.getData(
'SELECT ProdukId, STRING_AGG(Tag, \',\') AS Tags '
'FROM ProdukTag GROUP BY ProdukId',
);
CRUD Lengkap #
import 'package:mssql_connection/mssql_connection.dart';
import 'dart:convert';
class ProdukRepository {
final MssqlConnection _conn;
ProdukRepository(this._conn);
// CREATE — mengembalikan ID yang dibuat
Future<int> tambah({
required String nama,
required double harga,
required int stok,
required String kategori,
}) async {
// OUTPUT INSERTED.Id — T-SQL untuk mendapatkan ID yang baru disisipkan
final result = await _conn.getData(
'INSERT INTO Produk (Nama, Harga, Stok, Kategori, Aktif, DibuatPada) '
'OUTPUT INSERTED.Id '
'VALUES (@nama, @harga, @stok, @kat, 1, GETDATE())',
params: {
'@nama': nama,
'@harga': harga,
'@stok': stok,
'@kat': kategori,
},
);
final rows = jsonDecode(result) as List;
return rows.first['Id'] as int;
}
// READ semua dengan pagination
Future<List<Map<String, dynamic>>> ambilSemua({
int halaman = 1,
int perHalaman = 20,
}) async {
final offset = (halaman - 1) * perHalaman;
final result = await _conn.getData(
'SELECT Id, Nama, Harga, Stok, Kategori '
'FROM Produk '
'WHERE Aktif = 1 '
'ORDER BY DibuatPada DESC '
'OFFSET @offset ROWS FETCH NEXT @per ROWS ONLY',
params: {'@offset': offset, '@per': perHalaman},
);
return (jsonDecode(result) as List).cast<Map<String, dynamic>>();
}
// READ satu
Future<Map<String, dynamic>?> ambilById(int id) async {
final result = await _conn.getData(
'SELECT * FROM Produk WHERE Id = @id AND Aktif = 1',
params: {'@id': id},
);
final rows = jsonDecode(result) as List;
return rows.isEmpty ? null : rows.first as Map<String, dynamic>;
}
// UPDATE — writeData untuk operasi non-SELECT
Future<bool> perbarui(int id, {String? nama, double? harga, int? stok}) async {
final fields = <String>[];
final params = <String, dynamic>{'@id': id};
if (nama != null) { fields.add('Nama = @nama'); params['@nama'] = nama; }
if (harga != null) { fields.add('Harga = @harga'); params['@harga'] = harga; }
if (stok != null) { fields.add('Stok = @stok'); params['@stok'] = stok; }
if (fields.isEmpty) return false;
// Tambahkan DiubahPada
fields.add('DiubahPada = GETDATE()');
final rowsAffected = await _conn.writeData(
'UPDATE Produk SET ${fields.join(', ')} WHERE Id = @id',
params: params,
);
return rowsAffected > 0;
}
// DELETE (soft delete)
Future<bool> hapus(int id) async {
final rowsAffected = await _conn.writeData(
'UPDATE Produk SET Aktif = 0, DiubahPada = GETDATE() WHERE Id = @id',
params: {'@id': id},
);
return rowsAffected > 0;
}
}
Stored Procedure #
SQL Server sangat umum menggunakan stored procedure untuk logika bisnis di sisi database:
import 'package:mssql_connection/mssql_connection.dart';
import 'dart:convert';
// Memanggil stored procedure dengan EXEC
Future<List<Map<String, dynamic>>> panggilSP(
MssqlConnection conn,
String namaSP,
Map<String, dynamic> params,
) async {
// Bangun parameter string untuk EXEC
final paramStr = params.keys.map((k) => '$k = $k').join(', ');
final query = 'EXEC $namaSP $paramStr';
final result = await conn.getData(query, params: params);
return (jsonDecode(result) as List).cast<Map<String, dynamic>>();
}
// Stored procedure yang mengembalikan data
Future<void> contohStoredProcedure(MssqlConnection conn) async {
// Asumsi: CREATE PROCEDURE sp_CariProduk
// @Keyword NVARCHAR(100),
// @HargaMaks DECIMAL(18,2)
// AS BEGIN
// SELECT * FROM Produk WHERE Nama LIKE '%' + @Keyword + '%' AND Harga <= @HargaMaks
// END
final produk = await conn.getData(
'EXEC sp_CariProduk @Keyword = @keyword, @HargaMaks = @maks',
params: {'@keyword': 'laptop', '@maks': 10000000},
);
final list = jsonDecode(produk) as List;
for (final p in list) {
print('${p['Nama']}: Rp${p['Harga']}');
}
}
// Stored procedure INSERT/UPDATE — gunakan writeData
Future<int> buatPenggunaViaSP(MssqlConnection conn, String nama, String email) async {
// Menggunakan OUTPUT parameter via EXEC
final result = await conn.getData(
'EXEC sp_BuatPengguna @Nama = @nama, @Email = @email',
params: {'@nama': nama, '@email': email},
);
final rows = jsonDecode(result) as List;
return rows.isEmpty ? 0 : rows.first['IdBaru'] as int;
}
Transaksi #
import 'package:mssql_connection/mssql_connection.dart';
import 'dart:convert';
Future<void> transferSaldo(
MssqlConnection conn,
int idDari,
int idKe,
double jumlah,
) async {
// Mulai transaksi secara manual dengan T-SQL
await conn.writeData('BEGIN TRANSACTION');
try {
// Cek saldo pengirim
final saldoResult = await conn.getData(
'SELECT Saldo FROM Rekening WITH (UPDLOCK) WHERE Id = @id',
params: {'@id': idDari},
);
final rows = jsonDecode(saldoResult) as List;
if (rows.isEmpty) throw Exception('Rekening pengirim tidak ditemukan');
final saldo = (rows.first['Saldo'] as num).toDouble();
if (saldo < jumlah) throw Exception('Saldo tidak mencukupi');
// Kurangi saldo pengirim
await conn.writeData(
'UPDATE Rekening SET Saldo = Saldo - @jumlah WHERE Id = @id',
params: {'@jumlah': jumlah, '@id': idDari},
);
// Tambah saldo penerima
await conn.writeData(
'UPDATE Rekening SET Saldo = Saldo + @jumlah WHERE Id = @id',
params: {'@jumlah': jumlah, '@id': idKe},
);
// Catat log transfer
await conn.writeData(
'INSERT INTO LogTransfer (IdDari, IdKe, Jumlah, TanggalWaktu) '
'VALUES (@dari, @ke, @jumlah, GETDATE())',
params: {'@dari': idDari, '@ke': idKe, '@jumlah': jumlah},
);
// Commit jika semua berhasil
await conn.writeData('COMMIT TRANSACTION');
print('Transfer berhasil: Rp${jumlah.toStringAsFixed(0)} dari $idDari ke $idKe');
} catch (e) {
// Rollback jika ada yang gagal
await conn.writeData('ROLLBACK TRANSACTION');
print('Transfer gagal, rollback: $e');
rethrow;
}
}
Mapping ke Model Kelas #
Nilai dari MSSQL bisa bertipe int, double, String, atau bool — berbeda dari MySQL yang semuanya String. Gunakan cast yang tepat:
class Produk {
final int id;
final String nama;
final double harga;
final int stok;
final bool aktif;
final DateTime dibuatPada;
const Produk({
required this.id,
required this.nama,
required this.harga,
required this.stok,
required this.aktif,
required this.dibuatPada,
});
factory Produk.dariJson(Map<String, dynamic> json) {
return Produk(
// MSSQL bisa mengembalikan tipe native — cek dengan is/as
id: json['Id'] is int ? json['Id'] as int : int.parse(json['Id'].toString()),
nama: json['Nama'] as String? ?? '',
harga: json['Harga'] is double
? json['Harga'] as double
: (json['Harga'] as num).toDouble(),
stok: json['Stok'] is int ? json['Stok'] as int : int.parse(json['Stok'].toString()),
aktif: json['Aktif'] == true || json['Aktif'] == 1,
// SQL Server datetime format: "2024-11-15T14:30:00"
dibuatPada: DateTime.parse(json['DibuatPada'] as String),
);
}
}
// Penggunaan
Future<List<Produk>> ambilProduk(MssqlConnection conn) async {
final result = await conn.getData(
'SELECT Id, Nama, Harga, Stok, Aktif, DibuatPada FROM Produk WHERE Aktif = 1',
);
final list = jsonDecode(result) as List;
return list.map((json) => Produk.dariJson(json as Map<String, dynamic>)).toList();
}
Penanganan Error dan Koneksi #
import 'package:mssql_connection/mssql_connection.dart';
class DatabaseMSSQL {
static MssqlConnection? _instance;
static bool _terhubung = false;
static Future<MssqlConnection> dapatkan({
required String host,
required String database,
required String username,
required String password,
}) async {
if (_instance != null && _terhubung) return _instance!;
_instance = MssqlConnection.getInstance();
try {
_terhubung = await _instance!.connect(
ip: host,
port: '1433',
databaseName: database,
username: username,
password: password,
timeoutInSeconds: 15,
);
if (!_terhubung) {
throw Exception('Gagal terhubung ke SQL Server: $host/$database');
}
return _instance!;
} catch (e) {
_terhubung = false;
rethrow;
}
}
static Future<void> tutup() async {
if (_instance != null && _terhubung) {
await _instance!.disconnect();
_terhubung = false;
}
}
// Reconnect jika koneksi terputus
static Future<T> denganReconnect<T>(
Future<T> Function(MssqlConnection) operasi,
MssqlConnection conn,
) async {
try {
return await operasi(conn);
} catch (e) {
if (e.toString().contains('connection') || e.toString().contains('timeout')) {
print('Koneksi terputus, mencoba reconnect...');
_terhubung = false;
// Reconnect
final newConn = await dapatkan(
host: 'localhost',
database: 'TokoDB',
username: 'sa',
password: 'password',
);
return await operasi(newConn);
}
rethrow;
}
}
}
Perbandingan MSSQL dengan MySQL di Dart #
| Aspek | MySQL (mysql_client) |
MSSQL (mssql_connection) |
|---|---|---|
| Placeholder parameter | :namaParam |
@namaParam |
| Hasil query | ResultSet dengan .rows |
JSON string, perlu jsonDecode |
| INSERT + ID | result.lastInsertID |
OUTPUT INSERTED.Id di query |
| Rows affected | result.affectedRows |
return dari writeData() |
| Pagination | LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| Top N | LIMIT n |
TOP n |
| Tanggal sekarang | NOW() |
GETDATE() |
| Transaksi | conn.transactional() |
BEGIN/COMMIT/ROLLBACK TRANSACTION manual |
| Tipe hasil | Semua String? |
Native (int, double, bool, String) |
Ringkasan #
MssqlConnection.getInstance()mengembalikan singleton — tidak perlu membuat instance baru setiap kali, tapi pastikan reconnect jika koneksi terputus.getData()untuk SELECT (mengembalikan JSON string),writeData()untuk INSERT/UPDATE/DELETE (mengembalikan rows affected).- Selalu
jsonDecode()hasilgetData()— mengembalikanStringJSON, bukan objek langsung.- Parameter menggunakan
@namaParam(T-SQL syntax), berbeda dari MySQL yang menggunakan:namaParam.OUTPUT INSERTED.Iddalam INSERT untuk mendapatkan ID yang baru disisipkan — penggantilastInsertIDdari MySQL.- Pagination T-SQL:
OFFSET @offset ROWS FETCH NEXT @per ROWS ONLYdenganORDER BYwajib — berbeda dariLIMITMySQL.- Stored procedure dipanggil dengan
EXEC namaSP @param1 = @nilai1— umum di lingkungan enterprise SQL Server.- Transaksi harus dikelola manual dengan
BEGIN TRANSACTION,COMMIT TRANSACTION, danROLLBACK TRANSACTION— tidak ada helper sepertitransactional()di MySQL.- Tipe data dari MSSQL bisa native (int, double, bool) — berbeda dari MySQL yang selalu String. Gunakan pemeriksaan
issebelum cast.- Hasil datetime dari SQL Server dalam format ISO:
"2024-11-15T14:30:00"— bisa langsungDateTime.parse().