Node.js 数据库集成

核心知识点

数据库类型

在 Node.js 开发中,常用的数据库类型包括:

  • 关系型数据库:MySQL、PostgreSQL、SQLite、Oracle 等
  • NoSQL 数据库:MongoDB、Redis、Cassandra、CouchDB 等

数据库连接

连接数据库的基本步骤:

  1. 安装相应的数据库驱动或客户端
  2. 配置连接参数(主机、端口、用户名、密码、数据库名等)
  3. 建立连接
  4. 执行查询或操作
  5. 处理结果
  6. 关闭连接(或使用连接池)

连接池

连接池是一种管理数据库连接的机制,它可以:

  • 减少连接建立和关闭的开销
  • 控制并发连接数
  • 提高应用性能

ORM 工具

ORM(对象关系映射)工具可以:

  • 将数据库表映射为 JavaScript 对象
  • 提供面向对象的方式操作数据库
  • 简化数据库操作代码
  • 支持多种数据库

常用的 ORM 工具:

  • Sequelize:支持 MySQL、PostgreSQL、SQLite、MSSQL 等
  • Mongoose:专门用于 MongoDB
  • TypeORM:TypeScript 支持的 ORM
  • Prisma:现代 Node.js ORM

事务处理

事务是一组原子性的数据库操作,要么全部成功,要么全部失败。在 Node.js 中,事务处理通常包括:

  1. 开始事务
  2. 执行一系列操作
  3. 提交事务(如果全部成功)
  4. 回滚事务(如果有任何失败)

实用案例

案例一:MySQL 数据库集成

// 1. 安装依赖
// npm install mysql2

const mysql = require('mysql2');

// 创建连接池
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'testdb',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

// 执行查询
function getUsers() {
  return new Promise((resolve, reject) => {
    pool.query('SELECT * FROM users', (error, results) => {
      if (error) {
        return reject(error);
      }
      resolve(results);
    });
  });
}

function getUserById(id) {
  return new Promise((resolve, reject) => {
    pool.query('SELECT * FROM users WHERE id = ?', [id], (error, results) => {
      if (error) {
        return reject(error);
      }
      resolve(results[0]);
    });
  });
}

function createUser(user) {
  return new Promise((resolve, reject) => {
    pool.query('INSERT INTO users SET ?', user, (error, results) => {
      if (error) {
        return reject(error);
      }
      resolve({ id: results.insertId, ...user });
    });
  });
}

// 使用示例
async function main() {
  try {
    // 获取所有用户
    const users = await getUsers();
    console.log('所有用户:', users);

    // 根据 ID 获取用户
    const user = await getUserById(1);
    console.log('用户 ID 1:', user);

    // 创建新用户
    const newUser = await createUser({ name: '张三', email: 'zhangsan@example.com' });
    console.log('新创建的用户:', newUser);
  } catch (error) {
    console.error('错误:', error);
  } finally {
    // 关闭连接池
    pool.end();
  }
}

main();

案例二:MongoDB 数据库集成

// 1. 安装依赖
// npm install mongodb

const { MongoClient } = require('mongodb');

// MongoDB 连接 URL
const url = 'mongodb://localhost:27017';
const dbName = 'testdb';

// 连接数据库
async function connectDB() {
  const client = new MongoClient(url);
  try {
    await client.connect();
    console.log('连接到 MongoDB 成功');
    const db = client.db(dbName);
    return { db, client };
  } catch (error) {
    console.error('连接到 MongoDB 失败:', error);
    throw error;
  }
}

// 操作示例
async function main() {
  let client;
  try {
    const { db, client: mongoClient } = await connectDB();
    client = mongoClient;

    // 获取集合
    const usersCollection = db.collection('users');

    // 插入文档
    const insertResult = await usersCollection.insertOne({
      name: '李四',
      email: 'lisi@example.com',
      age: 25,
      createdAt: new Date()
    });
    console.log('插入结果:', insertResult);

    // 查询文档
    const users = await usersCollection.find({}).toArray();
    console.log('所有用户:', users);

    // 根据条件查询
    const user = await usersCollection.findOne({ name: '李四' });
    console.log('用户 李四:', user);

    // 更新文档
    const updateResult = await usersCollection.updateOne(
      { _id: user._id },
      { $set: { age: 26 } }
    );
    console.log('更新结果:', updateResult);

    // 删除文档
    const deleteResult = await usersCollection.deleteOne({ _id: user._id });
    console.log('删除结果:', deleteResult);

  } catch (error) {
    console.error('错误:', error);
  } finally {
    if (client) {
      await client.close();
      console.log('MongoDB 连接已关闭');
    }
  }
}

