Go: RESTful - um exemplo completo com persistência
Afluentes: Sistemas Distribuídos e Mobile
Modelo DAO (Data Access Object)
Base de Dados
Para nosso exemplo de CRUD, vamos precisar de um local para armazenar nossas informações de forma persistente. Para isso, criamos uma tabela chamada users
no Postgres. Abaixo temos a clausula CREATE TABLE da nossa tabela.
create table users (
id integer not null,
name varchar(50) not null,
email varchar(50) not null,
primary key (id)
);
Veja que ela é bastante simples, mas já serve como exemplo. Temos os atributros id
do tipo integer
e dois atributos, name
e email
, do tipo string, ou melhor, VARCHAR
. O id
é usado como chave primária.
Para nosso exemplo, criamos a base de dados chamada backendcrud
. Fique a vontade para mudar no nome da base de dados, mas lembre-se de alterar no código fonte do arquivo main.go
também.
Conforme nosso modelo DAO
, essa é nossa FONTE DE DADOS
.
database.go
Essa classe é meio que um padrão para a conexão com o banco de dados. O constructor, NewDatabase
inicializa o objeto, configurando as informações de conexão passadas como parâmetro e retornando o próprio objeto na função (ver mais sobre a criação de objetos em Go). A função Close
fecha a conexão com a base de dados.
Agora, a função que mais vamos utilizar é a Get
. Toda vez que formos fazer uma operação no nosso banco, vamos usar essa função para pegarmos a nossa conexão. Caso ainda não tenha sido aberta uma conexão, essa conexão é feita e retornada ao usuário, caso contrário, apenas retorna a conexão aberta. Dessa forma, não precisamos ficar abrindo e fechando conexão toda vez que precisarmos fazer uma operação, nem ficar abrindo um monte de conexões, o que seria um problema. :D
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
// Database class
type Database struct {
connection *sql.DB
dbtype string
info string
err error
}
// NewDatabase Constructor
func NewDatabase(dbhost string, dbtype string, dbname string, dbuser string, dbpass string) *Database {
db := new(Database)
db.dbtype = dbtype
db.info = fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", dbuser, dbpass, dbname)
return db
}
// Get return connection
func (db *Database) Get() *sql.DB {
if db.connection == nil {
db.connection, db.err = sql.Open(db.dbtype, db.info)
}
return db.connection
}
// Close function
func (db *Database) Close() {
db.connection.Close()
}
user.go
A classe User
é a classe que representa o nosso objeto da tabela. Observe também que também temos informações nos atributos para fazer a serialização dos objetos para o formato JSON. Os atributos que serão serializados devem ser públicos, ou seja, iniciar com letra maiúscula.
A função NewUser
serve para criar um objeto com os dados passados como parâmetro.
package main
type User struct {
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func NewUser(id int, name string, email string) *User {
user := new(User)
user.Id = id
user.Name = name
user.Email = email
return user
}
Seguindo nosso modelo DAO
essa classe cria nossos OBJETOS DE TRANSFERÊNCIA
.
userdao.go
A classe UserDAO
é, no DAO
, nosso OBJETO DE ACESSO
, responsável pelas operações com os OBJETOS DE TRANSFERÊNCIA
e utilizado pelo OBJETO DE APLICAÇÃO
.
Nessa classe temos todas as operações CRUD.
- CREATE: representada aqui pelo método
Insert
. Ao chamarmos o método, passamos como parâmetro umOBJETO DE TRANSFERÊNCIA
do tipoUsers
. Esse objeto é então gravado na base de dados caso oId
ainda não exista (isso porque é chave primária). - READ: a leitura dos dados é feita por dois métodos:
- método
GetAll
que retorna uma lista de todos os dados armazenados na base (o que não é aconselhável caso a base seja grande. O interessante é fazer filtros para trazer parte dos dados apenas. Fiz assim por ser um exemplo simples em sala de aula, buscando diminuir a quantidade de código do exemplo). - método
Get
que retorna apenas um objeto. Veja que passamos oId
do objeto que queremos retornar.
- método
- UPDATE: Usamos o método
Update
para alterar as informações de um registro. Passamos como parâmetro oOBJETO DE TRANSFERÊNCIA
que queremos alterar. Veja que ele irá alterar as informações do registro conforme as informações do objeto peloId
desse objeto. - DELETE: O método
Delete</code, exclui o registro com o
Id
passado como parâmetro.
package main
import "fmt"
type UserDAO struct {
db *Database
users []*User
}
func NewUserDAO(db *Database) *UserDAO {
userdao := new(UserDAO)
userdao.db = db
userdao.users = make([]*User, 0)
return userdao
}
func (userdao *UserDAO) GetAll() []*User {
userdao.users = make([]*User, 0)
db := userdao.db.Get()
rows, err := db.Query("select * from users")
if err != nil {
fmt.Println(err.Error())
}
for rows.Next() {
var id int
var name string
var email string
rows.Scan(&id, &name, &email)
userdao.users = append(userdao.users, NewUser(id, name, email))
}
return userdao.users
}
func (userdao *UserDAO) Insert(user *User) {
db := userdao.db.Get()
query := "insert into users (id, name, email) values ($1, $2, $3)"
_, err := db.Query(query, user.Id, user.Name, user.Email)
if err != nil {
fmt.Println(err.Error())
}
}
func (userdao *UserDAO) Get(id string) *User {
db := userdao.db.Get()
query := "select * from users where id=$1"
rows, err := db.Query(query, id)
if err != nil {
fmt.Println(err.Error())
}
if rows.Next() {
var id int
var name string
var email string
rows.Scan(&id, &name, &email)
return NewUser(id, name, email)
}
return nil
}
func (userdao *UserDAO) Update(user *User) string {
db := userdao.db.Get()
query := "update users set name=$2, email=$3 where id=$1"
_, err := db.Query(query, user.Id, user.Name, user.Email)
if err != nil {
fmt.Println(err.Error())
return err.Error()
}
return "ok"
}
func (userdao *UserDAO) Delete(id string) string {
db := userdao.db.Get()
query := "delete from users where id=$1"
_, err := db.Query(query, id)
if err != nil {
fmt.Println(err.Error())
return err.Error()
}
return "ok"
}
main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
db := NewDatabase("localhost", "postgres", "backendcrud", "saulo", "1234")
defer db.Close()
dao := NewUserDAO(db)
router := http.NewServeMux()
router.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dao.Insert(NewUser(user.Id, user.Name, user.Email))
})
router.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
users := dao.GetAll()
json.NewEncoder(w).Encode(users)
})
router.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
id := r.PathValue("id")
user := dao.Get(id)
json.NewEncoder(w).Encode(user)
})
router.HandleFunc("PUT /users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
out := dao.Update(NewUser(user.Id, user.Name, user.Email))
json.NewEncoder(w).Encode(out)
})
router.HandleFunc("DELETE /users/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
id := r.PathValue("id")
out := dao.Delete(id)
json.NewEncoder(w).Encode(out)
})
// Vamos precisar do OPTIONS porque o PUT primeiro manda um OPTION
// Assim, se for localhost, avisamos o CORS que permitimos local
router.HandleFunc("OPTIONS /users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
})
// O DELETE também chama o OPTIONS, mas envia também uma informação
router.HandleFunc("OPTIONS /users/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
})
err := http.ListenAndServe("localhost:8080", router)
if err != nil {
fmt.Println(err.Error())
}
}