如何使用 Sequelize 查询用户及关联的交易和付款记录?
支付老李
结论
Sequelize 会根据关联路径组织返回结构。当前路径是 User -> Customer -> Transaction/Payment,所以 Customers 位于顶层,Transactions 和 Payments 嵌套在各自的 Customer 下。只修改 include 的写法,无法把这两层关联自动提升到 User 顶层。
如果需要目标结构,可以选择以下方案:
- 保持现有表结构,分别查询用户、交易和付款,再组装返回结果。这种方式更稳妥。
- 在
Transaction和Payment中增加userId,建立User.hasMany(Transaction/Payment)直接关联。之后便可通过一次查询在顶层include。
方案一:保持现有表结构并组装结果
假设外键关系如下:
Customer.userId -> User.idTransaction.customerId -> Customer.idPayment.customerId -> Customer.idExpense.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,但仍需按用户、交易和付款聚合返回结果。
备注:内容仅供参考。
