Sequelize + TypeScript - 你需要知道的

2025-06-11

Sequelize + TypeScript - 你需要知道的

嗨,开发者社区👋🏾!

我刚刚决定要写一篇文章,所以就来了。我之前不知道该写什么,最后想到了Sequelize + TypeScript ,分享一下我使用 Sequelize 并添加 TypeScript 的经验。

目录

一点背景知识

这个想法来自于我正在管理的一个项目,客户决定他们希望昨天(像往常一样)交付项目,所以公司组建了一个由初级工程师组成的小组,加上我作为高级/首席工程师,这样我就可以负责选择堆栈,所以我决定使用:

  • Express/NodeJS(后端)
  • PostgreSQL(数据库)
  • 普通 React(前端)
  • 续篇(ORM)

由于初级工程师都没有使用 TypeScript 进行开发的经验,也没有使用基于微服务的架构的经验,所以我决定使用简单的 JS 和单片架构对我们来说会很好,因为这是他们目前所了解的,而且我不想减慢项目进度。

现在我听到你在问这和 Sequelize + TypeScript 有什么关系?嗯,完全相关。公司决定将这个项目作为另一个项目的基础,这时我们才意识到当前设置存在的所有限制和问题。仅举几例:

  1. 后端和前端都有很多重复的代码
  2. 缓慢的开发环境
  3. 导致项目崩溃的奇怪错误
  4. 很难理解什么在做什么
  5. 数据库模型混乱
  6. 还有更多...

因此,我在这里使用 TypeScript 重构项目,并设置基于事件通信的基于微服务的架构(应该如此),同时我的团队正在修复错误并等待我完成重构。

好了,说得够多了,让我们深入研究并设置 Sequelize 来使用 TypeScript。

设置 Sequelize

我将使用sequelize-cli工具,该工具根据.sequelizerc文件生成 Sequelize 文件夹结构和配置,如下所示

const path = require('path');

module.exports = {
  config: path.resolve('.', 'config.js'),
  'models-path': path.resolve('./db/models'),
  'seeders-path': path.resolve('./db/seeders'),
  'migrations-path': path.resolve('./db/migrations')
};
Enter fullscreen mode Exit fullscreen mode

现在您可以运行npx sequelize-cli init,您将拥有以下项目结构

- config.js
- db/
  - migrations/
  - models/
    - index.js
  - seeders/
Enter fullscreen mode Exit fullscreen mode

您的db/models/index.js文件将如下所示

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');

const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require('../../app/config')[env];
const db = {};

let sequelize;
if (config.url) {
  sequelize = new Sequelize(config.url, config);
} else {
  // another sequelize configuration
}

