Skip to content

Sequelize 关联关系详解 - 一对一、一对多、多对多关系处理

发布时间:2024-02-26
作者:一介布衣
标签:Sequelize, 关联关系, 一对多, 多对多, 外键

前言

前面几篇文章我们学习了模型定义和查询操作,今天咱们来学习 Sequelize 中最重要也是最复杂的部分 - 关联关系。说实话,关联关系是 ORM 的精髓所在,掌握好了关联关系,你就能构建出复杂而优雅的数据模型。

我记得刚开始学 Sequelize 的时候,最头疼的就是关联关系。什么 hasOnebelongsTohasManybelongsToMany,还有各种外键配置,真的是搞得我晕头转向。后来慢慢理解了关联的本质,发现其实是有规律可循的。

今天我就把这些关联关系的概念和用法详细讲解一下,让大家能够轻松掌握这个重要知识点。

关联关系基础概念

在开始之前,我们先理解几个核心概念:

  • 源模型(Source Model):定义关联的模型
  • 目标模型(Target Model):被关联的模型
  • 外键(Foreign Key):存储关联关系的字段
  • 关联别名(Alias):给关联起的别名,用于查询时引用

一对一关系(One-to-One)

1. hasOne 关系

一个用户有一个用户资料:

javascript
// 用户模型
const User = sequelize.define('User', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: DataTypes.STRING,
  email: DataTypes.STRING
});

// 用户资料模型
const Profile = sequelize.define('Profile', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  bio: DataTypes.TEXT,
  avatar: DataTypes.STRING,
  userId: {  // 外键
    type: DataTypes.INTEGER,
    references: {
      model: User,
      key: 'id'
    }
  }
});

// 建立关联
User.hasOne(Profile, {
  foreignKey: 'userId',  // Profile 表中的外键
  as: 'profile'          // 别名
});

2. belongsTo 关系

从另一个方向建立关联:

javascript
Profile.belongsTo(User, {
  foreignKey: 'userId',
  as: 'user'
});

3. 双向关联

通常我们会建立双向关联:

javascript
// 用户有一个资料
User.hasOne(Profile, {
  foreignKey: 'userId',
  as: 'profile'
});

// 资料属于一个用户
Profile.belongsTo(User, {
  foreignKey: 'userId',
  as: 'user'
});

4. 使用一对一关联

javascript
// 创建用户和资料
const user = await User.create({
  name: '张三',
  email: 'zhangsan@example.com'
});

const profile = await Profile.create({
  bio: '这是我的个人简介',
  avatar: 'avatar.jpg',
  userId: user.id
});

// 查询用户及其资料
const userWithProfile = await User.findByPk(1, {
  include: ['profile']
});

console.log(userWithProfile.profile.bio);

// 查询资料及其用户
const profileWithUser = await Profile.findByPk(1, {
  include: ['user']
});

console.log(profileWithUser.user.name);

// 使用关联方法
const user = await User.findByPk(1);
const profile = await user.getProfile();  // 获取关联的资料
await user.setProfile(newProfile);        // 设置关联的资料
await user.createProfile({                // 创建关联的资料
  bio: '新的简介'
});

一对多关系(One-to-Many)

1. hasMany 关系

一个用户有多篇文章:

javascript
// 文章模型
const Post = sequelize.define('Post', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  title: DataTypes.STRING,
  content: DataTypes.TEXT,
  userId: {  // 外键
    type: DataTypes.INTEGER,
    references: {
      model: User,
      key: 'id'
    }
  }
});

// 建立关联
User.hasMany(Post, {
  foreignKey: 'userId',
  as: 'posts'
});

Post.belongsTo(User, {
  foreignKey: 'userId',
  as: 'author'
});

2. 使用一对多关联

javascript
// 创建用户和文章
const user = await User.create({
  name: '李四',
  email: 'lisi@example.com'
});

// 为用户创建文章
await user.createPost({
  title: '我的第一篇文章',
  content: '这是文章内容...'
});

// 查询用户及其所有文章
const userWithPosts = await User.findByPk(1, {
  include: ['posts']
});

console.log(`\${userWithPosts.name} 写了 \${userWithPosts.posts.length} 篇文章`);

// 查询文章及其作者
const postWithAuthor = await Post.findByPk(1, {
  include: ['author']
});

console.log(`文章《\${postWithAuthor.title}》的作者是 \${postWithAuthor.author.name}`);

// 使用关联方法
const user = await User.findByPk(1);
const posts = await user.getPosts();           // 获取所有文章
const postCount = await user.countPosts();     // 统计文章数量
await user.addPost(existingPost);              // 添加现有文章
await user.setPosts([post1, post2]);           // 设置文章列表
await user.removePost(post);                   // 移除文章

3. 带条件的关联查询

