обработка кода, подверженного ошибкам в условиях гонки, с помощью sequenceize

Я пытаюсь создать очень маленький проект, чтобы попрактиковаться в продолжении. В основном у меня есть пользователи, продукты, категории и покупки.

  • Пользователи могут купить не более 3 товаров
  • Пользователь не может покупать один и тот же товар более одного раза.
  • Каждый продукт относится к категории

И я хочу поддерживать с помощью кода три совокупных счетчика:

  • затраченные деньги и PurchaseCount (для пользователя)
  • productCount (для категории)

Мне известны такие варианты, как простые запросы, триггеры, представления и т. д. для агрегатных операций. Я просто хочу попрактиковаться в продолжении.

Итак, я сделал это, и, насколько я понимаю, это работает. Мой вопрос касается методов: 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;

что мне кажется хорошо.

Это код (соответствующая часть. Я также использую экспресс) Если в моем коде существуют условия гонки, как я могу их протестировать?

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


    }

}


person InglouriousBastard    schedule 18.01.2021    source источник
comment
По крайней мере, вы не используете транзакцию в incrementProductCount.   -  person Anatoly    schedule 19.01.2021
comment
Строка проверки @Anatoly: await category.incrementProductCount({transaction: t})   -  person InglouriousBastard    schedule 19.01.2021
comment
В яблочко! Вы передаете его, но не используете в incrementProductCount: function() и this.save()   -  person Anatoly    schedule 19.01.2021
comment
@Анатолий ты прав. Сделанный.   -  person InglouriousBastard    schedule 19.01.2021


Ответы (1)


Это работает. Я протестировал его, и он работает. Вы можете избавиться от пользовательских методов, таких как покупка и обработка этой логики внутри класса обслуживания. Но это было бы то же самое. Кроме того, при необходимости вы можете реализовать логику повторных попыток для OptimisticLockError (но это не был первоначальный вопрос)

person InglouriousBastard    schedule 20.01.2021