Entity Pattern¶
Entities are the core data structures in Dynorm. They represent your DynamoDB items with automatic ID generation, timestamps, and lifecycle management.
Base Entity¶
All entities embed dynorm.Entity which provides:
type Entity struct {
ID string `dynorm:"pk"` // ULID, auto-generated
CreatedAt time.Time `dynorm:"createdAt"` // Derived from ULID timestamp
UpdatedAt time.Time `dynorm:"updatedAt"` // Set on every save
DeletedAt *time.Time `dynorm:"deletedAt"` // For soft deletes (optional)
}
Defining Entities¶
Focus only on your business fields - Dynorm handles the rest:
type User struct {
dynorm.Entity
// Business fields only
Email string `dynorm:"gsi:ByEmail"`
Username string `dynorm:"gsi:ByUsername"`
FirstName string
LastName string
Password string // hashed
Status string
Role string
}
type Post struct {
dynorm.Entity `soft_deletes:"true"` // Enable soft deletes
Title string
Content string
AuthorID string `dynorm:"gsi:ByAuthor"`
Published bool
Tags []string
}
ULID-Based IDs¶
Dynorm uses ULID (Universally Unique Lexicographically Sortable Identifier) for IDs:
user := &User{Email: "test@example.com"}
err := UserRepo.Save(user)
fmt.Println(user.ID)
// Output: 01HQ3K4N5M6P7Q8R9S0T1U2V3W
Why ULID?¶
| Feature | ULID | UUID v4 | Auto-increment |
|---|---|---|---|
| Sortable | |||
| Distributed | |||
| URL-safe | |||
| Contains timestamp | |||
| Size | 26 chars | 36 chars | Variable |
ULID Properties
- 26 characters, alphanumeric (0-9, A-Z)
- Lexicographically sortable (time-ordered)
- Contains millisecond timestamp
- Globally unique
- URL-safe
Entity Lifecycle¶
stateDiagram-v2
[*] --> New: Create struct
New --> Saved: Save()
Saved --> Updated: Save()
Updated --> Updated: Save()
Updated --> SoftDeleted: SoftDelete()
SoftDeleted --> Updated: Restore()
SoftDeleted --> [*]: HardDelete()
Updated --> [*]: Delete() Lifecycle Methods¶
// Check if entity is new (no ID yet)
if user.IsNew() {
fmt.Println("This is a new user")
}
// Check if soft deleted
if post.IsDeleted() {
fmt.Println("This post is deleted")
}
// Get timestamps
fmt.Printf("Created: %s\n", user.GetCreatedAt())
fmt.Printf("Updated: %s\n", user.GetUpdatedAt())
// Soft delete
post.SoftDelete()
err := PostRepo.Save(post)
// Restore soft deleted entity
post.Restore()
err := PostRepo.Save(post)
Timestamps¶
CreatedAt¶
Derived from the ULID timestamp when the entity is first saved:
user := &User{Email: "test@example.com"}
fmt.Println(user.CreatedAt) // 0001-01-01 00:00:00 (zero value)
err := UserRepo.Save(user)
fmt.Println(user.CreatedAt) // 2024-01-15 10:30:45.123 (from ULID)
UpdatedAt¶
Automatically set to current time on every save:
user, _ := UserRepo.Find("01HQ3K...")
fmt.Println(user.UpdatedAt) // 2024-01-15 10:30:45
user.Status = "verified"
err := UserRepo.Save(user)
fmt.Println(user.UpdatedAt) // 2024-01-15 11:45:30 (updated)
Soft Deletes¶
Enable soft deletes to mark entities as deleted without removing them:
Using Soft Deletes¶
// Soft delete a post
post.SoftDelete()
err := PostRepo.Save(post)
// Post still exists in database with DeletedAt set
found, _ := PostRepo.Find(post.ID)
fmt.Println(found.IsDeleted()) // true
fmt.Println(found.DeletedAt) // 2024-01-15 12:00:00
// Restore the post
post.Restore()
err = PostRepo.Save(post)
fmt.Println(post.IsDeleted()) // false
// Query excluding soft deleted (default behavior)
activePosts, _ := PostRepo.
Where("Published", "=", true).
GetAll() // Excludes soft deleted posts
Dirty Tracking¶
Dynorm tracks changes to entities loaded from the database:
user, _ := UserRepo.Find("01HQ3K...")
// Check if loaded from database
fmt.Println(user.IsLoaded()) // true
// Check if specific field changed
user.Email = "new@example.com"
fmt.Println(user.IsDirty("Email")) // true
fmt.Println(user.IsDirty("FirstName")) // false
// Check if any field changed
fmt.Println(user.HasChanges()) // true
// Get list of changed fields
fields := user.DirtyFields() // ["Email"]
// Get map of changes
changes := user.Changes() // {"Email": "new@example.com"}
// Get original value
original, ok := user.OriginalValue("Email")
fmt.Println(original) // "old@example.com"
Field Naming¶
Fields are automatically converted to snake_case:
type User struct {
dynorm.Entity
FirstName string // → first_name
LastName string // → last_name
EmailAddress string // → email_address
PhoneNumber string // → phone_number
}
This applies to:
- DynamoDB attribute names
- JSON serialization
- YAML serialization
Complex Types¶
Dynorm supports various Go types:
type Product struct {
dynorm.Entity
Name string
Price float64
Tags []string // Stored as DynamoDB List
Attributes map[string]string // Stored as DynamoDB Map
InStock bool
Quantity int
LaunchDate time.Time // Stored as RFC3339 string
}
Struct Tags Reference¶
| Tag | Description | Example |
|---|---|---|
pk | Partition key | dynorm:"pk" |
sk | Sort key | dynorm:"sk" |
gsi:IndexName | GSI partition key | dynorm:"gsi:ByEmail" |
gsi:IndexName:sk | GSI sort key | dynorm:"gsi:ByEmail:sk" |
lsi:IndexName | LSI sort key | dynorm:"lsi:ByDate" |
version | Optimistic locking | dynorm:"version" |
ttl | TTL attribute | dynorm:"ttl" |
Best Practices¶
Do
- Keep entities focused on business fields
- Use meaningful GSI names (
ByEmail,ByAuthor) - Consider soft deletes for audit trails
- Let Dynorm generate ULIDs
Don't
- Override auto-generated IDs
- Manually modify timestamps
- Add database logic to entities
Complete Example¶
type User struct {
dynorm.Entity `soft_deletes:"true"`
// Indexed fields
Email string `dynorm:"gsi:ByEmail"`
Username string `dynorm:"gsi:ByUsername"`
// Profile
FirstName string
LastName string
Avatar string
// Auth
PasswordHash string
LastLoginAt *time.Time
// Status
Status string // active, suspended, banned
Role string // user, admin, moderator
Verified bool
// Versioning for optimistic locking
Version int `dynorm:"version"`
}
Next Steps¶
- Repository Pattern - Learn about repository operations
- Collection - Work with query results
- Query Builder - Build complex queries