文章

MySQL基础深度解析

MySQL基础深度解析

一句话概括

MySQL 是世界上最流行的开源关系型数据库,本文从前端开发者的视角出发,用 JavaScript/Node.js 的类比来讲解 MySQL 的核心概念——CRUD 操作、索引原理和事务机制,帮助前端同学快速建立后端数据库的思维模型。

背景与意义

作为一个前端开发者,你可能会想:「我为什么要学数据库?」

看看你日常开发中会遇到的场景:登录注册需要查询用户表、商品列表需要分页查询、文章详情需要 JOIN 关联查询、点赞需要事务保证一致性……现代前端开发早已不是「切页面调接口」的简单工作,前后端分离、SSR(服务端渲染)、全栈开发的发展趋势,要求前端工程师对后端技术有足够的理解。

更重要的是,掌握了数据库思维,你会发现前端和后端在数据层面是相通的:Redux Store 可以看作内存中的「数据库」、Vuex 的 mutations 就像事务操作、组件间的状态管理其实就是一个「查询与更新的过程」。从数据库的角度去理解后端,会让你的全栈能力产生质的飞跃。

概念与定义

数据库(Database):按照数据结构来组织、存储和管理数据的仓库。类比:你手机里的「相册」App——照片是数据,按相册分类就是数据库的表。

表(Table):数据库中存储数据的载体,由行和列组成。类比:Excel 表格的 sheet。每一列(Column)是字段,每一行(Row)是一条记录。

SQL(Structured Query Language):结构化查询语言,用于与数据库通信的标准语言。CRUD 是其中最基本的四种操作。

索引(Index):加速数据检索的数据结构,类似于书的目录。没有索引就像从一本没有目录的书中找某个关键词——需要翻遍整本书。

事务(Transaction):一组逻辑上不可分割的数据库操作,要么全部成功,要么全部失败。类比:支付宝转账——扣款和收款必须同时成功或同时失败。

核心知识点拆解

1. CRUD 操作:从 JavaScript 视角理解

对于前端开发者,可以把 MySQL 的表想象成一个「持久化的 JSON 数组」,CRUD 就是我们每天在数组中做的增删改查操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
-- 创建一个用户表
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  email VARCHAR(100) NOT NULL,
  age INT DEFAULT 18,
  city VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ======== CREATE(增) ========

-- 插入单条记录
INSERT INTO users (username, email, age, city) 
VALUES ('alice', 'alice@example.com', 25, 'Beijing');

-- 插入多条记录
INSERT INTO users (username, email, age, city) VALUES 
('bob', 'bob@example.com', 30, 'Shanghai'),
('charlie', 'charlie@example.com', 22, 'Guangzhou'),
('diana', 'diana@example.com', 28, 'Shenzhen');

-- ======== READ(查) ========

-- 查询所有列的所有记录
SELECT * FROM users;

-- 查询特定列
SELECT username, email FROM users;

-- 条件查询(WHERE)
SELECT * FROM users WHERE age > 25;

-- 多条件查询(AND/OR)
SELECT * FROM users 
WHERE city = 'Beijing' AND age BETWEEN 20 AND 30;

-- 模糊查询(LIKE)
SELECT * FROM users WHERE email LIKE '%@example.com';

-- 排序(ORDER BY)
SELECT * FROM users ORDER BY age DESC LIMIT 3;

-- 分组统计(GROUP BY)
SELECT city, COUNT(*) as user_count, AVG(age) as avg_age
FROM users
GROUP BY city
HAVING user_count >= 1;

-- ======== UPDATE(改) ========

-- 更新(⚠️ 务必加 WHERE,否则更新全表!)
UPDATE users SET age = 26 WHERE username = 'alice';

-- 多字段更新
UPDATE users 
SET city = 'Hangzhou', age = age + 1 
WHERE username = 'bob';

-- ======== DELETE(删) ========

-- 删除记录(⚠️ 务必加 WHERE!)
DELETE FROM users WHERE username = 'charlie';

-- 清空表(快速删除所有记录,重置自增 ID)
TRUNCATE TABLE users;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 前端类比:用 JavaScript 理解 SQL CRUD
 */
