Alcune note su Go & PostgreSQL

In questo articolo si parla di come connettersi a PostgreSQL utilizzando il linguaggio di programmazione Go (“golang” per i motori di ricerca).

Questo non è un articolo adatto a principianti poiché presuppone una discreta conoscenza di questo linguaggio e si danno per scontate parecchie cose.
Per quanto riguarda PostgreSQL, ci saranno alcuni riferimenti a MySQL, utili a chi sa già utilizzare questo DBMS.

Un’ottima guida è la seguente alla connessione ai DBMS con Go è Go database/sql tutorial (http://go-database-sql.org/index.html), alla cui lettura senz’altro si rimanda.

Il presente articolo si limita a spiegare come superare alcuni ostacoli cui ci si trova inevitabilmente di fronte quando si utilizzano Go e PostgreSQL nella pratica.

COMINCIAMO

Go (https://golang.org/) è un linguaggio di programmazione opensource sviluppato da Google.
Rilasciato nel 2009, deriva principalmente dal C, con caratteristiche prese in prestito da Python e altri linguaggi.

Go è un linguaggio compilato, con prestazioni di tutto rispetto.
Alcuni benchmark sono disponibili al seguente link:
https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=go&lang2=gcc.

PostgreSQL (https://www.postgresql.org/) è uno storico DBMS a oggetti rilasciato con licenza BSD.

Entrambi sono disponibili per numerosi sistemi operativi.

Per imparare o approfondire la propria conoscenza del Go, a mio parere il miglior libro è The Go Programming Language, di Alan A. A. Donovan e Brian W. Kernighan, edizioni Addison-Wesley.
Kernighan è già stato autore del noto The C Programming Language e non ha certo bisogno di presentazioni. Non si tratta comunque di libri per principianti assoluti.

Per questo articolo ho utilizzato Go in versione 1.6.2 per Darwin/Amd64 e Postgres.app in versione 9.5.4 (http://postgresapp.com/).
Ho inoltre usato pgAdmin3 in versione 1.22.1 (https://www.pgadmin.org/).

Quest’ultimo tool non è strettamente necessario, in quanto si può amministrare PostgreSQL direttamente da linea di comando.
Per chi è abituato a phpMyAdmin, è possibile installare Adminer (https://www.adminer.org/), un tool per la gestione di MySQL, PostgreSQL e altri DBMS composto da un unico file PHP.
Ovviamente Adminer necessità di un apposito ambiente per poter essere eseguito.
Personalmente l’ho testato con MAMP (https://www.mamp.info/) e WAMP (http://www.wampserver.com/en/). pgAdmin3 resta comunque molto più completo.

CREAIAMO DATABASE E TABELLE

Per i nostri esperimenti partiamo creando un database. In Postgresql ciò può essere fatto con il seguente comando (che in questo caso è stato lanciato dal prompt dei comandi del client di PostgreSQL:

postgres-# CREATE DATABASE example WITH OWNER = postgres TEMPLATE = template0 ENCODING = ‘UTF8’ TABLESPACE = pg_default LC_COLLATE = ‘C’ LC_CTYPE = ‘C’ CONNECTION LIMIT = -1;

Per controllare il risultato dell’operazione possiamo digitare:

postgres-# \l

Il comndo \l corrisponde a SHOW DATABASES in MySQL.

Per connetterci al database appena creato utilizziamo:

postgres-# \connect example

Il comando \connect corrisponde al comando USE di MySLQ

Creiamo ora una una semplice tabella, con la seguente query.

CREATE TABLE public.utilizer
(
uid bigserial NOT NULL,
name character varying(50) NOT NULL,
email character varying(50) NOT NULL,
sex character(1) NOT NULL,
birth_date date,
active boolean NOT NULL DEFAULT false,
creation_date timestamp without time zone NOT NULL
DEFAULT now(),
CONSTRAINT utilizer_pkey PRIMARY KEY (uid),
CONSTRAINT utilizer_email_key UNIQUE (email)
);

Ricordiamo che public è il nome dello schema di Postgresql che stiamo utilizzando.
Per approfondimenti in merito al concetto di schema si vedano i seguenti link:

http://stackoverflow.com/questions/1152405/postgresql-is-it-better-using-multiple-databases-with-1-schema-each-or-1-datab

https://www.postgresql.org/docs/9.5/static/ddl-schemas.html

PAROLE RISERVATE

Abbiamo digitato utilizer al posto di user perché user è una parola riservata.

L’elenco delle parole riservate si PostgreSQL è disponibile qui:
https://www.postgresql.org/docs/9.5/static/sql-keywords-appendix.html

È comunque possibile usare una parola riservata, mettendola però tra doppi apici, che poi andranno utilizzati sempre. Esempio:

"user"

Alla stessa maniera, i nomi delle tabelle e delle colonne vengono trattati come case insensitive se non sono racchiusi tra doppi apici.

RIEMPIAMO LA TABELLA

Possiamo ora popolare la tabella con questa query:

INSERT INTO public.utilizer(name, email, sex, birth_date, active) VALUES
('Mario Rossi', 'mario@example.com', 'M', '1960–01–13', TRUE),
('Giuseppe Bianchi', 'giuseppe@example.com', 'M', '1970–04–28', TRUE);

Si noti che stringhe, caratteri e date vengono racchiusi tra apici singoli (in questo caso PostgreSQL è meno flessibile di MySQL).

IL DRIVER

A questo punto possiamo cominciare a programmare.
Ci serve un driver. Il più diffuso è probabilmente pq (https://github.com/lib/pq).

Per caricarlo si esegue, da terminale, il seguente comando:

$ go get "github.com/lib/pq"

Creiamo una directory chiamata prova_pg (o altro) nella directory src della GOROOT:

Per maggiori informazioni sulla GOROOT si veda: https://golang.org/doc/install.

All’interno di prova_pg creiamo il file main.go (Esempio 1).

Per motivi di spazio abbiamo “compresso” la gestione degli errori in un unica riga, a discapito della leggibilità del codice.

// Esempio 1
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()
    err = db.Ping()
if err != nil { log.Fatal(err) }
    fmt.Println(db)
}

Notiamo che ci connettiamo al DBMS senza password, quanto di default quando si installa PostgreSQL l’utente postgres viene appunto creato senza password. 
Ovviamente è consigliabile assegnargliene una.

Con il terminale ci posizioniamo all’interno di GOROOT/src/prova_pg/ e eseguiamo il seguente comando:

$ go run main.go

Se non ci sono errori di battitura o altro, nel terminale dovrebbero comparire alcune informazioni, ma non messaggi d’errore.

OTTENERE I DATI

Possiamo quindi espandere il nostro codice, come nell’Esempio 2.

Ricordiamo che i placehorder, in PostegreSQL, sono $1, $2, $3 e così via e non ? come in MySQL.

// Esempio 2
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()
if err != nil { log.Fatal(err) }
    sql := "SELECT uid, name, email, sex, birth_date, active,     creation_date FROM public.utilizer WHERE uid = $1"
stmt, err := db.Prepare(sql)
if err != nil { log.Fatal(err) }
defer stmt.Close()
    rows, err := stmt.Query(1)
if err != nil { log.Fatal(err) }
defer rows.Close()

var (
uid string
name string
email string
sex string
birth_date string
active string
creation_date string
)

for rows.Next() {
err := rows.Scan(&uid, &name, &email, &sex, &birth_date, &active, &creation_date)
if err != nil { log.Fatal(err) }
fmt.Println(uid, name, email, sex, birth_date, active, creation_date)
}
    if err = rows.Err(); err != nil { log.Fatal(err) }
}

Nell’Esempio 2 abbiamo passato a Scan() gli indirizzi di memoria delle variabili, tutte dichiarate string (uid, name ecc.); pertanto i valori ottenuti dalla query vengono convertiti in strighe.
Avremmo potuto anche dichiarare le variabili in questa maniera:

var ( 
uid int64
name string
email string
sex string
birth_date time.Time
active bool
creation_date time.Time
)

Ovviamente, se vogliamo usare una variabile di tipo time.Time, dobbiamo anche importare time all’inizio del programma, altrimenti la compilazione darà errore.

import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
"time"
)

Complichiamo le cose (Esempio 3) e cerchiamo di passare uno slice a Scan().
In realtà avremmo voluto passare a Scan() gli indirizzi di memoria dei singoli elementi di una map, ma è un operazione che in Go non si può assolutamente fare.

// Esempio 3
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
"reflect"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()
    err = db.Ping()
if err != nil { log.Fatal(err) }
    sql := "SELECT uid, name, email, sex, birth_date, active, creation_date FROM public.utilizer WHERE uid = $1"
stmt, err := db.Prepare(sql)
if err != nil { log.Fatal(err) }
defer stmt.Close()
    rows, err := stmt.Query(1)
if err != nil { log.Fatal(err) }
defer rows.Close()

res := make([]interface{}, 7) // Dove 7 è il numero delle colonne.
for i, _ := range res {
var ii interface{}
res[i] = &ii
}
    for rows.Next() {
err := rows.Scan(res...)
if err != nil { log.Fatal(err) }
for i, _ := range res {
v := *(res[i].(*interface{}))
fmt.Println(v, reflect.TypeOf(v))
}
}
    if err = rows.Err(); err != nil { log.Fatal(err) }
}

Con reflect.TypeOf() è possibile sapere il tipo di una variabile.

INSERIMENTO DATI

Nell’Esempio 4 vediamo come inserire dei dati nella nostra tabella.

// Esempio 4
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()
    err = db.Ping()
if err != nil { log.Fatal(err) }
    var (
name string = "Pinco Pallino"
email string = "pinco@example"
sex string = "M"
birth_date string = "1965-02-28"
active bool = true
)
    sql := "INSERT INTO public.utilizer (name, email, sex, birth_date, active)"
sql += " VALUES($1, $2, $3, $4, $5)"
stmt, err := db.Prepare(sql)
if err != nil { log.Fatal(err) }
defer stmt.Close()
    res, err := stmt.Exec(name, email, sex, birth_date, active)
if err != nil { log.Fatal(err) }
    fmt.Println(res)
}

Abbiamo usato cinque variabili (non tutte dello stesso tipo). Possiamo utilizzare al loro posto uno slice in intefacce. Questa tecnica è mostrata nell’Esempio 5.

// Esempio 5
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()
    err = db.Ping()
if err != nil { log.Fatal(err) }
    values := make([]interface{}, 0)
values = append(values, "Tizio Caio")
values = append(values, "tizio@example")
values = append(values, "M")
values = append(values, "1989-05-23")
values = append(values, true)
    sql := "INSERT INTO public.utilizer (name, email, sex, birth_date, active)"
sql += " VALUES($1, $2, $3, $4, $5)"
stmt, err := db.Prepare(sql)
if err != nil { log.Fatal(err) }
defer stmt.Close()
    res, err := stmt.Exec(values...)
if err != nil { log.Fatal(err) }
    fmt.Println(res)
}

Sarebbe interessate, dopo aver inserito un nuovo record di dati, ottenere l’id (in questo caso chiamato uid) del record inserito.

Si potrebbe usare qualcosa come LastInsertId()… se funzionasse anche con PostgreSQL.

Va, invece, utilizzato RETURNING uid al termine della query, come nell’Esempio 6.

// Esempio 6
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()
    err = db.Ping()
if err != nil { log.Fatal(err) }
    values := make([]interface{}, 0)
values = append(values, "Tizio Caio 2")
values = append(values, "tizio2@example")
values = append(values, "M")
values = append(values, "1989-05-23")
values = append(values, true)
    sql := "INSERT INTO public.utilizer (name, email, sex, birth_date, active)"
sql += " VALUES($1, $2, $3, $4, $5) RETURNING uid"
stmt, err := db.Prepare(sql)
if err != nil { log.Fatal(err) }
defer stmt.Close()
    var uid int64
err = stmt.QueryRow(values...).Scan(&uid)
if err != nil { log.Fatal(err) }
    fmt.Println(uid)
}

FUNZIONI DI POSTGRESQL

Creiamo una funzione di PostgreSQL, con il seguente comando (si lancia come una normale query):

CREATE OR REPLACE FUNCTION public.select_by_uid(
IN bigint,
OUT flag boolean,
OUT _uid bigint,
OUT _name character varying,
OUT _email character varying,
OUT _sex character,
OUT _birth_date date,
OUT _active boolean,
OUT _creation_date timestamp without time zone)
RETURNS record AS
$BODY$
BEGIN
SELECT uid, name, email, sex, birth_date, active, creation_date
INTO _uid, _name, _email, _sex, _birth_date, _active, _creation_date
FROM utilizer
WHERE uid = $1;
IF FOUND THEN
flag := TRUE;
RETURN;
ELSE
flag := FALSE;
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql;

Abbiamo usato il linguaggio PL/pgSQL per scrivere questa funzione, che riceve l’uid di un utilizer e restituisce i valori del record che ha quello uid (se esiste, ovviamente).

Da notare che il primo valore restitutito è un flag booleano che può essere vero (se il record è stato trovato) oppure falso.

Come si richiama una tale funzione? Proviamo con il seguente comando.

postgres-# SELECT select_by_uid(1);

In questo caso otteniamo un unico valore:

(t,1,”Mario Rossi”,mario@example.com,M,1960–01–13,t,”2016–08–16 10:23:16.727504")

Molto meglio usare il comando

example=# SELECT * FROM select_by_uid(1);

Che ci restituisce un valore per campo, ognuno col proprio tipo.

Da punto di vista del Go, ciò può essere realizzato come nell’Esempio 7.

// Esempio 7
package main
import (
"database/sql"
_ "github.com/lib/pq"
"fmt"
"log"
"reflect"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=example sslmode=disable")
if err != nil { log.Fatal(err) }
defer db.Close()

err = db.Ping()
if err != nil { log.Fatal(err) }

sql := "SELECT * FROM select_by_uid($1);"
stmt, err := db.Prepare(sql)
if err != nil { log.Fatal(err) }
defer stmt.Close()

rows, err := stmt.Query(1)
if err != nil { log.Fatal(err) }
defer rows.Close()

res := make([]interface{}, 8)
for i, _ := range res {
var ii interface{}
res[i] = &ii
}
for rows.Next() {
err := rows.Scan(res...)
if err != nil { log.Fatal(err) }
for i, _ := range res {
v := *(res[i].(*interface{}))
fmt.Println(v, reflect.TypeOf(v))
}
}
if err = rows.Err(); err != nil { log.Fatal(err) }
}

Notiamo che il primo valore restitutito è il nostro flag:

true bool

Se passiamo alla query un uid non esistente, ad esempio con

rows, err := stmt.Query(999)

Otterremo un flag uguale a false e una serie di valori nil.

false bool
<nil> <nil>
<nil> <nil>
<nil> <nil>
<nil> <nil>
<nil> <nil>
<nil> <nil>
<nil> <nil>

E con questo, per ora, è tutto.

A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.