Ir para o conteúdo

Backend - Repositories

Este documento descreve o padrão Repository implementado no backend, incluindo interfaces, implementações e exemplos de uso.

Índice

  1. Visão Geral
  2. Padrão Repository
  3. Repositories Existentes
  4. Estrutura de Repository
  5. Boas Práticas

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

  1. Testabilidade: Services podem receber mocks de repositories
  2. Flexibilidade: Pode trocar GORM por outra implementação
  3. 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
}
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)
}

Referências