const users = [
  { id: 1, username: 'alice', email: 'alice@example.com', age: 25, city: 'Beijing' },
  { id: 2, username: 'bob', email: 'bob@example.com', age: 30, city: 'Shanghai' },
];

// INSERT INTO users (username, email) VALUES ('alice', 'alice@example.com')
users.push({ id: 3, username: 'charlie', email: 'charlie@example.com' });

// SELECT * FROM users WHERE city = 'Beijing'
users.filter(u => u.city === 'Beijing');

// SELECT username, email FROM users ORDER BY age DESC LIMIT 2
users
  .sort((a, b) => b.age - a.age)
  .slice(0, 2)
  .map(u => ({ username: u.username, email: u.email }));

// UPDATE users SET age = 26 WHERE username = 'alice'
const target = users.find(u => u.username === 'alice');
if (target) target.age = 26;

// DELETE FROM users WHERE username = 'charlie'
const idx = users.findIndex(u => u.username === 'charlie');
if (idx !== -1) users.splice(idx, 1);

在 Node.js 中使用 MySQL(mysql2 驱动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const mysql = require('mysql2/promise');

async function demo() {
  // 连接数据库
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'myapp',
  });

  // 查询所有用户
  const [rows] = await connection.execute(
    'SELECT * FROM users WHERE age > ? ORDER BY created_at DESC',
    [18]
  );
  console.log('查询结果:', rows);

  // 插入新用户
  const [result] = await connection.execute(
    'INSERT INTO users (username, email, age, city) VALUES (?, ?, ?, ?)',
    ['eve', 'eve@example.com', 27, 'Chengdu']
  );
  console.log('插入成功,ID:', result.insertId);

  // 更新(使用参数化查询防止 SQL 注入)
  await connection.execute(
    'UPDATE users SET age = ? WHERE username = ?',
    [28, 'eve']
  );

  // 删除
  await connection.execute(
    'DELETE FROM users WHERE username = ?',
    ['eve']
  );

  // 事务操作
  await connection.execute('START TRANSACTION');
  try {
    await connection.execute(
      'UPDATE accounts SET balance = balance - 100 WHERE id = 1'
    );
    await connection.execute(
      'UPDATE accounts SET balance = balance + 100 WHERE id = 2'
    );
    await connection.execute('COMMIT');
  } catch (err) {
    await connection.execute('ROLLBACK');
    console.error('事务回滚:', err);
  }

  await connection.end();
}

2. 索引:提升查询速度的关键

索引是 MySQL 性能优化的第一手段。没有索引时 MySQL 需要「全表扫描」——就像你在 10 万张照片中找一张自拍,只能一张一张翻。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- 创建索引的常用方式

-- 方式 1:创建表时定义索引
CREATE TABLE products (
  id INT AUTO_INCREMENT PRIMARY KEY,  -- 主键自动成为聚簇索引
  name VARCHAR(100),
  category_id INT,
  price DECIMAL(10, 2),
  created_at TIMESTAMP,
  -- 普通索引
  INDEX idx_category (category_id),
  -- 唯一索引
  UNIQUE INDEX idx_name (name),
  -- 复合索引(联合索引)
  INDEX idx_category_price (category_id, price)
);

-- 方式 2:表创建后添加索引
ALTER TABLE products ADD INDEX idx_created (created_at);
CREATE INDEX idx_price ON products (price);

-- ======== 索引使用验证 ========

-- 使用 EXPLAIN 分析查询是否使用了索引
EXPLAIN SELECT * FROM products WHERE category_id = 10;

-- 复合索引的最左前缀法则
-- 对于 idx_category_price (category_id, price):
-- ✅ 使用了索引: WHERE category_id = ?
-- ✅ 使用了索引: WHERE category_id = ? AND price > ?
-- ✅ 使用了索引: WHERE category_id = ? ORDER BY price
-- ❌ 不使用索引: WHERE price > ?(跳过了最左列)
-- ❌ 不使用索引: WHERE category_id = ? OR price > ?

-- ======== 索引的代价 ========
-- 索引不是越多越好:
-- 1. 占用磁盘空间
-- 2. 降低写入速度(每次 INSERT/UPDATE/DELETE 都要更新索引)
-- 3. 查询优化器可能选错索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 前端类比:用 JavaScript 理解索引的作用
 */

