Sequelize 教程

项目概述

Sequelize 是一个基于 Promise 的 Node.js ORM(对象关系映射)库,支持 PostgreSQL、MySQL、SQLite 和 Microsoft SQL Server 等多种关系型数据库。它提供了强大的模型定义、查询构建、关联关系处理、事务支持等功能,大幅简化了数据库操作的复杂性。

安装设置

1. 安装 Sequelize

# 安装 Sequelize 核心包
npm install sequelize

# 安装对应的数据库驱动(选择其一)
# PostgreSQL
npm install pg pg-hstore

# MySQL
npm install mysql2

# SQLite
npm install sqlite3

# Microsoft SQL Server
npm install tedious

2. 连接数据库

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

// 方法 1: 传递连接参数
const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: 'postgres' // 或 'mysql', 'sqlite', 'mssql'
});

// 方法 2: 传递连接 URL
const sequelize = new Sequelize('postgres://username:password@localhost:5432/database');

// 测试连接
async function testConnection() {
  try {
    await sequelize.authenticate();
    console.log('数据库连接成功');
  } catch (error) {
    console.error('数据库连接失败:', error);
  }
}

testConnection();

3. 配置选项

const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: 'postgres',
  dialectOptions: {
    // 额外的方言特定选项
  },
  pool: {
    max: 5, // 最大连接数
    min: 0, // 最小连接数
    acquire: 30000, // 连接超时时间(毫秒)
    idle: 10000 // 空闲连接超时时间(毫秒)
  },
  logging: console.log, // 日志记录选项
  define: {
    timestamps: true, // 自动添加 createdAt 和 updatedAt 字段
    underscored: false, // 使用驼峰命名法
    freezeTableName: false // 表名自动复数
  }
});

核心功能

1. 模型定义

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

// 定义 User 模型
const User = sequelize.define('User', {
  // 定义字段
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      len: [2, 50] // 长度验证
    }
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true // 邮箱验证
    }
  },
  age: {
    type: DataTypes.INTEGER,
    validate: {
      min: 0,
      max: 150
    }
  },
  isActive: {
    type: DataTypes.BOOLEAN,
    defaultValue: true
  }
}, {
  // 模型选项
  tableName: 'users', // 自定义表名
  timestamps: true // 自动添加时间戳
});

// 同步模型到数据库
async function syncModels() {
  try {
    // 创建表(如果不存在)
    await User.sync({ force: false }); // force: true 会删除现有表并重新创建
    console.log('模型同步成功');
  } catch (error) {
    console.error('模型同步失败:', error);
  }
}

syncModels();

2. 基本 CRUD 操作

创建记录

// 方法 1: 使用 create 方法
const user = await User.create({
  name: '张三',
  email: 'zhangsan@example.com',
  age: 25
});
console.log('创建用户成功:', user.toJSON());

// 方法 2: 先构建实例,再保存
const user = User.build({
  name: '李四',
  email: 'lisi@example.com',
  age: 30
});
await user.save();
console.log('创建用户成功:', user.toJSON());

查询记录

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

// 根据条件查询单个记录
const user = await User.findOne({
  where: {
    name: '张三'
  }
});
console.log('查询用户:', user?.toJSON());

// 根据主键查询
const user = await User.findByPk(1);
console.log('根据主键查询:', user?.toJSON());

// 带条件的查询
const users = await User.findAll({
  where: {
    age: {
      [Op.gte]: 18 // 年龄大于等于 18
    },
    isActive: true
  },
  order: [
    ['age', 'DESC'] // 按年龄降序排序
  ],
  limit: 10, // 限制返回 10 条
  offset: 0 // 偏移量
});

更新记录

// 方法 1: 更新单个实例
const user = await User.findByPk(1);
if (user) {
  user.name = '张三(更新)';
  user.age = 26;
  await user.save();
  console.log('更新用户成功:', user.toJSON());
}

// 方法 2: 批量更新
const result = await User.update(
  { isActive: false },
  {
    where: {
      age: {
        [Op.lt]: 18
      }
    }
  }
);
console.log('批量更新成功,影响行数:', result[0]);

删除记录

// 方法 1: 删除单个实例
const user = await User.findByPk(1);
if (user) {
  await user.destroy();
  console.log('删除用户成功');
}

// 方法 2: 批量删除
const result = await User.destroy({
  where: {
    isActive: false
  }
});
console.log('批量删除成功,影响行数:', result);

3. 关联关系

一对一关系

// 定义 Profile 模型
const Profile = sequelize.define('Profile', {
  bio: DataTypes.TEXT,
  avatar: DataTypes.STRING
});

