Feathers.js + Sequelize 基础集成 - 关系型数据库的完美搭档
发布时间:2024-07-20
作者:一介布衣
标签:Feathers.js, Sequelize, ORM, 关系型数据库, 集成
前言
在前面的 Feathers.js 系列文章中,我们学习了如何使用 Knex.js 操作 SQL 数据库。今天咱们来学习另一个强大的选择 - Sequelize ORM。说实话,Sequelize 是 Node.js 生态中最成熟的 ORM 之一,它提供了丰富的功能和优雅的 API。
我记得刚开始用 Sequelize 的时候,被它的功能震撼了:自动的表关联、数据验证、事务支持、迁移系统等等。而且 Feathers.js 对 Sequelize 的支持非常好,两者结合起来开发效率特别高。
今天我就带大家从零开始,学习如何在 Feathers.js 中集成和使用 Sequelize。
环境搭建
1. 安装依赖
bash
# 安装 Feathers.js Sequelize 适配器
npm install @feathersjs/sequelize sequelize
# 安装数据库驱动(选择一个)
npm install pg pg-hstore # PostgreSQL
npm install mysql2 # MySQL
npm install mariadb # MariaDB
npm install sqlite3 # SQLite
npm install tedious # SQL Server
# 安装开发工具
npm install --save-dev sequelize-cli
npm install --save-dev @types/sequelize # TypeScript 支持
2. 初始化 Sequelize
bash
# 初始化 Sequelize 配置
npx sequelize-cli init
# 这会创建以下目录结构:
# config/config.json - 数据库配置
# models/ - 模型定义
# migrations/ - 数据库迁移
# seeders/ - 种子数据
3. 数据库配置
javascript
// config/config.js
module.exports = {
development: {
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'feathers_dev',
host: process.env.DB_HOST || '127.0.0.1',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
},
test: {
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'feathers_test',
host: process.env.DB_HOST || '127.0.0.1',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false
},
production: {
use_env_variable: 'DATABASE_URL',
dialect: 'postgres',
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false
}
},
pool: {
max: 20,
min: 5,
acquire: 60000,
idle: 10000
},
logging: false
}
};
Sequelize 集成配置
1. 创建 Sequelize 实例
javascript
// src/sequelize.js
const { Sequelize } = require('sequelize');
const config = require('../config/config');
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
let sequelize;
if (dbConfig.use_env_variable) {
sequelize = new Sequelize(process.env[dbConfig.use_env_variable], dbConfig);
} else {
sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
dbConfig
);
}
module.exports = function (app) {
// 设置 Sequelize 实例
app.set('sequelizeClient', sequelize);
// 测试连接
sequelize.authenticate()
.then(() => {
console.log('✅ Sequelize 数据库连接成功');
})
.catch(err => {
console.error('❌ Sequelize 数据库连接失败:', err);
process.exit(1);
});
// 同步数据库(开发环境)
if (process.env.NODE_ENV === 'development') {
sequelize.sync({ alter: true })
.then(() => {
console.log('📊 数据库同步完成');
})
.catch(err => {
console.error('❌ 数据库同步失败:', err);
});
}
return sequelize;
};
2. 应用配置
javascript
// src/app.js
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const socketio = require('@feathersjs/socketio');
const sequelize = require('./sequelize');
const services = require('./services');
const app = express(feathers());
// 配置 Sequelize
app.configure(sequelize);
// 其他配置...
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.configure(express.rest());
app.configure(socketio());
// 配置服务
app.configure(services);
module.exports = app;
模型定义
1. 用户模型
javascript
// src/models/users.model.js
const { DataTypes } = require('sequelize');
module.exports = function (app) {
const sequelizeClient = app.get('sequelizeClient');
const users = sequelizeClient.define('users', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
// 基本信息
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
len: [3, 30],
isAlphanumeric: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [6, 255]
}
},
// 个人资料
firstName: {
type: DataTypes.STRING,
field: 'first_name',
validate: {
len: [1, 50]
}
},
lastName: {
type: DataTypes.STRING,
field: 'last_name',
validate: {
len: [1, 50]
}
},
avatar: {
type: DataTypes.STRING,
validate: {
isUrl: true
}
},
bio: {
type: DataTypes.TEXT
},
birthDate: {
type: DataTypes.DATEONLY,
field: 'birth_date',
validate: {
isDate: true,
isBefore: new Date().toISOString()
}
},
// 联系信息
phone: {
type: DataTypes.STRING,
validate: {
is: /^[\+]?[1-9][\d]{0,15}$/
}
},
website: {
type: DataTypes.STRING,
validate: {
isUrl: true
}
},
// 地址信息
country: DataTypes.STRING,
city: DataTypes.STRING,
address: DataTypes.TEXT,
postalCode: {
type: DataTypes.STRING,
field: 'postal_code'
},
// 系统字段
role: {
type: DataTypes.ENUM('user', 'moderator', 'admin'),
defaultValue: 'user'
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'suspended'),
defaultValue: 'active'
},
emailVerified: {
type: DataTypes.BOOLEAN,
defaultValue: false,
field: 'email_verified'
},
emailVerificationToken: {
type: DataTypes.STRING,
field: 'email_verification_token'
},
// 偏好设置(JSON 字段)
preferences: {
type: DataTypes.JSON,
defaultValue: {
theme: 'light',
language: 'zh-CN',
notifications: {
email: true,
push: true,
sms: false
}
}
},
// 统计信息
postsCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'posts_count'
},
followersCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'followers_count'
},
followingCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'following_count'
},
lastLoginAt: {
type: DataTypes.DATE,
field: 'last_login_at'
}
}, {
// 表选项
tableName: 'users',
timestamps: true,
underscored: true,
// 索引
indexes: [
{
unique: true,
fields: ['email']
},
{
unique: true,
fields: ['username']
},
{
fields: ['status']
},
{
fields: ['role']
},
{
fields: ['created_at']
}
],
// 作用域
scopes: {
active: {
where: {
status: 'active'
}
},
withoutPassword: {
attributes: {
exclude: ['password', 'emailVerificationToken']
}
},
publicProfile: {
attributes: [
'id', 'username', 'firstName', 'lastName',
'avatar', 'bio', 'website', 'createdAt'
]
}
},
// 钩子
hooks: {
beforeCreate: (user, options) => {
// 在创建前的处理
if (user.email) {
user.email = user.email.toLowerCase();
}
},
beforeUpdate: (user, options) => {
// 在更新前的处理
if (user.changed('email')) {
user.email = user.email.toLowerCase();
}
}
}
});
// 实例方法
users.prototype.toSafeObject = function() {
const values = this.get({ plain: true });
delete values.password;
delete values.emailVerificationToken;
return values;
};
users.prototype.getFullName = function() {
return `\${this.firstName || ''} \${this.lastName || ''}`.trim();
};
users.prototype.hasRole = function(roles) {
const roleArray = Array.isArray(roles) ? roles : [roles];
return roleArray.includes(this.role);
};
// 类方法
users.findByEmail = function(email) {
return this.findOne({
where: { email: email.toLowerCase() }
});
};
users.findByUsername = function(username) {
return this.findOne({
where: { username }
});
};
// 关联定义(在所有模型加载后)
users.associate = function(models) {
// 用户发布的文章
users.hasMany(models.posts, {
foreignKey: 'authorId',
as: 'posts'
});
// 用户的评论
users.hasMany(models.comments, {
foreignKey: 'authorId',
as: 'comments'
});
// 用户关注关系
users.belongsToMany(users, {
through: 'user_follows',
as: 'followers',
foreignKey: 'followingId',
otherKey: 'followerId'
});
users.belongsToMany(users, {
through: 'user_follows',
as: 'following',
foreignKey: 'followerId',
otherKey: 'followingId'
});
};
return users;
};
2. 文章模型
javascript
// src/models/posts.model.js
const { DataTypes } = require('sequelize');
module.exports = function (app) {
const sequelizeClient = app.get('sequelizeClient');
const posts = sequelizeClient.define('posts', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [1, 255]
}
},
slug: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
is: /^[a-z0-9-]+$/
}
},
excerpt: {
type: DataTypes.TEXT,
validate: {
len: [0, 500]
}
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
// 关联字段
authorId: {
type: DataTypes.UUID,
allowNull: false,
field: 'author_id',
references: {
model: 'users',
key: 'id'
}
},
categoryId: {
type: DataTypes.UUID,
field: 'category_id',
references: {
model: 'categories',
key: 'id'
}
},
// 状态
status: {
type: DataTypes.ENUM('draft', 'published', 'archived'),
defaultValue: 'draft'
},
isFeatured: {
type: DataTypes.BOOLEAN,
defaultValue: false,
field: 'is_featured'
},
allowComments: {
type: DataTypes.BOOLEAN,
defaultValue: true,
field: 'allow_comments'
},
// 媒体
featuredImage: {
type: DataTypes.STRING,
field: 'featured_image',
validate: {
isUrl: true
}
},
gallery: {
type: DataTypes.JSON,
defaultValue: []
},
// SEO
metaTitle: {
type: DataTypes.STRING,
field: 'meta_title'
},
metaDescription: {
type: DataTypes.TEXT,
field: 'meta_description'
},
metaKeywords: {
type: DataTypes.JSON,
field: 'meta_keywords',
defaultValue: []
},
// 统计
viewCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'view_count'
},
likeCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'like_count'
},
commentCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'comment_count'
},
shareCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
field: 'share_count'
},
// 时间
publishedAt: {
type: DataTypes.DATE,
field: 'published_at'
}
}, {
tableName: 'posts',
timestamps: true,
underscored: true,
indexes: [
{
fields: ['author_id']
},
{
fields: ['category_id']
},
{
fields: ['status']
},
{
fields: ['published_at']
},
{
unique: true,
fields: ['slug']
},
{
fields: ['is_featured']
}
],
scopes: {
published: {
where: {
status: 'published'
}
},
featured: {
where: {
isFeatured: true
}
},
withAuthor: {
include: [{
model: sequelizeClient.models.users,
as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'avatar']
}]
}
}
});
// 实例方法
posts.prototype.getReadingTime = function() {
const wordsPerMinute = 200;
const wordCount = this.content.split(/\s+/).length;
return Math.ceil(wordCount / wordsPerMinute);
};
posts.prototype.isPublished = function() {
return this.status === 'published' && this.publishedAt;
};
// 类方法
posts.findBySlug = function(slug) {
return this.findOne({
where: { slug },
include: ['author', 'category', 'tags']
});
};
posts.findPublished = function(options = {}) {
return this.scope('published').findAll({
order: [['publishedAt', 'DESC']],
...options
});
};
// 关联定义
posts.associate = function(models) {
// 文章作者
posts.belongsTo(models.users, {
foreignKey: 'authorId',
as: 'author'
});
// 文章分类
posts.belongsTo(models.categories, {
foreignKey: 'categoryId',
as: 'category'
});
// 文章标签(多对多)
posts.belongsToMany(models.tags, {
through: 'post_tags',
foreignKey: 'postId',
otherKey: 'tagId',
as: 'tags'
});
// 文章评论
posts.hasMany(models.comments, {
foreignKey: 'postId',
as: 'comments'
});
};
return posts;
};
服务实现
1. 用户服务
javascript
// src/services/users/users.class.js
const { Service } = require('feathers-sequelize');
class UsersService extends Service {
constructor(options, app) {
super(options, app);
this.app = app;
}
async find(params) {
const { query = {} } = params;
// 构建查询选项
const sequelizeOptions = {
include: [],
where: {},
order: [['createdAt', 'DESC']]
};
// 搜索功能
if (query.search) {
const { Op } = require('sequelize');
sequelizeOptions.where[Op.or] = [
{ username: { [Op.iLike]: `%\${query.search}%` } },
{ firstName: { [Op.iLike]: `%\${query.search}%` } },
{ lastName: { [Op.iLike]: `%\${query.search}%` } }
];
delete query.search;
}
// 角色筛选
if (query.role) {
sequelizeOptions.where.role = query.role;
delete query.role;
}
// 状态筛选
if (query.status) {
sequelizeOptions.where.status = query.status;
delete query.status;
}
// 关联查询
if (query.$populate) {
const populate = Array.isArray(query.$populate) ? query.$populate : [query.$populate];
if (populate.includes('posts')) {
sequelizeOptions.include.push({
model: this.app.service('posts').Model,
as: 'posts',
where: { status: 'published' },
required: false
});
}
delete query.$populate;
}
// 默认排除敏感字段
if (!query.$select) {
sequelizeOptions.attributes = {
exclude: ['password', 'emailVerificationToken']
};
}
// 合并查询参数
params.sequelize = sequelizeOptions;
return super.find(params);
}
async get(id, params) {
const { query = {} } = params;
const sequelizeOptions = {
include: [],
attributes: {
exclude: ['password', 'emailVerificationToken']
}
};
// 关联查询
if (query.$populate) {
const populate = Array.isArray(query.$populate) ? query.$populate : [query.$populate];
if (populate.includes('posts')) {
sequelizeOptions.include.push({
model: this.app.service('posts').Model,
as: 'posts',
where: { status: 'published' },
required: false,
limit: 10,
order: [['publishedAt', 'DESC']]
});
}
}
params.sequelize = sequelizeOptions;
return super.get(id, params);
}
async create(data, params) {
// 数据预处理
const userData = {
...data,
email: data.email.toLowerCase()
};
// 生成用户名(如果没有提供)
if (!userData.username && userData.email) {
userData.username = await this.generateUsername(userData.email);
}
const result = await super.create(userData, params);
// 创建后处理
await this.initializeUserData(result.id);
return result;
}
async patch(id, data, params) {
// 邮箱转小写
if (data.email) {
data.email = data.email.toLowerCase();
}
return super.patch(id, data, params);
}
// 生成唯一用户名
async generateUsername(email) {
const baseUsername = email.split('@')[0].toLowerCase();
let username = baseUsername;
let counter = 1;
while (await this.Model.findByUsername(username)) {
username = `\${baseUsername}\${counter}`;
counter++;
}
return username;
}
// 初始化用户数据
async initializeUserData(userId) {
// 创建默认分类等初始化操作
console.log(`初始化用户 \${userId} 的数据`);
}
// 获取用户统计
async getUserStats(userId) {
const user = await this.Model.findByPk(userId, {
include: [
{
model: this.app.service('posts').Model,
as: 'posts',
attributes: []
}
],
attributes: [
'id',
[this.Model.sequelize.fn('COUNT', this.Model.sequelize.col('posts.id')), 'postsCount']
],
group: ['users.id']
});
return user;
}
}
module.exports = UsersService;
2. 服务注册
javascript
// src/services/users/users.service.js
const { createModel } = require('../../models/users.model');
const UsersService = require('./users.class');
const hooks = require('./users.hooks');
module.exports = function (app) {
const options = {
Model: createModel(app),
paginate: app.get('paginate')
};
// 注册服务
app.use('/users', new UsersService(options, app));
// 获取服务实例
const service = app.service('users');
service.hooks(hooks);
};
总结
通过这篇文章,我们学习了 Feathers.js 与 Sequelize 的基础集成:
✅ 环境搭建:
- Sequelize 安装和配置
- 数据库连接设置
- 项目结构组织
✅ 模型定义:
- 完整的模型配置
- 数据验证和约束
- 实例和类方法
✅ 服务实现:
- Sequelize 服务类扩展
- 复杂查询处理
- 关联数据加载
✅ 最佳实践:
- 代码组织结构
- 错误处理机制
- 性能优化技巧
掌握了这些基础知识,你就能够在 Feathers.js 中高效地使用 Sequelize ORM 了。
下一篇文章,我们将深入学习模型关联和复杂查询。
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!