// 没有索引 = 全表扫描(线性搜索)
function findUserWithoutIndex(users, username) {
  // O(n) — 遍历所有用户
  for (const user of users) {
    if (user.username === username) return user;
  }
  return null;
}

// 有索引 = 哈希表/二叉搜索树
const userIndex = new Map(); // 类似于哈希索引
function buildIndex(users) {
  for (const user of users) {
    userIndex.set(user.username, user);
  }
}

function findUserWithIndex(username) {
  // O(1) — 直接查索引
  return userIndex.get(username);
}

// B+ 树索引:类似于 JavaScript 中的有序 Map
// 适合范围查询 >, <, BETWEEN, ORDER BY
const sortedMap = new Map();
// sortedMap.keys() 是自动排序的,类似于 B+ 树

B+ 树索引的关键特性(知道这些就够了)

特性说明前端类比
多叉平衡树每个节点有多个子节点,树的高度通常仅 2-4 层类 CSS 选择器的层叠规则
叶子节点存数据所有实际数据都在叶子节点Map 的 values
叶子节点链表连接叶子节点相互链接,支持范围扫描链表结构
自平衡插入/删除自动保持平衡Redux 不可变更新的树比较
聚簇索引主键索引的叶子节点直接存整行数据数组的索引直接对应元素
二级索引非主键索引的叶子节点存主键值(需要回表)引用类型的指针

3. 事务:保证数据的一致性与完整性

事务是数据库操作不可分割的基本单位。想象一下银行转账:A 转 100 元给 B,这个操作包含两个步骤——A 减 100、B 加 100。如果第一步成功了第二步失败,钱就凭空消失了。事务确保这两个步骤要么全部成功,要么全部失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
-- 事务的 ACID 特征

-- 基本使用
START TRANSACTION;

-- 执行一系列操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 要么全部提交
COMMIT;

-- 要么全部回滚
ROLLBACK;

-- ======== 事务隔离级别 ========

-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别(会话级别)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- MySQL 支持四种隔离级别:
-- 1. READ UNCOMMITTED(读未提交)—— 可能读到未提交的数据(脏读)
-- 2. READ COMMITTED(读已提交)—— 避免脏读,但可能不可重复读
-- 3. REPEATABLE READ(可重复读)—— MySQL 默认,避免脏读和不可重复读
-- 4. SERIALIZABLE(串行化)—— 最高级别,避免所有问题,但性能最差

-- ======== 事务中的锁 ========

-- 共享锁(S Lock):SELECT ... LOCK IN SHARE MODE
-- 排他锁(X Lock):SELECT ... FOR UPDATE

-- 悲观锁示例
START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
-- 其他事务查询同一条记录会被阻塞
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 1;
COMMIT;

-- 乐观锁示例(通过版本号实现)
-- 假设表有 version 字段
UPDATE inventory 
SET quantity = quantity - 1, version = version + 1 
WHERE product_id = 1 AND version = 3;
-- 如果 affected_rows === 0,说明版本号不匹配,其他人已经修改过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
 * 前端类比:用 JavaScript 模拟事务
 */
class SimpleTransaction {
  constructor(db) {
    this.db = db;
    this.operations = [];
    this.committed = false;
  }

  async execute(sql, params) {
    // 记录操作,不真正执行
    this.operations.push({ sql, params });
  }

  async commit() {
    if (this.operations.length === 0) return;
    
    // 模拟事务:尝试执行所有操作
    try {
      const snapshots = [];
      
      for (const op of this.operations) {
        // 执行前保存快照(用于回滚)
        snapshots.push(await this.db.takeSnapshot(op));
      }
      
      // 如果全部成功,标记完成
      this.committed = true;
      console.log('事务提交成功');
      
    } catch (err) {
      // 如果任何一个操作失败,回滚所有操作
      await this.rollback();
      throw err;
    }
  }

  async rollback() {
    // 回滚:撤销所有已执行的操作
    this.operations = [];
    this.committed = false;
    console.log('事务回滚');
  }
}