// 定义一对一关系
User.hasOne(Profile, {
  foreignKey: 'userId',
  as: 'profile'
});
Profile.belongsTo(User, {
  foreignKey: 'userId',
  as: 'user'
});

// 创建关联数据
const user = await User.create({
  name: '张三',
  email: 'zhangsan@example.com',
  profile: {
    bio: '这是个人简介',
    avatar: 'avatar.jpg'
  }
}, {
  include: 'profile'
});

// 查询关联数据
const userWithProfile = await User.findByPk(1, {
  include: 'profile'
});
console.log('用户及其资料:', userWithProfile.toJSON());

一对多关系

// 定义 Post 模型
const Post = sequelize.define('Post', {
  title: DataTypes.STRING,
  content: DataTypes.TEXT
});

// 定义一对多关系
User.hasMany(Post, {
  foreignKey: 'userId',
  as: 'posts'
});
Post.belongsTo(User, {
  foreignKey: 'userId',
  as: 'user'
});

// 创建关联数据
const user = await User.create({
  name: '张三',
  email: 'zhangsan@example.com'
});

await Post.create({
  title: '第一篇文章',
  content: '这是内容',
  userId: user.id
});

// 查询关联数据
const userWithPosts = await User.findByPk(1, {
  include: 'posts'
});
console.log('用户及其文章:', userWithPosts.toJSON());

多对多关系

// 定义 Tag 模型
const Tag = sequelize.define('Tag', {
  name: DataTypes.STRING
});

// 定义多对多关系
const PostTag = sequelize.define('PostTag', {
  postId: {
    type: DataTypes.INTEGER,
    references: {
      model: 'Posts',
      key: 'id'
    }
  },
  tagId: {
    type: DataTypes.INTEGER,
    references: {
      model: 'Tags',
      key: 'id'
    }
  }
});

Post.belongsToMany(Tag, {
  through: PostTag,
  as: 'tags'
});
Tag.belongsToMany(Post, {
  through: PostTag,
  as: 'posts'
});

// 创建关联数据
const post = await Post.create({
  title: '第一篇文章',
  content: '这是内容'
});

const tag1 = await Tag.create({ name: '技术' });
const tag2 = await Tag.create({ name: '教程' });

await post.addTags([tag1, tag2]);

// 查询关联数据
const postWithTags = await Post.findByPk(1, {
  include: 'tags'
});
console.log('文章及其标签:', postWithTags.toJSON());

4. 事务

// 使用事务
async function transferMoney(fromUserId, toUserId, amount) {
  const transaction = await sequelize.transaction();
  
  try {
    // 查找发送方
    const fromUser = await User.findByPk(fromUserId, { transaction });
    if (!fromUser || fromUser.balance < amount) {
      throw new Error('余额不足');
    }
    
    // 查找接收方
    const toUser = await User.findByPk(toUserId, { transaction });
    if (!toUser) {
      throw new Error('接收方不存在');
    }
    
    // 更新余额
    fromUser.balance -= amount;
    toUser.balance += amount;
    
    await fromUser.save({ transaction });
    await toUser.save({ transaction });
    
    // 提交事务
    await transaction.commit();
    console.log('转账成功');
    return true;
  } catch (error) {
    // 回滚事务
    await transaction.rollback();
    console.error('转账失败:', error);
    return false;
  }
}

// 使用
await transferMoney(1, 2, 100);

5. 数据验证

const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      len: [2, 50],
      notEmpty: true
    }
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true,
      isUnique: async function(value) {
        const existingUser = await User.findOne({
          where: { email: value }
        });
        if (existingUser) {
          throw new Error('邮箱已被使用');
        }
      }
    }
  },
  age: {
    type: DataTypes.INTEGER,
    validate: {
      min: 0,
      max: 150,
      isInt: true
    }
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      len: [6, Infinity],
      isStrongPassword: function(value) {
        // 自定义密码强度验证
        if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
          throw new Error('密码必须包含大小写字母和数字');
        }
      }
    }
  }
});

// 尝试创建无效用户
try {
  const user = await User.create({
    name: 'A', // 长度不足
    email: 'invalid-email', // 无效邮箱
    age: -1, // 年龄为负数
    password: '123' // 密码强度不足
  });
} catch (error) {
  console.error('验证失败:', error.errors.map(err => err.message));
}

高级功能

1. 钩子(Hooks)

const User = sequelize.define('User', {
  // 字段定义
});

// 钩子示例
User.beforeCreate(async (user) => {
  // 创建前加密密码
  if (user.password) {
    user.password = await hashPassword(user.password);
  }
});

