Як виявляти та усувати race conditions у додатках Laravel

0
Перекладено ШІ
Оригінал: Laravel News
Оновлено: 12 березня, 2026
Дізнайтеся, як виявляти та усувати гонки доступу (race conditions) у Laravel з MongoDB за допомогою атомарних операцій на реальному прикладі оформлення замовлення. У статті — тести, пояснення помилки read‑modify‑write і практичні приклади $inc/$set з відкатами для безпечних оновлень балансу й запасів.

Дізнайтеся, як знаходити race conditions у Laravel + MongoDB застосунках і виправляти їх за допомогою атомарних операцій. Практичний приклад — оформлення замовлення в e‑commerce — покаже, чому патерн Eloquent «read‑modify‑write» ламається під навантаженням.

Передумови

Перед початком бажано мати:

Опціонально, але корисно:

Чого ви навчитеся

Вступ

Уявіть: ви реалізували flash sale для магазину. На локалі все працює, тести проходять. У production — через кілька хвилин після старту розпродажу — лунають тикети: клієнтів списали двічі, баланси гаманців стали від’ємними, а продано більше товару, ніж було на складі.

Найдивніше: логів із помилками немає. Усі операції повернули успішний результат, але дані в базі — неконсистентні.

Це і є race conditions — баги, які ховаються під час розробки і вилізають лише при реальній конкуренції. Покажу, як їх помічати, розуміти і вирішувати за допомогою атомарних операцій MongoDB у Laravel.

Налаштування Laravel з MongoDB

Перш ніж перейти до проблеми, налаштуємо проект Laravel для роботи з MongoDB.

Сконфігуруйте файл .env:

DB_CONNECTION=mongodb
DB_HOST=127.0.0.1
DB_PORT=27017
DB_DATABASE=ecommerce
DB_USERNAME=
DB_PASSWORD=

Додайте з’єднання MongoDB у config/database.php:

'connections' => [
    'mongodb' => [
        'driver' => 'mongodb',
        'host' => env('DB_HOST', '127.0.0.1'),
        'port' => env('DB_PORT', 27017),
        'database' => env('DB_DATABASE', 'ecommerce'),
        'username' => env('DB_USERNAME', ''),
        'password' => env('DB_PASSWORD', ''),
        'options' => [
            'database' => env('DB_AUTHENTICATION_DATABASE', 'admin'),
        ],
    ],
],

Створюємо моделі для e‑commerce

Зробимо просту систему checkout з трьома сутностями: wallets (баланси користувачів), products (інвентар) та orders.

Згенеруйте моделі:

php artisan make:model Wallet
php artisan make:model Product
php artisan make:model Order

Ось модель Wallet:

<?php
 
namespace App\Models;
 
use MongoDB\Laravel\Eloquent\Model;
 
class Wallet extends Model
{
    protected $connection = 'mongodb';
    protected $collection = 'wallets';
 
    protected $fillable = [
        'user_id',
        'balance'
    ];
 
    protected $casts = [
        'balance' => 'decimal:2'
    ];
}

Модель Product:

<?php
 
namespace App\Models;
 
use MongoDB\Laravel\Eloquent\Model;
 
class Product extends Model
{
    protected $connection = 'mongodb';
    protected $collection = 'products';
 
    protected $fillable = [
        'name',
        'price',
        'stock'
    ];
 
    protected $casts = [
        'price' => 'decimal:2',
        'stock' => 'integer'
    ];
}

І модель Order:

<?php
 
namespace App\Models;
 
use MongoDB\Laravel\Eloquent\Model;
 
class Order extends Model
{
    protected $connection = 'mongodb';
    protected $collection = 'orders';
 
    protected $fillable = [
        'user_id',
        'product_id',
        'quantity',
        'amount',
        'status'
    ];
 
    protected $casts = [
        'amount' => 'decimal:2',
        'quantity' => 'integer'
    ];
}

Початковий потік оформлення замовлення

Створимо контролер checkout. Це імплементація, що здається коректною, але провалиться під навантаженням:

