Nesta era de microsserviços e desenvolvimento ágil, a necessidade de APIs flexíveis e eficientes é mais importante do que nunca. Hoje, gostaria de compartilhar um projeto interessante que desenvolvi recentemente: uma API RESTful personalizável em Go (Golang) que permite a realização de consultas em um banco de dados Oracle.
Go, uma linguagem de programação criada no Google, é conhecida por sua simplicidade e eficiência. Seu suporte integrado para a manipulação de concorrência e sua velocidade tornam-na uma escolha ideal para o desenvolvimento de APIs. Além disso, sua sintaxe clara e sua robusta biblioteca padrão ajudam a tornar o código mais legível e manutenível.
Começamos nosso programa importando as bibliotecas necessárias. O pacote database/sql
é usado para interagir com o banco de dados Oracle e o pacote github.com/gorilla/mux
é usado para o roteamento HTTP.
Definimos uma estrutura chamada App
que encapsula um roteador mux.Router
e uma conexão com o banco de dados sql.DB
. A função Initialize
é usada para estabelecer a conexão com o banco de dados Oracle e para inicializar o roteador.
Em seguida, definimos nossas rotas com a função initializeRoutes
e criamos a função getTableData
para ser o manipulador da nossa rota /api/{table}
.
A getTableData
constrói uma string de query SQL com base nos parâmetros fornecidos na URL, como o nome da tabela, cláusulas de seleção, filtragem, ordenação e agrupamento. Depois, executa a consulta e retorna os resultados no formato solicitado pelo usuário: JSON, CSV ou HTML Table. Isto é conseguido com as funções rowsToCSV
, rowsToHTMLTable
e rowsToJSON
.
Embora esse código tenha suas vantagens, ele também tem suas limitações. Atualmente, o código não implementa nenhuma forma de autenticação ou autorização e não protege contra ataques de injeção SQL. Portanto, recomenda-se implementar essas características antes de usar o código em um ambiente de produção.
package main
import (
"io/ioutil"
"database/sql"
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"flag"
"log"
"os"
"github.com/gorilla/mux"
_ "github.com/godror/godror"
"html/template"
)
type App struct {
Router *mux.Router
DB *sql.DB
}
var (
Debug *log.Logger
)
func (a *App) Run(addr string) {
http.ListenAndServe(addr, a.Router)
}
func (a *App) Initialize(user, password, host, port, dbname string) {
// Constrói a string de conexão usando os argumentos fornecidos
connectionString := fmt.Sprintf("%s/%s@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=%s)(PORT=%s)))(CONNECT_DATA=(SERVICE_NAME=%s)))", user, password, host, port, dbname)
var err error
a.DB, err = sql.Open("godror", connectionString)
if err != nil {
panic(err)
}
a.Router = mux.NewRouter()
a.initializeRoutes()
}
func (a *App) initializeRoutes() {
a.Router.HandleFunc("/api/{table}", a.getTableData).Methods("GET")
}
func (a *App) getTableData(w http.ResponseWriter, r *http.Request) {
table := mux.Vars(r)["table"]
params := r.URL.Query()
selectClause := params.Get("$select")
filterClause, err := url.QueryUnescape(params.Get("$filter"))
orderByClause, err := url.QueryUnescape(params.Get("$orderby"))
groupByClause, err := url.QueryUnescape(params.Get("$groupby"))
outputFormat := params.Get("$output")
query := buildQuery(table, selectClause, filterClause, orderByClause, groupByClause)
rows, err := a.DB.Query(query)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
switch outputFormat {
case "csv":
w.Header().Set("Content-Type", "text/csv")
cw := csv.NewWriter(w)
err = rowsToCSV(cw, rows)
case "table":
w.Header().Set("Content-Type", "text/html")
err = rowsToHTMLTable(w, rows)
default: // Default to JSON
w.Header().Set("Content-Type", "application/json")
err = rowsToJSON(w, rows)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func buildQuery(table, selectClause, filterClause, orderByClause, groupByClause string) string {
if selectClause == "" {
selectClause = "*"
}
query := fmt.Sprintf("SELECT %s FROM %s", selectClause, table)
if filterClause != "" {
filterClause = odataToSQL(filterClause)
query = fmt.Sprintf("%s WHERE %s", query, filterClause)
}
if groupByClause != "" {
query = fmt.Sprintf("%s GROUP BY %s", query, groupByClause)
}
if orderByClause != "" {
query = fmt.Sprintf("%s ORDER BY %s", query, orderByClause)
}
return query
}
func odataToSQL(filter string) string {
filter = strings.Replace(filter, " eq ", " = ", -1)
filter = strings.Replace(filter, " ne ", " != ", -1)
filter = strings.Replace(filter, " gt ", " > ", -1)
filter = strings.Replace(filter, " lt ", " < ", -1)
filter = strings.Replace(filter, " ge ", " >= ", -1)
filter = strings.Replace(filter, " le ", " <= ", -1)
filter = strings.Replace(filter, " and ", " AND ", -1)
filter = strings.Replace(filter, " or ", " OR ", -1)
filter = strings.Replace(filter, " not ", " NOT ", -1)
return filter
}
func rowsToCSV(cw *csv.Writer, rows *sql.Rows) error {
// Fetch column names
columns, err := rows.Columns()
if err != nil {
return err
}
// Write the column names as the first row
if err := cw.Write(columns); err != nil {
return err
}
// Prepare a slice to hold the row data
values := make([]sql.RawBytes, len(columns))
scanArgs := make([]interface{}, len(values))
for i := range values {
scanArgs[i] = &values[i]
}
// Iterate over the rows, writing each one to the CSV writer
for rows.Next() {
err := rows.Scan(scanArgs...)
if err != nil {
return err
}
writeValues := make([]string, len(values))
for i, v := range values {
writeValues[i] = string(v)
}
if err := cw.Write(writeValues); err != nil {
return err
}
}
cw.Flush()
if err := rows.Err(); err != nil {
return err
}
if err := cw.Error(); err != nil {
return err
}
return nil
}
func rowsToHTMLTable(w http.ResponseWriter, rows *sql.Rows) error {
columns, err := rows.Columns()
if err != nil {
return err
}
// Prepare a slice to hold the row data
values := make([]sql.RawBytes, len(columns))
scanArgs := make([]interface{}, len(values))
for i := range values {
scanArgs[i] = &values[i]
}
var data []map[string]string
for rows.Next() {
err := rows.Scan(scanArgs...)
if err != nil {
return err
}
row := make(map[string]string)
for i, v := range values {
row[columns[i]] = string(v)
}
data = append(data, row)
}
tmpl, err := template.New("table").Parse(`
<table>
<thead>
<tr>
{{ range $key, $value := index . 0 }}
<th>{{$key}}</th>
{{ end }}
</tr>
</thead>
<tbody>
{{ range $i, $row := . }}
<tr>
{{ range $key, $value := $row }}
<td>{{$value}}</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
`)
if err != nil {
return err
}
if err := tmpl.Execute(w, data); err != nil {
return err
}
return nil
}
func rowsToJSON(w http.ResponseWriter, rows *sql.Rows) error {
// Obtém os nomes das colunas
columns, err := rows.Columns()
if err != nil {
return err
}
// Cria os slices para conter os valores das linhas
values := make([]sql.RawBytes, len(columns))
scanArgs := make([]interface{}, len(values))
for i := range values {
scanArgs[i] = &values[i]
}
// Cria um slice de maps para conter todas as linhas
var data []map[string]string
// Itera sobre as linhas
for rows.Next() {
// Lê os valores da linha
err := rows.Scan(scanArgs...)
if err != nil {
return err
}
// Cria um map para conter a linha
row := make(map[string]string)
// Adiciona os valores ao map
for i, v := range values {
row[columns[i]] = string(v)
}
// Adiciona o map ao slice de maps
data = append(data, row)
}
// Converte o slice de maps para JSON
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
// Escreve o JSON na resposta
w.Write(jsonData)
return nil
}
func InitializeLogger(debugMode bool) {
if debugMode {
Debug = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime|log.Lshortfile)
} else {
Debug = log.New(ioutil.Discard, "", 0) // When not in debug mode, discard debug logs
}
}
func main() {
args := os.Args
debugMode := false
for _, arg := range args {
if strings.HasPrefix(arg, "debug=") {
debugMode = strings.TrimPrefix(arg, "debug=") == "1"
}
}
InitializeLogger(debugMode)
Debug.Println("This is a debug log message.")
a := App{}
a.Initialize("username", "password", "host", "port", "service_name")
a.Run(":8080")
}
Exemplo Prático
Suponha que tenhamos uma tabela chamada ‘vendas’ com as colunas ‘ano’, ‘produto’ e ‘quantidade’. Podemos consultar esta tabela da seguinte maneira:
GET /api/venda/api/vendas?$select=ano,produto,quantidade&$filter=ano%20eq%202020&$orderby=produto&$output=table
Output em table
ANO | PRODUTO | QUANTIDADE |
---|---|---|
2020 | Celular | 210 |
2020 | Laptop | 150 |
Output em Json
[
{"ANO":"2020","PRODUTO":"Celular","QUANTIDADE":"210"},
{"ANO":"2020","PRODUTO":"Laptop","QUANTIDADE":"150"}
]
Output em CSV
ANO,PRODUTO,QUANTIDADE
2020,Celular,210
2020,Laptop,150
Em resumo, construímos uma API RESTful personalizável em Go que se conecta a um banco de dados Oracle e retorna os resultados das consultas em três formatos diferentes. Este projeto destaca a flexibilidade e a eficiência de Go para o desenvolvimento de APIs, enquanto mostra uma maneira poderosa e flexível de interagir com bancos de dados Oracle.