Anar Framework Models Documentation#
Introduction#
The Anar framework provides a robust model system built on top of GORM (Go Object Relational Mapping) that simplifies database interactions and provides type-safe operations. The framework includes custom field types that extend basic Go types with additional functionality for JSON serialization, database storage, and validation.
Model Structure#
Basic Model Definition#
Models in Anar are Go structs that embed gorm.Model and define database table structure:
type User struct {
gorm.Model
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"unique" validate:"required,min=3,max=20"`
Pass string `json:"pass" validate:"required,min=6"`
Is_admin bool `json:"is_admin"`
Email string `json:"email" gorm:"unique" validate:"required,email"`
Confirmed bool `json:"confirmed" gorm:"default:false"`
Token string `json:"token"`
}
Model Tags#
Anar models use three types of struct tags:
json: Controls JSON serialization/deserializationgorm: GORM-specific database configurationvalidate: Field validation rules
Base Model Components#
gorm.Model Embedding#
All Anar models should embed gorm.Model which provides:
ID: Primary key (uint)CreatedAt: Record creation timestampUpdatedAt: Last update timestampDeletedAt: Soft delete timestamp (for soft deletes)
type MyModel struct {
gorm.Model
// Your fields here
}
Primary Key Definition#
While gorm.Model provides an ID field, you can override it:
ID uint `json:"id" gorm:"primaryKey"`
Custom Field Types#
The Anar framework provides several custom field types in the types package that extend basic Go types with enhanced functionality.
1. FileField#
Purpose: Stores file paths and automatically expands them to full URLs in JSON responses.
Usage:
type Product struct {
gorm.Model
Name string `json:"name"`
Image types.FileField `json:"image"`
}
Features:
- Stores relative paths in database
- Automatically prepends app URL when marshaling to JSON
- Handles absolute URLs (http/https) without modification
- GORM compatible with Scan() and Value() methods
JSON Output Example:
// Database value: "uploads/product.jpg"
// JSON output: "https://myapp.com/uploads/product.jpg"
2. JSONField#
Purpose: Stores arbitrary JSON objects in the database as text.
Usage:
type Settings struct {
gorm.Model
UserID uint `json:"user_id"`
Preferences types.JSONField `json:"preferences"`
}
Features:
- Stores map[string]interface{} as JSON text in database
- Automatic marshaling/unmarshaling
- Handles null values gracefully
- Type-safe JSON operations
Example:
settings := Settings{
Preferences: types.JSONField{
"theme": "dark",
"notifications": true,
"language": "en",
},
}
3. StringArray#
Purpose: Stores arrays of strings as JSON in the database.
Usage:
type Article struct {
gorm.Model
Title string `json:"title"`
Tags types.StringArray `json:"tags"`
}
Features:
- Stores []string as JSON array in database
- Maintains order of elements
- Handles empty arrays gracefully
Example:
article := Article{
Title: "Go Programming",
Tags: types.StringArray{"go", "programming", "tutorial"},
}
4. TimeField#
Purpose: Nullable timestamp field with ISO8601 JSON serialization.
Usage:
type Event struct {
gorm.Model
Name string `json:"name"`
StartTime types.TimeField `json:"start_time"`
EndTime types.TimeField `json:"end_time"`
}
Features:
- Nullable timestamp (can be null/nil)
- ISO8601 format for JSON (RFC3339)
- GORM compatible
- Helper constructor: types.NewTimeField(time.Now())
JSON Format:
{
"start_time": "2023-12-25T10:00:00Z",
"end_time": null
}
5. EmailField#
Purpose: Email field with strict RFC5322 validation.
Usage:
type Contact struct {
gorm.Model
Name string `json:"name"`
Email types.EmailField `json:"email"`
}
Features:
- Strict email validation on database operations
- RFC5322 compliant regex validation
- Handles empty values gracefully
- Validation occurs during Scan() and Value() operations
Model Definition Examples#
Basic User Model#
package models
import (
"gorm.io/gorm"
"codeberg.org/anarproject/anar/types"
)
type User struct {
gorm.Model
Username string `json:"username" gorm:"unique;not null" validate:"required,min=3,max=20"`
Email types.EmailField `json:"email" gorm:"unique;not null" validate:"required"`
Password string `json:"-" validate:"required,min=8"` // Hidden from JSON
IsActive bool `json:"is_active" gorm:"default:true"`
Avatar types.FileField `json:"avatar"`
LastLogin types.TimeField `json:"last_login"`
Preferences types.JSONField `json:"preferences"`
}
Blog Post Model#
type Post struct {
gorm.Model
Title string `json:"title" gorm:"not null" validate:"required,max=200"`
Slug string `json:"slug" gorm:"unique;not null"`
Content string `json:"content" gorm:"type:text"`
Published bool `json:"published" gorm:"default:false"`
Tags types.StringArray `json:"tags"`
FeaturedImg types.FileField `json:"featured_image"`
PublishedAt types.TimeField `json:"published_at"`
AuthorID uint `json:"author_id" gorm:"not null"`
Author User `json:"author" gorm:"foreignKey:AuthorID"`
Metadata types.JSONField `json:"metadata"`
}
E-commerce Product Model#
type Product struct {
gorm.Model
Name string `json:"name" gorm:"not null" validate:"required,max=100"`
Description string `json:"description" gorm:"type:text"`
Price float64 `json:"price" gorm:"not null" validate:"gte=0"`
SKU string `json:"sku" gorm:"unique;not null"`
Images types.StringArray `json:"images"`
MainImage types.FileField `json:"main_image"`
InStock bool `json:"in_stock" gorm:"default:true"`
Attributes types.JSONField `json:"attributes"`
CategoryID uint `json:"category_id"`
Category Category `json:"category" gorm:"foreignKey:CategoryID"`
}
type Category struct {
gorm.Model
Name string `json:"name" gorm:"unique;not null"`
ParentID *uint `json:"parent_id"`
Parent *Category `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
}
Field Validation#
Validation Tags#
Anar checks the validate tag (go-playground/validator syntax) on every create and update that goes through the generated CRUD handlers:
type User struct {
Name string `validate:"required,min=3,max=50"`
Email string `validate:"required,email"`
Age int `validate:"min=18,max=120"`
Website string `validate:"omitempty,url"`
}
Common Validation Rules#
required: Field must not be emptymin=n: Minimum length/valuemax=n: Maximum length/valueemail: Valid email formaturl: Valid URL formatomitempty: Skip validation if emptyunique: Database-level uniqueness (GORM tag)
required rejects the zero value, so don't put it on a number that may be 0 or a bool that may be false. Use gte=0, or a pointer type, instead.
How Validation Runs#
- Create (
POST): every rule is checked. - Update (
PUTorPATCH): only the fields present in the body are checked and written. Fields you leave out keep their stored values, sorequireddoesn't apply to them. A field sent with a zero value ("stock": 0) is checked and saved. - Nested structs the client leaves empty (for example an
Authorassociation when onlyauthor_idis sent) are not checked against their own rules.
A request that breaks a rule is rejected with 422 Unprocessable Entity before anything is written. The fields are listed by their JSON names:
{
"error": {
"code": "validation_failed",
"message": "Validation failed",
"fields": {
"name": "must be at least 3 characters long",
"price": "must be greater than 0",
"category": "is required"
}
}
}
Hand-written handlers that bind their own request structs should use Gin's binding:"..." tag instead.
Database Integration#
GORM Tags#
Common GORM tags for database schema:
type Model struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"unique;not null;size:100"`
Description string `gorm:"type:text"`
Price float64 `gorm:"precision:10;scale:2"`
CreatedBy uint `gorm:"index"`
IsActive bool `gorm:"default:true"`
}
Relationships#
One-to-Many#
type User struct {
gorm.Model
Posts []Post `json:"posts" gorm:"foreignKey:UserID"`
}
type Post struct {
gorm.Model
UserID uint `json:"user_id"`
User User `json:"user" gorm:"foreignKey:UserID"`
}
Many-to-Many#
type User struct {
gorm.Model
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
}
type Role struct {
gorm.Model
Users []User `json:"users" gorm:"many2many:user_roles;"`
}
Database Hooks#
GORM provides hooks for model lifecycle events:
func (u *User) BeforeCreate(tx *gorm.DB) error {
// Hash password before saving
if u.Password != "" {
hashedPassword, err := hashPassword(u.Password)
if err != nil {
return err
}
u.Password = hashedPassword
}
return nil
}
func (u *User) AfterFind(tx *gorm.DB) error {
// Load additional data after finding
return nil
}
Best Practices#
1. Field Naming#
- Use PascalCase for exported fields
- Use snake_case for JSON tags
- Use descriptive names
2. JSON Handling#
type User struct {
ID uint `json:"id"`
Password string `json:"-"` // Hidden from JSON
Email string `json:"email,omitempty"` // Omit if empty
}
3. Custom Methods#
func (u *User) IsAdmin() bool {
return u.Is_admin
}
func (u *User) FullName() string {
return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
}
func (u *User) HasRole(role string) bool {
for _, r := range u.Roles {
if r.Name == role {
return true
}
}
return false
}
4. Validation#
- Always validate required fields
- Use appropriate validation rules
- Implement custom validation when needed
5. Performance Considerations#
- Use indexes for frequently queried fields
- Use
gorm:"index"tag for single column indexes - Use
gorm:"index:idx_name,composite"for composite indexes - Consider eager loading vs lazy loading for relationships
6. Security#
- Never expose sensitive fields in JSON (use
json:"-") - Validate all user inputs
- Use prepared statements (GORM handles this automatically)
- Implement proper access control
8. Migration Considerations#
// Use consistent field types
// Consider backward compatibility
// Use migrations for schema changes
type User struct {
gorm.Model
// Add new fields with appropriate defaults
NewField string `json:"new_field" gorm:"default:'default_value'"`
}
This documentation provides a comprehensive guide to working with models in the Anar framework, covering all the custom field types, validation, relationships, and best practices for building robust database-backed applications.