Skip to content

Feathers.js + Sequelize 模型设计与关联 - 构建复杂的数据关系

发布时间:2024-07-25
作者:一介布衣
标签:Feathers.js, Sequelize, 模型关联, 数据关系, ORM

前言

上一篇文章我们学习了 Feathers.js 与 Sequelize 的基础集成,今天咱们来深入学习模型设计和关联关系。说实话,关系型数据库的精髓就在于表之间的关联,而 Sequelize 在这方面做得特别好,它提供了非常直观的 API 来定义和操作各种关联关系。

我记得刚开始设计数据库的时候,总是搞不清楚一对一、一对多、多对多的区别,更别说在代码中如何实现了。后来用了 Sequelize,发现原来可以这么简单优雅地处理复杂的数据关系。

今天我就带大家深入学习 Sequelize 的模型设计和关联系统,让你能够构建出结构清晰、关系明确的数据模型。

关联关系类型

1. 一对一关联 (One-to-One)

javascript
// src/models/users.model.js - 用户模型
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const users = sequelizeClient.define('users', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    email: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    username: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    }
  }, {
    tableName: 'users',
    timestamps: true,
    underscored: true
  });

  // 关联定义
  users.associate = function(models) {
    // 一对一:用户 -> 用户资料
    users.hasOne(models.userProfiles, {
      foreignKey: 'userId',
      as: 'profile',
      onDelete: 'CASCADE'
    });
  };

  return users;
};

// src/models/user-profiles.model.js - 用户资料模型
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const userProfiles = sequelizeClient.define('userProfiles', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    userId: {
      type: DataTypes.UUID,
      allowNull: false,
      unique: true,
      field: 'user_id',
      references: {
        model: 'users',
        key: 'id'
      }
    },
    
    firstName: {
      type: DataTypes.STRING,
      field: 'first_name'
    },
    
    lastName: {
      type: DataTypes.STRING,
      field: 'last_name'
    },
    
    avatar: DataTypes.STRING,
    bio: DataTypes.TEXT,
    
    birthDate: {
      type: DataTypes.DATEONLY,
      field: 'birth_date'
    },
    
    phone: DataTypes.STRING,
    website: DataTypes.STRING,
    
    // 地址信息
    country: DataTypes.STRING,
    city: DataTypes.STRING,
    address: DataTypes.TEXT,
    postalCode: {
      type: DataTypes.STRING,
      field: 'postal_code'
    },
    
    // 社交媒体
    socialLinks: {
      type: DataTypes.JSON,
      field: 'social_links',
      defaultValue: {}
    }
  }, {
    tableName: 'user_profiles',
    timestamps: true,
    underscored: true
  });

  // 关联定义
  userProfiles.associate = function(models) {
    // 反向关联:用户资料 -> 用户
    userProfiles.belongsTo(models.users, {
      foreignKey: 'userId',
      as: 'user'
    });
  };

  return userProfiles;
};

2. 一对多关联 (One-to-Many)

