В прошлый раз я добавил к своему боту SQLite и одновременно перенес обработку запросов в горутину. Это означает, что я ввел одновременный доступ к базе данных для моей кодовой базы.
Обычно надо думать, а потом делать. Хотя это и не идеально, но можно сделать и наоборот, используя git
. Но важно не забыть подумать в какой-то момент. Как я почти сделал и почти перешел к тому, чтобы накапливать больше ошибок поверх того, что я только что представил.
Я вспомнил, как на днях читал или смотрел что-то о SQLite, и они дали понять, что мне нужно следить за тем, чтобы не получать доступ к базе данных из нескольких мест одновременно. Go создан для параллелизма, поэтому я уверен, что go-sqlite3, который я использовал, держит его под контролем. Не совсем так, как выяснилось.
Из FAQ:
Могу ли я использовать это одновременно в нескольких процедурах?
Да только для чтения. Но нет для записи. См. №50, №51, №209, №274.
И вот так я спустился в большую кроличью нору. Я искал и исследовал, гуглил и запинался (нет, не совсем), читал источник go-sqlite3
и документы для SQLite (которые, кстати, довольно хороши). Все это просто для того, чтобы оказаться в ситуации, когда я не знаю, могу ли я доверять пакету go-sqlite3
выполнение моей работы с базой данных. Несмотря на то, что в FAQ указано, что параллелизм записи не поддерживается, я обнаружил противоречащие друг другу утверждения. Люди говорили, что это нормально, если я использую несколько подключений. Но я не собирался каждый раз открывать новое соединение.
Короче говоря, я нашел сообщение в блоге, в котором рассматривалась точно такая же проблема, и предлагал решение для нее (и нескольких других) в виде пакета Go.
go get -u crawshaw.io/sqlite
По словам Дэвида, автора сообщения и пакета, я могу доверять этому пакету в надежной обработке параллелизма. Теперь я не просто устанавливаю соединение с базой данных, а скорее создаю явный пул из тех, что мне нужны (в данном случае 16):
import "crawshaw.io/sqlite/sqlitex"
func openDB() *sqlitex.Pool {
db, err := sqlitex.Open("./since.db", 0, 16)
if err != nil {
log.Panic(err)
}
return db
}
Теперь, каждый раз, когда я хочу получить доступ к базе данных, мне нужно получить соединение из пула и не забыть вернуть его, когда я закончу. Получение соединения из пула может блокироваться, пока соединение не станет доступным.
func execSQL(db *sqlitex.Pool, sql string) {
connection := db.Get(nil)
defer db.Put(connection)
err := sqlitex.Exec(connection, sql, nil)
if err != nil {
log.Panic(err)
}
}
Замечание об обработке ошибок. В настоящее время я специально не обрабатываю ошибки, чтобы не тормозить себя. Но я их тоже не игнорирую. Как и любой уважающий себя программист, я паникую, когда получаю ошибку. В случае некоторых ошибок, таких как невозможность открыть базу данных при запуске, можно паниковать. Но если в одном из сообщений произошла ошибка, паника - не лучший выбор. Это похоже на выключение сервера, когда мы должны были только что вернуть HTTP / 404.
Теперь хранение входящего сообщения в базе данных выглядит следующим образом:
func store(message *tgbotapi.Message, db *sqlitex.Pool) {
connection := db.Get(nil)
defer db.Put(connection)
err := sqlitex.Exec(
connection,
"INSERT INTO events (user, name, date) VALUES (?, ?, ?);",
nil,
message.From.ID,
message.Text,
message.Date)
if err != nil {
log.Panic(err)
}
}
Одна мелочь, которая мне не нравится в этом, заключается в том, что я вынужден использовать позиционные аргументы SQL, если я хочу использовать sqlitex.Exec
. Если бы я хотел использовать имена столбцов, такие как "... VALUES ($user, $name, $date)"
, мне пришлось бы использовать гораздо более многословный API. Подготовьте заявление самостоятельно, а затем выполните его. Нравится:
func store(message *tgbotapi.Message, db *sqlitex.Pool) {
connection := db.Get(nil)
defer db.Put(connection)
insert := connection.Prep("INSERT INTO events (user, name, date) VALUES ($user, $name, $date);")
insert.SetInt64("$user", int64(message.From.ID))
insert.SetText("$name", message.Text)
insert.SetInt64("$date", int64(message.Date))
_, err := insert.Step()
if err != nil {
log.Panic(err)
}
// Done with this query
// TODO: Is it really needed? What happens when this isn't called?
err = insert.Reset()
if err != nil {
log.Panic(err)
}
}
Если я увижу, что позиционные аргументы становятся проблемой, я перейду на этот способ ведения дел.
Как бы я ни хотел сделать своего бота менее тупым на этот раз, мне удалось переключить библиотеки только при попытке защитить свои методы доступа к SQLite. Просто рефакторинг, никаких функций. Вот вам типичный день инженера-программиста. Что ж, тогда на следующий день.
Если вам интересно, код доступен на GitHub. Эта версия помечена тегом day-3
.
Первоначально опубликовано на detunized.net 1 апреля 2019 г.