A data object is a special purpose structure that is designed to hold data and track the changes to allow efficient serialization to a data store.
It follows the following principles:
- Has a default, non-argument constructor
- Has a unique identifier (ID) allowing to safely find the object among any other
- All the fields are private, thus non-modifiable from outside
- The fields are only accessed via public setter (mutator) and getter (accessor) methods
- All changes are tracked, and returned on request as a map of type map[string]string
- All data can be returned on request as a map of type map[string]string
Any object can be considered a data object as long as it adheres to the above principles, regardless of its specific implementation.
The implementation in this repo is just one way to implement the above principles. Other variations are possible to suit specific needs.
The concept is a bit similar to a POJO and a Java Bean.
The package provides the following constructor functions:
New()
- Creates a new data object with a generated IDNewFromData(data map[string]string)
- Creates a data object from existing dataNewFromJSON(jsonString string)
- Creates a data object from a JSON stringNewFromGob(gobData []byte)
- Creates a data object from a gob-encoded byte array
The following constructor functions are deprecated and will be removed in a future version:
NewDataObject()
- UseNew()
insteadNewDataObjectFromExistingData(data map[string]string)
- UseNewFromData(data)
insteadNewDataObjectFromJSON(jsonString string)
- UseNewFromJSON(jsonString)
instead
This is a full fledged example of a User data object taken from real life.
The example shows how to create new data object, set and get fields. Add helper methods to work with the fields.
Optional ORM-like relationship methods are also included. Use these with caution as these may create dependencies (i.e. with your services, repos, etc) that you may not want and need.
package models
import (
"github.com/dracory/dataobject"
"github.com/dracory/uid"
"github.com/golang-module/carbon/v2"
)
// User is a data object
type User struct {
dataobject.DataObject
}
// ============================= CONSTRUCTORS =============================
// NewUser instantiates a new user
func NewUser() *User {
o := &User{}
o.SetID(uid.HumanUid())
o.SetStatus("active")
o.SetCreatedAt(carbon.Now(carbon.UTC).ToDateTimeString(carbon.UTC))
o.SetUpdatedAt(carbon.Now(carbon.UTC).ToDateTimeString(carbon.UTC))
return o
}
// NewUserFromData helper method to hydrate an existing user data object
func NewUserFromData(data map[string]string) *User {
o := &User{}
o.Hydrate(data)
return o
}
// ======================== RELATIONS/ORM (OPTIONAL) ===========================
func (o *User) Messages() []Messages {
return NewMessageService.GetUserMessages(o.GetID())
}
func (o *User) Create() error {
return NewUserService.Create(o)
}
func (o *User) Update() error {
if !o.IsDirty() {
return nil // object has not been changed
}
return NewUserService.Update(o)
}
// ================================ METHODS ====================================
func (o *User) IsActive() bool {
return o.Status() == "active"
}
// ============================ GETTERS AND SETTERS ============================
func (o *User) CreatedAt() string {
return o.Get("created_at")
}
func (o *User) CreatedAtCarbon() carbon.Carbon {
return carbon.Parse(o.CreatedAt(), carbon.UTC)
}
func (o *User) SetCreatedAt(createdAt string) *User {
o.Set("created_at", createdAt)
return o
}
func (o *User) FirstName() string {
return o.Get("first_name")
}
func (o *User) SetFirstName(firstName string) *User {
o.Set("first_name", firstName)
return o
}
func (o *User) LastName() string {
return o.Get("last_name")
}
func (o *User) SetLastName(lastName string) *User {
o.Set("last_name", lastName)
return o
}
func (o *User) MiddleNames() string {
return o.Get("middle_names")
}
func (o *User) SetMiddleNames(middleNames string) *User {
o.Set("middle_names", middleNames)
return o
}
func (o *User) Status() string {
return o.Get("status")
}
func (o *User) SetStatus(status string) *User {
o.Set("status", status)
return o
}
func (o *User) UpdatedAt() string {
return o.Get("updated_at")
}
func (o *User) UpdatedAtCarbon() carbon.Carbon {
return carbon.Parse(o.UpdatedAt(), carbon.UTC)
}
func (o *User) SetUpdatedAt(updatedAt string) *User {
o.Set("updated_at", updatedAt)
return o
}
For an object using the above specifications:
// Create new user with autogenerated default ID
user := NewUser()
// Create new user with already existing data (i.e. from database)
user := NewUserFromData(data)
// returns the ID of the object
id := user.ID()
// example setter method
user.SetFirstName("John")
// example getter method
firstName := user.FirstName()
// find if the object has been modified
isDirty := user.IsDirty()
// returns the changed data
dataChanged := user.DataChanged()
// returns all the data
data := user.Data()
Here's how to use the DataObject directly:
// Create a new data object with a generated ID
do := dataobject.New()
// Create a data object from existing data
data := map[string]string{
"id": "unique-id-123",
"name": "Test Object",
"value": "42",
}
do := dataobject.NewFromData(data)
// Create a data object from JSON
jsonStr := `{"id":"json-id-456","name":"JSON Object","active":"true"}`
do, err := dataobject.NewFromJSON(jsonStr)
if err != nil {
// Handle error
}
// Create a data object from gob-encoded data
gobData, _ := existingDataObject.ToGob()
do, err := dataobject.NewFromGob(gobData)
if err != nil {
// Handle error
}
// Get and set values
do.Set("name", "New Name")
name := do.Get("name")
// Check if the object has been modified
if do.IsDirty() {
// Handle changes
changedData := do.DataChanged()
}
// Get all data
allData := do.Data()
DataObject has been compared with various similar patterns from different programming languages and paradigms:
- DataObject vs Java Bean - Comparison with the Java Bean pattern
- DataObject vs Magento DataObject - Comparison with Magento's PHP DataObject
- DataObject vs POJO/POCO - Comparison with Plain Old Java/CLR Objects
- DataObject vs PHP DTO - Comparison with PHP Data Transfer Objects
- DataObject vs Python Dataclasses - Comparison with Python's Dataclasses
- DataObject vs Go Structs - Comparison with standard Go structs
- DataObject vs Elixir Structs - Comparison with Elixir's immutable structs
These comparisons provide insights into the design decisions, trade-offs, and use cases for each approach.
Saving the data is left to the end user, as it is specific for each data store.
Some stores (i.e. relational databases) allow to only change specific fields, which is why the DataChanged() getter method should be used to identify the changed fields, then only save the changes efficiently.
func SaveUserChanges(user User) bool {
if !user.IsDirty() {
return true
}
changedData := user.DataChanged()
return save(changedData)
}
Note! Some stores (i.e. document stores, file stores) save all the fields each time, which is where the Data() getter method should be used
func SaveFullUser(user User) bool {
if !user.IsDirty() {
return true
}
allData := user.Data()
return save(allData)
}
jsonString, err := user.ToJSON()
if err != nil {
log.Fatal("Error serializing")
}
log.Println(jsonString)
user, err := NewFromJSON(jsonString)
if err != nil {
log.Fatal("Error deserializing")
}
user.Get("first_name")
gobData, err := user.ToGob()
if err != nil {
log.Fatal("Error serializing to gob")
}
// Use gobData for efficient Go-to-Go data transfer or storage
user, err := NewFromGob(gobData)
if err != nil {
log.Fatal("Error deserializing from gob")
}
user.Get("first_name")
To install the package, run the following command:
go get github.com/dracory/dataobject