javascript
// src/models/categories.model.js - 分类模型
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const categories = sequelizeClient.define('categories', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    name: {
      type: DataTypes.STRING,
      allowNull: false
    },
    
    slug: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    
    description: DataTypes.TEXT,
    color: DataTypes.STRING,
    icon: DataTypes.STRING,
    
    sortOrder: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'sort_order'
    },
    
    isActive: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
      field: 'is_active'
    },
    
    // 层级结构支持
    parentId: {
      type: DataTypes.UUID,
      field: 'parent_id',
      references: {
        model: 'categories',
        key: 'id'
      }
    },
    
    level: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    path: DataTypes.STRING,  // 存储层级路径
    
    // 统计信息
    postsCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'posts_count'
    }
  }, {
    tableName: 'categories',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['slug'] },
      { fields: ['parent_id'] },
      { fields: ['is_active'] },
      { fields: ['sort_order'] }
    ]
  });

  // 关联定义
  categories.associate = function(models) {
    // 一对多:分类 -> 文章
    categories.hasMany(models.posts, {
      foreignKey: 'categoryId',
      as: 'posts'
    });
    
    // 自关联:父分类 -> 子分类
    categories.hasMany(categories, {
      foreignKey: 'parentId',
      as: 'children'
    });
    
    // 反向自关联:子分类 -> 父分类
    categories.belongsTo(categories, {
      foreignKey: 'parentId',
      as: 'parent'
    });
  };

  // 实例方法
  categories.prototype.getAncestors = async function() {
    const ancestors = [];
    let current = this;
    
    while (current.parentId) {
      current = await categories.findByPk(current.parentId);
      if (current) {
        ancestors.unshift(current);
      } else {
        break;
      }
    }
    
    return ancestors;
  };

  categories.prototype.getDescendants = async function() {
    const descendants = [];
    
    const findChildren = async (parentId) => {
      const children = await categories.findAll({
        where: { parentId }
      });
      
      for (const child of children) {
        descendants.push(child);
        await findChildren(child.id);
      }
    };
    
    await findChildren(this.id);
    return descendants;
  };

  // 类方法
  categories.buildTree = async function(parentId = null) {
    const categories = await this.findAll({
      where: { parentId },
      order: [['sortOrder', 'ASC'], ['name', 'ASC']]
    });
    
    for (const category of categories) {
      category.dataValues.children = await this.buildTree(category.id);
    }
    
    return categories;
  };

  return categories;
};

// src/models/posts.model.js - 文章模型(关联部分)
posts.associate = function(models) {
  // 多对一:文章 -> 分类
  posts.belongsTo(models.categories, {
    foreignKey: 'categoryId',
    as: 'category'
  });
  
  // 多对一:文章 -> 作者
  posts.belongsTo(models.users, {
    foreignKey: 'authorId',
    as: 'author'
  });
};

3. 多对多关联 (Many-to-Many)

javascript
// src/models/tags.model.js - 标签模型
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const tags = sequelizeClient.define('tags', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    name: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    
    slug: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true
    },
    
    description: DataTypes.TEXT,
    color: DataTypes.STRING,
    
    postsCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'posts_count'
    }
  }, {
    tableName: 'tags',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['slug'] },
      { fields: ['name'] }
    ]
  });

  // 关联定义
  tags.associate = function(models) {
    // 多对多:标签 <-> 文章
    tags.belongsToMany(models.posts, {
      through: 'post_tags',
      foreignKey: 'tagId',
      otherKey: 'postId',
      as: 'posts'
    });
  };

  return tags;
};

// src/models/post-tags.model.js - 中间表模型
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const postTags = sequelizeClient.define('postTags', {
    postId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'post_id',
      references: {
        model: 'posts',
        key: 'id'
      }
    },
    
    tagId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'tag_id',
      references: {
        model: 'tags',
        key: 'id'
      }
    },
    
    // 额外的中间表字段
    order: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    addedBy: {
      type: DataTypes.UUID,
      field: 'added_by',
      references: {
        model: 'users',
        key: 'id'
      }
    }
  }, {
    tableName: 'post_tags',
    timestamps: true,
    underscored: true,
    
    indexes: [
      {
        unique: true,
        fields: ['post_id', 'tag_id']
      },
      { fields: ['post_id'] },
      { fields: ['tag_id'] }
    ]
  });

  return postTags;
};

// 文章模型中的多对多关联
posts.associate = function(models) {
  // 多对多:文章 <-> 标签
  posts.belongsToMany(models.tags, {
    through: models.postTags,
    foreignKey: 'postId',
    otherKey: 'tagId',
    as: 'tags'
  });
};

复杂关联示例

1. 评论系统(自关联 + 多层关联)