main();

案例三:使用 Sequelize ORM

// 1. 安装依赖
// npm install sequelize mysql2

const { Sequelize, DataTypes } = require('sequelize');

// 创建 Sequelize 实例
const sequelize = new Sequelize('testdb', 'root', 'password', {
  host: 'localhost',
  dialect: 'mysql',
  pool: {
    max: 10,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
});

// 定义模型
const User = sequelize.define('User', {
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  age: {
    type: DataTypes.INTEGER,
    allowNull: true
  }
}, {
  tableName: 'users',
  timestamps: true
});

// 同步模型到数据库
async function syncModels() {
  try {
    await sequelize.sync({ alter: true });
    console.log('模型同步成功');
  } catch (error) {
    console.error('模型同步失败:', error);
    throw error;
  }
}

// 操作示例
async function main() {
  try {
    // 同步模型
    await syncModels();

    // 创建用户
    const user = await User.create({
      name: '王五',
      email: 'wangwu@example.com',
      age: 30
    });
    console.log('创建的用户:', user.toJSON());

    // 查询所有用户
    const users = await User.findAll();
    console.log('所有用户:', users.map(u => u.toJSON()));

    // 根据 ID 查询
    const foundUser = await User.findByPk(1);
    console.log('用户 ID 1:', foundUser ? foundUser.toJSON() : '未找到');

    // 根据条件查询
    const filteredUsers = await User.findAll({
      where: {
        age: {
          [Sequelize.Op.gte]: 25
        }
      }
    });
    console.log('年龄大于等于 25 的用户:', filteredUsers.map(u => u.toJSON()));

    // 更新用户
    if (foundUser) {
      await foundUser.update({ age: 31 });
      console.log('更新后的用户:', foundUser.toJSON());
    }

    // 删除用户
    if (user) {
      await user.destroy();
      console.log('用户已删除');
    }

  } catch (error) {
    console.error('错误:', error);
  } finally {
    // 关闭连接
    await sequelize.close();
    console.log('数据库连接已关闭');
  }
}

main();

案例四:使用 Mongoose ORM

// 1. 安装依赖
// npm install mongoose

const mongoose = require('mongoose');

// 连接 MongoDB
async function connectDB() {
  try {
    await mongoose.connect('mongodb://localhost:27017/testdb', {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    console.log('连接到 MongoDB 成功');
  } catch (error) {
    console.error('连接到 MongoDB 失败:', error);
    throw error;
  }
}

// 定义模式
const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  age: {
    type: Number,
    min: 0
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 创建模型
const User = mongoose.model('User', userSchema);

// 操作示例
async function main() {
  try {
    // 连接数据库
    await connectDB();

    // 创建用户
    const user = new User({
      name: '赵六',
      email: 'zhaoliu@example.com',
      age: 28
    });
    const savedUser = await user.save();
    console.log('创建的用户:', savedUser);

    // 查询所有用户
    const users = await User.find();
    console.log('所有用户:', users);

    // 根据条件查询
    const foundUser = await User.findOne({ name: '赵六' });
    console.log('用户 赵六:', foundUser);

    // 更新用户
    if (foundUser) {
      foundUser.age = 29;
      const updatedUser = await foundUser.save();
      console.log('更新后的用户:', updatedUser);
    }

    // 删除用户
    if (savedUser) {
      await User.deleteOne({ _id: savedUser._id });
      console.log('用户已删除');
    }

  } catch (error) {
    console.error('错误:', error);
  } finally {
    // 关闭连接
    await mongoose.disconnect();
    console.log('MongoDB 连接已关闭');
  }
}

main();

案例五:事务处理

// MySQL 事务示例
const mysql = require('mysql2/promise');

async function main() {
  let connection;
  try {
    // 创建连接
    connection = await mysql.createConnection({
      host: 'localhost',
      user: 'root',
      password: 'password',
      database: 'testdb'
    });

    // 开始事务
    await connection.beginTransaction();
    console.log('事务开始');

    // 执行第一个操作
    const [insertResult] = await connection.execute(
      'INSERT INTO users (name, email) VALUES (?, ?)',
      ['事务测试', 'transaction@example.com']
    );
    const userId = insertResult.insertId;
    console.log('插入用户 ID:', userId);

    // 执行第二个操作
    await connection.execute(
      'INSERT INTO orders (user_id, product_name, amount) VALUES (?, ?, ?)',
      [userId, '测试产品', 100]
    );
    console.log('插入订单成功');

    // 提交事务
    await connection.commit();
    console.log('事务提交成功');

  } catch (error) {
    console.error('错误:', error);
    // 回滚事务
    if (connection) {
      await connection.rollback();
      console.log('事务回滚成功');
    }
  } finally {
    if (connection) {
      await connection.end();
      console.log('数据库连接已关闭');
    }
  }
}

main();

案例六:PostgreSQL 数据库集成

// 1. 安装依赖
// npm install pg

const { Client } = require('pg');

// 创建客户端
const client = new Client({
  host: 'localhost',
  port: 5432,
  user: 'postgres',
  password: 'password',
  database: 'testdb'
});

// 连接数据库
async function connectDB() {
  try {
    await client.connect();
    console.log('连接到 PostgreSQL 成功');
  } catch (error) {
    console.error('连接到 PostgreSQL 失败:', error);
    throw error;
  }
}

// 操作示例
async function main() {
  try {
    await connectDB();

    // 创建表
    await client.query(`
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        age INTEGER,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      )
    `);
    console.log('表创建成功');

    // 插入数据
    const insertResult = await client.query(
      'INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING *',
      ['钱七', 'qianqi@example.com', 32]
    );
    console.log('插入的用户:', insertResult.rows[0]);

    // 查询数据
    const selectResult = await client.query('SELECT * FROM users');
    console.log('所有用户:', selectResult.rows);

    // 更新数据
    const updateResult = await client.query(
      'UPDATE users SET age = $1 WHERE name = $2 RETURNING *',
      [33, '钱七']
    );
    console.log('更新后的用户:', updateResult.rows[0]);

    // 删除数据
    const deleteResult = await client.query(
      'DELETE FROM users WHERE name = $1',
      ['钱七']
    );
    console.log('删除的行数:', deleteResult.rowCount);

  } catch (error) {
    console.error('错误:', error);
  } finally {
    // 关闭连接
    await client.end();
    console.log('PostgreSQL 连接已关闭');
  }
}

main();

学习目标

  1. 掌握数据库连接:能够连接各种类型的数据库
  2. 使用连接池:学会配置和使用数据库连接池
  3. 执行基本查询:能够执行 SELECT、INSERT、UPDATE、DELETE 等操作
  4. 使用 ORM 工具:掌握 Sequelize、Mongoose 等 ORM 工具的使用
  5. 处理事务:学会使用事务处理保证数据一致性
  6. 错误处理:能够正确处理数据库操作中的错误
  7. 性能优化:了解数据库操作的性能优化技巧

代码优化建议

1. 使用环境变量存储数据库配置

不好的做法

const mysql = require('mysql2');

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password', // 硬编码密码
  database: 'testdb'
});

好的做法

const mysql = require('mysql2');
require('dotenv').config();

const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD || '',
  database: process.env.DB_NAME || 'testdb'
});

