Multi-Tenant - 02 - Exemplo em golang
Perfeito, vamos pro código 👇
Vou assumir:
int64paraidetenant_idtime.Timepara datasgithub.com/jmoiron/sqlx- Padrão de pastas por contexto de negócio (ex:
internal/usuarios,internal/gruposetc.)
Sinta-se à vontade pra ajustar nomes/paths.
1. domain.go (usuários, grupos, usuários_grupos)
// internal/identity/domain.go
package identity
import "time"
// Usuário do sistema (multi-tenant)
type Usuario struct {
ID int64 `db:"id" json:"id"`
TenantID int64 `db:"tenant_id" json:"tenant_id"`
Nome string `db:"nome" json:"nome"`
Emailstring `db:"email" json:"email"`
SenhaHashstring `db:"senha_hash" json:"-"`
Ativobool `db:"ativo" json:"ativo"`
UltimoLogin *time.Time `db:"ultimo_login_em" json:"ultimo_login_em,omitempty"`
CreatedAttime.Time `db:"created_at" json:"created_at"`
UpdatedAttime.Time `db:"updated_at" json:"updated_at"`
}
// Grupo/role de acesso
type Grupo struct {
IDint64 `db:"id" json:"id"`
TenantID int64 `db:"tenant_id" json:"tenant_id"`
Nome string`db:"nome" json:"nome"`
Descricao *string `db:"descricao" json:"descricao,omitempty"`
Ativo bool `db:"ativo" json:"ativo"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Junção usuário x grupo
type UsuarioGrupo struct {
IDint64 `db:"id" json:"id"`
TenantID int64 `db:"tenant_id" json:"tenant_id"`
UsuarioID int64 `db:"usuario_id" json:"usuario_id"`
GrupoID int64 `db:"grupo_id" json:"grupo_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// View de conveniência: usuário + lista de grupos
type UsuarioComGrupos struct {
Usuario Usuario `json:"usuario"`
Grupos []Grupo `json:"grupos"`
}
2. Repositório de Usuários (repository_usuarios.go)
// internal/identity/repository_usuarios.go
package identity
import (
"context"
"database/sql"
"github.com/jmoiron/sqlx"
)
type UsuariosRepository interface {
Create(ctx context.Context, u *Usuario) error
Update(ctx context.Context, u *Usuario) error
GetByID(ctx context.Context, tenantID, id int64) (*Usuario, error)
GetByEmail(ctx context.Context, tenantID int64, email string) (*Usuario, error)
ListByTenant(ctx context.Context, tenantID int64, limit, offset int) ([]Usuario, error)
SoftDelete(ctx context.Context, tenantID, id int64) error
}
type usuariosRepository struct {
db *sqlx.DB
}
func NewUsuariosRepository(db *sqlx.DB) UsuariosRepository {
return &usuariosRepository{db: db}
}
func (r *usuariosRepository) Create(ctx context.Context, u *Usuario) error {
query := `
INSERT INTO public.usuarios (
tenant_id, nome, email, senha_hash, ativo
) VALUES (
:tenant_id, :nome, :email, :senha_hash, :ativo
)
RETURNING id, created_at, updated_at
`
stmt, err := r.db.PrepareNamedContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
return stmt.QueryRowxContext(ctx, u).Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt)
}
func (r *usuariosRepository) Update(ctx context.Context, u *Usuario) error {
query := `
UPDATE public.usuarios
SET nome = :nome,
email = :email,
ativo = :ativo
WHERE tenant_id = :tenant_id
AND id = :id
RETURNING updated_at
`
stmt, err := r.db.PrepareNamedContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
return stmt.QueryRowxContext(ctx, u).Scan(&u.UpdatedAt)
}
func (r *usuariosRepository) GetByID(ctx context.Context, tenantID, id int64) (*Usuario, error) {
var u Usuario
query := `
SELECT id, tenant_id, nome, email, senha_hash, ativo,
ultimo_login_em, created_at, updated_at
FROM public.usuarios
WHERE tenant_id = $1
AND id = $2
`
err := r.db.GetContext(ctx, &u, query, tenantID, id)
if err != nil {
return nil, err
}
return &u, nil
}
func (r *usuariosRepository) GetByEmail(ctx context.Context, tenantID int64, email string) (*Usuario, error) {
var u Usuario
query := `
SELECT id, tenant_id, nome, email, senha_hash, ativo,
ultimo_login_em, created_at, updated_at
FROM public.usuarios
WHERE tenant_id = $1
AND lower(email) = lower($2)
`
err := r.db.GetContext(ctx, &u, query, tenantID, email)
if err != nil {
return nil, err
}
return &u, nil
}
func (r *usuariosRepository) ListByTenant(ctx context.Context, tenantID int64, limit, offset int) ([]Usuario, error) {
usuarios := []Usuario{}
query := `
SELECT id, tenant_id, nome, email, senha_hash, ativo,
ultimo_login_em, created_at, updated_at
FROM public.usuarios
WHERE tenant_id = $1
ORDER BY id
LIMIT $2 OFFSET $3
`
err := r.db.SelectContext(ctx, &usuarios, query, tenantID, limit, offset)
if err != nil {
return nil, err
}
return usuarios, nil
}
func (r *usuariosRepository) SoftDelete(ctx context.Context, tenantID, id int64) error {
query := `
UPDATE public.usuarios
SET ativo = FALSE
WHERE tenant_id = $1
AND id = $2
`
_, err := r.db.ExecContext(ctx, query, tenantID, id)
return err
}
// Opcional: atualizar último login
func (r *usuariosRepository) UpdateUltimoLogin(ctx context.Context, tenantID, id int64, ts sql.NullTime) error {
query := `
UPDATE public.usuarios
SET ultimo_login_em = $3
WHERE tenant_id = $1
AND id = $2
`
_, err := r.db.ExecContext(ctx, query, tenantID, id, ts)
return err
}
3. Repositório de Grupos (repository_grupos.go)
// internal/identity/repository_grupos.go
package identity
import (
"context"
"github.com/jmoiron/sqlx"
)
type GruposRepository interface {
Create(ctx context.Context, g *Grupo) error
Update(ctx context.Context, g *Grupo) error
GetByID(ctx context.Context, tenantID, id int64) (*Grupo, error)
GetByNome(ctx context.Context, tenantID int64, nome string) (*Grupo, error)
ListByTenant(ctx context.Context, tenantID int64) ([]Grupo, error)
}
type gruposRepository struct {
db *sqlx.DB
}
func NewGruposRepository(db *sqlx.DB) GruposRepository {
return &gruposRepository{db: db}
}
func (r *gruposRepository) Create(ctx context.Context, g *Grupo) error {
query := `
INSERT INTO public.grupos (
tenant_id, nome, descricao, ativo
) VALUES (
:tenant_id, :nome, :descricao, :ativo
)
RETURNING id, created_at, updated_at
`
stmt, err := r.db.PrepareNamedContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
return stmt.QueryRowxContext(ctx, g).Scan(&g.ID, &g.CreatedAt, &g.UpdatedAt)
}
func (r *gruposRepository) Update(ctx context.Context, g *Grupo) error {
query := `
UPDATE public.grupos
SET nome = :nome,
descricao = :descricao,
ativo = :ativo
WHERE tenant_id = :tenant_id
AND id = :id
RETURNING updated_at
`
stmt, err := r.db.PrepareNamedContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
return stmt.QueryRowxContext(ctx, g).Scan(&g.UpdatedAt)
}
func (r *gruposRepository) GetByID(ctx context.Context, tenantID, id int64) (*Grupo, error) {
var g Grupo
query := `
SELECT id, tenant_id, nome, descricao, ativo,
created_at, updated_at
FROM public.grupos
WHERE tenant_id = $1
AND id = $2
`
err := r.db.GetContext(ctx, &g, query, tenantID, id)
if err != nil {
return nil, err
}
return &g, nil
}
func (r *gruposRepository) GetByNome(ctx context.Context, tenantID int64, nome string) (*Grupo, error) {
var g Grupo
query := `
SELECT id, tenant_id, nome, descricao, ativo,
created_at, updated_at
FROM public.grupos
WHERE tenant_id = $1
AND lower(nome) = lower($2)
`
err := r.db.GetContext(ctx, &g, query, tenantID, nome)
if err != nil {
return nil, err
}
return &g, nil
}
func (r *gruposRepository) ListByTenant(ctx context.Context, tenantID int64) ([]Grupo, error) {
grupos := []Grupo{}
query := `
SELECT id, tenant_id, nome, descricao, ativo,
created_at, updated_at
FROM public.grupos
WHERE tenant_id = $1
ORDER BY nome
`
err := r.db.SelectContext(ctx, &grupos, query, tenantID)
if err != nil {
return nil, err
}
return grupos, nil
}
4. Repositório Usuários ↔ Grupos (repository_usuarios_grupos.go)
// internal/identity/repository_usuarios_grupos.go
package identity
import (
"context"
"github.com/jmoiron/sqlx"
)
type UsuariosGruposRepository interface {
AdicionarGrupoAoUsuario(ctx context.Context, tenantID, usuarioID, grupoID int64) error
RemoverGrupoDoUsuario(ctx context.Context, tenantID, usuarioID, grupoID int64) error
ListarGruposDoUsuario(ctx context.Context, tenantID, usuarioID int64) ([]Grupo, error)
ListarUsuariosDoGrupo(ctx context.Context, tenantID, grupoID int64) ([]Usuario, error)
}
type usuariosGruposRepository struct {
db *sqlx.DB
}
func NewUsuariosGruposRepository(db *sqlx.DB) UsuariosGruposRepository {
return &usuariosGruposRepository{db: db}
}
func (r *usuariosGruposRepository) AdicionarGrupoAoUsuario(ctx context.Context, tenantID, usuarioID, grupoID int64) error {
query := `
INSERT INTO public.usuarios_grupos (
tenant_id, usuario_id, grupo_id
) VALUES ($1, $2, $3)
ON CONFLICT (tenant_id, usuario_id, grupo_id) DO NOTHING
`
_, err := r.db.ExecContext(ctx, query, tenantID, usuarioID, grupoID)
return err
}
func (r *usuariosGruposRepository) RemoverGrupoDoUsuario(ctx context.Context, tenantID, usuarioID, grupoID int64) error {
query := `
DELETE FROM public.usuarios_grupos
WHERE tenant_id = $1
AND usuario_id = $2
AND grupo_id = $3
`
_, err := r.db.ExecContext(ctx, query, tenantID, usuarioID, grupoID)
return err
}
func (r *usuariosGruposRepository) ListarGruposDoUsuario(ctx context.Context, tenantID, usuarioID int64) ([]Grupo, error) {
grupos := []Grupo{}
query := `
SELECT g.id, g.tenant_id, g.nome, g.descricao, g.ativo,
g.created_at, g.updated_at
FROM public.grupos g
JOIN public.usuarios_grupos ug
ON ug.grupo_id = g.id
AND ug.tenant_id = g.tenant_id
WHERE ug.tenant_id = $1
AND ug.usuario_id = $2
ORDER BY g.nome;
`
err := r.db.SelectContext(ctx, &grupos, query, tenantID, usuarioID)
if err != nil {
return nil, err
}
return grupos, nil
}
func (r *usuariosGruposRepository) ListarUsuariosDoGrupo(ctx context.Context, tenantID, grupoID int64) ([]Usuario, error) {
usuarios := []Usuario{}
query := `
SELECT u.id, u.tenant_id, u.nome, u.email, u.senha_hash, u.ativo,
u.ultimo_login_em, u.created_at, u.updated_at
FROM public.usuarios u
JOIN public.usuarios_grupos ug
ON ug.usuario_id = u.id
AND ug.tenant_id = u.tenant_id
WHERE ug.tenant_id = $1
AND ug.grupo_id = $2
ORDER BY u.nome;
`
err := r.db.SelectContext(ctx, &usuarios, query, tenantID, grupoID)
if err != nil {
return nil, err
}
return usuarios, nil
}
5. Exemplo de uso no service (só um cheirinho)
// internal/identity/service.go
package identity
import "context"
type Service struct {
usuariosRepo UsuariosRepository
gruposRepo GruposRepository
usuariosGruposRepo UsuariosGruposRepository
}
func NewService(uRepo UsuariosRepository, gRepo GruposRepository, ugRepo UsuariosGruposRepository) *Service {
return &Service{
usuariosRepo: uRepo,
gruposRepo: gRepo,
usuariosGruposRepo: ugRepo,
}
}
func (s *Service) ListarUsuarioComGrupos(ctx context.Context, tenantID, usuarioID int64) (*UsuarioComGrupos, error) {
u, err := s.usuariosRepo.GetByID(ctx, tenantID, usuarioID)
if err != nil {
return nil, err
}
grupos, err := s.usuariosGruposRepo.ListarGruposDoUsuario(ctx, tenantID, usuarioID)
if err != nil {
return nil, err
}
return &UsuarioComGrupos{
Usuario: *u,
Grupos: grupos,
}, nil
}
Se quiser, no próximo passo a gente encaixa isso no padrão de arquitetura hexagonal que você já está usando (ex: domain.go, repository_sqlx.go, handler_http.go) com um exemplo de handler /tenants/{tenant_id}/usuarios.