Laravel 11 原生SQL语句执行

虽然 Laravel 的查询构建器和 Eloquent ORM 提供了丰富的数据库操作方法,但有时你仍然需要直接执行原生 SQL 语句,比如使用数据库特定的函数、复杂查询或优化性能。 Laravel 的 DB Facade 提供了多种方法来安全地执行原生 SQL,并支持参数绑定以防止 SQL 注入。

🎯 为什么要执行原生 SQL?

  • 使用数据库特有的函数或语法(如 MySQL 的 JSON_EXTRACT、PostgreSQL 的 tsvector)。
  • 优化复杂查询,避免查询构建器生成低效的 SQL。
  • 执行存储过程或调用数据库内置功能。
  • 进行批量操作(如 INSERT ... SELECT)。
💡 提示: 原生 SQL 虽灵活,但应尽量使用查询构建器或 Eloquent,以保持代码可读性和数据库无关性。

📦 使用 DB Facade

首先,导入 Illuminate\Support\Facades\DB 门面:

use Illuminate\Support\Facades\DB;

然后,可以通过 DB:: 调用各种方法执行 SQL。

🔍 查询语句(SELECT)

DB::select() 方法用于执行 SELECT 查询,返回一个数组,每个元素是 stdClass 对象:

$users = DB::select('SELECT * FROM users WHERE active = ?', [1]);

foreach ($users as $user) {
    echo $user->name;
}

你也可以使用命名绑定:

$users = DB::select('SELECT * FROM users WHERE age > :age', ['age' => 18]);

如果不需要参数,可以省略第二个参数:

$users = DB::select('SELECT * FROM users');

✍️ 插入语句(INSERT)

DB::insert() 执行 INSERT 语句,返回布尔值表示是否成功:

$inserted = DB::insert('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com']);

如果需要获取自增 ID,可以使用 DB::getPdo()->lastInsertId()

DB::insert('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com']);
$id = DB::getPdo()->lastInsertId();

🔄 更新语句(UPDATE)

DB::update() 执行更新,返回受影响的行数:

$affected = DB::update('UPDATE users SET age = ? WHERE id = ?', [30, 1]);

❌ 删除语句(DELETE)

DB::delete() 执行删除,返回受影响的行数:

$deleted = DB::delete('DELETE FROM users WHERE active = 0');

🔧 通用语句(无返回结果)

DB::statement() 用于执行不返回结果集的 SQL,如 CREATE TABLEALTERDROP 等:

DB::statement('DROP TABLE temp_users');

也可用于执行存储过程:

DB::statement('CALL refresh_user_stats(?)', [1]);

🔐 参数绑定与安全

上述方法都支持参数绑定,可以有效防止 SQL 注入。请务必使用问号占位符或命名占位符,切勿直接拼接用户输入

// ❌ 危险!不要这样做
DB::select("SELECT * FROM users WHERE name = '$name'");

// ✅ 正确:使用参数绑定
DB::select('SELECT * FROM users WHERE name = ?', [$name]);
⚠️ 注意: 使用 DB::raw() 或直接拼接字符串会绕过参数绑定,除非你完全确定内容安全,否则强烈推荐使用绑定。

📁 数据库事务

当需要确保多个 SQL 操作原子性时,可以使用事务。Laravel 提供了简洁的事务方法:

自动事务
DB::transaction(function () {
    DB::update('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
    DB::update('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
});

如果闭包内抛出异常,事务会自动回滚;否则自动提交。

手动事务
DB::beginTransaction();

try {
    DB::update('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
    DB::update('UPDATE accounts SET balance = balance + 100 WHERE id = 2');

    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    throw $e;
}

🔍 监听查询日志

调试时可以监听所有执行的 SQL:

临时启用日志
DB::enableQueryLog();

DB::select('SELECT * FROM users');
DB::update('UPDATE users SET age = ? WHERE id = ?', [30, 1]);

$queries = DB::getQueryLog();
foreach ($queries as $query) {
    echo $query['query'] . ' 绑定的参数: ' . implode(', ', $query['bindings']) . "\n";
}
使用事件监听(推荐)

在服务提供者中注册监听器,可以持久记录所有查询:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

public function boot()
{
    DB::listen(function ($query) {
        Log::info($query->sql, $query->bindings);
    });
}

🎨 实战示例:批量导入与存储过程

批量插入(使用原生 SQL 提升性能)

$chunks = collect($data)->chunk(1000);
foreach ($chunks as $chunk) {
    $values = implode(',', array_fill(0, count($chunk), '(?, ?)'));
    $params = [];
    foreach ($chunk as $item) {
        $params[] = $item['name'];
        $params[] = $item['email'];
    }
    DB::insert("INSERT INTO users (name, email) VALUES {$values}", $params);
}

调用存储过程

DB::statement('CALL update_user_rank(?, ?)', [$userId, $newRank]);

执行复杂查询并返回结果

$reports = DB::select('
    SELECT department, COUNT(*) as employee_count, AVG(salary) as avg_salary
    FROM employees
    WHERE hire_date >= ?
    GROUP BY department
    HAVING employee_count > ?
', ['2023-01-01', 5]);

⚖️ 查询构建器 vs 原生 SQL

实例
特性 查询构建器 原生 SQL
可读性 高,链式方法 取决于 SQL 复杂度
SQL 注入防护 自动参数绑定 需手动绑定
数据库无关性 高,自动转换语法 低,需适配不同数据库
性能 中等(额外解析) 直接执行,可能更优
适用场景 常规 CRUD 复杂查询、存储过程、批量操作
💡 最佳实践: 优先使用查询构建器或 Eloquent,仅在必要时使用原生 SQL。对于复杂查询,可先构建原生 SQL,然后通过参数绑定执行,以平衡安全与性能。

❓ 常见问题

表示查询没有匹配任何记录。不会返回 null,始终是数组。可以通过 empty($result) 判断是否有数据。

表名和字段名不能使用参数绑定,但可以使用白名单验证后直接拼接。例如:$allowedTables = ['users', 'orders']; if (in_array($table, $allowedTables)) { DB::select("SELECT * FROM `$table`"); }

使用 DB::listen 事件,每个查询对象包含 time 属性(以毫秒为单位)。或者使用 DB::enableQueryLog() 后查看日志中的时间。

📝 小结

Laravel 允许你通过 DB Facade 灵活执行原生 SQL,同时提供了参数绑定、事务和日志功能来保障安全性和可调试性。 掌握这些方法,可以在需要高性能或复杂数据库操作时游刃有余。但请记住,保持代码清晰和可维护性同样重要,不要滥用原生 SQL。 下一章我们将学习 Eloquent ORM 的基础,体验 Laravel 中最优雅的数据库交互方式。