javascript
// src/models/comments.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const comments = sequelizeClient.define('comments', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
    
    content: {
      type: DataTypes.TEXT,
      allowNull: false
    },
    
    // 关联字段
    postId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'post_id',
      references: {
        model: 'posts',
        key: 'id'
      }
    },
    
    authorId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'author_id',
      references: {
        model: 'users',
        key: 'id'
      }
    },
    
    // 自关联:回复评论
    parentId: {
      type: DataTypes.UUID,
      field: 'parent_id',
      references: {
        model: 'comments',
        key: 'id'
      }
    },
    
    // 层级信息
    level: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    
    path: DataTypes.STRING,  // 存储评论路径
    
    // 状态
    status: {
      type: DataTypes.ENUM('pending', 'approved', 'rejected'),
      defaultValue: 'pending'
    },
    
    isDeleted: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      field: 'is_deleted'
    },
    
    // 统计
    likesCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'likes_count'
    },
    
    repliesCount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      field: 'replies_count'
    }
  }, {
    tableName: 'comments',
    timestamps: true,
    underscored: true,
    
    indexes: [
      { fields: ['post_id'] },
      { fields: ['author_id'] },
      { fields: ['parent_id'] },
      { fields: ['status'] },
      { fields: ['created_at'] }
    ],
    
    scopes: {
      approved: {
        where: {
          status: 'approved',
          isDeleted: false
        }
      },
      
      topLevel: {
        where: {
          parentId: null
        }
      }
    }
  });

  // 关联定义
  comments.associate = function(models) {
    // 多对一:评论 -> 文章
    comments.belongsTo(models.posts, {
      foreignKey: 'postId',
      as: 'post'
    });
    
    // 多对一:评论 -> 作者
    comments.belongsTo(models.users, {
      foreignKey: 'authorId',
      as: 'author'
    });
    
    // 自关联:父评论 -> 子评论
    comments.hasMany(comments, {
      foreignKey: 'parentId',
      as: 'replies'
    });
    
    // 反向自关联:子评论 -> 父评论
    comments.belongsTo(comments, {
      foreignKey: 'parentId',
      as: 'parent'
    });
    
    // 多对多:评论点赞
    comments.belongsToMany(models.users, {
      through: 'comment_likes',
      foreignKey: 'commentId',
      otherKey: 'userId',
      as: 'likedBy'
    });
  };

  // 实例方法
  comments.prototype.getRepliesTree = async function() {
    const replies = await comments.findAll({
      where: { parentId: this.id },
      include: ['author'],
      order: [['createdAt', 'ASC']]
    });
    
    for (const reply of replies) {
      reply.dataValues.replies = await reply.getRepliesTree();
    }
    
    return replies;
  };

  // 类方法
  comments.buildCommentsTree = async function(postId) {
    const topLevelComments = await this.scope('approved', 'topLevel').findAll({
      where: { postId },
      include: ['author'],
      order: [['createdAt', 'ASC']]
    });
    
    for (const comment of topLevelComments) {
      comment.dataValues.replies = await comment.getRepliesTree();
    }
    
    return topLevelComments;
  };

  return comments;
};

2. 用户关注系统

javascript
// src/models/user-follows.model.js
module.exports = function (app) {
  const sequelizeClient = app.get('sequelizeClient');
  const { DataTypes } = require('sequelize');
  
  const userFollows = sequelizeClient.define('userFollows', {
    followerId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'follower_id',
      references: {
        model: 'users',
        key: 'id'
      }
    },
    
    followingId: {
      type: DataTypes.UUID,
      allowNull: false,
      field: 'following_id',
      references: {
        model: 'users',
        key: 'id'
      }
    },
    
    // 关注类型
    type: {
      type: DataTypes.ENUM('follow', 'block', 'mute'),
      defaultValue: 'follow'
    },
    
    // 通知设置
    notifications: {
      type: DataTypes.BOOLEAN,
      defaultValue: true
    }
  }, {
    tableName: 'user_follows',
    timestamps: true,
    underscored: true,
    
    indexes: [
      {
        unique: true,
        fields: ['follower_id', 'following_id']
      },
      { fields: ['follower_id'] },
      { fields: ['following_id'] },
      { fields: ['type'] }
    ],
    
    validate: {
      notSelfFollow() {
        if (this.followerId === this.followingId) {
          throw new Error('用户不能关注自己');
        }
      }
    }
  });

  return userFollows;
};

