我正在尝试创建一个非常小的项目,以便我可以练习 Sequelize 。
基本上我有用户、产品、类别和购买。
我想用代码维护三个聚合计数器:
我知道聚合操作的简单查询、触发器、 View 等选项。我只是想练习 Sequelize 。
所以,我做到了,据我所知,它有效。
我的问题是关于方法: createProduct 和 registerProductPurchase 。那些容易出现竞争条件吗?我不这么认为,因为我使用的是版本号和事务。
在 createProduct 中,我正在检查类别的版本号,在 registerProductPurchase 中检查用户的版本号。它们都在事务开始时加载。
createProduct 映射到此 sql:
Executing (9136e456-63f0-4753-98eb-a28099b0d881): START TRANSACTION;
Executing (9136e456-63f0-4753-98eb-a28099b0d881): SELECT "id", "name", "productCount", "version" FROM "categories" AS "ProductCategory" WHERE "ProductCategory"."id" = 2;
Executing (9136e456-63f0-4753-98eb-a28099b0d881): INSERT INTO "products" ("id","name","price","categoryId") VALUES (DEFAULT,$1,$2,$3) RETURNING "id","name","price","categoryId";
Executing (default): UPDATE "categories" SET "productCount"=$1,"version"=$2 WHERE "id" = $3 AND "version" = $4
Executing (9136e456-63f0-4753-98eb-a28099b0d881): COMMIT;
和 registerProductPurchase 映射到这个:Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): START TRANSACTION;
Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): SELECT "id", "name", "price", "categoryId" FROM "products" AS "Product" WHERE "Product"."id" = 2;
Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): SELECT "id", "email", "spentMoney", "purchaseCount", "version" FROM "users" AS "User" WHERE "User"."id" = 2;
Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): SELECT "purchaseDate", "userId", "productId" FROM "purchases" AS "Purchase" WHERE "Purchase"."productId" = 2 AND "Purchase"."userId" IN (2);
Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): INSERT INTO "purchases" ("userId","productId") VALUES (2,2) RETURNING "purchaseDate","userId","productId";
Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): UPDATE "users" SET "spentMoney"=$1,"purchaseCount"=$2,"version"=$3 WHERE "id" = $4 AND "version" = $5
Executing (54fa334a-fe94-4686-88ee-6c6ed6ceddbd): COMMIT;
这对我来说似乎很好。这是代码(相关部分。我也在使用 express )
如果我的代码中存在竞争条件,我该如何测试?
var sequelize = require("sequelize")
const db = new sequelize.Sequelize('postgres://root:root@localhost:5432/catalog')
var Category = db.define('ProductCategory', {
id: {
type: sequelize.DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: sequelize.DataTypes.STRING,
allowNull: false
},
productCount: {
type: sequelize.DataTypes.INTEGER,
allowNull: true,
defaultValue: 0
}
}, {
version: true,
timestamps: false,
tableName: 'categories',
modelName: 'ProductCategory'
});
var Product = db.define('Product', {
id: {
type: sequelize.DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: sequelize.DataTypes.STRING,
allowNull: false
},
price: {
type: sequelize.DataTypes.DOUBLE,
allowNull: false
}
}, {
timestamps: false,
tableName: 'products',
modelName: 'Product'
});
var User = db.define('User', {
id: {
type: sequelize.DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
email: {
type: sequelize.DataTypes.STRING,
allowNull: false,
unique: true
},
spentMoney: {
type: sequelize.DataTypes.DOUBLE,
allowNull: false,
defaultValue: 0
},
purchaseCount: {
type: sequelize.DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
}
}, {
version: true,
timestamps: false,
tableName: 'users',
modelName: 'User'
});
var Purchase = db.define('Purchase', {
purchaseDate: {
type: sequelize.DataTypes.DATEONLY,
allowNull: true
}
}, {
version: false,
timestamps: false,
tableName: 'purchases',
modelName: 'Purchase'
});
Category.hasMany(Product, { foreignKey: "categoryId" })
Product.belongsTo(Category, { foreignKey: "categoryId" })
Product.belongsToMany(User, { through: Purchase, foreignKey: "userId" })
User.belongsToMany(Product, { through: Purchase, foreignKey: "productId" })
Category.prototype.incrementProductCount = async function(options) {
this.productCount += 1
return await this.save(options)
}
User.prototype.buy = async function(product, options) {
if (this.purchaseCount === 3)
throw new Error("User can't buy more than 3 products")
try {
await this.addProduct(product, options)
} catch (err) {
if (err instanceof UniqueConstraintError) {
throw new Error("User already bought product " + p.id)
}
throw err
}
this.spentMoney += product.price
this.purchaseCount += 1
await this.save(options)
}
async function createProduct(categoryId, productInfo) {
let t
try {
t = await db.transaction()
let category = await Category.findByPk(categoryId, { transaction: t })
let product = await Product
.build({...productInfo, categoryId: categoryId })
.save({ transaction: t })
await category.incrementProductCount({ transaction: t })
await t.commit()
return product
} catch (error) {
if (t)
await t.rollback()
throw err
}
}
async function registerProductPurchase(userId, productId) {
let t
try {
t = await db.transaction()
let product = await Product.findByPk(productId, { transaction: t })
let user = await User.findByPk(userId, { transaction: t })
await user.buy(product, { transaction: t })
await t.commit()
} catch (error) {
if (t)
await t.rollback()
throw error
}
}
最佳答案
这有效。我已经测试过它并且它有效。您可以摆脱诸如“购买”之类的自定义方法并在服务类内部处理该逻辑。但是,它会是一样的。
此外,如果需要,您可以为 OptimisticLockError 实现重试逻辑(但这不是最初的问题)
关于sql - 使用 sequelize 处理竞争条件容易出错的代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65782628/