Transactions

NexaUI framework menyediakan dukungan untuk database transactions yang memungkinkan Anda melakukan operasi database secara atomik dan konsisten.

Pengenalan Transactions

Database transactions memungkinkan Anda melakukan serangkaian operasi database sebagai satu unit kerja. Jika semua operasi berhasil, transaction akan di-commit. Jika terjadi kesalahan, semua operasi akan di-rollback, mengembalikan database ke keadaan sebelumnya.

Manfaat Menggunakan Transactions

  • Atomicity: Semua operasi berhasil atau tidak sama sekali
  • Consistency: Database tetap dalam keadaan konsisten
  • Isolation: Operasi terisolasi dari operasi lain
  • Durability: Hasil operasi yang berhasil disimpan secara permanen

Penggunaan Dasar Transactions

NexaUI menyediakan API yang sederhana untuk bekerja dengan transactions:


// Memulai transaction
$db = $this->Storage();
$db->beginTransaction();

try {
    // Melakukan beberapa operasi database
    $db->table('users')->insert([
        'name' => 'John Doe',
        'email' => 'john@example.com'
    ]);
    
    $userId = $db->lastInsertId();
    
    $db->table('profiles')->insert([
        'user_id' => $userId,
        'bio' => 'Software Developer',
        'location' => 'Jakarta'
    ]);
    
    // Jika semua operasi berhasil, commit transaction
    $db->commit();
    return true;
} catch (\Exception $e) {
    // Jika terjadi error, rollback transaction
    $db->rollback();
    return false;
}
        

Nested Transactions

NexaUI mendukung nested transactions, memungkinkan Anda untuk memulai transaction baru di dalam transaction yang sudah berjalan:


$db = $this->Storage();

// Memulai transaction utama
$db->beginTransaction();

try {
    // Operasi database pertama
    $db->table('orders')->insert([
        'customer_id' => 1,
        'total' => 100.00
    ]);
    
    $orderId = $db->lastInsertId();
    
    // Memulai nested transaction
    $db->beginTransaction();
    
    try {
        // Operasi database dalam nested transaction
        $db->table('order_items')->insert([
            'order_id' => $orderId,
            'product_id' => 1,
            'quantity' => 2,
            'price' => 50.00
        ]);
        
        // Commit nested transaction
        $db->commit();
    } catch (\Exception $e) {
        // Rollback nested transaction
        $db->rollback();
        throw $e; // Re-throw exception untuk ditangkap oleh parent transaction
    }
    
    // Operasi database lain setelah nested transaction
    $db->table('order_logs')->insert([
        'order_id' => $orderId,
        'action' => 'created',
        'timestamp' => date('Y-m-d H:i:s')
    ]);
    
    // Commit transaction utama
    $db->commit();
    return true;
} catch (\Exception $e) {
    // Rollback transaction utama
    $db->rollback();
    return false;
}
        

Catatan: Tidak semua database mendukung nested transactions. Beberapa database seperti MySQL mengimplementasikan nested transactions dengan savepoints.

Transaction dengan Callback

NexaUI menyediakan metode transaction yang lebih sederhana dengan callback:


$db = $this->Storage();

// Menggunakan metode transaction dengan callback
$result = $db->transaction(function($db) {
    // Semua operasi database dalam callback ini akan dijalankan dalam transaction
    
    $db->table('users')->insert([
        'name' => 'Jane Smith',
        'email' => 'jane@example.com'
    ]);
    
    $userId = $db->lastInsertId();
    
    $db->table('profiles')->insert([
        'user_id' => $userId,
        'bio' => 'UX Designer',
        'location' => 'Bandung'
    ]);
    
    // Return true untuk commit, false untuk rollback
    return true;
});

// $result akan berisi nilai return dari callback
if ($result) {
    echo "Transaction berhasil";
} else {
    echo "Transaction gagal";
}
        

Transactions dalam Model

Mengimplementasikan transactions dalam model untuk operasi yang kompleks:


class OrderModel extends NexaModel
{
    protected $table = 'orders';
    
    /**
     * Membuat order baru dengan items
     * 
     * @param array $orderData Data order
     * @param array $items Data items
     * @return array|bool Order data jika berhasil, false jika gagal
     */
    public function createOrderWithItems(array $orderData, array $items)
    {
        $db = $this->Storage();
        
        return $db->transaction(function($db) use ($orderData, $items) {
            // Insert order
            $db->table('orders')->insert($orderData);
            $orderId = $db->lastInsertId();
            
            // Insert order items
            foreach ($items as $item) {
                $item['order_id'] = $orderId;
                $db->table('order_items')->insert($item);
            }
            
            // Update order total
            $total = array_sum(array_map(function($item) {
                return $item['price'] * $item['quantity'];
            }, $items));
            
            $db->table('orders')
               ->where('id', $orderId)
               ->update(['total' => $total]);
            
            // Ambil data order lengkap
            $order = $db->table('orders')->where('id', $orderId)->first();
            $order['items'] = $db->table('order_items')->where('order_id', $orderId)->get();
            
            return $order;
        });
    }
    
