Express+PostgreSQL+Sequelize+TS模型关联时报错:Cannot read properties of undefined (reading 'Sequelize')
问题描述
使用Express/PostgreSQL/Sequelize/TypeScript技术栈开发,创建了包含Company和Stakeholder的模型文件。单独调用Company.findAll()、Stakeholder.findAll()查询数据列表正常,但通过Position中间表建立多对多关联时,触发错误:TypeError: Cannot read properties of undefined (reading 'Sequelize')。
需要实现两者的多对多关系,理想结构为每个模型单独一个文件,通过index文件创建Sequelize连接实例,但缺少该index文件的参考示例。
当前依赖版本:
- "express": "^4.17.1"
- "sequelize": "^6.20.0"
- "pg": "^8.7.3"
- "pg-hstore": "^2.3.4"
- "ts-node-dev": "^1.1.8"
- "typescript": "^4.6.4"
- "@types/node": "^17.0.35"
- "@types/express": "^4.17.13"
错误原因
触发错误的核心原因是在模型初始化完成前调用了关联方法。原代码中Company.belongsToMany()写在Company.init()和Stakeholder.init()之前,此时模型未完成Sequelize实例绑定,导致读取Sequelize属性时出现undefined。同时,将两个模型和Sequelize实例放在同一文件中,不符合"单模型单文件"的结构需求,也降低了代码可维护性。
解决方案
1. 重构项目结构
按照"单模型单文件"的需求调整结构:
src/ ├── db/ │ ├── index.ts # Sequelize实例创建、模型导入与关联配置 │ ├── models/ │ │ ├── Company.ts # Company模型文件 │ │ └── Stakeholder.ts# Stakeholder模型文件 ├── ...其他业务目录
2. 编写db/index.ts
负责创建Sequelize实例、导入所有模型并配置关联:
import { Sequelize } from 'sequelize'; import logger from '../utils/logger/logger'; import { Company } from './models/Company'; import { Stakeholder } from './models/Stakeholder'; // 创建Sequelize数据库连接实例 export const sequelize = new Sequelize('postgres://test:test@localhost:5432/board4'); // 测试数据库连接 const testConnection = async () => { try { await sequelize.authenticate(); console.log('数据库连接成功'); } catch (error) { logger.error('数据库连接失败:', error); } }; testConnection(); // 配置多对多关联(需在所有模型初始化后执行) Company.belongsToMany(Stakeholder, { through: 'Position', foreignKey: 'positionCompanyId' }); Stakeholder.belongsToMany(Company, { through: 'Position', foreignKey: 'positionStakeholderId' }); // 导出所有模型和连接实例 export { Company, Stakeholder };
3. 编写Company.ts模型文件
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Association } from 'sequelize'; import { sequelize } from '../index'; import { Stakeholder } from './Stakeholder'; export class Company extends Model<InferAttributes<Company>, InferCreationAttributes<Company>> { declare companyId: CreationOptional<string>; declare companyName: string; declare companyEntityType: string | null; declare companyVatNumber: string | null; declare companyRegistrationNumber: string; declare companyPubliclyListed: boolean | null; declare companyDateOfIncorporation: Date | null; declare companyAddress1: string | null; declare companyAddress2: string | null; declare companyCity: string | null; declare companyPostCode: string | null; declare companyCountry: string | null; declare companyPhone: string | null; declare companyEmail: string | null; declare companyUrl: string | null; declare companyLogoUrl: string | null; declare createdAt: CreationOptional<Date>; declare updatedAt: CreationOptional<Date>; declare static associations: { stakeholders: Association<Company, Stakeholder>; }; } Company.init( { companyId: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, }, companyName: { type: DataTypes.STRING, allowNull: true, }, companyEntityType: DataTypes.STRING, companyVatNumber: DataTypes.STRING, companyRegistrationNumber: { type: DataTypes.STRING, allowNull: false, unique: { name: 'uniqueRegistrationNumber', msg: '公司注册号已被使用', }, validate: { notNull: { msg: '必须填写有效的注册号' }, }, }, companyDateOfIncorporation: DataTypes.DATE, companyPubliclyListed: DataTypes.BOOLEAN, companyAddress1: DataTypes.STRING, companyAddress2: DataTypes.STRING, companyCity: DataTypes.STRING, companyPostCode: DataTypes.STRING, companyCountry: DataTypes.STRING, companyPhone: DataTypes.STRING, companyEmail: DataTypes.STRING, companyUrl: DataTypes.STRING, companyLogoUrl: DataTypes.STRING, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, }, { sequelize, underscored: true, timestamps: true, tableName: 'companies', } );
4. 编写Stakeholder.ts模型文件
import { CreationOptional, DataTypes, ForeignKey, InferAttributes, InferCreationAttributes, Model, Association } from 'sequelize'; import { sequelize } from '../index'; import { Company } from './Company'; import logger from '../../utils/logger/logger'; export class Stakeholder extends Model<InferAttributes<Stakeholder>, InferCreationAttributes<Stakeholder>> { declare stakeholderId: string; declare stakeholderName: string; declare username: string; declare password: string; declare stakeholderLastname: string; declare stakeholderEmail: string; declare stakeholderJobTitle?: string | null; declare stakeholderSharesPercentage?: number | null; declare stakeholderDOB?: string | null; declare stakeholderNationality?: string | null; declare stakeholderAddress1?: string | null; declare stakeholderAddress2?: string | null; declare stakeholderPostCode?: string | null; declare stakeholderCity?: string | null; declare stakeholderCountry?: string | null; declare stakeholderPhone?: string | null; declare stakeholderPassport?: string | null; declare stakeholderProofOfAddress?: string | null; declare stakeholderRole: string; declare positionCompanyId: ForeignKey<string>; declare createdAt: CreationOptional<Date>; declare updatedAt: CreationOptional<Date>; declare static associations: { companies: Association<Stakeholder, Company>; }; } Stakeholder.init( { stakeholderId: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, }, username: { allowNull: true, type: DataTypes.STRING, }, password: { allowNull: true, type: DataTypes.STRING, }, stakeholderRole: { type: DataTypes.STRING, }, stakeholderName: DataTypes.STRING, stakeholderLastname: DataTypes.STRING, stakeholderJobTitle: DataTypes.STRING, stakeholderEmail: DataTypes.STRING, stakeholderSharesPercentage: DataTypes.INTEGER, stakeholderDOB: DataTypes.STRING, stakeholderNationality: DataTypes.STRING, stakeholderAddress1: DataTypes.STRING, stakeholderAddress2: DataTypes.STRING, stakeholderPostCode: DataTypes.STRING, stakeholderCity: DataTypes.STRING, stakeholderCountry: DataTypes.STRING, stakeholderPhone: DataTypes.STRING, stakeholderPassport: { type: DataTypes.STRING, set: function (val: any) { logger.debug(`${__filename}:${val} 是否有效?: ${val.fileName && val.fileName.split('.')[1] !== 'undefined'}`); if (val.fileName && val.fileName.split('.')[1] !== 'undefined') this.setDataValue('stakeholderPassport', val.fileName); }, }, stakeholderProofOfAddress: { type: DataTypes.STRING, set: function (val: any) { logger.debug(`${__filename}:${val} 是否有效?: ${val.fileName && val.fileName.split('.')[1] !== 'undefined'}`); if (val.fileName && val.fileName.split('.')[1] !== 'undefined') this.setDataValue('stakeholderProofOfAddress', val.fileName); }, }, createdAt: DataTypes.DATE, updatedAt: DataTypes.DATE, }, { sequelize, timestamps: true, tableName: 'stakeholders', } );
关键修正点
- 将
belongsToMany关联配置放在所有模型初始化完成之后,确保模型已绑定Sequelize实例 - 拆分模型到单独文件,通过db/index.ts统一管理连接实例和关联关系
- 修正关联外键:
Company.belongsToMany的外键指向中间表中关联Company的字段(positionCompanyId),Stakeholder.belongsToMany的外键对应positionStakeholderId,避免外键混淆
内容的提问来源于stack exchange,提问作者geuxor
相关产品推荐
相关产品推荐

