您现在的位置是:网站首页 > 数据库连接与ORM集成文章详情

数据库连接与ORM集成

数据库连接的基本概念

Express框架本身不直接提供数据库连接功能,需要通过第三方库来实现。常见的数据库连接方式包括直接使用数据库驱动、连接池技术以及ORM工具。Node.js生态系统中,几乎每种主流数据库都有对应的驱动包,例如mysql2pgmongodb等。

// 使用mysql2直接连接MySQL数据库示例
const mysql = require('mysql2');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'test_db'
});

connection.query('SELECT * FROM users', (err, results) => {
  if (err) throw err;
  console.log(results);
});

连接池是更高效的做法,特别是在高并发场景下:

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'test_db',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

pool.query('SELECT * FROM products', (err, results) => {
  if (err) throw err;
  console.log(results);
});

ORM的概念与优势

ORM(Object-Relational Mapping)是将数据库表结构映射到对象的技术,允许开发者以面向对象的方式操作数据库。在Express中集成ORM可以带来以下好处:

  1. 避免手写SQL语句,减少SQL注入风险
  2. 自动处理数据类型转换
  3. 提供统一的API操作不同数据库
  4. 简化复杂查询的构建过程
  5. 内置数据验证和关联关系管理

Node.js生态中流行的ORM包括Sequelize、TypeORM、Prisma等。以Sequelize为例:

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');

const User = sequelize.define('User', {
  username: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    unique: true
  }
});

(async () => {
  await sequelize.sync();
  const user = await User.create({
    username: 'john_doe',
    email: 'john@example.com'
  });
  console.log(user.toJSON());
})();

在Express中集成Sequelize

将Sequelize集成到Express应用中通常需要以下步骤:

  1. 创建数据库连接配置
  2. 定义模型(Models)
  3. 创建模型关联
  4. 在路由控制器中使用模型

典型项目结构示例:

project/
├── models/
│   ├── index.js
│   ├── user.model.js
│   └── product.model.js
├── routes/
│   └── user.routes.js
└── app.js

models/index.js通常包含数据库连接和模型加载逻辑:

const { Sequelize } = require('sequelize');
const path = require('path');
const fs = require('fs');

const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
  host: process.env.DB_HOST,
  dialect: 'mysql',
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
});

const db = {};

// 自动加载所有模型文件
fs.readdirSync(__dirname)
  .filter(file => file !== 'index.js' && file.endsWith('.js'))
  .forEach(file => {
    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
    db[model.name] = model;
  });

// 设置关联关系
Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

模型定义与关联

模型定义是ORM的核心部分。以用户和文章的一对多关系为例:

// user.model.js
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    name: {
      type: DataTypes.STRING(50),
      allowNull: false,
      validate: {
        len: [2, 50]
      }
    },
    email: {
      type: DataTypes.STRING(100),
      unique: true,
      validate: {
        isEmail: true
      }
    }
  }, {
    timestamps: true,
    paranoid: true // 软删除
  });

  User.associate = function(models) {
    User.hasMany(models.Post, {
      foreignKey: 'authorId',
      as: 'posts'
    });
  };

  return User;
};

// post.model.js
module.exports = (sequelize, DataTypes) => {
  const Post = sequelize.define('Post', {
    title: {
      type: DataTypes.STRING(100),
      allowNull: false
    },
    content: {
      type: DataTypes.TEXT,
      allowNull: false
    },
    published: {
      type: DataTypes.BOOLEAN,
      defaultValue: false
    }
  });

  Post.associate = function(models) {
    Post.belongsTo(models.User, {
      foreignKey: 'authorId',
      as: 'author'
    });
  };

  return Post;
};

在路由中使用ORM

定义好模型后,可以在Express路由中使用它们进行CRUD操作:

const express = require('express');
const router = express.Router();
const db = require('../models');

