Получение идентификатора вставленной строки
Так что это обычный шаблон в реляционных базах данных, где вы не можете создать яйцо, пока у вас нет уникального идентификатора курицы, которая его откладывает! Очевидно, что база данных должна сообщить вам, как она хочет ссылаться на курицу.
В Postgres вы можете просто использовать функцию .returning
Knex, чтобы явно указать, что вы хотите, чтобы возвращался столбец id
новой строки. Вам после успешной вставки. Таким образом, первая часть вашего запроса будет выглядеть так:
knexInstance
.insert(newUser)
.into('users')
.returning('id')
Примечание: не все базы данных поддерживают это одинаково. В частности, если вы разрабатываете локально с использованием SQLite, он вернет количество строк, затронутых запросом , а не идентификатор, поскольку SQLite не поддерживает SQL RETURNING
. Лучше всего разрабатывать локально, используя Postgres, чтобы избежать неприятных сюрпризов.
Хорошо, теперь мы знаем, какая курица нам нужна. Теперь нам нужно убедиться, что мы дождались правильного идентификатора, а затем использовать его:
.then(([ userId ]) => knexInstance
.insert({ user_id: userId,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT })
.into('quotas')
)
Или как бы вы ни выбрали заполнить эту вторую таблицу.
Примечание: DATE
является ключевым словом SQL. По этой причине, это не делает хорошее имя столбца. Как насчет created
или updated
вместо этого?
Отвечая разумными данными
Итак, это базовая стратегия "У меня есть идентификатор, давайте вставим в другую таблицу". Тем не менее, вы действительно хотите иметь возможность отвечать с созданным пользователем ... это похоже на разумное поведение API для ответа 201.
Что вы не хотите сделатьотвечает всей записью пользователя из базы данных, которая предоставит хэш пароля (как вы делаете в первом блоке кода из вашего вопроса). В идеале, вы, вероятно, хотели бы ответить с некоторой дружественной к UI комбинацией обеих таблиц.
К счастью, .returning
также принимает аргумент массива. Это позволяет нам передавать список столбцов, на которые мы хотели бы ответить, уменьшая риск случайного раскрытия чего-либо поверхности API, которую мы бы не хотели передавать.
const userColumns = [ 'id', 'username', 'glasses' ]
const quotaColumns = [ 'amount' ]
knexInstance
.insert(newUser)
.into('users')
.returning(userColumns)
.then(([ user]) => knexInstance
.insert({
user_id: user.id,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT
})
.into('quotas')
.returning(quotaColumns)
.then(([ quota ]) => res.status(201)
.json({
...user,
...quota
})
)
)
Асинхронизация / ожидание для удобочитаемости
В наши дни я бы, вероятно, избегал подобной цепочки обещаний в пользу синтаксического сахара, который нам предоставляет await
.
try {
const [ user ] = await knexInstance
.insert(newUser)
.into('users')
.returning(userColumns)
const [ quota ] = await knexInstance
.insert({
user_id: userId,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT
})
.into('quotas')
.returning(quotaColumns)
res
.status(201)
.json({
...user,
...quota
})
} catch (e) {
next(Error("Something went wrong while inserting a user!"))
}
Примечание о транзакциях
Здесь есть несколько предположений, но одно большое: мы предполагаем, что обе вставки будут успешными. Конечно, мы обеспечиваем некоторую обработку ошибок, но все еще существует вероятность того, что первая вставка будет успешной, а вторая - по какой-либо причине или время ожидания истекло.
Как правило, мы выполняем несколько вставок в блок транзакции. Вот как Knex справляется с этим:
try {
const userResponse = await knexInstance.transaction(async tx => {
const [ user ] = await tx.insert(...)
const [ quota ] = await tx.insert(...)
return {
...user,
...quota
}
})
res
.status(201)
.json(userResponse)
} catch (e) {
next(Error('...'))
}
Это хорошая общая практика для множественных вставок, которые зависят друг от друга, поскольку он устанавливает подход «все или ничего»: если что-то не получается, база данных возвращаетсяв прежнее состояние.