php artisan make:controller CheckoutController
<?php
 
namespace App\Http\Controllers;
 
use App\Models\Wallet;
use App\Models\Product;
use App\Models\Order;
use Illuminate\Http\Request;
 
class CheckoutController extends Controller
{
    public function checkout(Request $request)
    {
        $validated = $request->validate([
            'user_id' => 'required|string',
            'product_id' => 'required|string',
            'quantity' => 'required|integer|min:1',
        ]);
 
        $userId = $validated['user_id'];
        $productId = $validated['product_id'];
        $quantity = $validated['quantity'];
 
        // Step 1: Get the user's wallet
        $wallet = Wallet::where('user_id', $userId)->firstOrFail();
 
        // Step 2: Get the product
        $product = Product::findOrFail($productId);
        $amount = $product->price * $quantity;
 
        // Step 3: Check if user has enough funds
        if ($wallet->balance < $amount) {
            return response()->json([
                'error' => 'Insufficient funds'
            ], 400);
        }
 
        // Step 4: Check if enough stock
        if ($product->stock < $quantity) {
            return response()->json([
                'error' => 'Insufficient stock'
            ], 400);
        }
 
        // Step 5: Deduct from wallet
        $wallet->balance -= $amount;
        $wallet->save();
 
        // Step 6: Create the order
        $order = Order::create([
            'user_id' => $userId,
            'product_id' => $productId,
            'quantity' => $quantity,
            'amount' => $amount,
            'status' => 'completed'
        ]);
 
        // Step 7: Update inventory
        $product->stock -= $quantity;
        $product->save();
 
        return response()->json([
            'success' => true,
            'order' => $order
        ]);
    }
}

Додайте маршрут у routes/api.php:

Route::post('/checkout', [CheckoutController::class, 'checkout']);

Код виглядає чистим та логічним: "перевірити — потім виконати". Якщо запустити раз у браузері або Postman — все працює:

То у чому ж проблема?

Імітація одночасних запитів: момент лому

Проблема проявляється, коли одночасно надходять кілька запитів до одних даних. Створимо feature‑тест, який її виявить.

Створіть новий тест:

php artisan make:test CheckoutConcurrencyTest
<?php
 
namespace Tests\Feature;
 
use Tests\TestCase;
use App\Models\Wallet;
use App\Models\Product;
use App\Models\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
 
class CheckoutConcurrencyTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
 
        // Clean up collections before each test
        Wallet::truncate();
        Product::truncate();
        Order::truncate();
    }
 
    public function test_concurrent_checkout_reveals_race_condition()
    {
        // Create a product with only 1 item in stock
        $product = Product::create([
            'name' => 'Limited Edition Sneakers',
            'price' => 80.00,
            'stock' => 1
        ]);
 
        // Create 10 users, each with $100
        for ($i = 0; $i < 10; $i++) {
            Wallet::create([
                'user_id' => "user-{$i}",
                'balance' => 100.00
            ]);
        }
 
        // Simulate 10 users trying to buy the last item simultaneously
        $responses = [];
        $promises = [];
 
        for ($i = 0; $i < 10; $i++) {
            $promises[] = $this->postJson('/api/checkout', [
                'user_id' => "user-{$i}",
                'product_id' => $product->id,
                'quantity' => 1
            ]);
        }
 
        // Wait for all requests to complete
        foreach ($promises as $response) {
            $responses[] = $response;
        }
 
        // Check the results
        $product->refresh();
        $orderCount = Order::where('product_id', $product->id)->count();
 
        dump('=== RACE CONDITION RESULTS ===');
        dump('Stock remaining: ' . $product->stock);  // Expected: 0, Actual: negative!
        dump('Orders created: ' . $orderCount);        // Expected: 1, Actual: 10!
 
        // Check a sample user's wallet
        $wallet = Wallet::where('user_id', 'user-1')->first();
        dump('User-1 balance: ' . $wallet->balance);  // Could be anything!
 
        // These assertions will FAIL, proving the race condition
        $this->assertGreaterThan(1, $orderCount,
            'Race condition detected: Multiple orders created for 1 item in stock');
    }
}