fs.readdirSync(__dirname)
  .filter(file => {
    return (
      file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'
    );
  })
  .forEach(file => {
    const model = sequelize['import'](path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;
Enter fullscreen mode Exit fullscreen mode

是的,一堆代码,但这实际上简化了模型的使用,并减少了requires/imports您将来在模型中执行的操作量。

很酷的东西,不过我听说你想用 TypeScriptindex.js 。好的,那我们把第一个文件改成 TypeScript 吧。把文件重命名为index.ts,然后修改一下里面的内容

import { Sequelize } from 'sequelize';

const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config.js')[env];

const  sequelize = config.url
  ? new Sequelize(config.url, config)
  : new Sequelize(config.database, config.username, config.password, config);

export { Sequelize, sequelize };

Enter fullscreen mode Exit fullscreen mode

是的,就是这样!

好的,让我解释一下为什么我们从文件中删除了一堆代码,原因是TypeScript 本身就是 TypeScript。我只是不想db根据目录中的文件生成对象,而需要对其进行类型化models/。所以我们删除了所有这些代码,只导出了Sequelize(Class) 和sequelize(instance)。sequelize实例引用了数据库连接,因此在创建模型时,我们可以使用这个实例,这样就可以通过模型与数据库进行通信。

第一个文件已完成,还有一堆文件需要处理。

设置 Sequelize 模型

如果我们使用 JS,Sequelize 模型将会如下所示。

注意:我将声明两个示例模型:Author 和 Book。

首先我们来看看如何使用 JS 添加 Sequelize 模型。作者模型如下所示

// /models/author.js
module.exports = (sequelize, DataTypes) => {
  const Author = sequelize.define(
    'Author',
    {
      id: {
        allowNull: false,
        autoIncrement: false,
        primaryKey: true,
        type: DataTypes.UUID,
        unique: true,
      },
      firstName: {
        allowNull: true,
        type: DataTypes.TEXT,
      },
      lastName: {
        allowNull: false,
        type: DataTypes.TEXT,
      },
      email: {
        allowNull: true,
        type: DataTypes.TEXT,
      },
    },
    {}
  );

  return Author;
};
Enter fullscreen mode Exit fullscreen mode

所以让我们也用 TypeScript 来处理 Author 模型。将名称改为author.ts,然后定义一些接口

import { Model, Optional } from 'sequelize';

interface AuthorAttributes {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
};

/*
  We have to declare the AuthorCreationAttributes to
  tell Sequelize and TypeScript that the property id,
  in this case, is optional to be passed at creation time
*/
interface AuthorCreationAttributes
  extends Optional<AuthorAttributes, 'id'> {}

interface AuthorInstance
  extends Model<AuthorAttributes, AuthorCreationAttributes>,
    AuthorAttributes {
      createdAt?: Date;
      updatedAt?: Date;
    }
Enter fullscreen mode Exit fullscreen mode

我们需要这些接口来告诉 TypeScript 作者实例具有哪些属性。

现在让我们以 TS 方式定义 Author 模型

// model/author.ts
import { sequelize } from '.';

// ... instances code

const Author = sequelize.define<AuthorInstance>(
  'Author',
  {
    id: {
      allowNull: false,
      autoIncrement: false,
      primaryKey: true,
      type: DataTypes.UUID,
      unique: true,
    },
    firstName: {
      allowNull: true,
      type: DataTypes.TEXT,
    },
    lastName: {
      allowNull: false,
      type: DataTypes.TEXT,
    },
    email: {
      allowNull: true,
      type: DataTypes.TEXT,
    },
  }
);
Enter fullscreen mode Exit fullscreen mode

现在你应该会想,如果我最终还是会这么做,为什么还要经历所有这些?好吧,如果你在模型定义对象的任何属性上输入了拼写错误,TypeScript 会告诉你X缺少该属性。同样,如果你尝试添加一个接口上未定义的属性,AuthorAttributesTypeScript 也会告诉你该属性Y未在类型上定义。AuthorAttributes

现在我们知道如何使用 TypeScript 声明 Sequelize 模型,让我们定义该book.ts模型。

// /models/book.ts
import { Model, Optional } from 'sequelize';
import { sequelize } from '.';

interface BookAttributes {
  id: string;
  title: string;
  numberOfPages: number;
  authorId: string;
}

interface BookCreationAttributes
  extends Optional<BookAttributes, 'id'> {}

interface BookInstance
  extends Model<BookAttributes, BookCreationAttributes>,
    BookAttributes {
      createdAt?: Date;
      updatedAt?: Date;
    }

const Book = sequelize.define<BookInstance>(
  'Book',
  {
    id: {
      allowNull: false,
      autoIncrement: false,
      primaryKey: true,
      type: DataTypes.UUID,
      unique: true,
    },
    title: {
      allowNull: true,
      type: DataTypes.TEXT,
    },
    numberOfPages: {
      allowNull: false,
      type: DataTypes.INTEGER,
    },
    authorId: {
      allowNull: true,
      type: DataTypes.UUID,
    },
  }
);

export default Book;
Enter fullscreen mode Exit fullscreen mode

就是这样!这就是使用 TypeScript 定义 Sequelize 模型的方法。

添加模型关联

如果你想在模型中添加关联,那么An Author HAS MANY Books在 JS 中你可以这样做

// model/author.js
  Author.associate = models => {
    Author.hasMany(models.Book, {
      foreignKey: 'authorId',
    });
  };
Enter fullscreen mode Exit fullscreen mode
// model/book.js
  Book.associate = models => {
    Book.belongsTo(models.Author, {
      foreignKey: 'id'
    });
  };
Enter fullscreen mode Exit fullscreen mode

那么,你还记得我说过,运行时生成的代码npx sequelize-cli init会在我们尝试做事时提供帮助吗requires/imports?现在是时候了!

如果定义了关联函数,Sequelize 将尝试运行该函数,并将定义的关联添加到模型中。

但是我们这里讨论的是 TypeScript,而不是 JS,那么如何在 TypeScript 中定义的模型中做到这一点呢?让我们看看怎么做。

// models/author.ts
import Book from './book';

// ... Code Defining the Author Model

Author.hasMany(Book, {
  /*
    You can omit the sourceKey property
    since by default sequelize will use the primary key defined
    in the model - But I like to be explicit 
  */
  sourceKey: 'id',
  foreignKey: 'authorId',
  as: 'books'
});
Enter fullscreen mode Exit fullscreen mode
// models/book.ts
import Author from './author';

// ... Code Defining the Book Model

Book.belongsTo(Author, {
  foreignKey: 'authorId',
  as: 'author'
});
Enter fullscreen mode Exit fullscreen mode

Is that it?我听到你这么说了。好吧,如果这世上的事真这么简单,我就不会写这篇文章了😅。

那么为什么这行不通呢?Sequelize 的工作方式很奇怪,如果我们尝试在各自的文件中定义这种关联(就像在 JS 中一样),Sequelize 会抛出这个错误:

throw new Error("${source.name}.${_.lowerFirst(Type.name)} called with something that's not a subclass of Sequelize.Model");
      ^
"Error: Book.belongsTo called with something that's not a subclass of Sequelize.Model"
Enter fullscreen mode Exit fullscreen mode

起初,我以为我漏掉了什么,或者我没有将 Author 模型的类型设置为 Sequelize 模型,但仔细检查一切正常后,TypeScript 并没有报错,因为我尝试belongsTo使用非 Sequelize 模型来调用该方法。所以我很想知道为什么会发生这种情况?🤔

我阅读了 Sequelize 文档和网上的帖子,但没找到任何可行的方法,只有一个人(抱歉,没找到 StackOverflow 帖子的链接)建议将关联关系放在一个模型文件中。我对此持怀疑态度,因为我心想:既然我导出的模型和我的文件中的模型是同一个,这怎么可能行得通呢?不过,我还是尝试了一下。我将调用移到了文件belongsTomodels/author.ts

// model/author.ts
import Book from './book';

// ... a bunch of code here

Book.belongsTo(Author, {
  foreignKey: 'authorId',
  as: 'author'
});
Enter fullscreen mode Exit fullscreen mode

上帝保佑,我的项目正在运行:

The library Server is running on http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

这很奇怪,但似乎您必须在设置关联belongsTo的同一文件中定义关联hasManyhasOne

结论

最后,使用 TypeScript 设置 Sequelize 一点也不难,现在我们知道哪些属性属于哪个模型。

对我来说,这是一次宝贵的经验,因为我认为将我的 JS 文件转换为 TS 只需通过重命名文件即可,因为我相信 Sequelize 会在后台处理输入工作。

注意:我知道有一个名为sequelize-typescript的包,但我不想在我的项目中添加任何依赖项,而且这个包也不是由 Sequelize 团队维护的。不过,我刚刚尝试了一下,感觉还不错,所以如果你不想像我一样,可以试试。

到最后,我不想在我的项目中再添加一个依赖项,而且我能够仅使用纯 TS 来实现将 TypeScript 添加到 Sequlize 的目标,而无需第三方库。

抱歉,这篇文章太长了,但如果您想同时使用 Sequelize 和 TypeScript,我希望它对您有所帮助。👋🏾

鏂囩珷鏉ユ簮锛�https://dev.to/jctaveras/sequelize-typescript-what-you-need-to-know-41mj
PREV
现实世界中前端 JavaScript 的数据结构(附 React 代码示例)
NEXT
注意缺失的外键索引:Postgres 性能陷阱