2. 使用连接池管理连接

不好的做法

const mysql = require('mysql2');

// 每次操作都创建新连接
async function getUserById(id) {
  const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'testdb'
  });
  
  return new Promise((resolve, reject) => {
    connection.connect();
    connection.query('SELECT * FROM users WHERE id = ?', [id], (error, results) => {
      if (error) {
        reject(error);
      } else {
        resolve(results[0]);
      }
      connection.end();
    });
  });
}

好的做法

const mysql = require('mysql2');

// 创建连接池
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'testdb',
  connectionLimit: 10
});

// 使用连接池执行查询
async function getUserById(id) {
  return new Promise((resolve, reject) => {
    pool.query('SELECT * FROM users WHERE id = ?', [id], (error, results) => {
      if (error) {
        reject(error);
      } else {
        resolve(results[0]);
      }
    });
  });
}

3. 错误处理和日志

不好的做法

app.get('/users', (req, res) => {
  pool.query('SELECT * FROM users', (error, results) => {
    if (error) {
      res.status(500).send('错误');
    } else {
      res.json(results);
    }
  });
});

好的做法

app.get('/users', (req, res) => {
  pool.query('SELECT * FROM users', (error, results) => {
    if (error) {
      console.error('查询用户失败:', error);
      res.status(500).json({ error: '服务器内部错误' });
    } else {
      res.json(results);
    }
  });
});