User.afterCreate(async (user) => {
  // 创建后发送欢迎邮件
  await sendWelcomeEmail(user.email);
});

User.beforeUpdate(async (user) => {
  // 更新前检查
  if (user.changed('email')) {
    // 邮箱变更时的处理
  }
});

User.afterDestroy(async (user) => {
  // 删除后清理相关数据
  await cleanupUserRelatedData(user.id);
});

2. 作用域(Scopes)

const User = sequelize.define('User', {
  // 字段定义
}, {
  scopes: {
    active: {
      where: {
        isActive: true
      }
    },
    adult: {
      where: {
        age: {
          [Op.gte]: 18
        }
      }
    },
    withProfile: {
      include: 'profile'
    }
  }
});

// 使用作用域
const activeUsers = await User.scope('active').findAll();
const adultUsersWithProfile = await User.scope(['adult', 'withProfile']).findAll();

3. 原始查询

// 执行原始 SQL 查询
const [results, metadata] = await sequelize.query(
  'SELECT * FROM users WHERE age > :age',
  {
    replacements: { age: 18 },
    type: QueryTypes.SELECT
  }
);
console.log('原始查询结果:', results);

// 执行插入操作
const [id, metadata] = await sequelize.query(
  'INSERT INTO users (name, email) VALUES (:name, :email)',
  {
    replacements: { name: '王五', email: 'wangwu@example.com' },
    type: QueryTypes.INSERT
  }
);
console.log('插入成功,ID:', id);

4. 迁移

初始化迁移

# 安装迁移工具
npm install --save-dev sequelize-cli

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

创建迁移文件

# 创建用户表迁移
npx sequelize-cli model:generate --name User --attributes name:string,email:string,age:integer

# 创建帖子表迁移
npx sequelize-cli model:generate --name Post --attributes title:string,content:text,userId:integer

运行迁移

# 运行所有待执行的迁移
npx sequelize-cli db:migrate

# 回滚最后一次迁移
npx sequelize-cli db:migrate:undo

# 回滚所有迁移
npx sequelize-cli db:migrate:undo:all

种子数据

# 创建种子文件
npx sequelize-cli seed:generate --name demo-user

# 运行种子
npx sequelize-cli db:seed:all

# 回滚种子
npx sequelize-cli db:seed:undo:all

实际应用场景

1. 用户认证系统

const bcrypt = require('bcrypt');

const User = sequelize.define('User', {
  username: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      len: [6, Infinity]
    }
  }
});

// 密码加密钩子
User.beforeCreate(async (user) => {
  const salt = await bcrypt.genSalt(10);
  user.password = await bcrypt.hash(user.password, salt);
});

User.beforeUpdate(async (user) => {
  if (user.changed('password')) {
    const salt = await bcrypt.genSalt(10);
    user.password = await bcrypt.hash(user.password, salt);
  }
});

// 验证密码方法
User.prototype.validatePassword = async function(password) {
  return await bcrypt.compare(password, this.password);
};

// 登录方法
async function login(username, password) {
  try {
    const user = await User.findOne({
      where: {
        [Op.or]: [
          { username },
          { email: username }
        ]
      }
    });
    
    if (!user) {
      return { success: false, message: '用户不存在' };
    }
    
    const isValidPassword = await user.validatePassword(password);
    if (!isValidPassword) {
      return { success: false, message: '密码错误' };
    }
    
    return { success: true, user: user.toJSON() };
  } catch (error) {
    console.error('登录失败:', error);
    return { success: false, message: '登录失败' };
  }
};

// 使用
const result = await login('zhangsan', 'password123');
console.log(result);

2. 博客系统

// 定义模型
const User = sequelize.define('User', {
  name: DataTypes.STRING,
  email: DataTypes.STRING
});

const Post = sequelize.define('Post', {
  title: DataTypes.STRING,
  content: DataTypes.TEXT,
  status: DataTypes.ENUM('draft', 'published')
});

const Comment = sequelize.define('Comment', {
  content: DataTypes.TEXT,
  status: DataTypes.ENUM('approved', 'pending', 'rejected')
});

// 定义关联
User.hasMany(Post, { as: 'posts' });
Post.belongsTo(User, { as: 'author' });

Post.hasMany(Comment, { as: 'comments' });
Comment.belongsTo(Post, { as: 'post' });

Comment.belongsTo(User, { as: 'author' });
User.hasMany(Comment, { as: 'comments' });

