Rafael

Engenheiro de Software

Olá, Eu sou Rafael. Estou construindo coisas para a web e mobile.

Um desenvolvedor dedicado a criar experiências digitais de alto desempenho, acessíveis e limpas. Eu me especializo em transformar problemas complexos em soluções de engenharia elegantes.

SQLC – migração e transação

Vamos falar hoje sobre o SQLC ele elimina a utilização de ORM, é uma ferramenta de código aberto que compila códigos SQL em Go seguro (type-safe) e idiomático. Você cria seu select, update e insert e ele gera o código GO com tudo mapeado. Para instalar você pode utilizar a própria página deles aqui.

Eu optei por instalar dessa forma:

GO
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

Instale também o golang-migrate, link aqui. Eu usei o comando do MAC:

CLI
brew install golang-migrate

Para o MAC precisei colocar a pasta BIN no PATH:

CLI
echo ‘export PATH="$PATH:$(go env GOPATH)/bin"‘ >> ~/.zshrc
source ~/.zshrc

Nosso estrutura de pasta:

O arquivo sqlc.yaml é o arquivo de configuração central do SQLC:

YAML
version: "2"
sql:
– schema: "sql/migrations"
  queries: "sql/queries"
  engine: "mysql"
  gen:
    go:
      package: "db"
      out: "internal/db"
      overrides:
        – db_type: "decimal"
          go_type: "float64"

Ele aponta para um schema de migrations. A linha queries é onde vai ficar nosso SQL para ele gerar os código em GO e, o engine é o banco de dados, o parâmetro out é para onde vai os arquivos e por fim overrides é para todos decimals ficar em float64 para ele não gerar como string como no padrão dele.

Agora vamos ver o nossos dois arquivos da pasta migrations, 000001_init.up.sql:

MYSQL
CREATE TABLE authors (
    id   varchar(36)  NOT NULL PRIMARY KEY,
    name text    NOT NULL,
    bio  text
);

CREATE TABLE books (
  id   varchar(36)  NOT NULL PRIMARY KEY,
  authors_id   varchar(36)  NOT NULL,
  name text    NOT NULL,
  description  text,
  price  decimal(10,2)  NOT NULL,
  FOREIGN KEY (authors_id) REFERENCES authors(id)
);

000001_init.down.sql:

MYSQL
DROP TABLE IF EXISTS books;
DROP TABLE IF EXISTS authors;

Nosso query.sql dentro da pasta queries. Os parâmetros com exec é só para executar com many ou one é para retornar muitos ou só um:

MYSQL
— name: ListAuthors :many
SELECT * FROM authors;

— name: GetAuthor :one
SELECT * FROM authors
WHERE id = ?;

— name: CreateAuthor :exec
INSERT INTO authors (id, name, bio)
VALUES (?,?,?);

— name: UpdateAuthor :exec
UPDATE authors SET name = ?, bio = ?
WHERE id = ?;

— name: DeleteAuthor :exec
DELETE FROM authors WHERE id = ?;

— name: CreateBook :exec
INSERT INTO books (id, name, description, authors_id, price)
VALUES (?,?,?,?,?);

— name: ListBooks :many
SELECT b.*, a.name as author_name
FROM books b JOIN authors a ON b.authors_id = a.id;

Nosso Makefile:

MYSQL
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASS=root
DB_NAME=library

createmigration:
	migrate create -ext=sql -dir=sql/migrations -seq init

migrate:
	migrate -path=sql/migrations \
	-database "mysql://$(DB_USER):$(DB_PASS)@tcp($(DB_HOST):$(DB_PORT))/$(DB_NAME)" \
	-verbose up

migratedown:
	migrate -path=sql/migrations \
	-database "mysql://$(DB_USER):$(DB_PASS)@tcp($(DB_HOST):$(DB_PORT))/$(DB_NAME)" \
	-verbose down

.PHONY: migrate migratedown createmigration

Agora que passamos por todo conteúdo precisamos gerar os arquivos em GO que mapeia toda nossa estrutura para pasta internal/db. Para isso vamos executar na raiz o comando:

SCRIPT
sqlc generate

Agora vamos ver os arquvios que ele gerou na pasta internal/db. db.go:

GO
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.31.1

package db

import (
	"context"
	"database/sql"
)

type DBTX interface {
	ExecContext(context.Context, string, …interface{}) (sql.Result, error)
	PrepareContext(context.Context, string) (*sql.Stmt, error)
	QueryContext(context.Context, string, …interface{}) (*sql.Rows, error)
	QueryRowContext(context.Context, string, …interface{}) *sql.Row
}

func New(db DBTX) *Queries {
	return &Queries{db: db}
}

type Queries struct {
	db DBTX
}

func (q *Queries) WithTx(tx *sql.Tx) *Queries {
	return &Queries{
		db: tx,
	}
}

models.go:

GO
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.31.1

package db

import (
	"database/sql"
)

type Author struct {
	ID   string
	Name string
	Bio  sql.NullString
}

type Book struct {
	ID          string
	AuthorsID   string
	Name        string
	Description sql.NullString
	Price       float64
}