4. 使用参数化查询防止 SQL 注入

不好的做法

// 容易受到 SQL 注入攻击
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  pool.query(`SELECT * FROM users WHERE id = ${userId}`, (error, results) => {
    // 处理结果
  });
});

好的做法

// 使用参数化查询
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  pool.query('SELECT * FROM users WHERE id = ?', [userId], (error, results) => {
    // 处理结果
  });
});

5. 异步/await 优化

不好的做法

// 回调嵌套
pool.query('SELECT * FROM users', (error, users) => {
  if (error) {
    console.error(error);
  } else {
    users.forEach(user => {
      pool.query('SELECT * FROM orders WHERE user_id = ?', [user.id], (error, orders) => {
        if (error) {
          console.error(error);
        } else {
          user.orders = orders;
          console.log(user);
        }
      });
    });
  }
});

好的做法

// 使用 async/await
const mysql = require('mysql2/promise');

async function getUserWithOrders() {
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'testdb'
  });
  
  try {
    const [users] = await connection.query('SELECT * FROM users');
    
    for (const user of users) {
      const [orders] = await connection.query('SELECT * FROM orders WHERE user_id = ?', [user.id]);
      user.orders = orders;
    }
    
    return users;
  } finally {
    await connection.end();
  }
}

常见问题与解决方案

问题1:数据库连接失败

原因

  • 数据库服务未启动
  • 连接参数错误(主机、端口、用户名、密码等)
  • 防火墙阻止连接
  • 数据库权限不足

解决方案

  • 确保数据库服务已启动
  • 检查连接参数是否正确
  • 检查防火墙设置
  • 确保用户有正确的权限

问题2:SQL 注入攻击

原因

  • 使用字符串拼接构建 SQL 查询
  • 未对用户输入进行验证或转义

解决方案

  • 使用参数化查询
  • 使用 ORM 工具
  • 对用户输入进行验证和转义

问题3:连接泄漏

原因

  • 未正确关闭数据库连接
  • 连接池配置不当

解决方案

  • 使用连接池管理连接
  • 确保在 finally 块中关闭连接
  • 合理配置连接池参数

问题4:事务回滚失败

原因

  • 事务中执行了不支持回滚的操作
  • 连接在事务过程中断开
  • 代码逻辑错误导致未执行回滚

解决方案

  • 确保事务中只执行支持回滚的操作
  • 正确处理错误并执行回滚
  • 使用 try/catch/finally 确保回滚逻辑执行

问题5:数据库性能问题

原因

  • 缺少索引
  • 查询语句效率低下
  • 并发连接数过高
  • 数据量过大

解决方案

  • 为常用查询字段添加索引
  • 优化查询语句
  • 合理配置连接池
  • 考虑数据库分片或读写分离

总结

通过本教程的学习,你应该能够:

  1. 掌握 Node.js 中常见数据库的连接和操作方法
  2. 使用连接池管理数据库连接,提高应用性能
  3. 熟练使用 Sequelize、Mongoose 等 ORM 工具简化数据库操作
  4. 使用事务处理保证数据一致性
  5. 正确处理数据库操作中的错误
  6. 了解数据库操作的性能优化技巧
  7. 安全地处理用户输入,防止 SQL 注入攻击

数据库集成是 Node.js 后端开发的重要组成部分,选择合适的数据库类型和操作方式,对于构建高性能、可靠的应用至关重要。在实际开发中,你应该根据应用的需求和特点,选择最适合的数据库解决方案,并不断优化数据库操作,以提高应用的整体性能和用户体验。

« 上一篇 Express 中间件机制 下一篇 » Node.js 认证与授权