-
Notifications
You must be signed in to change notification settings - Fork 2
Getting Started
This guide is designed to get you up and running as quickly as possible and to serve as a functional example of how to use work units.
To kick things off, let's assume we have an entity in our code that we want to save somewhere:
type Starship struct {
ID int
Name string
}
rc := Starship{ID: 1, Name: "Razorcrest"}
The first thing we need to tackle is specifying a couple of options when creating our work unit! Options (work.UnitOption
) allow you to pass configuration to the work unit prior to creating it. Let's go ahead and create some now:
t := work.TypeNameOf(rc)
options := []work.UnitOption {
work.UnitInsertFunc(t, func(ctx context.Context, mCtx work.MapperContext, entities ...interface{}) error {
fmt.Println("Inserting %v", entities)
return nil
}),
work.UnitUpdateFunc(t, func(ctx context.Context, mCtx work.MapperContext, entities ...interface{}) error {
fmt.Println("Updating %v", entities)
return nil
}),
work.UnitDeleteFunc(t, func(ctx context.Context, mCtx work.MapperContext, entities ...interface{}) error {
fmt.Println("Deleting %v", entities)
return nil
}),
}
There are three options we have specified here:
work.UnitInsertFunc
work.UnitUpdateFunc
work.UnitDeleteFunc
Each of these options that we specified takes a type name (work.TypeName
) and a function (work.UnitDataMapperFunc
). The type name is a string communicating the Golang type of the entity; in this case "main.Starship"
. The functions specified in the options will insert, update, and delete the Starship
entities into a data store.
Finally, we create a new work unit by passing these options to the constructor function, like so:
u := work.NewUnit(options...)
🎉 That's it! You've created your first work unit.
As you've noticed, our mapper functions aren't actually storing our entities anywhere; they are only printing to the console. Before we move on, let's create a work unit that manages entities for an underlying map
. Here are some quick changes (with a few helper functions) to get us there:
// data store.
store := make(map[int]Starship{})
// helper functions.
put := func(entity interface{}) {
if s, ok := entity.(Starship); ok {
store[s.ID] = s
}
}
del := func(entity interface{}) {
if s, ok := entity.(Starship); ok {
delete(store, s.ID)
}
}
putMapperFunc := func(ctx context.Context, mCtx unit.MapperContext, entities ...interface{}) (err error) {
for _, e := range entities {
put(e)
}
return
}
delMapperFunc := func(ctx context.Context, mCtx unit.MapperContext, entities ...interface{}) (err error) {
for _, e := range entities {
del(e)
}
return
}
// unit options.
options := []unit.Option{
unit.InsertFunc(t, putMapperFunc),
unit.UpdateFunc(t, putMapperFunc),
unit.DeleteFunc(t, delMapperFunc),
}
u := work.NewUnit(options...)
Now we are rolling! Time to do some work!
NOTE: This example only covered a small set of the options available. Head on over here for a more in-depth look at the various options supported.
So we have our work unit, but what's next?
Essentially the work unit manages all of the operations you make to entities that you wish to store. For example, assume we have somewhere in our application that changes the name of the starship:
func changeName(ctx context.Context, u work.Unit, s Starship, newName string) error {
s.Name = newName
return u.Alter(ctx, s)
}
Notice the call to Alter
. We change the name of the starship to newName
, and then we tell the work unit about this operation, which in the case, was an alteration to an existing entity.
Work unit support several additional operations, including additions and removals, which are used to track when new entities are added or existing entities are removed (respectively). There is one additional operation, known as registration, that is used to kick things off and track the initial state of an entity prior to doing anything with it.
💡 One key concept to understand is that our
changeName
function has not yet stored the changes tos
, it has only tracked them. This is one of the main benefits of this package. Tracking operations on entities separately from saving them to an underlying store lets your code accumulate an arbitrary number of operations without being bogged down by constantly interacting with the data store. There are several other benefits as well, but we'll talk about those another time!
Now the fun part! After we have done all of the hard work of figuring out what entities to add, modify, and remove, it's time to persist the result of these operations.
This couldn't get simpler... wait for it ... 🥁
ctx := ctx.Background()
err := u.Save(ctx)
Yep, that's it. Saving your work unit is as simple as calling Save
.
ℹ️ A context (
context.Context
) is required as an argument to facilitate complex operations such as handling timeouts with underlying databases or APIs. In this example we callcontext.Background()
because we don't have an existing context to use. However, in most real-world applications, there is already a context present which would be passed toSave
.
Okay, sometimes it's not so simple. Sometimes something goes wrong during saves. Maybe your database goes down or is unresponsive. Maybe the API of the downstream system you using to store your entities returns an error.
Either way, we've got you covered. Work units automatically take care of rolling back the operations you were tracking if something goes wrong along the way. Additionally, work units by default retry failed saves 3
times before giving up, a behavior that can be configured using options.
What does "giving up" look like? Essentially Save
will return a non-nil value for the error
it returns:
ctx := ctx.Background()
if err := u.Save(ctx); err != nil {
// handle the error here!
}