// 用户模型中的关注关联
users.associate = function(models) {
  // 用户关注的人
  users.belongsToMany(users, {
    through: models.userFollows,
    as: 'following',
    foreignKey: 'followerId',
    otherKey: 'followingId'
  });
  
  // 关注用户的人
  users.belongsToMany(users, {
    through: models.userFollows,
    as: 'followers',
    foreignKey: 'followingId',
    otherKey: 'followerId'
  });
  
  // 直接访问关注关系
  users.hasMany(models.userFollows, {
    foreignKey: 'followerId',
    as: 'followingRelations'
  });
  
  users.hasMany(models.userFollows, {
    foreignKey: 'followingId',
    as: 'followerRelations'
  });
};

高级查询技巧

1. 复杂的关联查询

javascript
// src/services/posts/posts.class.js
class PostsService extends Service {
  async find(params) {
    const { query = {} } = params;
    
    const sequelizeOptions = {
      include: [
        // 包含作者信息
        {
          model: this.app.service('users').Model,
          as: 'author',
          attributes: ['id', 'username', 'firstName', 'lastName', 'avatar'],
          include: [
            {
              model: this.app.service('userProfiles').Model,
              as: 'profile',
              attributes: ['bio', 'website']
            }
          ]
        },
        
        // 包含分类信息
        {
          model: this.app.service('categories').Model,
          as: 'category',
          attributes: ['id', 'name', 'slug', 'color']
        },
        
        // 包含标签信息
        {
          model: this.app.service('tags').Model,
          as: 'tags',
          attributes: ['id', 'name', 'slug', 'color'],
          through: { attributes: [] }  // 不包含中间表字段
        },
        
        // 包含评论统计
        {
          model: this.app.service('comments').Model,
          as: 'comments',
          attributes: [],
          where: { status: 'approved' },
          required: false
        }
      ],
      
      // 添加计算字段
      attributes: {
        include: [
          [
            this.Model.sequelize.fn('COUNT', 
              this.Model.sequelize.col('comments.id')
            ),
            'commentsCount'
          ]
        ]
      },
      
      group: ['posts.id', 'author.id', 'author->profile.id', 'category.id', 'tags.id'],
      order: [['publishedAt', 'DESC']]
    };

    // 按分类筛选
    if (query.category) {
      sequelizeOptions.include[1].where = { slug: query.category };
      delete query.category;
    }

    // 按标签筛选
    if (query.tags) {
      const tags = Array.isArray(query.tags) ? query.tags : [query.tags];
      sequelizeOptions.include[2].where = { slug: { [Op.in]: tags } };
      sequelizeOptions.having = this.Model.sequelize.where(
        this.Model.sequelize.fn('COUNT', this.Model.sequelize.col('tags.id')),
        tags.length
      );
      delete query.tags;
    }

    // 按作者筛选
    if (query.author) {
      sequelizeOptions.include[0].where = { username: query.author };
      delete query.author;
    }

    params.sequelize = sequelizeOptions;
    
    return super.find(params);
  }

  // 获取热门文章
  async getPopularPosts(params = {}) {
    const { limit = 10, days = 7 } = params;
    const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
    
    return this.Model.findAll({
      where: {
        status: 'published',
        publishedAt: { [Op.gte]: startDate }
      },
      include: [
        {
          model: this.app.service('users').Model,
          as: 'author',
          attributes: ['username', 'firstName', 'lastName', 'avatar']
        },
        {
          model: this.app.service('categories').Model,
          as: 'category',
          attributes: ['name', 'slug', 'color']
        }
      ],
      attributes: {
        include: [
          [
            this.Model.sequelize.literal(
              '(view_count + like_count * 5 + comment_count * 10)'
            ),
            'popularityScore'
          ]
        ]
      },
      order: [
        [this.Model.sequelize.literal('popularityScore'), 'DESC'],
        ['publishedAt', 'DESC']
      ],
      limit
    });
  }

