第三章:SQL基础与CRUD操作
说实话,很多Node.js开发者一开始都想着跳过SQL,直接上ORM。我当年也这么干过。直到有一次线上事故,一条慢查询把数据库拖垮了,我才老老实实回来补SQL基础。
这一章,咱们就把CRUD操作彻底讲透。你想想看,无论你用Sequelize还是TypeORM,底层最终都会翻译成这些SQL语句。不懂SQL,你连ORM报错都看不懂。
3.1 SELECT:查询的艺术
SELECT是SQL里最常用的语句。说白了,就是从表里拿数据。
-- 最简单的查询
SELECT * FROM users;
-- 指定字段,我建议永远不要用*
SELECT id, name, email FROM users;
-- 带条件查询
SELECT id, name, email FROM users WHERE status = 'active';
我在项目中见过太多人用SELECT *了。有一次查一个日志表,字段有50多个,结果集动不动就几万行。前端页面直接卡死。嗯,这里要注意:只查你需要的字段,这是最基本的性能意识。
核心原则:能用WHERE过滤的,绝不在代码里过滤。数据库擅长做这个,别把压力转嫁给应用层。
3.2 INSERT:插入数据的门道
插入数据看起来简单,但坑不少。
-- 单条插入
INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@example.com', 28);
-- 批量插入(性能更好)
INSERT INTO users (name, email, age) VALUES
('李四', 'lisi@example.com', 32),
('王五', 'wangwu@example.com', 25),
('赵六', 'zhaoliu@example.com', 30);
批量插入的性能优势很明显。我曾经优化过一个导入功能,原来是一条一条INSERT,导入1万条数据要40秒。改成批量插入后,直接降到2秒。差别就这么大。
小技巧:批量插入时,一次不要超过1000条。太多的话,数据库的binlog会暴涨,主从同步也可能延迟。
3.3 UPDATE:更新操作要谨慎
UPDATE是最容易出事的操作。我见过有人忘记加WHERE条件,直接把整张表的数据都改了。
-- 正确写法:一定要加WHERE
UPDATE users SET age = 29 WHERE id = 1;
-- 更新多个字段
UPDATE users
SET name = '张三丰', email = 'zhangsf@example.com'
WHERE id = 1;
-- 带条件的更新
UPDATE users
SET status = 'inactive'
WHERE last_login < '2024-01-01' AND status = 'active';
警告:生产环境执行UPDATE之前,先跑一遍SELECT确认WHERE条件。我习惯的做法是:先写SELECT count(*)看看影响多少行,再改成UPDATE。这个习惯救过我很多次。
3.4 DELETE:删除数据要三思
DELETE和UPDATE一样,WHERE条件是命根子。
-- 删除单条记录
DELETE FROM users WHERE id = 1;
-- 批量删除
DELETE FROM users WHERE status = 'deleted' AND created_at < '2023-01-01';
-- 清空表(慎用!)
TRUNCATE TABLE users;
TRUNCATE和DELETE的区别你知道吗?TRUNCATE是DDL操作,不能回滚,而且会重置自增ID。DELETE是DML操作,可以回滚。我曾经在测试环境用TRUNCATE清空了一张表,结果发现搞错了库...嗯,从那以后我再也不敢在生产环境用TRUNCATE了。
3.5 参数化查询:防SQL注入的命门
这是本章最重要的内容。SQL注入是什么?说白了,就是攻击者把SQL代码塞到你的输入里,让你的数据库执行不该执行的命令。
反面教材:千万别这么写!
// ❌ 危险!字符串拼接SQL
const sql = `SELECT * FROM users WHERE name = '${userInput}'`;
// 如果userInput = "' OR '1'='1",你的用户表就裸奔了
正确的做法是参数化查询。Node.js里怎么搞?
// ✅ 使用mysql2的参数化查询
const mysql = require('mysql2/promise');
async function getUserByName(name) {
const sql = 'SELECT * FROM users WHERE name = ?';
const [rows] = await connection.execute(sql, [name]);
return rows;
}
// ✅ 多条查询
async function createUser(name, email, age) {
const sql = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
const [result] = await connection.execute(sql, [name, email, age]);
return result.insertId;
}
为什么参数化查询能防注入?因为数据库会把参数当成数据,而不是SQL代码。你传进去的' OR '1'='1,数据库会把它当成一个字符串值去匹配,而不是当成SQL逻辑去执行。
个人习惯:我写Node.js项目时,所有数据库操作都用参数化查询。哪怕是一个简单的查询,也绝不偷懒用字符串拼接。这个习惯让我在多次安全审计中全身而退。
3.6 综合示例:一个完整的CRUD
咱们来写一个完整的用户管理模块,把今天学的都用上。
const mysql = require('mysql2/promise');
class UserRepository {
constructor(connection) {
this.conn = connection;
}
// 查询用户
async findById(id) {
const sql = 'SELECT id, name, email, age FROM users WHERE id = ?';
const [rows] = await this.conn.execute(sql, [id]);
return rows[0] || null;
}
// 创建用户
async create(userData) {
const { name, email, age } = userData;
const sql = 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)';
const [result] = await this.conn.execute(sql, [name, email, age]);
return result.insertId;
}
// 更新用户
async update(id, userData) {
const fields = [];
const values = [];
if (userData.name !== undefined) {
fields.push('name = ?');
values.push(userData.name);
}
if (userData.email !== undefined) {
fields.push('email = ?');
values.push(userData.email);
}
if (userData.age !== undefined) {
fields.push('age = ?');
values.push(userData.age);
}
if (fields.length === 0) return false;
values.push(id);
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
const [result] = await this.conn.execute(sql, values);
return result.affectedRows > 0;
}
// 删除用户
async delete(id) {
const sql = 'DELETE FROM users WHERE id = ?';
const [result] = await this.conn.execute(sql, [id]);
return result.affectedRows > 0;
}
}
这个例子展示了几个要点:
- 所有SQL都用参数化查询,杜绝注入风险
- UPDATE时动态拼接字段,但值仍然通过参数传入
- 每个方法都返回明确的结果,方便调用方处理
3.7 避坑指南
最后,分享几个我踩过的坑:
- LIKE查询也要参数化:
SELECT * FROM users WHERE name LIKE CONCAT('%', ?, '%'),别把通配符拼到参数里。 - IN查询的参数化:
WHERE id IN (?, ?, ?),参数个数要动态生成占位符。 - ORDER BY和表名不能参数化:这些是SQL结构的一部分,只能用白名单校验。
- 事务中的CRUD:多条操作要放在事务里,保证原子性。
我曾经犯过的错:有一次在循环里执行SQL,每次查询都建立新连接。结果数据库连接数直接爆了。记住:连接要复用,查询要批量。
好了,SQL基础和CRUD操作就讲到这里。下一章咱们会深入Node.js的数据库驱动,看看连接池、事务这些高级特性怎么用。记住我今天说的:参数化查询是底线,WHERE条件是生命线。这两条守住了,你的数据库操作就稳了一大半。