I want to do something like the Expando Model that python supports on app engine.
Sometimes you don't want to declare your properties ahead of time. A special model subclass, Expando, changes the behavior of its entities so that any attribute assigned (as long as it doesn't start with an underscore) is saved to the Datastore.
How can I do this in Go?
Note beforehand:
There are 2 APIs. The one with import path appengine/datastore
uses channels as arguments. The other with import path google.golang.org/appengine/datastore
uses slices. Adjust the example below to your case. See this question for details: How to correctly import Golang appengine?
The key to an entity with dynamic properties is the PropertyLoadSaver
interface. By implementing this interface you can dynamically–at save time–construct the properties of the entity to be saved.
Also to not have to do this yourself the Go AppEngine platform provides a PropertyList
type which is basically a list (a slice) of properties Property
and it also implements PropertyLoadSaver
.
So the Expando model in Go is PropertyList
. Just add the properties you want your entity to have, and save this PropertyList
value.
Here is an example:
c := appengine.NewContext(r)
props := datastore.PropertyList{
datastore.Property{Name: "time", Value: time.Now()},
datastore.Property{Name: "email", Value: "[email protected]"},
}
k := datastore.NewIncompleteKey(c, "DynEntity", nil)
key, err := datastore.Put(c, k, &props)
c.Infof("%v %v", key, err)
This example saves an entity named "DynEntity"
with 2 dynamic properties: "time"
and "email"
.
As the PropertyList
type is a slice, you can also use the builtin append()
function to add properties to it, so you could also initialize props
like this:
var props datastore.PropertyList
props = append(props, datastore.Property{Name:"time", Value: time.Now()})
props = append(props, datastore.Property{Name:"email", Value: "[email protected]"})
map
into a dynamic entityThe PropertyLoadSaver
interface is not complicated, we can implement it ourselves. In the following example I implement it on a custom type which is a simple map
:
type DynEnt map[string]interface{}
func (d *DynEnt) Load(props []datastore.Property) error {
// Note: you might want to clear current values from the map or create a new map
for _, p := range props {
(*d)[p.Name] = p.Value
}
return nil
}
func (d *DynEnt) Save() (props []datastore.Property, err error) {
for k, v := range *d {
props = append(props, datastore.Property{Name: k, Value: v})
}
return
}
Here's how the implementation would look like with the "old" interface that used channels instead of slices:
type DynEnt map[string]interface{}
func (d *DynEnt) Load(ch <-chan datastore.Property) error {
// Note: you might want to clear current values from the map or create a new map
for p := range ch { // Read until channel is closed
(*d)[p.Name] = p.Value
}
return nil
}
func (d *DynEnt) Save(ch chan<- datastore.Property) error {
defer close(ch) // Channel must be closed
for k, v := range *d {
ch <- datastore.Property{Name: k, Value: v}
}
return nil
}
Now we can use our DynEnt
type just like any other maps in Go, and since it implements PropertyLoadSaver
, it can be saved as an entity (and any entities can be loaded into it):
c := appengine.NewContext(r)
d := DynEnt{"email": "[email protected]", "time": time.Now()}
k := datastore.NewIncompleteKey(c, "DynEntity", nil)
key, err := datastore.Put(c, k, &d)
c.Infof("%v %v", key, err)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With