# Generic CRUD Pattern The B12 SIS backend uses Go generics extensively to reduce boilerplate code while maintaining type safety. This pattern provides consistent CRUD operations across all modules. ## Overview The generic pattern consists of three layers: ``` BaseRepository[T] ↓ BaseService[T, CreateInput, UpdateInput] ↓ BaseController[T, CreateInput, UpdateInput] ``` Each layer builds on the one below, providing increasing levels of abstraction. ## BaseModel All domain entities embed `BaseModel`: ```go type BaseModel struct { Id string `gorm:"type:char(36);primary_key" json:"id"` Name string `gorm:"size:255;index" json:"name"` Description string `gorm:"type:text" json:"description"` TeamId string `gorm:"size:36;index" json:"team_id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` CreatedById string `gorm:"size:36;index" json:"created_by_id"` UpdatedById string `gorm:"size:36;index" json:"updated_by_id"` DeletedById string `gorm:"size:36;index" json:"deleted_by_id"` AssignedToId *string `gorm:"size:36;index" json:"assigned_to_id"` } ``` ### BeforeCreate Hook ```go func (m *BaseModel) BeforeCreate(tx *gorm.DB) error { if m.Id == "" { m.Id = uuid.New().String() } return nil } ``` ## BaseRepository The base repository provides generic database operations: ```go type BaseRepository[T any] struct { DB *gorm.DB Preloads []string TableName string } func NewBaseRepository[T any](tableName string, preloads []string) *BaseRepository[T] { return &BaseRepository[T]{ DB: database.GetDB(), Preloads: preloads, TableName: tableName, } } ``` ### Key Methods ```go // Create inserts a new record func (r *BaseRepository[T]) Create(ctx context.Context, entity *T) error // FindByID retrieves a record by ID with preloads func (r *BaseRepository[T]) FindByID(ctx context.Context, id string) (*T, error) // FindAll retrieves records with pagination and filtering func (r *BaseRepository[T]) FindAll(ctx context.Context, params ListParams) ([]T, int64, error) // Update modifies an existing record func (r *BaseRepository[T]) Update(ctx context.Context, id string, updates map[string]interface{}) error // Delete soft-deletes a record func (r *BaseRepository[T]) Delete(ctx context.Context, id string) error // FindAllWithFilter applies complex filter conditions func (r *BaseRepository[T]) FindAllWithFilter(ctx context.Context, params ListParams, filter FilterGroup) ([]T, int64, error) ``` ### Query Building The repository builds queries with automatic team filtering: ```go func (r *BaseRepository[T]) buildQuery(ctx context.Context) *gorm.DB { query := r.DB.WithContext(ctx).Model(new(T)) // Apply team filter unless disabled var entity T if !entity.DisableTeamFilter() { teamID := utils.GetTeamIDFromContext(ctx) teamIDs := utils.GetTeamAndChildrenIDs(ctx, teamID) query = query.Where("team_id IN ?", teamIDs) } // Apply preloads for _, preload := range r.Preloads { query = query.Preload(preload) } return query } ``` ## BaseService The base service provides business logic operations: ```go type BaseService[T any, CreateInput any, UpdateInput any] struct { Repository *repositories.BaseRepository[T] ModuleName string } func NewBaseService[T any, CI any, UI any]( repo *repositories.BaseRepository[T], moduleName string, ) *BaseService[T, CI, UI] { return &BaseService[T, CI, UI]{ Repository: repo, ModuleName: moduleName, } } ``` ### Key Methods ```go // Create validates input and creates a record func (s *BaseService[T, CI, UI]) Create(ctx context.Context, input CI) (*T, error) // GetByID retrieves and unsanitizes a record func (s *BaseService[T, CI, UI]) GetByID(ctx context.Context, id string) (*T, error) // GetAll retrieves records with pagination func (s *BaseService[T, CI, UI]) GetAll(ctx context.Context, params ListParams) (*ListResult[T], error) // Update validates and updates a record func (s *BaseService[T, CI, UI]) Update(ctx context.Context, id string, input UI) (*T, error) // Delete removes a record with audit func (s *BaseService[T, CI, UI]) Delete(ctx context.Context, id string) error // CreateMultiple bulk creates records func (s *BaseService[T, CI, UI]) CreateMultiple(ctx context.Context, inputs []CI) ([]T, error) ``` ### Input Mapping Services map input structs to models using reflection: ```go func (s *BaseService[T, CI, UI]) mapInputToEntity(input CI) (*T, error) { entity := new(T) inputVal := reflect.ValueOf(input) entityVal := reflect.ValueOf(entity).Elem() for i := 0; i < inputVal.NumField(); i++ { fieldName := inputVal.Type().Field(i).Name if entityField := entityVal.FieldByName(fieldName); entityField.IsValid() { entityField.Set(inputVal.Field(i)) } } return entity, nil } ``` ## BaseController The base controller provides HTTP handlers: ```go type BaseController[T any, CreateInput any, UpdateInput any] struct { Service *services.BaseService[T, CreateInput, UpdateInput] ModuleName string FilterMixin *FilterMixin } ``` ### Standard Handlers ```go // Create handles POST /{module} func (c *BaseController[T, CI, UI]) Create(ctx *gin.Context) // GetByID handles GET /{module}/:id func (c *BaseController[T, CI, UI]) GetByID(ctx *gin.Context) // GetAll handles POST /{module}/list func (c *BaseController[T, CI, UI]) GetAll(ctx *gin.Context) // Update handles PUT /{module}/:id func (c *BaseController[T, CI, UI]) Update(ctx *gin.Context) // Delete handles DELETE /{module}/:id func (c *BaseController[T, CI, UI]) Delete(ctx *gin.Context) // CreateMultiple handles POST /{module}/bulk func (c *BaseController[T, CI, UI]) CreateMultiple(ctx *gin.Context) // DeleteMultiple handles DELETE /{module}/bulk func (c *BaseController[T, CI, UI]) DeleteMultiple(ctx *gin.Context) // ExportExcel handles POST /{module}/export func (c *BaseController[T, CI, UI]) ExportExcel(ctx *gin.Context) // Import handles POST /{module}/import func (c *BaseController[T, CI, UI]) Import(ctx *gin.Context) ``` ## Creating a New Module ### Step 1: Define the Model ```go // internal/models/department.go package models type Department struct { BaseModel Code string `gorm:"size:50;index;uniqueIndex" json:"code"` HeadId *string `gorm:"size:36;index" json:"head_id"` Head *Teacher `gorm:"foreignKey:HeadId" json:"head"` FacilityId *string `gorm:"size:36;index" json:"facility_id"` Facility *Facility `gorm:"foreignKey:FacilityId" json:"facility"` } type DepartmentInput struct { Name string `json:"name" ctype:"text" crequired:"true"` Description string `json:"description" ctype:"textarea"` Code string `json:"code" ctype:"text" creatable:"true" editable:"false"` HeadId *string `json:"head_id" ctype:"relate" cmodule:"Teacher"` FacilityId *string `json:"facility_id" ctype:"relate" cmodule:"Facility"` } func (d *Department) GetListViewFields() []string { return []string{"name", "code", "head_id", "facility_id"} } func (d *Department) GetFieldsForSearch() []string { return []string{"name", "code"} } func (d *Department) DisableTeamFilter() bool { return false } ``` ### Step 2: Create Repository ```go // internal/repositories/department_repository.go package repositories type DepartmentRepository struct { *BaseRepository[models.Department] } func NewDepartmentRepository() *DepartmentRepository { preloads := []string{"Head", "Facility"} return &DepartmentRepository{ BaseRepository: NewBaseRepository[models.Department]("departments", preloads), } } // Add custom methods if needed func (r *DepartmentRepository) FindByCode(ctx context.Context, code string) (*models.Department, error) { var dept models.Department err := r.buildQuery(ctx).Where("code = ?", code).First(&dept).Error return &dept, err } ``` ### Step 3: Create Service ```go // internal/services/department_service.go package services type DepartmentService struct { *BaseService[models.Department, models.DepartmentInput, models.DepartmentInput] repo *repositories.DepartmentRepository } func NewDepartmentService() *DepartmentService { repo := repositories.NewDepartmentRepository() return &DepartmentService{ BaseService: NewBaseService[models.Department, models.DepartmentInput, models.DepartmentInput]( repo.BaseRepository, "Department", ), repo: repo, } } // Override Create to add custom logic func (s *DepartmentService) Create(ctx context.Context, input models.DepartmentInput) (*models.Department, error) { // Generate code if empty if input.Code == "" { code, _ := NewAutoCodeService().GenerateCode("Department") input.Code = code } return s.BaseService.Create(ctx, input) } ``` ### Step 4: Create Controller ```go // internal/controllers/department_controller.go package controllers type DepartmentController struct { *BaseController[models.Department, models.DepartmentInput, models.DepartmentInput] service *services.DepartmentService } func NewDepartmentController() *DepartmentController { service := services.NewDepartmentService() return &DepartmentController{ BaseController: NewBaseController[models.Department, models.DepartmentInput, models.DepartmentInput]( service.BaseService, "Department", ), service: service, } } // Add custom handlers if needed func (c *DepartmentController) GetByCode(ctx *gin.Context) { code := ctx.Param("code") dept, err := c.service.repo.FindByCode(utils.GetContext(ctx), code) if err != nil { utils.ErrorResponse(ctx, http.StatusNotFound, "Department not found", err.Error()) return } utils.SuccessResponse(ctx, "Department retrieved", dept) } ``` ### Step 5: Register Routes ```go // internal/routes/routes.go departmentController := controllers.NewDepartmentController() departments := protected.Group("/departments") { departments.POST("", departmentController.Create) departments.POST("/bulk", departmentController.CreateMultiple) departments.DELETE("/bulk", departmentController.DeleteMultiple) departments.POST("/list", departmentController.GetAll) departments.POST("/export", departmentController.ExportExcel) departments.GET("/:id", departmentController.GetByID) departments.GET("/code/:code", departmentController.GetByCode) // Custom endpoint departments.PUT("/:id", departmentController.Update) departments.DELETE("/:id", departmentController.Delete) } ``` ## Input Struct Tags The input struct uses tags for form generation and validation: | Tag | Description | Example | |-----|-------------|---------| | `ctype` | Field type | `ctype:"text"`, `ctype:"relate"`, `ctype:"enum"` | | `cmodule` | Related module name | `cmodule:"Teacher"` | | `crequired` | Required field | `crequired:"true"` | | `coptions` | Enum options | `coptions:"active;inactive"` | | `creatable` | Can be set on create | `creatable:"true"` | | `editable` | Can be modified on update | `editable:"false"` | | `label` | English label | `label:"Department Name"` | | `labelvi` | Vietnamese label | `labelvi:"Tên phòng ban"` | | `populateidfor` | Populate IDs for relation | `populateidfor:"Teachers"` | ## Model Interface Methods Models can implement these optional methods: ```go // GetListViewFields returns fields shown in list view func (m *Model) GetListViewFields() []string // GetFieldsForSearch returns fields used for search func (m *Model) GetFieldsForSearch() []string // GetFieldsForRelatedSearch returns fields for related search func (m *Model) GetFieldsForRelatedSearch() []string // DisableTeamFilter returns true to skip team filtering func (m *Model) DisableTeamFilter() bool // GetSummaryName returns display name for dropdowns func (m *Model) GetSummaryName() string // GetRelatedJoins returns JOIN clauses for related filtering func (m *Model) GetRelatedJoins() map[string]string ```