Многие реальные приложения, написанные на 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 .