В прошлый раз я добавил к своему боту 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 г.