Я использую knex.js для своей базы данных, и у меня есть запрос, который зависит от предыдущего запроса.
Пример:
пользовательская таблица
| имя пользователя (ПК) | имя_имя | фамилия |
таблица входа
| имя пользователя (pk/fk) | хэш |
Процесс:
Вставить пользователю> вставить для входа
вход в систему зависит от пользователя, поэтому он вернет ошибку, если вставка пользователю еще не завершена.
Это мой код:
const handleSignup = (req, res, db, logger, bcrypt) => {
const {
username,
password,
firstName,
lastName,
} = req.body;
const hash = bcrypt.hashSync(password);
if (username || !firstName || !lastName ) {
res.json({
haveEmpty: true
});
return;
} else {
db.transaction((trx) => {
db.select('*').from('user').where('username', '=', username)
.then(data => {
if (!data[0]) {
db('user')
.returning('*')
.insert({
username: username,
first_name: firstName,
last_name: lastName,
})
.then(user => {
db('login')
.returning('*')
.insert({
username: username,
hash: hash
})
.then(login => {
if (login[0]) {
res.json({
isSuccess: true
});
return;
} else {
res.json({
isSuccess: false
});
return;
}
})
.then(trx.commit)
.catch(err => {
logger.error(err);
trx.rollback;
res.render('pages/error-500');
});
})
.then(trx.commit)
.catch(err => {
logger.error(err);
trx.rollback;
res.render('pages/error-500');
});
} else {
res.json('User already Exist!');
return;
}
})
.then(trx.commit)
.catch(err => {
logger.error(err);
trx.rollback;
res.render('pages/error-500');
});
})
.catch(err => logger.error(err));
}
}
И я не знаю, правильно ли я использую транзакции. Но это то, что я придумал. Раньше, когда я разделяю запрос на два промиса, я получаю ошибку, потому что кажется, что первая вставка (пользовательская) не заканчивается.
Этот код работает, но я знаю, что есть более правильный способ его кодирования.





Возврат промиса внутри обратного вызова then выполнит промисы один за другим следующим образом:
const handleSignup = (req, res, db, logger, bcrypt) => {
const {
username,
password,
firstName,
lastName,
} = req.body;
const hash = bcrypt.hashSync(password);
if (username || !firstName || !lastName) {
res.json({
haveEmpty: true
});
return;
}
db.transaction((trx) => {
db.select('*').from('user').where('username', '=', username)
.then(data => {
if (data[0]) {
res.json('User already Exist!');
return;
}
return db('user')
.returning('*')
.insert({
username: username,
first_name: firstName,
last_name: lastName,
});
})
.then(user => {
return db('login')
.returning('*')
.insert({
username: username,
hash: hash
});
})
.then(login => {
if (!login[0]) {
res.json({
isSuccess: false
});
return;
}
res.json({
isSuccess: true
});
})
.then(trx.commit)
.then(trx.commit)
.then(trx.commit)
.catch(err => {
logger.error(err);
trx.rollback;
res.render('pages/error-500');
});
})
.catch(err => logger.error(err));
}
В чем я не уверен на 100% в вашем коде, так это в том, что вы будете откатывать только последний запрос, а не все из них. Следите за этим.
По моему опыту, обещания начинают казаться более естественными, как только вы перестанете пытаться впихнуть их все в одну и ту же функцию! (Но все мы, вероятно, когда-либо писали что-то похожее на ваш пример, не волнуйтесь.)
Небольшие фрагменты кода легче тестировать и отлаживать. Например, если вы знаете, что ваша проверка переменных в теле запроса верна, то, возможно, проблема лежит дальше по стеку.
Вот пример использования небольшого стека ПО промежуточного слоя. Это позволяет разбивать операции на небольшие куски, при этом гарантируя, что одно произойдет раньше другого.
const bcrypt = require("bcrypt");
const express = require("express");
const knex = require("knex");
const config = require("./knexfile").development;
const app = express();
app.use(express.json());
const db = knex(config);
const detailValidator = (req, res, next) => {
// You can do more robust validation here, of course
if (!req.body.firstName || !req.body.lastName) {
return next(new Error("Missing user details."));
}
next();
};
const userUniqueValidator = (req, res, next) => {
db("users")
.where("username", req.body.username)
.then(users => {
if (users.length !== 0) {
return next(new Error("User exists."));
}
next();
});
};
const userCreator = (req, res, next) => {
const { username, password, firstName, lastName } = req.body;
const hash = bcrypt.hashSync(password, 10);
db.transaction(trx =>
trx("users")
.insert({
username,
first_name: firstName,
last_name: lastName
})
.then(([userId]) => trx("auth").insert({ user_id: userId, hash }))
.then(() => res.json({ success: true }))
).catch(err => next(err));
};
app.post("/", detailValidator, userUniqueValidator, userCreator);
app.use((err, req, res, next) => res.json({ error: err.message }));
app.listen(4000, () => console.info("yup"));
Что касается транзакций в Knex: вам вообще не нужно вызывать commit, если вы используете приведенный выше синтаксис. Однако вам нужно использовать аргумент trx в качестве построителя запросов. В документации также предлагается другой вариант — синтаксис transacting: см. документы.
Наконец, я бы не рекомендовал использовать ваше имя пользователя в качестве первичного ключа. Их слишком часто требуется менять, и всегда есть риск случайной утечки в URL-адресе или журнале. Однако я бы рекомендовал включить уникальное ограничение. Что-то вроде этого, наверное?
exports.up = knex =>
knex.schema.createTable("users", t => {
t.increments("id");
t.string("username").unique();
t.string("first_name");
t.string("last_name");
});
exports.up = knex =>
knex.schema.createTable("auth", t => {
t.increments("id");
t.integer("user_id").references("users.id");
t.string("hash");
});
Стоит отметить, что для этого быстрого примера я использовал SQLite3, который поддерживает возврат идентификатора строки только после вставки (отсюда и [ userId ] в предложении then после вставки пользователем).
Меня беспокоит только то, как я могу использовать эти небольшие фрагменты функций в другом файле-скрипте? Потому что я стараюсь отделять каждый из них в отдельном файле, чтобы мой основной server.js не стал слишком большим.
Вы можете поставить их где угодно. Я помещаю все только в один «файл» в качестве примера. Поскольку промежуточное ПО Express всегда имеет одну и ту же сигнатуру, вы можете написать скучные старые JS-функции, которые что-то делают с данными в req или res перед передачей управления через next. Затем просто импортируйте/требуйте те, которые вам нужны в вашем стеке для определенного маршрута.
Плюс 1 за это — измельчение вещей на мелкие кусочки, подобное этому, также позволяет вам (более) легко тестировать каждый бит по отдельности, поэтому вы не потеряетесь, когда вещь никогда не сработает с первого раза. :)