blob: 6e30832235c58fea1121410bd7140287d2065283 [file] [log] [blame]
/*
Package goon provides an autocaching interface to the app engine datastore
similar to the python NDB package.
Goon differs from the datastore package in various ways: it remembers the
appengine Context, which need only be specified once at creation time; kinds
need not be specified as they are computed, by default, from a type's name;
keys are inferred from specially-tagged fields on types, removing the need to
pass key objects around.
In general, the difference is that Goon's API is identical to the datastore API,
it's just shorter.
Keys in Goon are stored in the structs themselves. Below is an example struct
with a field to specify the id (see the Key Specifications section below for
full documentation).
type User struct {
Id string `datastore:"-" goon:"id"`
Name string
}
Thus, to get a User with id 2:
userid := 2
g := goon.NewGoon(r)
u := &User{Id: userid}
g.Get(u)
Key Specifications
For both the Key and KeyError functions, src must be a S or *S for some
struct type S. The key is extracted based on various fields of S. If a field
of type int64 or string has a struct tag named goon with value "id", it is
used as the key's id. If a field of type *datastore.Key has a struct tag
named goon with value "parent", it is used as the key's parent. If a field
of type string has a struct tag named goon with value "kind", it is used
as the key's kind. The "kind" field supports an optional second parameter
which is the default kind name. If no kind field exists, the struct's name
is used. These fields should all have their datastore field marked as "-".
Example, with kind User:
type User struct {
Id string `datastore:"-" goon:"id"`
Read time.Time
}
Example, with kind U if _kind is the empty string:
type User struct {
_kind string `goon:"kind,U"`
Id string `datastore:"-" goon:"id"`
Read time.Time
}
To override kind of a single entity to UserKind:
u := User{_kind: "UserKind"}
An example with both parent and kind:
type UserData struct {
Id string `datastore:"-" goon:"id"`
_kind string `goon:"kind,UD"`
Parent *datastore.Key `datastore:"-" goon:"parent"`
Data []byte
}
Features
Datastore interaction with: Get, GetMulti, Put, PutMulti, Delete, DeleteMulti, Queries.
All key-based operations backed by memory and memcache.
Per-request, in-memory cache: fetch the same key twice, the second request is served from local memory.
Intelligent multi support: running GetMulti correctly fetches from memory, then memcache, then the datastore; each tier only sends keys off to the next one if they were missing.
Memcache control variance: long memcache requests are cancelled.
Transactions use a separate context, but locally cache any results on success.
Automatic kind naming: struct names are inferred by reflection, removing the need to manually specify key kinds.
Simpler API than appengine/datastore.
API comparison between goon and datastore
put with incomplete key
datastore:
type Group struct {
Name string
}
c := appengine.NewContext(r)
g := &Group{Name: "name"}
k := datastore.NewIncompleteKey(c, "Group", nil)
err := datastore.Put(c, k, g)
goon:
type Group struct {
Id int64 `datastore:"-" goon:"id"`
Name string
}
n := goon.NewGoon(r)
g := &Group{Name: "name"}
err := n.Put(g)
get with known key
datastore:
type Group struct {
Name string
}
c := appengine.NewContext(r)
g := &Group{}
k := datastore.NewKey(c, "Group", "", 1, nil)
err := datastore.Get(c, k, g)
goon:
type Group struct {
Id int64 `datastore:"-" goon:"id"`
Name string
}
n := goon.NewGoon(r)
g := &Group{Id: 1}
err := n.Get(g)
Memcache Control Variance
Memcache is generally fast. When it is slow, goon will timeout the memcache
requests and proceed to use the datastore directly. The memcache put and
get timeout variables determine how long to wait for various kinds of
requests. The default settings were determined experimentally and should
provide reasonable defaults for most applications.
See: http://talks.golang.org/2013/highperf.slide#23
PropertyLoadSaver support
Structs that implement the PropertyLoadSaver interface are guaranteed to call
the Save() method once and only once per Put/PutMulti call and never elsewhere.
Similarly the Load() method is guaranteed to be called once and only once per
Get/GetMulti/GetAll/Next call and never elsewhere.
Keep in mind that the goon local cache is just a pointer to the previous result.
This means that when you use Get to fetch something into a PropertyLoadSaver
implementing struct, that struct's pointer is saved. Subsequent calls to Get
will just return that pointer. This means that although Load() was called once
during the initial Get, the subsequent calls to Get won't call Load() again.
Generally this shouldn't be an issue, but e.g. if you generate some random
data in the Load() call then sequential calls to Get that share the same goon
local cache will always return the random data of the first call.
A gotcha can be encountered with the local cache where Load() is never called.
Specifically if you first call Get to load some entity into a struct S which
does *not* implement PropertyLoadSaver. The local cache will now have a pointer
to this struct S. When you now proceed to call Get again on the same id but into
a struct PLS which implements PropertyLoadSaver but is also convertible from S.
Then your struct PLS will be applied the value of S, however Load() will never
be called, because it wasn't the first time and it's never called when loading
from the local cache. This is a very specific edge case that won't affect 99.9%
of developers using goon. This issue does not exist with memcache/datastore,
so either flushing the local cache or doing the S->PLS migration in different
requests will solve the issue.
Local memory cache
Entities retrieved via Get/GetMulti/GetAll/Next are stored in the local cache.
The local cache is an in-memory per-goon-instance cache that keeps a pointer
to the returned result. When attempting to retrieve the same entity again you
will be returned a shallow copy of the pointer's value.
Imagine a struct type Foo:
type Foo struct {
Id int64 `datastore:"-" goon:"id"`
X int
Y []byte
}
Then behavior is as follows:
fA := &Foo{Id: 1} // fA.X == 0 | fA.Y == []
g.Get(fA) // fA.X == 1 | fA.Y == [1, 2] Initial datastore values
fA.X = 2 // fA.X == 2 | fA.Y == [1, 2] We change the int
fA.Y = []byte{3, 4} // fA.X == 2 | fA.Y == [3, 4] ..and create a new slice
fB := &Foo{Id: 1} // fB.X == 0 | fB.Y == []
g.Get(fB) // fB.X == 2 | fB.Y == [3, 4] fB is now a copy of fA
fB.X = 3 // fB.X == 3 | fB.Y == [3, 4] Changing fB.X is local
fB.Y[0] = 5 // fB.X == 3 | fB.Y == [5, 4] ..but changing deep data
// fA.X == 2 | fA.Y == [5, 4] will also appear in fA.Y
fA.Y = []byte{6, 7} // fA.X == 2 | fA.Y == [6, 7] However creating a new
// fB.X == 3 | fB.Y == [5, 4] slice will not propagate
The reason the cache works with pointers is because App Engine is very memory
limited and storing just pointers takes up very little memory. However this
means that any changes you perform on an entity that was cached will also be
reflected in any future gets for that same entity with the same goon instance.
What's more, the copy will be shallow. So any value-types likes primitive ints
can be then changed and will only be changed for that specific struct instance.
However deep modifications of slices will propagate to all copies of the struct.
Thus for the most care-free life you should treat fetched entities as immutable,
which means you never modify their data. That way the whole pointer/copy
strategy won't have any effect on your app logic. For modifications, always do
them inside a transaction because local cache is disabled there.
This means that the local cache is **not** thread-safe for modifications. Make
sure you either don't modify entities fetched outside of transactions or that
you never fetch the same id from different goroutines using one goon instance.
*/
package goon