// 创建博客文章
async function createPost(userId, title, content) {
  try {
    const post = await Post.create({
      title,
      content,
      status: 'draft',
      authorId: userId
    });
    return post;
  } catch (error) {
    console.error('创建文章失败:', error);
    throw error;
  }
}

// 获取文章详情(包含作者和评论)
async function getPostById(postId) {
  try {
    const post = await Post.findByPk(postId, {
      include: [
        { model: User, as: 'author' },
        {
          model: Comment, 
          as: 'comments',
          include: [{ model: User, as: 'author' }],
          where: { status: 'approved' },
          required: false
        }
      ]
    });
    return post;
  } catch (error) {
    console.error('获取文章失败:', error);
    throw error;
  }
}

// 发布文章
async function publishPost(postId) {
  try {
    const post = await Post.findByPk(postId);
    if (post) {
      post.status = 'published';
      await post.save();
      return post;
    }
    return null;
  } catch (error) {
    console.error('发布文章失败:', error);
    throw error;
  }
}

3. 电商系统

// 定义模型
const User = sequelize.define('User', {
  name: DataTypes.STRING,
  email: DataTypes.STRING
});

const Product = sequelize.define('Product', {
  name: DataTypes.STRING,
  price: DataTypes.DECIMAL(10, 2),
  stock: DataTypes.INTEGER
});

const Order = sequelize.define('Order', {
  total: DataTypes.DECIMAL(10, 2),
  status: DataTypes.ENUM('pending', 'paid', 'shipping', 'delivered', 'cancelled')
});

const OrderItem = sequelize.define('OrderItem', {
  quantity: DataTypes.INTEGER,
  price: DataTypes.DECIMAL(10, 2)
});

// 定义关联
User.hasMany(Order, { as: 'orders' });
Order.belongsTo(User, { as: 'user' });

Order.belongsToMany(Product, {
  through: OrderItem,
  as: 'products'
});
Product.belongsToMany(Order, {
  through: OrderItem,
  as: 'orders'
});

Order.hasMany(OrderItem, { as: 'items' });
OrderItem.belongsTo(Order, { as: 'order' });

Product.hasMany(OrderItem, { as: 'orderItems' });
OrderItem.belongsTo(Product, { as: 'product' });

// 创建订单
async function createOrder(userId, products) {
  const transaction = await sequelize.transaction();
  
  try {
    // 计算总金额
    let total = 0;
    const orderItems = [];
    
    for (const { productId, quantity } of products) {
      const product = await Product.findByPk(productId, { transaction });
      if (!product) {
        throw new Error(`产品 ${productId} 不存在`);
      }
      
      if (product.stock < quantity) {
        throw new Error(`产品 ${product.name} 库存不足`);
      }
      
      // 减少库存
      product.stock -= quantity;
      await product.save({ transaction });
      
      // 计算金额
      const itemTotal = product.price * quantity;
      total += itemTotal;
      
      orderItems.push({
        productId,
        quantity,
        price: product.price
      });
    }
    
    // 创建订单
    const order = await Order.create({
      userId,
      total,
      status: 'pending'
    }, { transaction });
    
    // 创建订单项
    for (const item of orderItems) {
      await OrderItem.create({
        ...item,
        orderId: order.id
      }, { transaction });
    }
    
    // 提交事务
    await transaction.commit();
    return order;
  } catch (error) {
    // 回滚事务
    await transaction.rollback();
    console.error('创建订单失败:', error);
    throw error;
  }
}

// 使用
await createOrder(1, [
  { productId: 1, quantity: 2 },
  { productId: 2, quantity: 1 }
]);

代码优化建议

  1. 连接池配置:根据应用规模配置合理的连接池大小,避免连接过多或过少
  2. 索引优化:为频繁查询的字段添加索引,提高查询性能
  3. 批量操作:对于大量数据操作,使用批量 API 减少数据库请求次数
  4. 延迟加载:使用延迟加载(懒加载)减少初始查询的数据量
  5. 分页查询:对于大量数据的查询,使用分页减少内存消耗
  6. 事务使用:在涉及多个操作的业务逻辑中使用事务,确保数据一致性
  7. 缓存策略:对频繁访问但不经常变化的数据使用缓存
  8. 错误处理:实现统一的错误处理机制,提高代码可维护性
  9. 日志记录:合理配置日志级别,便于调试和监控
  10. 代码组织
    • 将模型定义分离到单独的文件
    • 使用服务层封装业务逻辑
    • 实现中间件处理通用逻辑

参考资源

« 上一篇 Redis 教程 - 高性能内存数据结构存储 下一篇 » React Router 教程 - React 的声明式路由库