Sequelize 关联关系详解 - 一对一、一对多、多对多关系处理
发布时间:2024-02-26
作者:一介布衣
标签:Sequelize, 关联关系, 一对多, 多对多, 外键
前言
前面几篇文章我们学习了模型定义和查询操作,今天咱们来学习 Sequelize 中最重要也是最复杂的部分 - 关联关系。说实话,关联关系是 ORM 的精髓所在,掌握好了关联关系,你就能构建出复杂而优雅的数据模型。
我记得刚开始学 Sequelize 的时候,最头疼的就是关联关系。什么 hasOne
、belongsTo
、hasMany
、belongsToMany
,还有各种外键配置,真的是搞得我晕头转向。后来慢慢理解了关联的本质,发现其实是有规律可循的。
今天我就把这些关联关系的概念和用法详细讲解一下,让大家能够轻松掌握这个重要知识点。
关联关系基础概念
在开始之前,我们先理解几个核心概念:
- 源模型(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 的数据验证和约束,这是保证数据质量的重要手段。
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!