PostgreSQL Support in Go: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(65 intermediate revisions by the same user not shown)
Line 3: Line 3:
* [[PostgreSQL]]
* [[PostgreSQL]]
* [[jackc/pgx]]
* [[jackc/pgx]]
* [[Masterminds/squirrel]]


=Libraries=
=Overview=
[https://github.com/lib/pq lib/pq] (in maintenance mode) is used with the [[Go_Language_Modularization#Standard_Library|Standard Library]]. Other libraries: [https://github.com/go-pg/pg go-pg/pg] (maintenance mode), [https://bun.uptrace.dev/postgres/ bun], etc.


=Open a Connection=
PostgreSQL can be accessed from Go either with the <code>[[Go_Package_database#database/sql|database/sql]]</code> library, which is part of the Standard Library, or with a specialist library like <code>[[Jackc/pgx#Overview|jackc/pgx]]</code>.


<code>database/sql</code> provides a uniform SQL API but is quite generic since it has to be able to work uniformly with many different database platforms. It has to be configured with a database driver, like <code>[https://github.com/lib/pq lib/pq]</code> or the driver that comes with <code>[[Jackc/pgx#Overview|jackc/pgx]]</code>.
<code>[[Jackc/pgx#Overview|jackc/pgx]]</code> is a PostgreSQL specialist library that has its own API, and its own driver (which can be used with <code>database/sql</code>).
This article shows how to connect to the database and then perform DDL and DML operations with both <code>database/sql</code> (configured with a <code>jackc/pgx</code> driver) and <code>jackc/pgx</code> library.
=Connect to Database=
==With <tt>database/sql</tt> API and a pgx Driver==
<code>database/sql</code> must be configured with a PostgreSQL driver via a [[Go_Packages#Blank_Imports|blank import]]. This examples uses the <code>jackc/pgx</code> driver.
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
import (
import (
  "database/sql"
"database/sql"
  _ "github.com/lib/pq"
"fmt"
 
_ "github.com/jackc/pgx/v5/stdlib"
)
)


...
role := "postgres"
 
password := "somepasswd"
role := "ovidiu"
host := "localhost"
password := "something"
port := 5432
databaseHost := "localhost"
database := "postgres"
databaseName := "testdb"
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s", role, password, host, port, database)
connectionString := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", role, password, databaseHost, databaseName)
dbHandle, err := sql.Open("pgx", connectionString)
db, err := sql.Open("postgres", connectionString)
if err != nil {
if err != nil {
  ...
panic(err)
}
}
defer func() {
defer func() {
  if err := db.Close(); err != nil {
if err := dbHandle.Close(); err != nil {
    ...
fmt.Printf("failed to close the connection\n")
  }
}
}()
}()
 
if err := dbHandle.Ping(); err != nil {
err = db.Ping()
fmt.Printf("failed to ping the database: %v\n", err)
if err != nil {
} else {
  ...
fmt.Printf("database OK\n")
}
}
</syntaxhighlight>
</syntaxhighlight>
<code>sql.Open("pgx", ...)</code> returns a database handle, and it must be called with a driver name recognized by the driver library initialized with the blank import. In this case is "pgx". The implementation maintains a pool of zero or more underlying connections, and it is safe for concurrent use by multiple goroutines.
The example shows how to automatically close the connection on exit, and also how to ping the database to ensure the connection works.
==With <tt>database/sql</tt> API and a <tt>lib/pq</tt> Driver==
The code is quasi-identical, with three differences:
* the name of the blank import used to initialize the driver
* the name of the driver <code>sql.Open()</code> is invoked with ("postgres" instead of "pgx")
* details of the connection string: the "lib/pg" driver attempts to connect with SSL enabled and if the database is not connected for SSL, it fails.
<syntaxhighlight lang='go'>
import (
    ...


<code>"postgres://<user>:<password>@<host>/<database-name>?...."</code> is called connection string.
_ "github.com/lib/pq" // different driver
)


<code>sql.Open()</code> returns a database handle. The implementation maintains a pool of zero or more underlying connections, and it is safe for concurrent use by multiple goroutines.
...
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", role, password, host, port, database) // different connection string parameters
dbHandle, err := sql.Open("postgres", connectionString) // different driver name
...
</syntaxhighlight>


=Create a Table=
==With <tt>jackc/pgx</tt> API==
The API is a bit different when using the specialized library (<code>pgx.Connect()</code> instead of the <code>sql.Open()</code>, etc.), but the overall structure of the database connection sequence is identical:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
sql := `CREATE TABLE IF NOT EXISTS TEST ("ID" int, "NAME" varchar(10))`
import (
_, err = db.Exec(sql)
"context"
"fmt"
 
"github.com/jackc/pgx/v5"
)
 
role := "postgres"
password := "somepasswd"
host := "localhost"
port := 5432
database := "postgres"
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", role, password, host, port, database)
ctx := context.Background()
connectionHandle, err := pgx.Connect(ctx, connectionString)
if err != nil {
if err != nil {
...
panic(err)
}
}
</syntaxhighlight>
defer func() {
=Delete a Table=
if err := connectionHandle.Close(ctx); err != nil {
<syntaxhighlight lang='go'>
fmt.Printf("failed to close the connection\n")
sql := `DROP TABLE TEST`
}
_, err = db.Exec(sql)
}()
if err != nil {
if err := connectionHandle.Ping(ctx); err != nil {
...
fmt.Printf("failed to ping the database: %v\n", err)
} else {
fmt.Printf("database OK\n")
}
}
</syntaxhighlight>
</syntaxhighlight>


=Insert a Row=
=DDL=
Create a schema, create a table within the schema, drop the table and the schema.
==With <tt>database/sql</tt>==
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
s := `INSERT INTO TEST ("ID", "NAME") VALUES (10, 'Bob')`
_, err := dbHandle.Exec("CREATE SCHEMA IF NOT EXISTS someschema")
_, err = db.Exec(s)
if err != nil { ... }
if err != nil {
_, err = dbHandle.Exec("CREATE TABLE IF NOT EXISTS someschema.sometable (id integer PRIMARY KEY, name text)")
  ...
if err != nil { ... }
}
_, err = dbHandle.Exec("DROP TABLE someschema.sometable")
if err != nil { ... }
_, err = dbHandle.Exec("DROP SCHEMA someschema CASCADE")
if err != nil { ... }
</syntaxhighlight>
</syntaxhighlight>


Alternative:
==With <tt>jackc/pgx</tt>==
The API is quasi-indentical, the <code>Exec()</code> function requires an additional <code>context.Context</code>:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
var ctx context.Context ...
ctx := context.Background()
var id title description director string
_, err := connectionHandle.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS someschema")
s := `INSERT INTO movies ("id", "title", "description", "director") VALUES ($1, $2, $3, $4)`
if err != nil { ... }
_, err := r.db.ExecContext(ctx, s, id, title, description, director)
_, err = connectionHandle.Exec(ctx, "CREATE TABLE IF NOT EXISTS someschema.sometable (id integer PRIMARY KEY, name text)")
if err != nil {
if err != nil { ... }
  ...
_, err = connectionHandle.Exec(ctx, "DROP TABLE someschema.sometable")
}
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "DROP SCHEMA someschema CASCADE")
if err != nil { ... }
</syntaxhighlight>
</syntaxhighlight>


=Update a Row=
=DML=
Insert a row in a table, then update that row, then delete it:
==With <tt>database/sql</tt>==
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
sql := `UPDATE TEST SET "ID"=$1 WHERE "NAME"=$2`
_, err := dbHandle.Exec("INSERT INTO someschema.sometable (id, name) VALUES ($1, $2)", 1, "Alice")
_, err = db.Exec(sql, 20, "Bob")
if err != nil { ... }
if err != nil {
_, err = dbHandle.Exec("UPDATE someschema.sometable SET name = $1 WHERE id = $2", "Bob", 1)
  ...
if err != nil { ... }
}
_, err = dbHandle.Exec("DELETE FROM someschema.sometable WHERE id = $1", 1)
if err != nil { ... }
</syntaxhighlight>
</syntaxhighlight>
=Delete a Row=
 
===Behavior on <tt>nil</tt> Positional Parameters===
 
If a <code>nil</code> value is provided for a positional parameter, <code>NULL</code> will be inserted in the corresponding column, provided that <code>NULL</code> values are allowed for that column.
 
==With <tt>jackc/pgx</tt>==
The API is quasi-indentical, the <code>Exec()</code> function requires an additional <code>context.Context</code>:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
sql := `DELETE FROM TEST WHERE "ID"=$1`
ctx := context.Background()
_, err = db.Exec(sql, 20)
_, err := connectionHandle.Exec(ctx, "INSERT INTO someschema.sometable (id, name) VALUES ($1, $2)", 1, "Alice")
if err != nil {
if err != nil { ... }
  ...
_, err = connectionHandle.Exec(ctx, "UPDATE someschema.sometable SET name = $1 WHERE id = $2", "Bob", 1)
}
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "DELETE FROM someschema.sometable WHERE id = $1", 1)
if err != nil { ... }
</syntaxhighlight>
</syntaxhighlight>
=Query=
The API has the same [[#Behavior_on_nil_Positional_Parameters|behavior on <code>nil</code>]] as in case of the <code>database/sql</code> API.
 
=Queries=
==With <tt>database/sql</tt>==
If one single row is expected as result of the query, then <code>QueryRow()</code>/<code>QueryRowContext()</code> can be used. Otherwise <code>Query()</code>/<code>QueryContext()</code> can be used to retrieve multiple rows:
===Single Row===
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
s := `SELECT "ID", "NAME" FROM TEST`
row := dbHandle.QueryRow("SELECT id, name, city FROM someschema.sometable WHERE id = $1", 1)
rows, err := db.Query(s)
var (
if err != nil {
id   int
  ...
name string
}
city sql.NullString
defer rows.Close()
)
for rows.Next() {
err := row.Scan(&id, &name, &city)
   var id int
if err != nil { ... }
  var name string
  err = rows.Scan(&id, &name)
  if err != nil {
    ...
  }
}
</syntaxhighlight>
</syntaxhighlight>
 
===Multiple Rows===
Other query methods:
 
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
s := `SELECT "value", "user_id" FROM ratings WHERE "record_id"=$1 AND "record_type"=$2`
rows, err := dbHandle.Query("SELECT id, name, city FROM someschema.sometable WHERE city IS NOT NULL AND id <> $1", 1)
rows, e := r.db.QueryContext(ctx, s, recordID, recordType)
if err != nil { ... }
defer func() {
defer func() {
  if e := rows.Close(); e != nil {
if err = rows.Close(); err != nil { ... }
    ...
  }
}()
}()
if e != nil {
  ...
}
for rows.Next() {
for rows.Next() {
   var rating int
var (
  var userID string
id   int
  if e := rows.Scan(&rating, &userID); e != nil {
name string
     ...
city sql.NullString
  }
)
  // do something with each result rating, userID
err = rows.Scan(&id, &name, &city)
if err != nil { ... }
     ...  
}
}
</syntaxhighlight>
</syntaxhighlight>


<code>QueryRowContex()</code> is expected to return at most a row:
===Handling <tt>NULL</tt> Results===
<syntaxhighlight lang='go'>
If a column value can be <code>NULL</code>, providing the pointer to a <code>string</code> to <code>Scan()</code> can lead to:
var title, description, director string
<font size=-2>
q := `SELECT "title", "description", "director" FROM movies WHERE "id"=$1`
Scan error on column index 2, name "city": converting NULL to string is unsupported
row := r.db.QueryRowContext(ctx, q, id)
</font>
if err := row.Scan(&title, &description, &director); err != nil {
 
  ...
<code>sql.NullString</code> and equivalents should be used instead.
}
 
</syntaxhighlight>
==With <tt>jackc/pgx</tt>==
<font color=darkkhaki>TODO when needed.</font>

Latest revision as of 18:34, 31 May 2024

Internal

Overview

PostgreSQL can be accessed from Go either with the database/sql library, which is part of the Standard Library, or with a specialist library like jackc/pgx.

database/sql provides a uniform SQL API but is quite generic since it has to be able to work uniformly with many different database platforms. It has to be configured with a database driver, like lib/pq or the driver that comes with jackc/pgx.

jackc/pgx is a PostgreSQL specialist library that has its own API, and its own driver (which can be used with database/sql).

This article shows how to connect to the database and then perform DDL and DML operations with both database/sql (configured with a jackc/pgx driver) and jackc/pgx library.

Connect to Database

With database/sql API and a pgx Driver

database/sql must be configured with a PostgreSQL driver via a blank import. This examples uses the jackc/pgx driver.

import (
	"database/sql"
	"fmt"

	_ "github.com/jackc/pgx/v5/stdlib"
)

role := "postgres"
password := "somepasswd"
host := "localhost"
port := 5432
database := "postgres"
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s", role, password, host, port, database)
dbHandle, err := sql.Open("pgx", connectionString)
if err != nil {
	panic(err)
}
defer func() {
	if err := dbHandle.Close(); err != nil {
		fmt.Printf("failed to close the connection\n")
	}
}()
if err := dbHandle.Ping(); err != nil {
	fmt.Printf("failed to ping the database: %v\n", err)
} else {
	fmt.Printf("database OK\n")
}

sql.Open("pgx", ...) returns a database handle, and it must be called with a driver name recognized by the driver library initialized with the blank import. In this case is "pgx". The implementation maintains a pool of zero or more underlying connections, and it is safe for concurrent use by multiple goroutines.

The example shows how to automatically close the connection on exit, and also how to ping the database to ensure the connection works.

With database/sql API and a lib/pq Driver

The code is quasi-identical, with three differences:

  • the name of the blank import used to initialize the driver
  • the name of the driver sql.Open() is invoked with ("postgres" instead of "pgx")
  • details of the connection string: the "lib/pg" driver attempts to connect with SSL enabled and if the database is not connected for SSL, it fails.
import (
    ...

	_ "github.com/lib/pq" // different driver
)

...
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", role, password, host, port, database) // different connection string parameters
dbHandle, err := sql.Open("postgres", connectionString) // different driver name
...

With jackc/pgx API

The API is a bit different when using the specialized library (pgx.Connect() instead of the sql.Open(), etc.), but the overall structure of the database connection sequence is identical:

import (
	"context"
	"fmt"

	"github.com/jackc/pgx/v5"
)

role := "postgres"
password := "somepasswd"
host := "localhost"
port := 5432
database := "postgres"
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", role, password, host, port, database)
ctx := context.Background()
connectionHandle, err := pgx.Connect(ctx, connectionString)
if err != nil {
	panic(err)
}
defer func() {
	if err := connectionHandle.Close(ctx); err != nil {
		fmt.Printf("failed to close the connection\n")
	}
}()
if err := connectionHandle.Ping(ctx); err != nil {
	fmt.Printf("failed to ping the database: %v\n", err)
} else {
	fmt.Printf("database OK\n")
}

DDL

Create a schema, create a table within the schema, drop the table and the schema.

With database/sql

_, err := dbHandle.Exec("CREATE SCHEMA IF NOT EXISTS someschema")
if err != nil { ... }
_, err = dbHandle.Exec("CREATE TABLE IF NOT EXISTS someschema.sometable (id integer PRIMARY KEY, name text)")
if err != nil { ... }
_, err = dbHandle.Exec("DROP TABLE someschema.sometable")
if err != nil { ... }
_, err = dbHandle.Exec("DROP SCHEMA someschema CASCADE")
if err != nil { ... }

With jackc/pgx

The API is quasi-indentical, the Exec() function requires an additional context.Context:

ctx := context.Background()
_, err := connectionHandle.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS someschema")
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "CREATE TABLE IF NOT EXISTS someschema.sometable (id integer PRIMARY KEY, name text)")
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "DROP TABLE someschema.sometable")
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "DROP SCHEMA someschema CASCADE")
if err != nil { ... }

DML

Insert a row in a table, then update that row, then delete it:

With database/sql

_, err := dbHandle.Exec("INSERT INTO someschema.sometable (id, name) VALUES ($1, $2)", 1, "Alice")
if err != nil { ... }
_, err = dbHandle.Exec("UPDATE someschema.sometable SET name = $1 WHERE id = $2", "Bob", 1)
if err != nil { ... }
_, err = dbHandle.Exec("DELETE FROM someschema.sometable WHERE id = $1", 1)
if err != nil { ... }

Behavior on nil Positional Parameters

If a nil value is provided for a positional parameter, NULL will be inserted in the corresponding column, provided that NULL values are allowed for that column.

With jackc/pgx

The API is quasi-indentical, the Exec() function requires an additional context.Context:

ctx := context.Background()
_, err := connectionHandle.Exec(ctx, "INSERT INTO someschema.sometable (id, name) VALUES ($1, $2)", 1, "Alice")
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "UPDATE someschema.sometable SET name = $1 WHERE id = $2", "Bob", 1)
if err != nil { ... }
_, err = connectionHandle.Exec(ctx, "DELETE FROM someschema.sometable WHERE id = $1", 1)
if err != nil { ... }

The API has the same behavior on nil as in case of the database/sql API.

Queries

With database/sql

If one single row is expected as result of the query, then QueryRow()/QueryRowContext() can be used. Otherwise Query()/QueryContext() can be used to retrieve multiple rows:

Single Row

row := dbHandle.QueryRow("SELECT id, name, city FROM someschema.sometable WHERE id = $1", 1)
var (
	id   int
	name string
	city sql.NullString
)
err := row.Scan(&id, &name, &city)
if err != nil { ... }

Multiple Rows

rows, err := dbHandle.Query("SELECT id, name, city FROM someschema.sometable WHERE city IS NOT NULL AND id <> $1", 1)
if err != nil { ... }
defer func() {
	if err = rows.Close(); err != nil { ... }
}()
for rows.Next() {
	var (
		id   int
		name string
		city sql.NullString
	)
	err = rows.Scan(&id, &name, &city)
	if err != nil { ... }
    ... 
}

Handling NULL Results

If a column value can be NULL, providing the pointer to a string to Scan() can lead to:

Scan error on column index 2, name "city": converting NULL to string is unsupported

sql.NullString and equivalents should be used instead.

With jackc/pgx

TODO when needed.