Запустіть тест:

php artisan test --filter=test_concurrent_checkout_reveals_race_condition

Результати шокують:

=== RACE CONDITION RESULTS ===
Stock remaining: -9
Orders created: 10
User-1 balance: 20.00

Ми створили 10 замовлень на 1 товар. Інвентар став негативним. Декількох користувачів списали кошти, хоча товара вистачило лише на одного. Усе це — без жодної помилки в логах.

Що сталося

Прослідкуймо за двома паралельними запитами. Коротко:

Time    Request A (user-1)              Request B (user-2)              Database
----    ------------------              ------------------              --------
t1      Read wallet: $100                                               balance: $100
t2                                      Read wallet: $100               balance: $100
t3      Read stock: 1                                                   stock: 1
t4                                      Read stock: 1                   stock: 1
t5      Check: $100 >= $80 ✓
t6      Check: 1 >= 1 ✓
t7                                      Check: $100 >= $80 ✓
t8                                      Check: 1 >= 1 ✓
t9      Calculate: $100 - $80 = $20
t10     Save wallet: $20                                                balance: $20
t11                                     Calculate: $100 - $80 = $20
t12                                     Save wallet: $20                balance: $20 ❌
t13     Calculate: 1 - 1 = 0
t14     Save stock: 0                                                   stock: 0
t15                                     Calculate: 1 - 1 = 0
t16                                     Save stock: 0                   stock: 0 ❌

Обидва запити прочитали один і той самий стан раніше, ніж хтось записав зміни. Вони:

Запит B використав застарілі дані, і його запис стер результат A. Це і є race condition — коли правильність програми залежить від порядку й часу паралельних операцій.

Спокусливе, але недостатнє рішення

Можна подумати: «Додамо умовну перевірку у запит оновлення!» Спробуємо з where():

public function checkout(Request $request)
{
    // ... validation ...
 
    $wallet = Wallet::where('user_id', $userId)->first();
    $product = Product::find($productId);
    $amount = $product->price * $quantity;
 
    // Add a condition to ensure balance is sufficient at update time
    $walletUpdated = Wallet::where('user_id', $userId)
        ->where('balance', '>=', $amount)
        ->update(['balance' => $wallet->balance - $amount]);
 
    if ($walletUpdated === 0) {
        return response()->json(['error' => 'Insufficient funds'], 400);
    }
 
    // Similarly for stock
    $stockUpdated = Product::where('_id', $productId)
        ->where('stock', '>=', $quantity)
        ->update(['stock' => $product->stock - $quantity]);
 
    if ($stockUpdated === 0) {
        // Refund the wallet
        Wallet::where('user_id', $userId)
            ->update(['balance' => $wallet->balance]);
 
        return response()->json(['error' => 'Out of stock'], 400);
    }
 
    // Create order...
}

Це здається безпечнішим — перевірка прямо при оновленні. Але якщо прогнати тест конкуренції, помилки залишаться. Чому?

Проблема в тому, що ми усе ще рахуємо нове значення на основі застарілих даних:

$wallet = Wallet::where('user_id', $userId)->first();
// ↓
// Time passes here. Other requests might update the wallet.
// ↓
Wallet::where('user_id', $userId)
    ->where('balance', '>=', $amount)
    ->update(['balance' => $wallet->balance - $amount]);  // ← Using old data!

Умовний where('balance', '>=', $amount) допомагає, але ми все одно використовуємо в додатку зчитане раніше значення. За цей проміжок інший запит міг змінити balance.

Цей патерн має назву: read‑modify‑write (RMW). Це одна з найпоширеніших помилок при роботі з будь‑якою БД під конкурентним навантаженням.

Правильне рішення: атомарні операції MongoDB

Ключова думка: навіщо взагалі читати wallet?