    /**
     * Update order dan items
     * 
     * @param int $orderId ID order
     * @param array $orderData Data order yang diupdate
     * @param array $items Data items yang diupdate
     * @return bool True jika berhasil, false jika gagal
     */
    public function updateOrderWithItems(int $orderId, array $orderData, array $items)
    {
        $db = $this->Storage();
        
        return $db->transaction(function($db) use ($orderId, $orderData, $items) {
            // Update order
            $db->table('orders')
               ->where('id', $orderId)
               ->update($orderData);
            
            // Delete existing items
            $db->table('order_items')
               ->where('order_id', $orderId)
               ->delete();
            
            // Insert updated items
            foreach ($items as $item) {
                $item['order_id'] = $orderId;
                $db->table('order_items')->insert($item);
            }
            
            // Update order total
            $total = array_sum(array_map(function($item) {
                return $item['price'] * $item['quantity'];
            }, $items));
            
            $db->table('orders')
               ->where('id', $orderId)
               ->update(['total' => $total]);
            
            return true;
        });
    }
}

// Penggunaan
$orderModel = new OrderModel();

// Membuat order baru dengan items
$orderData = [
    'customer_id' => 5,
    'status' => 'pending',
    'created_at' => date('Y-m-d H:i:s')
];

$items = [
    [
        'product_id' => 101,
        'quantity' => 2,
        'price' => 75.00
    ],
    [
        'product_id' => 102,
        'quantity' => 1,
        'price' => 50.00
    ]
];

$order = $orderModel->createOrderWithItems($orderData, $items);

if ($order) {
    echo "Order #{$order['id']} berhasil dibuat dengan " . count($order['items']) . " items";
} else {
    echo "Gagal membuat order";
}
        

Best Practices

  • Gunakan transactions untuk operasi yang memerlukan atomicity
  • Batasi durasi transaction sesingkat mungkin untuk mengurangi lock contention
  • Akses tabel dalam urutan yang konsisten untuk menghindari deadlock
  • Gunakan try-catch untuk menangani error dan melakukan rollback
  • Pertimbangkan isolation level yang sesuai dengan kebutuhan aplikasi
  • Implementasikan retry logic untuk menangani deadlock
  • Hindari operasi non-database dalam transaction (seperti HTTP request)
  • Gunakan transaction callback untuk kode yang lebih bersih
  • Lakukan testing dengan concurrent access untuk memastikan transaction berfungsi dengan benar

Contoh Kasus Penggunaan

Transfer Dana


class AccountModel extends NexaModel
{
    protected $table = 'accounts';
    
    /**
     * Transfer dana antar rekening
     * 
     * @param int $fromAccountId ID rekening pengirim
     * @param int $toAccountId ID rekening penerima
     * @param float $amount Jumlah yang ditransfer
     * @return bool True jika berhasil, false jika gagal
     */
    public function transferFunds(int $fromAccountId, int $toAccountId, float $amount)
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("Jumlah transfer harus lebih dari 0");
        }
        
        $db = $this->Storage();
        
        return $db->transaction(function($db) use ($fromAccountId, $toAccountId, $amount) {
            // Lock rekening pengirim untuk update (FOR UPDATE)
            $fromAccount = $db->table('accounts')
                             ->where('id', $fromAccountId)
                             ->lockForUpdate()
                             ->first();
            
            if (!$fromAccount) {
                return false;
            }
            
            // Cek saldo cukup
            if ($fromAccount['balance'] < $amount) {
                return false;
            }
            
            // Lock rekening penerima untuk update
            $toAccount = $db->table('accounts')
                           ->where('id', $toAccountId)
                           ->lockForUpdate()
                           ->first();
            
            if (!$toAccount) {
                return false;
            }
            
            // Kurangi saldo pengirim
            $db->table('accounts')
               ->where('id', $fromAccountId)
               ->update(['balance' => $fromAccount['balance'] - $amount]);
            
            // Tambah saldo penerima
            $db->table('accounts')
               ->where('id', $toAccountId)
               ->update(['balance' => $toAccount['balance'] + $amount]);
            
            // Catat transaksi
            $db->table('transactions')->insert([
                'from_account_id' => $fromAccountId,
                'to_account_id' => $toAccountId,
                'amount' => $amount,
                'timestamp' => date('Y-m-d H:i:s')
            ]);
            
            return true;
        });
    }
}

// Penggunaan
$accountModel = new AccountModel();
$success = $accountModel->transferFunds(1, 2, 500.00);

if ($success) {
    echo "Transfer berhasil";
} else {
    echo "Transfer gagal";
}