You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel中带WHERE子查询的复杂INSERT语句实现方案问询

用Laravel Query Builder和Eloquent实现你的交易插入逻辑

我明白你想实现的核心需求:只有当用户还没有持有某个商品时(也就是该商品的买入和卖出交易抵消后净持有量为0),才允许插入新的购买交易。原生SQL用了INSERT...SELECT的原子操作来避免竞态问题,下面我帮你把这个逻辑转换成Laravel的Query Builder和Eloquent写法,两种方式各有适用场景:

一、Query Builder 写法(贴近原生SQL,原子操作)

这种方式直接对应你的原生SQL,利用insertUsing方法实现INSERT...SELECT,是原子性的,不会出现高并发下的竞态问题(比如两个请求同时检测到净持有量为0然后都插入)。

简洁版

use Illuminate\Support\Facades\DB;

$userId = 1;
$itemId = 186808;

DB::table('transactions')
    ->insertUsing(
        ['user_id', 'item_id', 'type', 'created_at', 'updated_at'],
        DB::table('transactions')
            ->selectRaw('?, ?, ?, NOW(), NOW()', [$userId, $itemId, 'bought'])
            ->whereRaw(
                '(SELECT SUM(CASE WHEN type = ? THEN 1 WHEN type = ? THEN -1 END) FROM transactions WHERE user_id = ? AND item_id = ?) = 0',
                ['bought', 'sold', $userId, $itemId]
            )
    );

更易读的结构化版本

把净持有量的子查询单独抽出来,代码更清晰:

use Illuminate\Support\Facades\DB;

$userId = 1;
$itemId = 186808;

// 构建计算净持有量的子查询
$netHoldingSubquery = DB::table('transactions')
    ->where('user_id', $userId)
    ->where('item_id', $itemId)
    ->selectRaw('SUM(CASE WHEN type = "bought" THEN 1 WHEN type = "sold" THEN -1 END) as net');

// 执行带条件的插入
DB::table('transactions')
    ->insertUsing(
        ['user_id', 'item_id', 'type', 'created_at', 'updated_at'],
        DB::table('transactions')
            ->select(
                DB::raw("$userId as user_id"),
                DB::raw("$itemId as item_id"),
                DB::raw('"bought" as type'),
                DB::raw('NOW() as created_at'),
                DB::raw('NOW() as updated_at')
            )
            ->whereExists(function ($query) use ($netHoldingSubquery) {
                $query->select(DB::raw(1))
                    ->fromSub($netHoldingSubquery, 'sub')
                    ->where('sub.net', 0);
            })
    );

二、Eloquent 写法(面向对象风格)

如果你习惯用Eloquent模型操作,可以先查询净持有量,再判断是否插入。不过要注意,这种“先查后插”的方式在高并发场景下可能有竞态问题,需要配合事务和锁来保证原子性。

基础版(适合低并发场景)

use App\Models\Transaction;

$userId = 1;
$itemId = 186808;

// 计算用户对该商品的净持有量,默认0(如果没有任何交易记录)
$netHolding = Transaction::where('user_id', $userId)
    ->where('item_id', $itemId)
    ->selectRaw('SUM(CASE WHEN type = "bought" THEN 1 WHEN type = "sold" THEN -1 END) as net')
    ->value('net') ?? 0;

// 只有净持有量为0时才插入购买记录
if ($netHolding === 0) {
    Transaction::create([
        'user_id' => $userId,
        'item_id' => $itemId,
        'type' => 'bought',
        // 可以不用手动设置created_at和updated_at,只要模型开启了时间戳(默认开启)
    ]);
}

高并发安全版(带事务和锁)

用lockForUpdate锁住查询的交易记录,防止其他事务在这段时间内修改数据,保证整个操作的原子性:

use App\Models\Transaction;
use Illuminate\Support\Facades\DB;

$userId = 1;
$itemId = 186808;

DB::transaction(function () use ($userId, $itemId) {
    // 锁住该用户该商品的所有交易记录,防止并发修改
    $netHolding = Transaction::where('user_id', $userId)
        ->where('item_id', $itemId)
        ->lockForUpdate()
        ->selectRaw('SUM(CASE WHEN type = "bought" THEN 1 WHEN type = "sold" THEN -1 END) as net')
        ->value('net') ?? 0;

    if ($netHolding === 0) {
        Transaction::create([
            'user_id' => $userId,
            'item_id' => $itemId,
            'type' => 'bought',
        ]);
    }
});

两种方式对比

  • Query Builder的insertUsing:完全对应原生SQL的原子操作,性能好,无竞态问题,适合高并发场景。
  • Eloquent写法:更符合Laravel的面向对象风格,代码可读性高,但需要额外处理并发问题,适合低并发或者需要在插入前后做更多业务逻辑的场景。

内容的提问来源于stack exchange,提问作者Sakibul Alam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:37:16