MongoDB може виконувати обчислення прямо в базі атомарно, без нашого попереднього читання. Це усуває вікно race condition.

MongoDB має атомарні оператори — $inc, $set, $push та інші. Пакет MongoDB для Laravel дає доступ до них через метод raw().

Перепишемо checkout з використанням атомарних операцій:

<?php
 
namespace App\Http\Controllers;
 
use App\Models\Wallet;
use App\Models\Product;
use App\Models\Order;
use Illuminate\Http\Request;
use MongoDB\BSON\ObjectId;
 
class CheckoutController extends Controller
{
    public function checkout(Request $request)
    {
        $validated = $request->validate([
            'user_id' => 'required|string',
            'product_id' => 'required|string',
            'quantity' => 'required|integer|min:1',
        ]);
 
        $userId = $validated['user_id'];
        $productId = $validated['product_id'];
        $quantity = $validated['quantity'];
 
        // Get product price (read-only, doesn't need atomicity)
        $product = Product::findOrFail($productId);
        $amount = $product->price * $quantity;
 
        // ATOMIC OPERATION: Debit wallet
        // This performs the calculation inside MongoDB
        $walletResult = Wallet::raw(function ($collection) use ($userId, $amount) {
            return $collection->updateOne(
                [
                    'user_id' => $userId,
                    'balance' => ['$gte' => $amount]  // Only update if balance >= amount
                ],
                [
                    '$inc' => ['balance' => -$amount]  // Atomically decrement
                ]
            );
        });
 
        if ($walletResult->getModifiedCount() === 0) {
            return response()->json([
                'error' => 'Insufficient funds'
            ], 400);
        }
 
        // ATOMIC OPERATION: Decrease inventory
        $inventoryResult = Product::raw(function ($collection) use ($productId, $quantity) {
            return $collection->updateOne(
                [
                    '_id' => new ObjectId($productId),
                    'stock' => ['$gte' => $quantity]  // Only update if stock >= quantity
                ],
                [
                    '$inc' => ['stock' => -$quantity]  // Atomically decrement
                ]
            );
        });
 
        if ($inventoryResult->getModifiedCount() === 0) {
            // Out of stock! Rollback the wallet debit
            Wallet::raw(function ($collection) use ($userId, $amount) {
                return $collection->updateOne(
                    ['user_id' => $userId],
                    ['$inc' => ['balance' => $amount]]  // Refund
                );
            });
 
            return response()->json([
                'error' => 'Insufficient stock'
            ], 400);
        }
 
        // Create the order (no atomicity needed for insert)
        $order = Order::create([
            'user_id' => $userId,
            'product_id' => $productId,
            'quantity' => $quantity,
            'amount' => $amount,
            'status' => 'completed'
        ]);
 
        return response()->json([
            'success' => true,
            'order' => $order
        ]);
    }
}

Що робить цю операцію атомарною:

Оператор $inc:

['$inc' => ['balance' => -$amount]]

Говорить MongoDB: «Знайди документ і відніми $amount від поля balance — зроби це однією, неділимою операцією, не читаючи попереднє значення».

Умовне оновлення:

[
    'user_id' => $userId,
    'balance' => ['$gte' => $amount]
]

Це гарантує, що оновлення відбудеться тільки якщо балансу вистачає. Якщо між обчисленням і оновленням інший запит витратив кошти, getModifiedCount() поверне 0 — ми знаємо, що оновлення не пройшло.

Механізм відкату:

if ($inventoryResult->getModifiedCount() === 0) {
    Wallet::raw(function ($collection) use ($userId, $amount) {
        return $collection->updateOne(
            ['user_id' => $userId],
            ['$inc' => ['balance' => $amount]]
        );
    });
}

Якщо списали гроші, але інвентар виявився відсутнім, ми атомарно повертаємо суму назад.

Тестуємо атомарне рішення

Перевіримо, що воно працює. Оновимо наш тест:

