'How to automatically update virtual model property when updating its foreign key reference?
I have a order
table that refers to status
table by foreign key status_id
:
$ select order_id, status_id from "order";
order_id | status_id
----------+-----------
(0 rows)
The Order
model in Sequelize also includes (virtual?) .status
property for convenience:
I'm using NestJS and
sequlize-typescript
.
class Order extends Model {
@Column({
field: 'order_id',
primaryKey: true,
})
readonly id!: string;
readonly statusID!: string;
@BelongsTo(() => Status, {
foreignKey: {
name: 'statusID',
field: 'status_id',
},
})
readonly status!: Status;
}
class Status extends Model { … }
To progress a given order to another status, I want to use .update()
method of a model. To do that, I have to set both status
and statusID
:
const order = await Order.finByPk(…);
const newStatus = await Status.findOne(…);
await order.update({
status: newStatus,
statusID: newStatus.id,
});
This is logic duplication, it is error-prone, I don't want to do that. But:
- if I set only
status
, thestatusID
is not updated on theorder
instance, and the foreign key reference is not updated in the table, rendering the whole change as moot; - if I set only
statusID
, the table is properly updated, butorder.status
still points to the old status, requiring another read from the table (Order.findByPk(…)
);
In both cases, the order
instance becomes inconsistent: order.statusID
and order.status.id
have different values.
Is it possible to set only statusID
in .update(…)
, and force Sequelize to automatically update order.status
?
Solution 1:[1]
(Partial answer)
An alternative to using Model.findByPk(…)
(which both performs a read and creates a new instance of Model
) would be using model.reload()
, which preserves model instance while making the read.
So, instead of:
await order.update({ statusID: newStatus.id });
const orderUpdated = await Order.findByPk(order.id);
return orderUpdated;
… it is:
await order.update({ statusID: newStatus.id });
await order.reload({ include: ['status'] });
return order;
This is more concise and definitely better, however, not the best, since the issue of requiring another read from the table still persists.
Solution 2:[2]
To fix logic duplication, one can define a model instance method and encapsulate the logic there:
class Order extends Model {
readonly statusID!: string;
readonly status!: Status;
async setStatus(newValue: Status): Promise<void> {
await this.update({
statusID: newValue.id,
status: newValue,
});
}
}
… which can then be generalized in order to be useful for other models:
import type { Model } from 'sequelize-typescript';
import type { Attributes } from 'sequelize/types';
const createForeignModelSetter = <
OriginalModel extends Model = Model,
ForeignModel extends Model = Model,
>(
foreignKey: keyof Attributes<OriginalModel>,
foreignModelKey: keyof Attributes<OriginalModel>,
foreignModelPrimaryKey: keyof Attributes<ForeignModel>,
) =>
async function (this: OriginalModel, newValue: ForeignModel): Promise<void> {
await this.update({
[foreignKey]: newValue[foreignModelPrimaryKey],
[foreignModelKey]: newValue,
});
};
class Status extends Model<{ id: string }> { … }
class Order extends Model<{ statusID: string, status: Status }> {
readonly setStatus = createForeignModelSetter<Order, Status>(
'statusID',
'status',
'id',
);
}
This significantly improves the logic duplication situation, but not fixes completely (hiding is not fixing).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Dima Parzhitsky |
Solution 2 |