第三章: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 避坑指南

最后,分享几个我踩过的坑:

  1. LIKE查询也要参数化SELECT * FROM users WHERE name LIKE CONCAT('%', ?, '%'),别把通配符拼到参数里。
  2. IN查询的参数化WHERE id IN (?, ?, ?),参数个数要动态生成占位符。
  3. ORDER BY和表名不能参数化:这些是SQL结构的一部分,只能用白名单校验。
  4. 事务中的CRUD:多条操作要放在事务里,保证原子性。

我曾经犯过的错:有一次在循环里执行SQL,每次查询都建立新连接。结果数据库连接数直接爆了。记住:连接要复用,查询要批量。

好了,SQL基础和CRUD操作就讲到这里。下一章咱们会深入Node.js的数据库驱动,看看连接池、事务这些高级特性怎么用。记住我今天说的:参数化查询是底线,WHERE条件是生命线。这两条守住了,你的数据库操作就稳了一大半。