您现在的位置是:网站首页 > 数据库连接与ORM集成文章详情
数据库连接与ORM集成
陈川
【
Node.js
】
62761人已围观
13568字
数据库连接的基本概念
Express框架本身不直接提供数据库连接功能,需要通过第三方库来实现。常见的数据库连接方式包括直接使用数据库驱动、连接池技术以及ORM工具。Node.js生态系统中,几乎每种主流数据库都有对应的驱动包,例如mysql2
、pg
、mongodb
等。
// 使用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可以带来以下好处:
- 避免手写SQL语句,减少SQL注入风险
- 自动处理数据类型转换
- 提供统一的API操作不同数据库
- 简化复杂查询的构建过程
- 内置数据验证和关联关系管理
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应用中通常需要以下步骤:
- 创建数据库连接配置
- 定义模型(Models)
- 创建模型关联
- 在路由控制器中使用模型
典型项目结构示例:
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使用不当可能导致性能问题,以下是一些优化建议:
- 合理使用预加载(eager loading)避免N+1查询问题
- 只查询需要的字段
- 使用批量操作代替循环中的单个操作
- 合理使用索引
- 监控慢查询
// 优化查询示例
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时仍需注意安全问题:
- 始终验证用户输入,即使ORM提供了参数化查询
- 限制批量操作的记录数量
- 谨慎处理关联数据的加载深度
- 使用事务保证数据一致性
- 定期更新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
上一篇: WebSocket集成方案
下一篇: 安全防护与最佳实践