javascript
// 查询用户及其已发布的文章
const userWithPublishedPosts = await User.findByPk(1, {
  include: [{
    model: Post,
    as: 'posts',
    where: { status: 'published' }
  }]
});

// 查询最近的文章
const userWithRecentPosts = await User.findByPk(1, {
  include: [{
    model: Post,
    as: 'posts',
    order: [['createdAt', 'DESC']],
    limit: 5
  }]
});

多对多关系(Many-to-Many)

1. belongsToMany 关系

用户和角色的多对多关系:

javascript
// 角色模型
const Role = sequelize.define('Role', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: DataTypes.STRING,
  description: DataTypes.STRING
});

// 中间表模型(可选,用于存储额外信息)
const UserRole = sequelize.define('UserRole', {
  userId: {
    type: DataTypes.INTEGER,
    references: {
      model: User,
      key: 'id'
    }
  },
  roleId: {
    type: DataTypes.INTEGER,
    references: {
      model: Role,
      key: 'id'
    }
  },
  assignedAt: {
    type: DataTypes.DATE,
    defaultValue: DataTypes.NOW
  },
  assignedBy: DataTypes.INTEGER
});

// 建立多对多关联
User.belongsToMany(Role, {
  through: UserRole,      // 中间表
  foreignKey: 'userId',   // User 在中间表的外键
  otherKey: 'roleId',     // Role 在中间表的外键
  as: 'roles'
});

Role.belongsToMany(User, {
  through: UserRole,
  foreignKey: 'roleId',
  otherKey: 'userId',
  as: 'users'
});

2. 使用多对多关联

javascript
// 创建用户和角色
const user = await User.create({
  name: '王五',
  email: 'wangwu@example.com'
});

const adminRole = await Role.create({
  name: 'admin',
  description: '管理员'
});

const editorRole = await Role.create({
  name: 'editor',
  description: '编辑'
});

// 为用户分配角色
await user.addRole(adminRole);
await user.addRoles([adminRole, editorRole]);

// 查询用户及其角色
const userWithRoles = await User.findByPk(1, {
  include: ['roles']
});

console.log(`\${userWithRoles.name} 的角色:`, 
  userWithRoles.roles.map(role => role.name)
);

// 查询角色及其用户
const roleWithUsers = await Role.findByPk(1, {
  include: ['users']
});

// 使用关联方法
const user = await User.findByPk(1);
const roles = await user.getRoles();           // 获取所有角色
const hasAdminRole = await user.hasRole(adminRole);  // 检查是否有某个角色
await user.removeRole(editorRole);             // 移除角色
await user.setRoles([adminRole]);              // 设置角色列表

3. 中间表额外字段

javascript
// 为用户分配角色时添加额外信息
await user.addRole(adminRole, {
  through: {
    assignedAt: new Date(),
    assignedBy: currentUserId
  }
});

// 查询时包含中间表信息
const userWithRoles = await User.findByPk(1, {
  include: [{
    model: Role,
    as: 'roles',
    through: {
      attributes: ['assignedAt', 'assignedBy']  // 包含中间表字段
    }
  }]
});

// 访问中间表数据
userWithRoles.roles.forEach(role => {
  console.log(`角色 \${role.name} 分配时间: \${role.UserRole.assignedAt}`);
});

复杂关联场景

1. 自关联

用户的上下级关系:

javascript
// 用户自关联
User.hasMany(User, {
  foreignKey: 'managerId',
  as: 'subordinates'  // 下属
});

User.belongsTo(User, {
  foreignKey: 'managerId',
  as: 'manager'       // 上级
});

// 使用自关联
const manager = await User.findByPk(1, {
  include: ['subordinates']  // 查询经理及其下属
});

const employee = await User.findByPk(2, {
  include: ['manager']       // 查询员工及其经理
});

2. 多层关联

用户 -> 文章 -> 评论:

javascript
// 评论模型
const Comment = sequelize.define('Comment', {
  content: DataTypes.TEXT,
  postId: DataTypes.INTEGER,
  userId: DataTypes.INTEGER
});

// 建立关联
Post.hasMany(Comment, { foreignKey: 'postId', as: 'comments' });
Comment.belongsTo(Post, { foreignKey: 'postId', as: 'post' });
Comment.belongsTo(User, { foreignKey: 'userId', as: 'author' });

// 多层查询
const userWithPostsAndComments = await User.findByPk(1, {
  include: [{
    model: Post,
    as: 'posts',
    include: [{
      model: Comment,
      as: 'comments',
      include: [{
        model: User,
        as: 'author',
        attributes: ['name']
      }]
    }]
  }]
});

3. 条件关联

javascript
// 只查询已发布的文章
const userWithPublishedPosts = await User.findAll({
  include: [{
    model: Post,
    as: 'posts',
    where: { status: 'published' },
    required: false  // LEFT JOIN,即使没有已发布文章也返回用户
  }]
});

