Работа с PostgreSQL в языке Go при помощи pgx
25 декабря 2019
Многие реальные приложения, написанные на Go, используют ту или иную РСУБД. Притом, последней нередко является PostgreSQL. Для работы с постгресом в мире Go существует больше одной библиотеки, в связи с чем возникает закономерный вопрос — а какую выбрать? Неплохим и достаточно популярным вариантом является jackc/pgx, с которым мы и познакомимся.
Модуль pgx имеет интерфейс, похожий на интерфейс database/sql, про который ранее уже рассказывалось в гостевом посте Владимира Солонина. Главное отличие между модулями заключается в том, что database/sql поддерживает разные РСУБД, а pgx — только PostgreSQL. За счет этого pgx может обеспечить большую производительность, и предложить больше возможностей, специфичных именно для постгреса. Если в приложении требуется поддерживать больше одной РСУБД одновременно (например, вы пишите CMS), берите database/sql. Иначе берите PostgreSQL и, соответственно, модуль pgx.
По устоявшейся традиции, писать будем телефонную книгу c REST-интерфейсом. Касаемо написания непосредственно REST на языке Go мне нечего добавить к другому гостевому посту Владимира, только вместо httprouter я решил использовать mux. Насколько мне известно, между ними нет особой разницы, только в mux хэндлеры имеют меньше аргументов. Субъективно такой код чуть красивее, только и всего. Для парсинга аргументов была использована cobra, а для парсинга файла конфигурации — viper.
На время разработки PostgreSQL удобно запускать через Docker:
docker run -d --name postgresql -e POSTGRES_DB=restservice \
-e POSTGRES_PASSWORD=s3cr3t -p 5432:5432 postgres:11
При необходимости получить доступ к базе данных можно так:
docker exec -it e7042bd737f8 psql -h localhost \
-U postgres restservice -W
В приложении для подключения к СУБД будем использовать пул соединений, который уже реализован в pgx:
if err != nil {
log.Fatalf("Unable to connection to database: %v\n", err)
}
defer pool.Close()
log.Infof("Connected!")
Пример получения соединения из пула для выполнения миграции:
if err != nil {
log.Fatalf("Unable to acquire a database connection: %v\n", err)
}
migrateDatabase(conn.Conn())
conn.Release()
В самом pgx миграции не реализованы, но они есть в утилите jackc/tern. Там кода буквально на 300 строк. Они с легкостью переписываются на последнюю актуальную версию v4 модуля pgx, после чего в migrateDatabase
остается написать только:
migrator, err := migrate.NewMigrator(conn, "schema_version")
if err != nil {
log.Fatalf("Unable to create a migrator: %v\n", err)
}
err = migrator.LoadMigrations("./migrations")
if err != nil {
log.Fatalf("Unable to load migrations: %v\n", err)
}
err = migrator.Migrate()
if err != nil {
log.Fatalf("Unable to migrate: %v\n", err)
}
ver, err := migrator.GetCurrentVersion()
if err != nil {
log.Fatalf("Unable to get current schema version: %v\n", err)
}
log.Infof("Migration done. Current schema version: %v\n", ver)
}
Содержимое файла миграции:
name VARCHAR(64), phone VARCHAR(64));
---- create above / drop below ----
DROP TABLE phonebook;
Таким образом, приложение может само создавать себе схему базы данных или изменять ее при обновлении. Локи реализованы на функции pg_advisory_lock, что исключает возможность запуска двух миграций параллельно.
Далее все очень похоже на database/sql. Так, например, происходит вставка новой записи:
Id int `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
}
func Insert(p *pgxpool.Pool, w http.ResponseWriter, r *http.Request) {
var rec Record
err := json.NewDecoder(r.Body).Decode(&rec)
if err != nil { // bad request
w.WriteHeader(400)
return
}
conn, err := p.Acquire(context.Background())
if err != nil {
log.Errorf("Unable to acquire a database connection: %v\n", err)
w.WriteHeader(500)
return
}
defer conn.Release()
row := conn.QueryRow(context.Background(),
"INSERT INTO phonebook (name, phone) VALUES ($1, $2) RETURNING id",
rec.Name, rec.Phone)
var id uint64
err = row.Scan(&id)
if err != nil {
log.Errorf("Unable to INSERT: %v\n", err)
w.WriteHeader(500)
return
}
resp := make(map[string]string, 1)
resp["id"] = strconv.FormatUint(id, 10)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
err = json.NewEncoder(w).Encode(resp)
if err != nil {
log.Errorf("Unable to encode json: %v\n", err)
w.WriteHeader(500)
return
}
}
А так — удаление:
vars := mux.Vars(r)
id, err := strconv.ParseUint(vars["id"], 10, 64)
if err != nil { // bad request
w.WriteHeader(400)
return
}
conn, err := p.Acquire(context.Background())
if err != nil {
log.Errorf("Unable to acquire a database connection: %v\n", err)
w.WriteHeader(500)
return
}
defer conn.Release()
ct, err := conn.Exec(context.Background(),
"DELETE FROM phonebook WHERE id = $1", id)
if err != nil {
log.Errorf("Unable to DELETE: %v\n", err)
w.WriteHeader(500)
return
}
if ct.RowsAffected() == 0 {
w.WriteHeader(404)
return
}
w.WriteHeader(200)
}
Полную версию исходников вы найдете в этом репозитории на GitHub. Примеры запросов к сервису:
curl -vvv -XPOST -H 'Content-Type: application/json' \
-d '{"name":"Alice","phone":"123"}' localhost:8080/api/v1/records
# чтение:
curl -vvv localhost:8080/api/v1/records/123
# обновление:
curl -vvv -XPUT -H 'Content-Type: application/json' \
-d '{"name":"Bob","phone":"456"}' localhost:8080/api/v1/records/456
# удаление:
curl -vvv -XDELETE localhost:8080/api/v1/records/789
Заинтересованные читатели могут подумать над тем, как написать тесты к этому коду, а также реализовать API GET /api/v1/records
, который сейчас показывает лишь сообщение «Under construction». API возвращает все записи, которые имеются в базе, но не более 1000 штук за один запрос. Принимаемые аргументы: limit
, offset
и order
. Первый аргумент опционален и позволяет возвращать менее 1000 записей. Через второй аргумент можно передать id последней записи, которую вернул предыдущий запрос. Тогда текущий запрос вернет записи, идущие после offset
. Наконец, order
— это строковый параметр, который может быть либо asc
(сортировка по возрастанию id), либо desc
(по убыванию). Если order
не указан, считается, что он asc
.
Дополнение: Вас также могут заинтересовать посты Генерация SQL-запросов в Go с помощью squirrel и Тестирование проектов на Go с dockertest.
Дополнение: Я переписал код так, чтобы сервис был совместим с CockroachDB.
Метки: Go, PostgreSQL, СУБД.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.