Entity Change Tracking¶
Entity change tracking lets you load an entity, modify specific fields, and then save only the changed fields as a PATCH request. This is the safest and most efficient way to update entities - you never accidentally overwrite fields you did not intend to change.
TrackEntity¶
TrackEntity[T] wraps an entity in a TrackedEntity[T] that records which fields have changed:
var product Product
err := client.From("Products").Key(1).Into(ctx, &product)
if err != nil {
log.Fatal(err)
}
tracked := traverse.TrackEntity(product)
Making Changes¶
Use Set to change a field. Only fields changed through Set are considered dirty:
You can also modify the entity directly and call MarkDirty:
Checking Dirty State¶
fmt.Println(tracked.IsDirty()) // true
fmt.Println(tracked.DirtyFields()) // ["UnitPrice", "UnitsInStock"]
changes := tracked.Changes()
// map[string]any{"UnitPrice": 29.99, "UnitsInStock": 100}
SaveChanges¶
SaveChanges sends a PATCH request containing only the dirty fields:
HTTP exchange:
Fields like ProductName, CategoryID, etc. are not included even though they exist on the struct.
MarshalJSON¶
TrackedEntity[T] implements json.Marshaler and emits only dirty fields:
This makes TrackedEntity[T] safe to pass directly to Update:
Reset and Discard¶
Reset clears the dirty set but keeps the current values (marks as clean):
Discard reverts the entity to its original state (both values and dirty set):
tracked.Set("UnitPrice", 999.99)
tracked.Discard()
p := tracked.Get()
fmt.Println(p.UnitPrice) // original value
fmt.Println(tracked.IsDirty()) // false
Full Example¶
package main
import (
"context"
"fmt"
"log"
"github.com/jhonsferg/traverse"
)
type Product struct {
ID int `json:"ProductID"`
Name string `json:"ProductName"`
UnitPrice float64 `json:"UnitPrice"`
UnitsInStock int `json:"UnitsInStock"`
Discontinued bool `json:"Discontinued"`
}
func main() {
client, _ := traverse.New(traverse.WithBaseURL("https://example.com/odata"))
defer client.Close()
ctx := context.Background()
// Load entity
var product Product
err := client.From("Products").Key(1).Into(ctx, &product)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Loaded: %s at $%.2f\n", product.Name, product.UnitPrice)
// Track and modify
tracked := traverse.TrackEntity(product)
tracked.Set("UnitPrice", 24.99)
tracked.Set("UnitsInStock", tracked.Get().UnitsInStock + 10)
fmt.Println("Dirty fields:", tracked.DirtyFields())
// Dirty fields: [UnitPrice, UnitsInStock]
// Save only changed fields
err = tracked.SaveChanges(ctx, client, "Products", 1)
if err != nil {
log.Fatal(err)
}
fmt.Println("Saved successfully")
// PATCH body: {"UnitPrice": 24.99, "UnitsInStock": <updated value>}
// Entity is still usable after save
fmt.Println(tracked.IsDirty()) // false (reset after save)
}
TrackedEntity[T] API¶
| Method | Description |
|---|---|
Get() *T | Returns a pointer to the tracked entity |
Set(field string, value any) | Sets a field value and marks it dirty |
IsDirty() bool | Returns true if any field has been modified |
DirtyFields() []string | Returns the names of all dirty fields |
Changes() map[string]any | Returns a map of dirty field names to new values |
Reset() | Clears the dirty set, keeping current values |
Discard() | Reverts all changes to the original loaded state |
MarshalJSON() ([]byte, error) | Emits only dirty fields as JSON |
SaveChanges(ctx, client, entitySet, key) | PATCHes only dirty fields |
Related Pages¶
- CRUD Operations - Standard update patterns
- ETag & Concurrency - Combine change tracking with ETags
- Change Tracking Reference - Full API reference