// 查询有文章的用户
const usersWithPosts = await User.findAll({
  include: [{
    model: Post,
    as: 'posts',
    required: true  // INNER JOIN,只返回有文章的用户
  }]
});

关联配置选项

1. 外键配置

javascript
User.hasMany(Post, {
  foreignKey: {
    name: 'authorId',      // 外键名称
    allowNull: false,      // 不允许为空
    onDelete: 'CASCADE',   // 删除时级联
    onUpdate: 'CASCADE'    // 更新时级联
  },
  as: 'articles'
});

2. 作用域关联

javascript
// 定义作用域
Post.addScope('published', {
  where: { status: 'published' }
});

// 使用作用域关联
User.hasMany(Post.scope('published'), {
  foreignKey: 'userId',
  as: 'publishedPosts'
});

// 查询时自动应用作用域
const user = await User.findByPk(1, {
  include: ['publishedPosts']
});

3. 关联钩子

javascript
User.hasMany(Post, {
  foreignKey: 'userId',
  as: 'posts',
  hooks: true  // 启用关联钩子
});

// 关联钩子
Post.addHook('afterCreate', async (post, options) => {
  // 文章创建后更新用户的文章数量
  const user = await User.findByPk(post.userId);
  await user.increment('postCount');
});

性能优化

1. 预加载优化

javascript
// 避免 N+1 查询
const users = await User.findAll({
  include: ['posts']  // 一次查询获取所有数据
});

// 分离查询(适合大数据量)
const users = await User.findAll({
  include: [{
    model: Post,
    as: 'posts',
    separate: true,  // 分离查询
    limit: 5         // 每个用户最多5篇文章
  }]
});

2. 选择性加载

javascript
// 只加载需要的字段
const users = await User.findAll({
  attributes: ['id', 'name'],
  include: [{
    model: Post,
    as: 'posts',
    attributes: ['id', 'title', 'createdAt']
  }]
});

3. 计数优化

javascript
// 使用子查询计数
const users = await User.findAll({
  attributes: [
    'id',
    'name',
    [
      sequelize.literal('(SELECT COUNT(*) FROM posts WHERE posts.user_id = User.id)'),
      'postCount'
    ]
  ]
});

实战案例

博客系统关联设计

javascript
// 完整的博客系统关联
class BlogModels {
  static init(sequelize) {
    // 用户模型
    const User = sequelize.define('User', {
      name: DataTypes.STRING,
      email: DataTypes.STRING
    });

    // 分类模型
    const Category = sequelize.define('Category', {
      name: DataTypes.STRING,
      slug: DataTypes.STRING
    });

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

    // 文章模型
    const Post = sequelize.define('Post', {
      title: DataTypes.STRING,
      content: DataTypes.TEXT,
      status: DataTypes.ENUM('draft', 'published'),
      publishedAt: DataTypes.DATE
    });

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

    // 建立关联
    // 用户 -> 文章 (一对多)
    User.hasMany(Post, { foreignKey: 'authorId', as: 'posts' });
    Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' });

    // 分类 -> 文章 (一对多)
    Category.hasMany(Post, { foreignKey: 'categoryId', as: 'posts' });
    Post.belongsTo(Category, { foreignKey: 'categoryId', as: 'category' });

    // 文章 <-> 标签 (多对多)
    Post.belongsToMany(Tag, {
      through: 'PostTags',
      foreignKey: 'postId',
      otherKey: 'tagId',
      as: 'tags'
    });
    Tag.belongsToMany(Post, {
      through: 'PostTags',
      foreignKey: 'tagId',
      otherKey: 'postId',
      as: 'posts'
    });

    return { User, Category, Tag, Post, Comment };
  }
}

// 使用示例
async function getBlogPost(postId) {
  return await Post.findByPk(postId, {
    include: [
      {
        model: User,
        as: 'author',
        attributes: ['id', 'name']
      },
      {
        model: Category,
        as: 'category',
        attributes: ['id', 'name', 'slug']
      },
      {
        model: Tag,
        as: 'tags',
        attributes: ['id', 'name', 'slug'],
        through: { attributes: [] }  // 不包含中间表字段
      }
    ]
  });
}

总结

今天我们深入学习了 Sequelize 的关联关系:

  • ✅ 一对一关系的建立和使用
  • ✅ 一对多关系的各种场景
  • ✅ 多对多关系和中间表处理
  • ✅ 复杂关联场景的解决方案
  • ✅ 关联配置和性能优化
  • ✅ 实际项目中的关联设计

掌握了这些知识,你就能够:

  • 设计合理的数据模型关联
  • 处理复杂的业务关系
  • 优化关联查询性能
  • 构建完整的应用数据层

下一篇文章,我们将学习 Sequelize 的数据验证和约束,这是保证数据质量的重要手段。


相关文章推荐:

有问题欢迎留言讨论,我会及时回复大家!