// 使用
async function transferMoney(fromId, toId, amount) {
  const tx = new SimpleTransaction(db);
  
  try {
    await tx.execute(
      'UPDATE accounts SET balance = balance - ? WHERE id = ?',
      [amount, fromId]
    );
    await tx.execute(
      'UPDATE accounts SET balance = balance + ? WHERE id = ?',
      [amount, toId]
    );
    
    await tx.commit();
    console.log('转账成功');
  } catch (err) {
    console.error('转账失败,已回滚:', err);
  }
}

实战案例

案例一:设计一个博客系统的数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-- 博客系统的核心表设计

-- 用户表
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  avatar_url VARCHAR(500),
  bio TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 文章表
CREATE TABLE posts (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  title VARCHAR(200) NOT NULL,
  content TEXT NOT NULL,
  status ENUM('draft', 'published', 'deleted') DEFAULT 'published',
  view_count INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  -- 外键约束
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  -- 复合索引:按用户查找文章
  INDEX idx_user_status (user_id, status),
  -- 全文索引:搜索标题和内容
  FULLTEXT INDEX ft_search (title, content)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 标签表
CREATE TABLE tags (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL UNIQUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 文章-标签关联表
CREATE TABLE post_tags (
  post_id INT NOT NULL,
  tag_id INT NOT NULL,
  PRIMARY KEY (post_id, tag_id),
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 评论表
CREATE TABLE comments (
  id INT AUTO_INCREMENT PRIMARY KEY,
  post_id INT NOT NULL,
  user_id INT NOT NULL,
  parent_id INT DEFAULT NULL,  -- 支持嵌套回复
  content TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_post_created (post_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * 配套的 Node.js 查询
 */
async function getPostWithDetails(connection, postId) {
  // 查询文章基本信息
  const [posts] = await connection.execute(
    `SELECT p.*, u.username, u.avatar_url 
     FROM posts p 
     JOIN users u ON p.user_id = u.id 
     WHERE p.id = ?`,
    [postId]
  );
  
  if (posts.length === 0) return null;
  
  // 查询文章的标签
  const [tags] = await connection.execute(
    `SELECT t.id, t.name 
     FROM tags t 
     JOIN post_tags pt ON t.id = pt.tag_id 
     WHERE pt.post_id = ?`,
    [postId]
  );
  
  // 查询评论(分页)
  const [comments] = await connection.execute(
    `SELECT c.*, u.username 
     FROM comments c 
     JOIN users u ON c.user_id = u.id 
     WHERE c.post_id = ? 
     ORDER BY c.created_at DESC 
     LIMIT 20`,
    [postId]
  );
  
  return {
    ...posts[0],
    tags: tags.map(t => t.name),
    comments,
  };
}

案例二:分页查询与性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 传统的 LIMIT/OFFSET 分页(有性能问题)
-- 当 OFFSET 很大时(如第 10000 条),MySQL 仍然要扫描前 10000 条
SELECT * FROM posts ORDER BY id DESC LIMIT 20 OFFSET 10000;

-- 优化 1:基于游标的翻页(推荐!)
-- 记住上一页最后一条记录的 ID
SELECT * FROM posts 
WHERE id < 10020  -- 上一页最后记录的 id
ORDER BY id DESC 
LIMIT 20;

-- 优化 2:延迟关联(需要查询大字段时)
-- 先查索引(快),再回表查数据
SELECT p.* FROM (
  SELECT id FROM posts 
  ORDER BY created_at DESC 
  LIMIT 20 OFFSET 10000
) AS tmp
JOIN posts p ON tmp.id = p.id;

底层原理

InnoDB 存储引擎

MySQL 的默认存储引擎 InnoDB 拥有几个关键特性:

  1. 聚簇索引:数据按主键顺序物理存储,主键查询极快
  2. 缓冲池(Buffer Pool):内存缓存数据和索引,减少磁盘 I/O
  3. MVCC(多版本并发控制):通过行记录的多个版本来实现事务隔离,无需锁住整张表
  4. Redo Log:写前日志,保证事务持久性
  5. Undo Log:回滚日志,支持事务回滚和 MVCC

Buffer Pool 工作原理(前端友好的类比):

1
2
3
4
5
磁盘数据  →  Buffer Pool (内存)  →  查询结果
                    ↓
        修改时记录到 Redo Log
                    ↓
        后台线程异步刷回磁盘

这就像浏览器渲染中的图层合成——先在内存中处理好,再一次性更新到屏幕上,而不是每次修改都重新绘制。

高频面试题解析

面试题1:VARCHAR 和 CHAR 有什么区别?

:VARCHAR 是变长字符串,CHAR 是定长字符串。VARCHAR 根据实际内容长度占用空间(额外 1-2 字节用于记录长度),CHAR 始终使用声明长度。VARCHAR 适合长度变化大的字段(如用户名、标题),CHAR 适合固定长度的场景(如手机号、MD5 值)。

面试题2:什么是 SQL 注入?如何防范?

:SQL 注入是通过在用户输入中嵌入恶意 SQL 代码,拼接查询字符串的攻击方式。

1
2
3
4
5
6
7
8
9
10
11
// ❌ 危险!拼接 SQL
const sql = `SELECT * FROM users WHERE username = '${userInput}'`;
// 如果 userInput = "' OR '1'='1",SQL 变成:
// SELECT * FROM users WHERE username = '' OR '1'='1'
// 返回所有用户!

// ✅ 安全:参数化查询
connection.execute(
  'SELECT * FROM users WHERE username = ?',
  [userInput]
);

参数化查询将数据和 SQL 指令分离,数据库引擎会自动转义输入中的特殊字符,从根本上杜绝 SQL 注入。

面试题3:MySQL 的 JOIN 有哪些类型?LEFT JOIN 和 INNER JOIN 的区别?

1
2
3
4
5
6
7
8
9
10
-- INNER JOIN:只返回两表中匹配的记录
SELECT * FROM orders o INNER JOIN users u ON o.user_id = u.id;
-- = 只返回有订单的用户

-- LEFT JOIN:返回左表所有记录,右表无匹配则填充 NULL
SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id;
-- = 所有用户,包括没有下过订单的用户(订单字段为 NULL)

-- RIGHT JOIN:与 LEFT 相反
-- FULL OUTER JOIN:MySQL 不支持,可以用 UNION 模拟

面试题4:什么是 MySQL 的慢查询?如何优化?

:执行时间超过 long_query_time(默认 10 秒)的查询。通过 slow_query_log 开启记录。

优化手段(按优先级排序):

  1. 加索引:检查 WHERE、JOIN、ORDER BY 列是否被索引
  2. 优化 SQL:避免 SELECT *、避免在 WHERE 中对列使用函数
  3. 减少关联表的数量:JOIN 一般不超过 3 个表
  4. 分页优化:使用游标分页代替 OFFSET 分页
  5. 读写分离:主库写、从库读

面试题5:InnoDB 和 MyISAM 的区别?

特性InnoDBMyISAM
事务支持
行级锁❌(只有表级锁)
外键约束
全文索引✅(MySQL 5.6+)
崩溃恢复✅(通过 Redo Log)❌(可能丢失数据)
表总行数实时不准SELECT COUNT(*) 极快
适用场景OLTP(读写频繁)只读/数据仓库

总结与扩展

MySQL 对前端开发者来说并不遥远。当你把数据库的表想象成 JSON 数组、把索引想象成哈希表、把事务想象成一个 try-catch 块,数据库的操作就变得亲切可懂了。

学习路线

  1. 基础:CRUD + WHERE + ORDER BY + LIMIT
  2. 进阶:JOIN 关联查询 + GROUP BY 分组统计 + 子查询
  3. 优化:索引设计与 EXPLAIN 分析 + 慢查询优化
  4. 高级:事务隔离级别 + 锁机制 + 分库分表

扩展方向

  • MongoDB:如果你被 SQL 的 JOIN 和 Schema 约束困扰,可以看看 NoSQL
  • ORM 框架:Sequelize、Prisma、TypeORM 将 SQL 抽象为 JavaScript 对象
  • 连接池mysql2 连接池的使用,控制并发数据库连接数
  • ORM vs 原生 SQL:什么时候用 ORM,什么时候手写 SQL
本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量