  // 获取相关文章
  async getRelatedPosts(postId, params = {}) {
    const { limit = 5 } = params;
    
    // 获取当前文章的标签
    const currentPost = await this.Model.findByPk(postId, {
      include: ['tags']
    });
    
    if (!currentPost || !currentPost.tags.length) {
      return [];
    }
    
    const tagIds = currentPost.tags.map(tag => tag.id);
    
    return this.Model.findAll({
      where: {
        id: { [Op.ne]: postId },
        status: 'published'
      },
      include: [
        {
          model: this.app.service('tags').Model,
          as: 'tags',
          where: { id: { [Op.in]: tagIds } },
          through: { attributes: [] }
        },
        {
          model: this.app.service('users').Model,
          as: 'author',
          attributes: ['username', 'firstName', 'lastName', 'avatar']
        }
      ],
      attributes: {
        include: [
          [
            this.Model.sequelize.fn('COUNT', 
              this.Model.sequelize.col('tags.id')
            ),
            'matchingTags'
          ]
        ]
      },
      group: ['posts.id', 'author.id'],
      order: [
        [this.Model.sequelize.literal('matchingTags'), 'DESC'],
        ['publishedAt', 'DESC']
      ],
      limit
    });
  }
}

2. 聚合查询示例

javascript
// 用户统计服务
class UserStatsService {
  constructor(app) {
    this.app = app;
    this.User = app.service('users').Model;
    this.Post = app.service('posts').Model;
    this.Comment = app.service('comments').Model;
  }

  // 获取用户详细统计
  async getUserDetailedStats(userId) {
    const stats = await this.User.findByPk(userId, {
      include: [
        {
          model: this.Post,
          as: 'posts',
          attributes: [],
          where: { status: 'published' },
          required: false
        },
        {
          model: this.Comment,
          as: 'comments',
          attributes: [],
          where: { status: 'approved' },
          required: false
        }
      ],
      attributes: [
        'id',
        'username',
        [this.User.sequelize.fn('COUNT', this.User.sequelize.col('posts.id')), 'postsCount'],
        [this.User.sequelize.fn('COUNT', this.User.sequelize.col('comments.id')), 'commentsCount'],
        [this.User.sequelize.fn('SUM', this.User.sequelize.col('posts.view_count')), 'totalViews'],
        [this.User.sequelize.fn('SUM', this.User.sequelize.col('posts.like_count')), 'totalLikes']
      ],
      group: ['users.id']
    });

    return stats;
  }

  // 获取活跃用户排行
  async getActiveUsersRanking(params = {}) {
    const { limit = 10, days = 30 } = params;
    const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
    
    return this.User.findAll({
      include: [
        {
          model: this.Post,
          as: 'posts',
          attributes: [],
          where: {
            status: 'published',
            createdAt: { [Op.gte]: startDate }
          },
          required: false
        },
        {
          model: this.Comment,
          as: 'comments',
          attributes: [],
          where: {
            status: 'approved',
            createdAt: { [Op.gte]: startDate }
          },
          required: false
        }
      ],
      attributes: [
        'id',
        'username',
        'firstName',
        'lastName',
        'avatar',
        [this.User.sequelize.fn('COUNT', this.User.sequelize.col('posts.id')), 'recentPosts'],
        [this.User.sequelize.fn('COUNT', this.User.sequelize.col('comments.id')), 'recentComments'],
        [
          this.User.sequelize.literal(
            '(COUNT(posts.id) * 10 + COUNT(comments.id) * 2)'
          ),
          'activityScore'
        ]
      ],
      group: ['users.id'],
      order: [
        [this.User.sequelize.literal('activityScore'), 'DESC'],
        ['createdAt', 'ASC']
      ],
      limit
    });
  }
}

总结

通过这篇文章,我们深入学习了 Sequelize 的模型设计和关联系统:

关联关系类型

  • 一对一关联的设计和实现
  • 一对多关联和层级结构
  • 多对多关联和中间表

复杂关联示例

  • 评论系统的自关联
  • 用户关注系统设计
  • 多层级数据结构

高级查询技巧

  • 复杂的关联查询
  • 聚合统计查询
  • 性能优化策略

最佳实践

  • 模型设计原则
  • 关联定义规范
  • 查询优化技巧

掌握了这些知识,你就能够设计出结构清晰、关系明确的复杂数据模型,并高效地进行各种查询操作。

下一篇文章,我们将学习高级查询和性能优化。


相关文章推荐:

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