Backend - Repositories¶
Este documento descreve o padrão Repository implementado no backend, incluindo interfaces, implementações e exemplos de uso.
Índice¶
Visão Geral¶
Repositories abstraem o acesso a dados, encapsulando queries e operações de banco de dados. Isso permite:
- Testabilidade: Facilita mock de repositories em testes
- Flexibilidade: Permite trocar implementação sem afetar services
- Separação de Responsabilidades: Services não conhecem detalhes de implementação de banco
Localização: internal/repositories/
ORM: GORM (implementação atual)
Padrão Repository¶
Interface vs Implementação¶
Cada repository define uma interface e uma implementação:
// Interface
type Repository interface {
FindByID(id uint) (*models.Entity, error)
Create(entity *models.Entity) error
Update(entity *models.Entity) error
Delete(id uint) error
}
// Implementação
type GormRepository struct {
db *gorm.DB
}
func NewGormRepository(db *gorm.DB) Repository {
return &GormRepository{db: db}
}
Vantagens¶
- Testabilidade: Services podem receber mocks de repositories
- Flexibilidade: Pode trocar GORM por outra implementação
- Isolamento: Services não conhecem detalhes de SQL/GORM
Repositories Existentes¶
Auth Repository¶
Localização: internal/repositories/auth/auth_repository.go
Interface:
type Repository interface {
// Usuários
FindUserByUsername(username string) (*models.User, error)
FindUserByEmail(email string) (*models.User, error)
FindUserByID(id uint) (*models.User, error)
CreateUser(user *models.User) error
UpdateUser(user *models.User) error
UpdateUserPassword(userID uint, hashedPassword string) error
// Email Verification
CreateEmailVerification(emailVerification *models.EmailVerification) error
FindEmailVerificationByToken(token string) (*models.EmailVerification, error)
FindLastEmailVerificationByUserID(userID uint) (*models.EmailVerification, error)
InvalidatePreviousEmailVerifications(userID uint) error
UpdateEmailVerification(emailVerification *models.EmailVerification) error
// Password Reset
CreatePasswordReset(passwordReset *models.PasswordReset) error
FindPasswordResetByToken(token string) (*models.PasswordReset, error)
UpdatePasswordReset(passwordReset *models.PasswordReset) error
}
Implementação: GormAuthRepository
Uso:
authRepo := authRepo.NewGormAuthRepository(db)
user, err := authRepo.FindUserByEmail("user@example.com")
UserAuthProvider Repository¶
Localização: internal/repositories/auth/user_auth_provider_repository.go
Interface:
type Repository interface {
Create(provider *models.UserAuthProvider) error
FindByProviderAndProviderUserID(provider models.AuthProvider, providerUserID string) (*models.UserAuthProvider, error)
FindByUserIDAndProvider(userID uint, provider models.AuthProvider) (*models.UserAuthProvider, error)
FindByUserID(userID uint) ([]models.UserAuthProvider, error)
Delete(id uint) error
}
Uso: Gerenciamento de identidades OAuth (Google, Apple)
Account Repository¶
Localização: internal/repositories/accounts/account_repository.go
Interface:
type Repository interface {
FindAll(userID uint, userType string) ([]models.Account, error)
FindByIDWithAccess(id uint, userID uint, userType string) (*models.Account, error)
Create(account *models.Account) error
Update(account *models.Account) error
Delete(id uint) error
}
Características:
- FindAll filtra por userID se userType == "client"
- FindByIDWithAccess valida acesso baseado em userType
Uso:
accountRepo := accountRepo.NewGormAccountRepository(db)
accounts, err := accountRepo.FindAll(userID, userType)
Plan Repository¶
Localização: internal/repositories/plans/plan_repository.go
Interface:
type Repository interface {
FindAll(userType string, search string, page, limit int) ([]models.Plan, int64, error)
FindByID(id uint) (*models.Plan, error)
Create(plan *models.Plan) error
Update(plan *models.Plan) error
Delete(id uint) error
CountActiveSubscriptions(planID uint) (int64, error)
}
Características:
- FindAll suporta paginação e pesquisa
- Filtra por status se não for admin
- Retorna total de registros para paginação
Uso:
planRepo := planRepo.NewGormPlanRepository(db)
plans, total, err := planRepo.FindAll(userType, search, page, limit)
Professional Profile Repository¶
Localização: internal/repositories/professional/professional_profile_repository.go
Interface:
type ProfessionalProfileRepository interface {
FindByUserID(userID uint) (*models.ProfessionalProfile, error)
FindByID(id uint) (*models.ProfessionalProfile, error)
FindPending() ([]models.ProfessionalProfile, error)
Create(profile *models.ProfessionalProfile) error
Update(profile *models.ProfessionalProfile) error
}
Características:
- FindByUserID faz preload de User
- FindPending retorna perfis aguardando aprovação
Uso:
profileRepo := profRepo.NewGormProfessionalProfileRepository(db)
profile, err := profileRepo.FindByUserID(userID)
Professional Patient Repository¶
Localização: internal/repositories/professional/professional_patient_repository.go
Interface:
type ProfessionalPatientRepository interface {
FindByProfessionalID(professionalID uint, search string, page, limit int) ([]models.ProfessionalPatient, int64, error)
FindByPatientID(patientID uint) (*models.ProfessionalPatient, error)
FindByProfessionalAndPatient(professionalID, patientID uint) (*models.ProfessionalPatient, error)
Create(professionalPatient *models.ProfessionalPatient) error
Update(professionalPatient *models.ProfessionalPatient) error
Delete(professionalID, patientID uint) error
HardDeleteByPatientID(patientID uint) error
FindUserByID(id uint) (*models.User, error)
FindUserByIDAndType(id uint, userType string) (*models.User, error)
FindProfessionalProfileByUserID(userID uint) (*models.ProfessionalProfile, error)
FindClientProfileByUserID(userID uint) (*models.ClientProfile, error)
UpdateClientProfile(profile *models.ClientProfile) error
}
Características:
- FindByProfessionalID suporta paginação e pesquisa
- HardDeleteByPatientID faz delete físico (não soft delete)
- Inclui métodos auxiliares para buscar usuários e perfis
Uso:
patientRepo := profRepo.NewGormProfessionalPatientRepository(db)
patients, total, err := patientRepo.FindByProfessionalID(professionalID, search, page, limit)
Availability Repository¶
Localização: internal/repositories/professional/availability_repository.go
Interface:
type AvailabilityRepository interface {
FindByProfessionalID(professionalID uint, fromDate time.Time) ([]models.AvailabilitySlot, error)
FindByIDAndProfessionalID(slotID, professionalID uint) (*models.AvailabilitySlot, error)
FindExisting(professionalID uint, date models.Date, startTime, endTime string) (*models.AvailabilitySlot, error)
Create(slot *models.AvailabilitySlot) error
CreateBulk(slots []models.AvailabilitySlot) error
Update(slot *models.AvailabilitySlot) error
Delete(slotID, professionalID uint) error
}
Características:
- FindByProfessionalID filtra slots a partir de uma data
- FindExisting verifica se slot já existe (evita duplicatas)
- CreateBulk cria múltiplos slots em uma transação
Uso:
availabilityRepo := profRepo.NewGormAvailabilityRepository(db)
slots, err := availabilityRepo.FindByProfessionalID(professionalID, time.Now())
Professional Financial Repository¶
Localização: internal/repositories/professional/professional_financial_repository.go
Interface:
type ProfessionalFinancialRepository interface {
FindBankAccountByProfessionalID(professionalID uint) (*models.ProfessionalBankAccount, error)
CreateOrUpdateBankAccount(account *models.ProfessionalBankAccount) error
FindPaymentsByProfessionalID(professionalID uint, page, limit int) ([]models.Payment, int64, error)
}
Características:
- CreateOrUpdateBankAccount cria ou atualiza conta bancária
- FindPaymentsByProfessionalID suporta paginação
Estrutura de Repository¶
Padrão de Implementação¶
package repository
import (
"billings-ease-backend/internal/models"
"gorm.io/gorm"
)
// Interface
type Repository interface {
FindByID(id uint) (*models.Entity, error)
Create(entity *models.Entity) error
Update(entity *models.Entity) error
Delete(id uint) error
}
// Implementação
type GormRepository struct {
db *gorm.DB
}
// Construtor
func NewGormRepository(db *gorm.DB) Repository {
return &GormRepository{db: db}
}
// Métodos
func (r *GormRepository) FindByID(id uint) (*models.Entity, error) {
var entity models.Entity
if err := r.db.First(&entity, id).Error; err != nil {
return nil, err
}
return &entity, nil
}
func (r *GormRepository) Create(entity *models.Entity) error {
return r.db.Create(entity).Error
}
func (r *GormRepository) Update(entity *models.Entity) error {
return r.db.Save(entity).Error
}
func (r *GormRepository) Delete(id uint) error {
return r.db.Delete(&models.Entity{}, id).Error
}
Operações Comuns¶
CRUD Básico¶
Create:
func (r *Repository) Create(entity *models.Entity) error {
return r.db.Create(entity).Error
}
Read:
func (r *Repository) FindByID(id uint) (*models.Entity, error) {
var entity models.Entity
if err := r.db.First(&entity, id).Error; err != nil {
return nil, err
}
return &entity, nil
}
Update:
func (r *Repository) Update(entity *models.Entity) error {
return r.db.Save(entity).Error
}
Delete (Soft Delete):
func (r *Repository) Delete(id uint) error {
return r.db.Delete(&models.Entity{}, id).Error
}
Delete (Hard Delete):
func (r *Repository) HardDelete(id uint) error {
return r.db.Unscoped().Delete(&models.Entity{}, id).Error
}
Queries com Filtros¶
Filtro Simples:
func (r *Repository) FindByUserID(userID uint) (*models.Entity, error) {
var entity models.Entity
if err := r.db.Where("user_id = ?", userID).First(&entity).Error; err != nil {
return nil, err
}
return &entity, nil
}
Filtro Múltiplo:
func (r *Repository) FindByStatusAndType(status string, entityType string) ([]models.Entity, error) {
var entities []models.Entity
if err := r.db.Where("status = ? AND type = ?", status, entityType).
Find(&entities).Error; err != nil {
return nil, err
}
return entities, nil
}
Paginação¶
func (r *Repository) FindAll(page, limit int) ([]models.Entity, int64, error) {
var entities []models.Entity
var total int64
query := r.db
// Contar total
if err := query.Model(&models.Entity{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// Aplicar paginação
offset := (page - 1) * limit
if err := query.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
return nil, 0, err
}
return entities, total, nil
}
Pesquisa (Search)¶
func (r *Repository) FindAll(search string, page, limit int) ([]models.Entity, int64, error) {
var entities []models.Entity
var total int64
query := r.db
// Aplicar pesquisa
if len(search) >= 3 {
searchTerm := "%" + search + "%"
query = query.Where("name ILIKE ? OR description ILIKE ?", searchTerm, searchTerm)
}
// Contar total
if err := query.Model(&models.Entity{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// Aplicar paginação
offset := (page - 1) * limit
if err := query.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
return nil, 0, err
}
return entities, total, nil
}
Preload de Relacionamentos¶
func (r *Repository) FindByIDWithRelations(id uint) (*models.Entity, error) {
var entity models.Entity
if err := r.db.
Preload("User").
Preload("Observations").
First(&entity, id).Error; err != nil {
return nil, err
}
return &entity, nil
}
Validação de Acesso¶
func (r *Repository) FindByIDWithAccess(id uint, userID uint, userType string) (*models.Entity, error) {
query := r.db.Where("id = ?", id)
// Cliente só pode acessar seus próprios dados
if userType == "client" {
query = query.Where("user_id = ?", userID)
}
// Admin e professional podem acessar todos
var entity models.Entity
if err := query.First(&entity).Error; err != nil {
return nil, err
}
return &entity, nil
}
Boas Práticas¶
1. Interfaces Pequenas e Específicas¶
✅ BOM:
type Repository interface {
FindByID(id uint) (*models.Entity, error)
FindByUserID(userID uint) ([]models.Entity, error)
Create(entity *models.Entity) error
}
❌ EVITAR:
type Repository interface {
// Muitos métodos genéricos
Find(...interface{}) (interface{}, error)
Query(string, ...interface{}) (interface{}, error)
}
2. Retornar Erros Específicos¶
func (r *Repository) FindByID(id uint) (*models.Entity, error) {
var entity models.Entity
if err := r.db.First(&entity, id).Error; err != nil {
// GORM retorna gorm.ErrRecordNotFound quando não encontra
return nil, err
}
return &entity, nil
}
Service trata o erro:
entity, err := repo.FindByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("Entity not found")
}
return nil, err
}
3. Evitar Lógica de Negócio¶
❌ ERRADO:
func (r *Repository) CreateCycle(cycle *models.Cycle) error {
// Lógica de negócio no repository
if cycle.StartDate.Before(time.Now().AddDate(0, 0, -30)) {
return errors.New("Invalid date")
}
return r.db.Create(cycle).Error
}
✅ CORRETO:
func (r *Repository) Create(cycle *models.Cycle) error {
// Apenas operação de banco
return r.db.Create(cycle).Error
}
4. Usar Transações Quando Necessário¶
func (r *Repository) CreateWithRelations(entity *models.Entity, related []models.Related) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(entity).Error; err != nil {
return err
}
for _, rel := range related {
rel.EntityID = entity.ID
if err := tx.Create(&rel).Error; err != nil {
return err
}
}
return nil
})
}
5. Preload Apenas Quando Necessário¶
Evitar N+1 queries:
// ❌ ERRADO - N+1 queries
entities, _ := repo.FindAll()
for _, e := range entities {
user, _ := repo.FindUser(e.UserID) // Query para cada entidade
}
// ✅ CORRETO - Preload
entities, _ := repo.FindAllWithUsers() // Uma query com JOIN
6. Soft Delete por Padrão¶
GORM aplica soft delete automaticamente se o modelo tem DeletedAt:
// Soft delete (padrão)
r.db.Delete(&models.Entity{}, id)
// Hard delete (quando necessário)
r.db.Unscoped().Delete(&models.Entity{}, id)
7. Validação de Dados Básica¶
Repositories podem validar dados básicos (não regras de negócio):
func (r *Repository) Create(entity *models.Entity) error {
// Validação básica de estrutura
if entity.Name == "" {
return errors.New("name is required")
}
return r.db.Create(entity).Error
}
Testes¶
Mock de Repository¶
Para testar services, use mocks de repositories:
type MockRepository struct {
FindByIDFunc func(id uint) (*models.Entity, error)
}
func (m *MockRepository) FindByID(id uint) (*models.Entity, error) {
if m.FindByIDFunc != nil {
return m.FindByIDFunc(id)
}
return nil, errors.New("not implemented")
}
Uso em testes:
func TestService_GetEntity(t *testing.T) {
mockRepo := &MockRepository{
FindByIDFunc: func(id uint) (*models.Entity, error) {
return &models.Entity{ID: id}, nil
},
}
service := NewService(mockRepo)
entity, err := service.GetEntity(1)
assert.NoError(t, err)
assert.Equal(t, uint(1), entity.ID)
}
Testes de Repository¶
Para testar repositories, use banco de teste ou sqlmock:
func TestRepository_FindByID(t *testing.T) {
// Setup banco de teste
db := setupTestDB(t)
defer cleanupTestDB(t, db)
repo := NewGormRepository(db)
// Criar entidade de teste
entity := &models.Entity{Name: "Test"}
repo.Create(entity)
// Testar
found, err := repo.FindByID(entity.ID)
assert.NoError(t, err)
assert.Equal(t, entity.Name, found.Name)
}