query.sql.go:

GO
// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.31.1
// source: query.sql

package db

import (
	"context"
	"database/sql"
)

const createAuthor = `– name: CreateAuthor :exec
INSERT INTO authors (id, name, bio)
VALUES (?,?,?)
`

type CreateAuthorParams struct {
	ID   string
	Name string
	Bio  sql.NullString
}

func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) error {
	_, err := q.db.ExecContext(ctx, createAuthor, arg.ID, arg.Name, arg.Bio)
	return err
}

const createBook = `– name: CreateBook :exec
INSERT INTO books (id, name, description, authors_id, price)
VALUES (?,?,?,?,?)
`

type CreateBookParams struct {
	ID          string
	Name        string
	Description sql.NullString
	AuthorsID   string
	Price       float64
}

func (q *Queries) CreateBook(ctx context.Context, arg CreateBookParams) error {
	_, err := q.db.ExecContext(ctx, createBook,
		arg.ID,
		arg.Name,
		arg.Description,
		arg.AuthorsID,
		arg.Price,
	)
	return err
}

const deleteAuthor = `– name: DeleteAuthor :exec
DELETE FROM authors WHERE id = ?
`

func (q *Queries) DeleteAuthor(ctx context.Context, id string) error {
	_, err := q.db.ExecContext(ctx, deleteAuthor, id)
	return err
}

const getAuthor = `– name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = ?
`

func (q *Queries) GetAuthor(ctx context.Context, id string) (Author, error) {
	row := q.db.QueryRowContext(ctx, getAuthor, id)
	var i Author
	err := row.Scan(&i.ID, &i.Name, &i.Bio)
	return i, err
}

const listAuthors = `– name: ListAuthors :many
SELECT id, name, bio FROM authors
`

func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
	rows, err := q.db.QueryContext(ctx, listAuthors)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Author
	for rows.Next() {
		var i Author
		if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listBooks = `– name: ListBooks :many
SELECT b.id, b.authors_id, b.name, b.description, b.price, a.name as author_name
FROM books b JOIN authors a ON b.authors_id = a.id
`

type ListBooksRow struct {
	ID          string
	AuthorsID   string
	Name        string
	Description sql.NullString
	Price       float64
	AuthorName  string
}

func (q *Queries) ListBooks(ctx context.Context) ([]ListBooksRow, error) {
	rows, err := q.db.QueryContext(ctx, listBooks)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListBooksRow
	for rows.Next() {
		var i ListBooksRow
		if err := rows.Scan(
			&i.ID,
			&i.AuthorsID,
			&i.Name,
			&i.Description,
			&i.Price,
			&i.AuthorName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateAuthor = `– name: UpdateAuthor :exec
UPDATE authors SET name = ?, bio = ?
WHERE id = ?
`

type UpdateAuthorParams struct {
	Name string
	Bio  sql.NullString
	ID   string
}

func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error {
	_, err := q.db.ExecContext(ctx, updateAuthor, arg.Name, arg.Bio, arg.ID)
	return err
}

Agora enfim temos todos os arquivos gerados! É só chamar as funções. Vamos ver um exemplo, crie na pasta cmd uma pasta chamada execSQLC e adicione um arquivo main.go nela:

GO
package main

import (
	"context"
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/google/uuid"
	"github.com/rafaelsouzaribeiro/sqlc/internal/db"
)

func main() {
	ctx := context.Background()
	dbConn, err := sql.Open("mysql", "root:root@tcp(localhost:3306)/library")
	if err != nil {
		panic(err)
	}
	defer dbConn.Close()

	queries := db.New(dbConn)

	// Criar um novo autor
	err = queries.CreateAuthor(ctx, db.CreateAuthorParams{
		ID:   uuid.New().String(),
		Name: "J.K. Rowling",
		Bio:  sql.NullString{String: "British author, best known for the Harry Potter series.", Valid: true},
	})

	if err != nil {
		panic(err)
	}

	// Listar autores
	authors, err := queries.ListAuthors(ctx)
	if err != nil {
		panic(err)
	}

	for _, author := range authors {
		println("Author:", author.Name, "Bio:", author.Bio.String)
	}

	// Criar um novo livro para o autor
	err = queries.CreateBook(ctx, db.CreateBookParams{
		ID:          uuid.New().String(),
		Name:        "Harry Potter and the Sorcerer’s Stone",
		Description: sql.NullString{String: "The first book in the Harry Potter series.", Valid: true},
		Price:       19.99,
		AuthorsID:   authors[0].ID, // Associar o livro ao primeiro autor
	})

	if err != nil {
		panic(err)
	}

	// Listar Author com seus livros
	books, err := queries.ListBooks(ctx)
	if err != nil {
		panic(err)
	}

	for _, book := range books {
		println("Book:", book.Name, "Author:", book.AuthorName, "Price:", book.Price)
	}

}

Pronto! Está feito. Vamos falar sobre transação com SQLC para acessar clique aqui.

0 comentários

Deixe seu comentário