public function test_atomic_operations_prevent_race_conditions()
{
    // Create test data
    $product = Product::create([
        'name' => 'Limited Edition Sneakers',
        'price' => 80.00,
        'stock' => 1
    ]);
 
    for ($i = 0; $i < 10; $i++) {
        Wallet::create([
            'user_id' => "user-{$i}",
            'balance' => 100.00
        ]);
    }
 
    // 10 concurrent checkout attempts
    $responses = [];
    for ($i = 0; $i < 10; $i++) {
        $responses[] = $this->postJson('/api/checkout', [
            'user_id' => "user-{$i}",
            'product_id' => $product->id,
            'quantity' => 1
        ]);
    }
 
    // Count successes and failures
    $successCount = collect($responses)
        ->filter(fn($r) => $r->status() === 200)
        ->count();
 
    $failureCount = collect($responses)
        ->filter(fn($r) => $r->status() === 400)
        ->count();
 
    // Check the results
    $product->refresh();
    $orderCount = Order::where('product_id', $product->id)->count();
 
    dump('=== ATOMIC OPERATIONS RESULTS ===');
    dump('Successful checkouts: ' . $successCount);
    dump('Failed checkouts: ' . $failureCount);
    dump('Stock remaining: ' . $product->stock);
    dump('Orders created: ' . $orderCount);
 
    // Assertions
    $this->assertEquals(1, $successCount, 'Exactly 1 checkout should succeed');
    $this->assertEquals(9, $failureCount, '9 checkouts should fail');
    $this->assertEquals(0, $product->stock, 'Stock should be 0');
    $this->assertEquals(1, $orderCount, 'Exactly 1 order should exist');
 
    // Verify the winning user's wallet
    $orders = Order::all();
    $winningUserId = $orders[0]->user_id;
    $winningWallet = Wallet::where('user_id', $winningUserId)->first();
    $this->assertEquals(20.00, $winningWallet->balance, 'Winner should have $20 left');
 
    // Verify all losing users still have $100
    $losingWallets = Wallet::where('user_id', '!=', $winningUserId)->get();
    foreach ($losingWallets as $wallet) {
        $this->assertEquals(100.00, $wallet->balance,
            "User {$wallet->user_id} should still have $100");
    }
}

Запустіть тест:

php artisan test --filter=test_atomic_operations_prevent_race_conditions

Результат:

=== ATOMIC OPERATIONS RESULTS ===
Successful checkouts: 1
Failed checkouts: 9
Stock remaining: 0
Orders created: 1
 
PASS  Tests\Feature\CheckoutConcurrencyTest
✓ atomic operations prevent race conditions

Ідеально — race condition зникла. Один користувач отримав товар, дев’ятьам повернули помилку «Insufficient stock», баланси вірні.

Інші атомарні оператори MongoDB

$inc підходить для нашого кейсу, але MongoDB має й інші оператори для різних задач:

$set — встановити значення поля:

Product::raw(function ($collection) use ($productId) {
    return $collection->updateOne(
        ['_id' => new ObjectId($productId)],
        ['$set' => ['featured' => true, 'updated_at' => new UTCDateTime()]]
    );
});

$push — додати в масив:

Order::raw(function ($collection) use ($orderId, $comment) {
    return $collection->updateOne(
        ['_id' => new ObjectId($orderId)],
        ['$push' => ['comments' => $comment]]
    );
});

$pull — прибрати з масиву:

User::raw(function ($collection) use ($userId, $itemId) {
    return $collection->updateOne(
        ['_id' => new ObjectId($userId)],
        ['$pull' => ['wishlist' => $itemId]]
    );
});

$mul — помножити значення поля:

Product::raw(function ($collection) use ($productId) {
    return $collection->updateOne(
        ['_id' => new ObjectId($productId)],
        ['$mul' => ['price' => 1.1]]  // 10% price increase
    );
});

$min та $max — оновлювати тільки якщо нове значення менше/більше:

Product::raw(function ($collection) use ($productId, $newPrice) {
    return $collection->updateOne(
        ['_id' => new ObjectId($productId)],
        ['$min' => ['lowest_price' => $newPrice]]  // Only update if newPrice is lower
    );
});

