PAYATHON 2026

如何使用 Sequelize 查询用户及关联的交易和付款记录?

支付老李

结论

Sequelize 会根据关联路径组织返回结构。当前路径是 User -> Customer -> Transaction/Payment,所以 Customers 位于顶层,Transactions 和 Payments 嵌套在各自的 Customer 下。只修改 include 的写法,无法把这两层关联自动提升到 User 顶层。

如果需要目标结构,可以选择以下方案:

  1. 保持现有表结构,分别查询用户、交易和付款,再组装返回结果。这种方式更稳妥。
  2. 在 Transaction 和 Payment 中增加 userId,建立 User.hasMany(Transaction/Payment) 直接关联。之后便可通过一次查询在顶层 include。

方案一:保持现有表结构并组装结果

假设外键关系如下:

  • Customer.userId -> User.id
  • Transaction.customerId -> Customer.id
  • Payment.customerId -> Customer.id
  • Expense.userId -> User.id

关联可以这样定义:

User.hasMany(Customer, {
  foreignKey: 'userId',
  as: 'Customers'
});

Customer.belongsTo(User, {
  foreignKey: 'userId',
  as: 'User'
});

Customer.hasMany(Transaction, {
  foreignKey: 'customerId',
  as: 'Transactions'
});

Transaction.belongsTo(Customer, {
  foreignKey: 'customerId',
  as: 'Customer'
});

Customer.hasMany(Payment, {
  foreignKey: 'customerId',
  as: 'Payments'
});

Payment.belongsTo(Customer, {
  foreignKey: 'customerId',
  as: 'Customer'
});

User.hasMany(Expense, {
  foreignKey: 'userId',
  as: 'Expenses'
});

Expense.belongsTo(User, {
  foreignKey: 'userId',
  as: 'User'
});

接着并行查询用户、交易和付款:

const [user, transactions, payments] = await Promise.all([
  User.findByPk(userId, {
    include: [
      {
        model: Expense,
        as: 'Expenses'
      }
    ]
  }),

  Transaction.findAll({
    include: [
      {
        model: Customer,
        as: 'Customer',
        required: true,
        where: {
          userId
        }
      }
    ]
  }),

  Payment.findAll({
    include: [
      {
        model: Customer,
        as: 'Customer',
        required: true,
        where: {
          userId
        }
      }
    ]
  })
]);

if (!user) {
  return null;
}

const result = {
  ...user.toJSON(),
  Transactions: transactions.map(item => item.toJSON()),
  Payments: payments.map(item => item.toJSON())
};

得到的结构如下:

{
  "id": 1,
  "name": "John Doe",
  "Transactions": [
    {
      "id": 1,
      "quantity": 10,
      "Customer": {
        "id": 1,
        "name": "Customer A",
        "email": "customer@example.com"
      }
    }
  ],
  "Payments": [
    {
      "id": 1,
      "amount": 100,
      "Customer": {
        "id": 2,
        "name": "Customer B",
        "email": "another@example.com"
      }
    }
  ],
  "Expenses": []
}

也可以通过顶层条件筛选关联模型:

const transactions = await Transaction.findAll({
  where: {
    '$Customer.userId$': userId
  },
  include: [
    {
      model: Customer,
      as: 'Customer',
      required: true
    }
  ]
});

不过,把条件直接写在 include.where 中通常更直观,也能明确生成 INNER JOIN。

方案二:增加直接关联

如果每笔交易和付款在业务上都明确属于某个用户,可以在 Transaction 和 Payment 表中增加 userId:

User.hasMany(Transaction, {
  foreignKey: 'userId',
  as: 'Transactions'
});

Transaction.belongsTo(User, {
  foreignKey: 'userId',
  as: 'User'
});

User.hasMany(Payment, {
  foreignKey: 'userId',
  as: 'Payments'
});

Payment.belongsTo(User, {
  foreignKey: 'userId',
  as: 'User'
});

Transaction.belongsTo(Customer, {
  foreignKey: 'customerId',
  as: 'Customer'
});

Payment.belongsTo(Customer, {
  foreignKey: 'customerId',
  as: 'Customer'
});

建立直接关联后,可以这样查询:

const user = await User.findByPk(userId, {
  include: [
    {
      model: Transaction,
      as: 'Transactions',
      include: [
        {
          model: Customer,
          as: 'Customer'
        }
      ]
    },
    {
      model: Payment,
      as: 'Payments',
      include: [
        {
          model: Customer,
          as: 'Customer'
        }
      ]
    },
    {
      model: Expense,
      as: 'Expenses'
    }
  ],
  distinct: true
});

Sequelize 会按照关联定义生成目标层级。

为什么不能简单定义 User.hasMany(Transaction)

hasMany 要求目标表中存在指向源表的外键。例如:

User.hasMany(Transaction, {
  foreignKey: 'userId'
});

这段定义表示 Sequelize 预期存在 Transaction.userId。如果 Transaction 只有 customerId,用户关系就必须经过 Customer 才能确定,普通的 hasMany 不会自动跨过中间表。

belongsToMany({ through: Customer }) 通常也不适用于这种情况。Customer 是独立的业务实体,不是只保存 userId 和 transactionId 的标准连接表,而且实际的外键 customerId 位于 Transaction 表中。

注意事项

  • 查询中的 as 必须与关联定义完全一致,否则 Sequelize 会报告关联或别名不匹配。
  • 同时 include 多个 hasMany 会产生多重笛卡尔组合。交易、付款和费用较多时,单条 SQL 的数据量可能快速膨胀,分别查询通常更容易控制性能。
  • 如果增加冗余的 Transaction.userId 和 Payment.userId,必须确保它们与对应的 Customer.userId 一致,否则会出现数据归属冲突。
  • toJSON() 或 get({ plain: true }) 可以把 Sequelize 实例转换为普通对象。不要依赖直接修改实例的 dataValues 来长期维护返回结构。
  • 如果必须使用一条 SQL,又不能调整表结构,可以考虑原生 SQL、视图或 CTE,但仍需按用户、交易和付款聚合返回结果。

备注:内容仅供参考。