Skip to content

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 开发的最佳实践之一,强烈推荐大家在项目中使用!


相关文章推荐:

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