Ці оператори — інструменти для побудови операцій без race conditions у MongoDB.

Порівняння продуктивності: Eloquent vs атомарні операції

Порівняємо традиційний Eloquent‑підхід і атомарні оновлення:

public function test_performance_comparison()
{
    // Setup: 100 products and 100 users
    for ($i = 0; $i < 100; $i++) {
        Product::create([
            'name' => "Product {$i}",
            'price' => 50.00,
            'stock' => 100
        ]);
 
        Wallet::create([
            'user_id' => "user-{$i}",
            'balance' => 1000.00
        ]);
    }
 
    // Test 1: Eloquent read-modify-write (without race conditions for fair comparison)
    $startEloquent = microtime(true);
 
    for ($i = 0; $i < 100; $i++) {
        $wallet = Wallet::where('user_id', "user-{$i}")->first();
        $wallet->balance -= 50;
        $wallet->save();
    }
 
    $eloquentTime = (microtime(true) - $startEloquent) * 1000;
 
    // Reset wallets
    Wallet::raw(function ($collection) {
        return $collection->updateMany(
            [],
            ['$set' => ['balance' => 1000.00]]
        );
    });
 
    // Test 2: Atomic operations
    $startAtomic = microtime(true);
 
    for ($i = 0; $i < 100; $i++) {
        Wallet::raw(function ($collection) use ($i) {
            return $collection->updateOne(
                ['user_id' => "user-{$i}"],
                ['$inc' => ['balance' => -50]]
            );
        });
    }
 
    $atomicTime = (microtime(true) - $startAtomic) * 1000;
 
    dump("Eloquent: {$eloquentTime}ms");
    dump("Atomic: {$atomicTime}ms");
    dump("Improvement: " . round(($eloquentTime - $atomicTime) / $eloquentTime * 100, 1) . "%");
 
    $this->assertLessThan($eloquentTime, $atomicTime,
        'Atomic operations should be faster');
}

Типові результати:

Eloquent: 245ms
Atomic: 156ms
Improvement: 36.3%

Атомарні операції не лише безпечніші — вони часто швидші, бо прибирають потрібність читати поточне значення.

Кращі практики для атомарних операцій у Laravel

Підсумовуючи, кілька правил:

1. Використовуйте атомарні операції для числових змін при конкуренції:

// ✅ GOOD: Atomic
Wallet::raw(fn($c) => $c->updateOne(
    ['user_id' => $userId],
    ['$inc' => ['balance' => -$amount]]
));
 
// ❌ BAD: Read-modify-write
$wallet = Wallet::where('user_id', $userId)->first();
$wallet->balance -= $amount;
$wallet->save();

2. Завжди перевіряйте getModifiedCount(), щоб підтвердити успіх:

$result = Wallet::raw(function ($collection) use ($userId, $amount) {
    return $collection->updateOne(
        ['user_id' => $userId, 'balance' => ['$gte' => $amount]],
        ['$inc' => ['balance' => -$amount]]
    );
});
 
if ($result->getModifiedCount() === 0) {
    // Handle failure - either insufficient balance or user doesn't exist
}

3. Використовуйте умовні оновлення для бізнес‑правил:

// Only decrement if stock is available
$result = Product::raw(function ($collection) use ($productId, $quantity) {
    return $collection->updateOne(
        [
            '_id' => new ObjectId($productId),
            'stock' => ['$gte' => $quantity],
            'status' => 'active'  // Additional business rule
        ],
        ['$inc' => ['stock' => -$quantity]]
    );
});

4. Реалізуйте механізми відкату:

// If subsequent operation fails, rollback the first
if ($inventoryResult->getModifiedCount() === 0) {
    Wallet::raw(fn($c) => $c->updateOne(
        ['user_id' => $userId],
        ['$inc' => ['balance' => $amount]]  // Refund
    ));
}

5. Знайте, коли Eloquent підходить:

