Sequelize TypeScript 集成指南 - 类型安全的数据库操作
发布时间:2024-03-23
作者:一介布衣
标签:Sequelize, TypeScript, 类型安全, 开发体验
前言
今天咱们来聊聊如何在 TypeScript 项目中使用 Sequelize。说实话,TypeScript 和 Sequelize 的结合真的是太香了!类型安全、智能提示、编译时错误检查,这些特性让开发体验提升了不止一个档次。
我记得刚开始用 JavaScript 写 Sequelize 的时候,经常因为字段名写错、类型不匹配等问题在运行时才发现错误。后来切换到 TypeScript,这些问题在编译时就能发现,开发效率提升了很多。
今天我就把 Sequelize + TypeScript 的最佳实践分享给大家。
环境搭建
1. 安装依赖
bash
# 安装 Sequelize 和 TypeScript 相关依赖
npm install sequelize
npm install --save-dev @types/node typescript ts-node
# 安装数据库驱动(以 MySQL 为例)
npm install mysql2
npm install --save-dev @types/mysql2
# 可选:安装 sequelize-typescript(更好的 TS 支持)
npm install sequelize-typescript
npm install reflect-metadata
2. TypeScript 配置
创建 tsconfig.json
:
json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
基础类型定义
1. 模型接口定义
typescript
// src/types/user.types.ts
export interface UserAttributes {
id: number;
username: string;
email: string;
password: string;
firstName?: string;
lastName?: string;
isActive: boolean;
lastLoginAt?: Date;
createdAt: Date;
updatedAt: Date;
}
// 创建时的属性(排除自动生成的字段)
export interface UserCreationAttributes
extends Omit<UserAttributes, 'id' | 'createdAt' | 'updatedAt'> {
firstName?: string;
lastName?: string;
}
// 更新时的属性(所有字段都是可选的)
export interface UserUpdateAttributes
extends Partial<Omit<UserAttributes, 'id' | 'createdAt' | 'updatedAt'>> {}
2. 使用原生 Sequelize 定义模型
typescript
// src/models/User.ts
import {
DataTypes,
Model,
InferAttributes,
InferCreationAttributes,
CreationOptional,
NonAttribute,
Association
} from 'sequelize';
import { sequelize } from '../config/database';
import { Post } from './Post';
export class User extends Model<
InferAttributes<User>,
InferCreationAttributes<User>
> {
// 主键和自动生成的字段
declare id: CreationOptional<number>;
declare createdAt: CreationOptional<Date>;
declare updatedAt: CreationOptional<Date>;
// 必填字段
declare username: string;
declare email: string;
declare password: string;
declare isActive: boolean;
// 可选字段
declare firstName: string | null;
declare lastName: string | null;
declare lastLoginAt: Date | null;
// 关联关系(不存储在数据库中)
declare posts?: NonAttribute<Post[]>;
// 关联定义
declare static associations: {
posts: Association<User, Post>;
};
// 实例方法
getFullName(): string {
return `\${this.firstName || ''} \${this.lastName || ''}`.trim();
}
// 静态方法
static async findByEmail(email: string): Promise<User | null> {
return await this.findOne({ where: { email } });
}
}
// 初始化模型
User.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false
},
firstName: {
type: DataTypes.STRING(50),
allowNull: true
},
lastName: {
type: DataTypes.STRING(50),
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
},
lastLoginAt: {
type: DataTypes.DATE,
allowNull: true
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE
}, {
sequelize,
modelName: 'User',
tableName: 'users'
});
使用 sequelize-typescript
1. 装饰器风格的模型定义
typescript
// src/models/User.ts
import {
Table,
Column,
Model,
DataType,
PrimaryKey,
AutoIncrement,
Unique,
AllowNull,
Default,
Validate,
HasMany,
CreatedAt,
UpdatedAt,
BeforeCreate,
BeforeUpdate
} from 'sequelize-typescript';
import bcrypt from 'bcryptjs';
import { Post } from './Post';
@Table({
tableName: 'users',
timestamps: true
})
export class User extends Model {
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
id!: number;
@Unique
@AllowNull(false)
@Column(DataType.STRING(50))
username!: string;
@Unique
@AllowNull(false)
@Validate({
isEmail: true
})
@Column(DataType.STRING(100))
email!: string;
@AllowNull(false)
@Column(DataType.STRING(255))
password!: string;
@Column(DataType.STRING(50))
firstName?: string;
@Column(DataType.STRING(50))
lastName?: string;
@Default(true)
@Column(DataType.BOOLEAN)
isActive!: boolean;
@Column(DataType.DATE)
lastLoginAt?: Date;
@CreatedAt
createdAt!: Date;
@UpdatedAt
updatedAt!: Date;
// 关联关系
@HasMany(() => Post)
posts!: Post[];
// 实例方法
getFullName(): string {
return `\${this.firstName || ''} \${this.lastName || ''}`.trim();
}
async validatePassword(password: string): Promise<boolean> {
return await bcrypt.compare(password, this.password);
}
// 钩子方法
@BeforeCreate
@BeforeUpdate
static async hashPassword(instance: User): Promise<void> {
if (instance.changed('password')) {
instance.password = await bcrypt.hash(instance.password, 10);
}
}
// 静态方法
static async findByEmail(email: string): Promise<User | null> {
return await this.findOne({ where: { email } });
}
}
2. 文章模型
typescript
// src/models/Post.ts
import {
Table,
Column,
Model,
DataType,
PrimaryKey,
AutoIncrement,
AllowNull,
BelongsTo,
ForeignKey,
Default,
Validate
} from 'sequelize-typescript';
import { User } from './User';
@Table({
tableName: 'posts',
timestamps: true
})
export class Post extends Model {
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
id!: number;
@AllowNull(false)
@Validate({
len: [5, 200]
})
@Column(DataType.STRING(200))
title!: string;
@AllowNull(false)
@Column(DataType.TEXT)
content!: string;
@Default('draft')
@Column(DataType.ENUM('draft', 'published', 'archived'))
status!: 'draft' | 'published' | 'archived';
@Column(DataType.DATE)
publishedAt?: Date;
// 外键
@ForeignKey(() => User)
@AllowNull(false)
@Column(DataType.INTEGER)
userId!: number;
// 关联关系
@BelongsTo(() => User)
author!: User;
createdAt!: Date;
updatedAt!: Date;
// 实例方法
async publish(): Promise<void> {
this.status = 'published';
this.publishedAt = new Date();
await this.save();
}
isPublished(): boolean {
return this.status === 'published';
}
}
数据库配置
1. 数据库连接配置
typescript
// src/config/database.ts
import { Sequelize } from 'sequelize-typescript';
import { User } from '../models/User';
import { Post } from '../models/Post';
interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
database: string;
dialect: 'mysql' | 'postgres' | 'sqlite' | 'mariadb' | 'mssql';
}
const config: DatabaseConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
username: process.env.DB_USER || 'root',
password: process.env.DB_PASS || '',
database: process.env.DB_NAME || 'myapp',
dialect: 'mysql'
};
export const sequelize = new Sequelize({
...config,
models: [User, Post], // 注册模型
logging: process.env.NODE_ENV === 'development' ? console.log : false,
pool: {
max: 10,
min: 0,
acquire: 30000,
idle: 10000
}
});
// 测试连接
export async function testConnection(): Promise<void> {
try {
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
} catch (error) {
console.error('❌ 数据库连接失败:', error);
throw error;
}
}
// 同步数据库
export async function syncDatabase(force = false): Promise<void> {
try {
await sequelize.sync({ force });
console.log('✅ 数据库同步完成');
} catch (error) {
console.error('❌ 数据库同步失败:', error);
throw error;
}
}
服务层类型安全
1. 用户服务
typescript
// src/services/UserService.ts
import { User } from '../models/User';
import { UserCreationAttributes, UserUpdateAttributes } from '../types/user.types';
export interface CreateUserRequest {
username: string;
email: string;
password: string;
firstName?: string;
lastName?: string;
}
export interface UpdateUserRequest {
username?: string;
email?: string;
firstName?: string;
lastName?: string;
isActive?: boolean;
}
export interface UserResponse {
id: number;
username: string;
email: string;
firstName?: string;
lastName?: string;
fullName: string;
isActive: boolean;
lastLoginAt?: Date;
createdAt: Date;
updatedAt: Date;
}
export class UserService {
static async createUser(data: CreateUserRequest): Promise<UserResponse> {
const user = await User.create(data);
return this.toUserResponse(user);
}
static async getUserById(id: number): Promise<UserResponse | null> {
const user = await User.findByPk(id);
return user ? this.toUserResponse(user) : null;
}
static async getUserByEmail(email: string): Promise<UserResponse | null> {
const user = await User.findByEmail(email);
return user ? this.toUserResponse(user) : null;
}
static async updateUser(id: number, data: UpdateUserRequest): Promise<UserResponse | null> {
const user = await User.findByPk(id);
if (!user) return null;
await user.update(data);
return this.toUserResponse(user);
}
static async deleteUser(id: number): Promise<boolean> {
const result = await User.destroy({ where: { id } });
return result > 0;
}
static async getUsersWithPosts(): Promise<UserResponse[]> {
const users = await User.findAll({
include: ['posts']
});
return users.map(user => this.toUserResponse(user));
}
private static toUserResponse(user: User): UserResponse {
return {
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName || undefined,
lastName: user.lastName || undefined,
fullName: user.getFullName(),
isActive: user.isActive,
lastLoginAt: user.lastLoginAt || undefined,
createdAt: user.createdAt,
updatedAt: user.updatedAt
};
}
}
2. 文章服务
typescript
// src/services/PostService.ts
import { Post } from '../models/Post';
import { User } from '../models/User';
export interface CreatePostRequest {
title: string;
content: string;
userId: number;
status?: 'draft' | 'published' | 'archived';
}
export interface UpdatePostRequest {
title?: string;
content?: string;
status?: 'draft' | 'published' | 'archived';
}
export interface PostResponse {
id: number;
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
publishedAt?: Date;
author: {
id: number;
username: string;
fullName: string;
};
createdAt: Date;
updatedAt: Date;
}
export class PostService {
static async createPost(data: CreatePostRequest): Promise<PostResponse> {
const post = await Post.create(data);
await post.reload({ include: ['author'] });
return this.toPostResponse(post);
}
static async getPostById(id: number): Promise<PostResponse | null> {
const post = await Post.findByPk(id, {
include: ['author']
});
return post ? this.toPostResponse(post) : null;
}
static async getPostsByUser(userId: number): Promise<PostResponse[]> {
const posts = await Post.findAll({
where: { userId },
include: ['author'],
order: [['createdAt', 'DESC']]
});
return posts.map(post => this.toPostResponse(post));
}
static async updatePost(id: number, data: UpdatePostRequest): Promise<PostResponse | null> {
const post = await Post.findByPk(id, { include: ['author'] });
if (!post) return null;
await post.update(data);
return this.toPostResponse(post);
}
static async publishPost(id: number): Promise<PostResponse | null> {
const post = await Post.findByPk(id, { include: ['author'] });
if (!post) return null;
await post.publish();
return this.toPostResponse(post);
}
private static toPostResponse(post: Post): PostResponse {
return {
id: post.id,
title: post.title,
content: post.content,
status: post.status,
publishedAt: post.publishedAt,
author: {
id: post.author.id,
username: post.author.username,
fullName: post.author.getFullName()
},
createdAt: post.createdAt,
updatedAt: post.updatedAt
};
}
}
控制器层类型安全
1. 用户控制器
typescript
// src/controllers/UserController.ts
import { Request, Response } from 'express';
import { UserService, CreateUserRequest, UpdateUserRequest } from '../services/UserService';
export class UserController {
static async create(req: Request, res: Response): Promise<void> {
try {
const userData: CreateUserRequest = req.body;
const user = await UserService.createUser(userData);
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
message: error instanceof Error ? error.message : '创建用户失败'
});
}
}
static async getById(req: Request, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
res.status(400).json({
success: false,
message: '无效的用户ID'
});
return;
}
const user = await UserService.getUserById(id);
if (!user) {
res.status(404).json({
success: false,
message: '用户不存在'
});
return;
}
res.json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
static async update(req: Request, res: Response): Promise<void> {
try {
const id = parseInt(req.params.id);
const updateData: UpdateUserRequest = req.body;
const user = await UserService.updateUser(id, updateData);
if (!user) {
res.status(404).json({
success: false,
message: '用户不存在'
});
return;
}
res.json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
message: error instanceof Error ? error.message : '更新用户失败'
});
}
}
}
高级类型技巧
1. 泛型服务基类
typescript
// src/services/BaseService.ts
import { Model, ModelStatic, FindOptions, CreateOptions, UpdateOptions } from 'sequelize';
export abstract class BaseService<T extends Model> {
protected model: ModelStatic<T>;
constructor(model: ModelStatic<T>) {
this.model = model;
}
async findById(id: number, options?: FindOptions<T>): Promise<T | null> {
return await this.model.findByPk(id, options);
}
async findAll(options?: FindOptions<T>): Promise<T[]> {
return await this.model.findAll(options);
}
async create(data: any, options?: CreateOptions<T>): Promise<T> {
return await this.model.create(data, options);
}
async update(id: number, data: any, options?: UpdateOptions<T>): Promise<[number, T[]]> {
return await this.model.update(data, {
where: { id } as any,
returning: true,
...options
});
}
async delete(id: number): Promise<number> {
return await this.model.destroy({
where: { id } as any
});
}
}
// 使用泛型基类
export class TypedUserService extends BaseService<User> {
constructor() {
super(User);
}
async findByEmail(email: string): Promise<User | null> {
return await this.model.findOne({ where: { email } });
}
}
2. 查询构建器类型
typescript
// src/utils/QueryBuilder.ts
import { Model, ModelStatic, FindOptions, WhereOptions, Order } from 'sequelize';
export class TypedQueryBuilder<T extends Model> {
private options: FindOptions<T> = {};
constructor(private model: ModelStatic<T>) {}
where(conditions: WhereOptions<T>): this {
this.options.where = conditions;
return this;
}
include(associations: string[]): this {
this.options.include = associations;
return this;
}
order(orderBy: Order): this {
this.options.order = orderBy;
return this;
}
limit(limit: number): this {
this.options.limit = limit;
return this;
}
offset(offset: number): this {
this.options.offset = offset;
return this;
}
async findOne(): Promise<T | null> {
return await this.model.findOne(this.options);
}
async findAll(): Promise<T[]> {
return await this.model.findAll(this.options);
}
async findAndCountAll(): Promise<{ rows: T[]; count: number }> {
return await this.model.findAndCountAll(this.options);
}
}
// 使用示例
const users = await new TypedQueryBuilder(User)
.where({ isActive: true })
.include(['posts'])
.order([['createdAt', 'DESC']])
.limit(10)
.findAll();
错误处理和验证
1. 自定义错误类型
typescript
// src/types/errors.ts
export class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: any
) {
super(message);
this.name = 'ValidationError';
}
}
export class NotFoundError extends Error {
constructor(resource: string, id: string | number) {
super(`\${resource} with id \${id} not found`);
this.name = 'NotFoundError';
}
}
export class DatabaseError extends Error {
constructor(
message: string,
public originalError: Error
) {
super(message);
this.name = 'DatabaseError';
}
}
2. 类型安全的验证
typescript
// src/validators/UserValidator.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
username: z.string()
.min(3, '用户名至少3个字符')
.max(50, '用户名最多50个字符')
.regex(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
email: z.string()
.email('请输入有效的邮箱地址')
.max(100, '邮箱最多100个字符'),
password: z.string()
.min(8, '密码至少8个字符')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, '密码必须包含大小写字母和数字'),
firstName: z.string()
.max(50, '名字最多50个字符')
.optional(),
lastName: z.string()
.max(50, '姓氏最多50个字符')
.optional()
});
export const UpdateUserSchema = CreateUserSchema.partial();
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
// 验证中间件
export function validateCreateUser(req: Request, res: Response, next: NextFunction) {
try {
CreateUserSchema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({
success: false,
message: '数据验证失败',
errors: error.errors.map(err => ({
field: err.path.join('.'),
message: err.message
}))
});
} else {
next(error);
}
}
}
测试
1. 模型测试
typescript
// src/tests/models/User.test.ts
import { User } from '../../models/User';
import { sequelize } from '../../config/database';
describe('User Model', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
});
afterAll(async () => {
await sequelize.close();
});
beforeEach(async () => {
await User.destroy({ where: {} });
});
describe('创建用户', () => {
it('应该成功创建用户', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'Test',
lastName: 'User'
};
const user = await User.create(userData);
expect(user.id).toBeDefined();
expect(user.username).toBe(userData.username);
expect(user.email).toBe(userData.email);
expect(user.getFullName()).toBe('Test User');
expect(user.isActive).toBe(true);
});
it('应该验证邮箱格式', async () => {
const userData = {
username: 'testuser',
email: 'invalid-email',
password: 'password123'
};
await expect(User.create(userData)).rejects.toThrow();
});
});
describe('实例方法', () => {
it('getFullName 应该返回完整姓名', async () => {
const user = await User.create({
username: 'testuser',
email: 'test@example.com',
password: 'password123',
firstName: 'John',
lastName: 'Doe'
});
expect(user.getFullName()).toBe('John Doe');
});
});
describe('静态方法', () => {
it('findByEmail 应该根据邮箱查找用户', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
await User.create(userData);
const foundUser = await User.findByEmail('test@example.com');
expect(foundUser).toBeTruthy();
expect(foundUser!.email).toBe('test@example.com');
});
});
});
总结
今天我们深入学习了 Sequelize 与 TypeScript 的集成:
- ✅ 环境搭建和基础配置
- ✅ 原生 Sequelize 和 sequelize-typescript 的使用
- ✅ 类型安全的模型定义
- ✅ 服务层和控制器的类型安全
- ✅ 高级类型技巧和泛型应用
- ✅ 错误处理和数据验证
- ✅ 单元测试的编写
掌握了这些知识,你就能够:
- 享受类型安全带来的开发体验提升
- 在编译时发现潜在错误
- 获得更好的 IDE 智能提示
- 编写更健壮和可维护的代码
TypeScript + Sequelize 的组合真的是现代 Node.js 开发的最佳实践之一,强烈推荐大家在项目中使用!
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!