// 创建用户
router.post('/users', async (req, res) => {
  try {
    const user = await db.User.create(req.body);
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// 获取用户及其所有文章
router.get('/users/:id', async (req, res) => {
  try {
    const user = await db.User.findByPk(req.params.id, {
      include: [{
        model: db.Post,
        as: 'posts'
      }]
    });
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// 更新用户
router.put('/users/:id', async (req, res) => {
  try {
    const [updated] = await db.User.update(req.body, {
      where: { id: req.params.id }
    });
    if (updated) {
      const updatedUser = await db.User.findByPk(req.params.id);
      return res.json(updatedUser);
    }
    throw new Error('User not found');
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// 删除用户(软删除)
router.delete('/users/:id', async (req, res) => {
  try {
    const deleted = await db.User.destroy({
      where: { id: req.params.id }
    });
    if (deleted) {
      return res.status(204).send();
    }
    throw new Error('User not found');
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;

高级查询与事务处理

ORM提供了强大的查询构建能力,可以处理复杂查询场景:

// 复杂查询示例
router.get('/posts', async (req, res) => {
  try {
    const { published, authorName, page = 1, limit = 10 } = req.query;
    
    const where = {};
    if (published) where.published = published === 'true';
    
    const include = [];
    if (authorName) {
      include.push({
        model: db.User,
        as: 'author',
        where: {
          name: {
            [db.Sequelize.Op.like]: `%${authorName}%`
          }
        }
      });
    }
    
    const offset = (page - 1) * limit;
    
    const posts = await db.Post.findAndCountAll({
      where,
      include,
      limit: parseInt(limit),
      offset,
      order: [['createdAt', 'DESC']]
    });
    
    res.json({
      total: posts.count,
      pages: Math.ceil(posts.count / limit),
      currentPage: parseInt(page),
      data: posts.rows
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

事务处理对于保证数据一致性至关重要:

// 事务处理示例
router.post('/orders', async (req, res) => {
  const t = await db.sequelize.transaction();
  
  try {
    const { userId, products } = req.body;
    
    // 创建订单
    const order = await db.Order.create({
      userId,
      status: 'pending'
    }, { transaction: t });
    
    // 添加订单项并更新库存
    for (const product of products) {
      await db.OrderItem.create({
        orderId: order.id,
        productId: product.id,
        quantity: product.quantity,
        price: product.price
      }, { transaction: t });
      
      // 更新库存
      const updated = await db.Product.decrement('stock', {
        by: product.quantity,
        where: { id: product.id },
        transaction: t
      });
      
      if (updated[0][1] === 0) {
        throw new Error(`Product ${product.id} out of stock`);
      }
    }
    
    // 提交事务
    await t.commit();
    res.status(201).json(order);
  } catch (error) {
    // 回滚事务
    await t.rollback();
    res.status(400).json({ error: error.message });
  }
});

性能优化与调试

ORM使用不当可能导致性能问题,以下是一些优化建议:

  1. 合理使用预加载(eager loading)避免N+1查询问题
  2. 只查询需要的字段
  3. 使用批量操作代替循环中的单个操作
  4. 合理使用索引
  5. 监控慢查询
// 优化查询示例
router.get('/users-with-posts', async (req, res) => {
  try {
    // 不好的做法:会导致N+1查询问题
    // const users = await db.User.findAll();
    // for (const user of users) {
    //   const posts = await user.getPosts();
    // }
    
    // 好的做法:使用include预加载关联数据
    const users = await db.User.findAll({
      include: [{
        model: db.Post,
        as: 'posts',
        attributes: ['id', 'title', 'createdAt'] // 只选择需要的字段
      }],
      attributes: ['id', 'name'] // 同样只选择需要的用户字段
    });
    
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

调试SQL查询可以通过以下方式:

// 启用SQL日志
const sequelize = new Sequelize('database', 'username', 'password', {
  dialect: 'mysql',
  logging: console.log // 输出SQL到控制台
});

// 或者在特定查询中启用
const users = await db.User.findAll({
  logging: console.log
});

数据验证与钩子函数

ORM通常提供数据验证功能,可以在模型定义中添加验证规则:

module.exports = (sequelize, DataTypes) => {
  const Product = sequelize.define('Product', {
    name: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        notEmpty: true,
        len: [3, 50]
      }
    },
    price: {
      type: DataTypes.DECIMAL(10, 2),
      validate: {
        isDecimal: true,
        min: 0
      }
    },
    stock: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      validate: {
        isInt: true,
        min: 0
      }
    }
  });
  
  return Product;
};

钩子函数(Hooks)可以在模型生命周期特定时间点执行自定义逻辑:

module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    email: {
      type: DataTypes.STRING,
      unique: true
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    }
  });

  // 保存前钩子
  User.beforeCreate(async (user) => {
    if (user.password) {
      user.password = await hashPassword(user.password);
    }
  });

  // 更新前钩子
  User.beforeUpdate(async (user) => {
    if (user.changed('password')) {
      user.password = await hashPassword(user.password);
    }
  });

  // 实例方法
  User.prototype.comparePassword = function(candidatePassword) {
    return comparePassword(candidatePassword, this.password);
  };

  return User;
};

多数据库支持与迁移

Sequelize支持多种关系型数据库,包括MySQL、PostgreSQL、SQLite和SQL Server。配置时只需更改dialect参数:

// PostgreSQL配置示例
const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialect: 'postgres',
  protocol: 'postgres',
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  }
});

数据库迁移(Migrations)是管理数据库结构变更的重要工具:

# 安装Sequelize CLI
npm install --save-dev sequelize-cli

# 初始化迁移配置
npx sequelize-cli init

这会创建迁移目录结构:

migrations/
  └── config.json

创建迁移文件:

npx sequelize-cli migration:generate --name create-users-table

生成的迁移文件示例:

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: {
        type: Sequelize.STRING,
        allowNull: false
      },
      email: {
        type: Sequelize.STRING,
        unique: true
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface) => {
    await queryInterface.dropTable('Users');
  }
};

执行迁移:

npx sequelize-cli db:migrate

回滚迁移:

npx sequelize-cli db:migrate:undo

测试与Mocking

测试ORM相关的代码时,通常需要Mock数据库操作。可以使用内存数据库或Mock库:

// 使用SQLite内存数据库进行测试
const { Sequelize } = require('sequelize');

beforeAll(async () => {
  const sequelize = new Sequelize('sqlite::memory:');
  
  // 定义模型
  const User = sequelize.define('User', {
    name: DataTypes.STRING,
    email: DataTypes.STRING
  });
  
  // 同步数据库
  await sequelize.sync();
});

test('should create a user', async () => {
  const user = await User.create({
    name: 'Test User',
    email: 'test@example.com'
  });
  
  expect(user.name).toBe('Test User');
  expect(user.email).toBe('test@example.com');
});

或者使用Mock库如sinon

const sinon = require('sinon');
const db = require('../models');

describe('User Controller', () => {
  afterEach(() => {
    sinon.restore();
  });
  
  it('should return all users', async () => {
    const mockUsers = [{ id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }];
    sinon.stub(db.User, 'findAll').resolves(mockUsers);
    
    const req = {};
    const res = {
      json: sinon.stub()
    };
    
    await getAllUsers(req, res);
    sinon.assert.calledWith(res.json, mockUsers);
  });
});

安全最佳实践

使用ORM时仍需注意安全问题:

  1. 始终验证用户输入,即使ORM提供了参数化查询
  2. 限制批量操作的记录数量
  3. 谨慎处理关联数据的加载深度
  4. 使用事务保证数据一致性
  5. 定期更新ORM库以获取安全补丁
// 限制批量操作示例
router.post('/products/bulk', async (req, res) => {
  try {
    const products = req.body;
    
    // 限制一次最多处理100条记录
    if (products.length > 100) {
      return res.status(400).json({ error: 'Too many records in one request' });
    }
    
    const createdProducts = await db.Product.bulkCreate(products);
    res.status(201).json(createdProducts);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// 防止过度加载关联数据
router.get('/products/:id', async (req, res) => {
  try {
    const include = [];
    
    // 只允许加载reviews关联,如果请求参数中指定
    if (req.query.include === 'reviews') {
      include.push({
        model: db.Review,
        as: 'reviews',
        limit: 50 // 限制关联记录数量
      });
    }
    
    const product = await db.Product.findByPk(req.params.id, {
      include,
      attributes: { exclude: ['internalNotes'] } // 排除敏感字段
    });
    
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    
    res.json(product);
  } catch (error) {
    res.status(500).json({ error: error

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步