// ✅ GOOD: Single insert, no race condition possible
Order::create([
    'user_id' => $userId,
    'amount' => $amount
]);
 
// ✅ GOOD: Read-only operation
$product = Product::find($productId);

Коли використовувати Eloquent, а коли — атомарні операції

Коротке дерево рішень:

Використовуйте Eloquent, коли:

Використовуйте атомарні операції, коли:

Висновки

FAQ

Чим відрізняються оновлення через Eloquent і атомарні операції в MongoDB?

Eloquent використовує патерн read‑modify‑write: зчитує документ, змінює його в PHP і зберігає назад — це створює вікно race condition. Атомарні операції (наприклад, $inc) змінюють документ без попереднього читання, роблячи операцію неділимою. Використовуйте Eloquent, коли гонки не є проблемою; атомарні операції — для конкурентних сценаріїв.

Як тестувати race conditions у Laravel + MongoDB?

Створіть feature‑тест, що відправляє багато одночасних запитів до одного endpoint. У циклі надішліть 10–50 POST‑запитів на одну й ту саму ресурсну операцію (наприклад, купити останню одиницю товару). Після завершення перевірте консистентність: має бути саме одне замовлення, без негативних балансів, коректний інвентар. Запускайте ці тести через php artisan test.

Чи можна комбінувати атомарні операції з Eloquent‑відношеннями?

Метод raw() обходить систему відношень Eloquent, тому роботу з відношеннями доведеться робити вручну. Ви можете використовувати Eloquent для отримання суміжних даних до або після атомарної операції. Загалом — атомарні операції для критичних числових оновлень, Eloquent — для решти логіки.

Які ще атомарні оператори MongoDB доступні в Laravel?

Окрім $inc, є: $set, $unset, $push, $pull, $mul, $min, $max, $currentDate та багато інших. Усі доступні через raw(). Деталі — у документації MongoDB.

Коли краще використовувати Eloquent замість raw MongoDB операцій?

Використовуйте Eloquent для вставок, читань, оновлень полів, де нове значення не залежить від старого, а також коли важлива зручність моделей, подій, аксесорів і відношень. Використовуйте raw() для гарантії консистентності під конкуренцією — фінанси, інвентар, лічильники. Eloquent читабельніший, тож віддавайте йому перевагу, якщо атомарність не потрібна.

Що далі

Ми вирішили проблему гонок для операцій над одним документом за допомогою атомарних оновлень. Але що робити, коли checkout має атомарно змінити декілька документів — wallet, inventory і створити order — і при будь‑якій помилці все треба відкотити?

У наступній статті — "Building Transaction‑Safe Multi‑Document Operations in Laravel MongoDB" — розглянемо ACID‑транзакції MongoDB для багатодокументних операцій і коли атомарних операторів замало.

Ресурси

Популярні

Logomark Logotype

Інтеграція Laravel Socialite з бібліотекою Google Client PHP

Ви хочете навчитися, як інтегрувати Google OAuth у вашому проекті Laravel, використовуючи Socialite? Дізнайтеся, як налаштувати доступ до сервісів Google, таких як Календар, у нашій сьогоднішній статті

Logomark Logotype

"SQLSTATE[HY000] [2002] Connection refused" у Laravel в GitHub Actions

Чи стикалися ви з помилкою «SQLSTATE[HY000] [2002] Connection refused» під час налаштування GitHub Actions для вашого додатку на Laravel? У нашій статті ми розглянемо три поширені причини цієї помилки та надамо рішення для їх усунення. Читайте далі, щоб дізнатися, як ваш CI/CD потік може працювати бездоганно!

Logomark Logotype

Що нового в PHP 8.5

PHP 8.5 обіцяє безліч нових можливостей, таких як оператор Pipe, функції `array_first()` та `array_last()`, а також нове розширення URI. Чи готові ви дізнатися, як ці функції можуть спростити вашу розробку? Читайте далі, щоб дізнатися більше про ці захоплюючі нововведення