CRUD
Insert a document, read it, change it, delete it, count the rest. It shows if_version, the optimistic lock of ndb.
When to use
Your app owns the data and writes it through ndb (engine mode). Use if_version when two writers can change the same document.
Collection
Declared in examples/ndb.yml:
users:
id: user_id
indexes: [plan]Connect
Each SDK reads NDB_URL and NDB_TOKEN. The Go and TypeScript code declares the collection from its type; Ruby declares it with ensure_collection. The declaration agrees with ndb.yml, and the indexes are united.
db, err := ndb.Open(addr) // NDB_TOKEN is read by the SDK
if err != nil {
return err
}
defer db.Close()
users, err := ndb.Collection[User](ctx, db, "users")
if err != nil {
return err
}ndb = Ndb::Client.new(url: ENV.fetch("NDB_URL", "unix:///tmp/ndb-examples.sock")) # NDB_TOKEN is read by the SDK
ndb.ensure_collection("users", id: "user_id", indexes: ["plan"])Ndb::Rails.configure do |c|
c.url = ENV.fetch("NDB_URL", "unix:///tmp/ndb-examples.sock") # NDB_TOKEN is read by the SDK
endconst ndb = connect(process.env.NDB_URL ?? 'unix:///tmp/ndb-examples.sock') // NDB_TOKEN is read by the SDK
const users = ndb.model('users', {
id: 'user_id',
fields: {
user_id: { type: 'string' },
email: { type: 'string' },
plan: { type: 'string', index: true },
},
})Model (Go and Rails)
Go reads the ndb struct tags. Ndb::Record reads attribute lines. _v is the document version.
type User struct {
UserID string `ndb:"user_id,id"`
Email string `ndb:"email"`
Plan string `ndb:"plan,index"`
Version uint64 `ndb:"_v,version"`
}class User < Ndb::Record
primary_key :user_id
attribute :email, :string
attribute :plan, :string, index: true
validates :email, presence: true
endInsert
One call inserts both users in one transaction. Every new document has version 1.
results, err := users.Insert(ctx,
User{UserID: "u1", Email: "ada@example.com", Plan: "free"},
User{UserID: "u2", Email: "grace@example.com", Plan: "free"},
)
if err != nil {
return err
}
fmt.Printf("insert u1, u2 -> v%d, v%d\n", results[0].V, results[1].V)results = ndb.insert("users", [
{ user_id: "u1", email: "ada@example.com", plan: "free" },
{ user_id: "u2", email: "grace@example.com", plan: "free" }
])
puts "insert u1, u2 -> v#{results[0]['v']}, v#{results[1]['v']}"User.create!([
{ user_id: "u1", email: "ada@example.com", plan: "free" },
{ user_id: "u2", email: "grace@example.com", plan: "free" }
])
puts "insert u1, u2 -> v#{User.find('u1').version}, v#{User.find('u2').version}"const results = await users.insert([
{ user_id: 'u1', email: 'ada@example.com', plan: 'free' },
{ user_id: 'u2', email: 'grace@example.com', plan: 'free' },
])
console.log(`insert u1, u2 -> v${results[0]?.v}, v${results[1]?.v}`)results=$(ndb POST /v1/c/users/insert '{"insert":[
{"user_id":"u1","email":"ada@example.com","plan":"free"},
{"user_id":"u2","email":"grace@example.com","plan":"free"}
]}')
echo "insert u1, u2 -> $(jq -r '"v\(.results[0].v), v\(.results[1].v)"' <<<"$results")"Get by id
Ask for the version (versions: true in the query) so you can send it back.
ada, err := users.Get(ctx, "u1")
if err != nil {
return err
}
fmt.Printf("get u1: %s plan=%s v%d\n", ada.Email, ada.Plan, ada.Version)ada = ndb.find("users", where: { user_id: "u1" }, versions: true).rows.first
puts "get u1: #{ada['email']} plan=#{ada['plan']} v#{ada['_v']}"ada = User.find("u1")
puts "get u1: #{ada.email} plan=#{ada.plan} v#{ada.version}"const ada = await users.find('u1')
console.log(`get u1: ${ada?.email} plan=${ada?.plan} v${ada?._v}`)ada=$(ndb POST /v1/c/users/find '{"where":{"user_id":"u1"},"versions":true}')
version=$(jq -r '._v' <<<"$ada")
echo "get u1: $(jq -r '"\(.email) plan=\(.plan) v\(._v)"' <<<"$ada")"Update with if_version
The write succeeds only if the stored version is the one you read.
v, err := users.Update(ctx, "u1", map[string]any{"plan": "pro"}, ndb.IfVersion(ada.Version))
if err != nil {
return err
}
fmt.Printf("update u1 plan=pro (if_version %d) -> v%d\n", ada.Version, v)saved = ndb.update("users", "u1", set: { plan: "pro" }, if_version: ada["_v"])
puts "update u1 plan=pro (if_version #{ada['_v']}) -> v#{saved['v']}"copy = User.find("u1")
ada.update!(plan: "pro")
puts "update u1 plan=pro (if_version 1) -> v#{ada.version}"const saved = await users.update('u1', { plan: 'pro' }, { ifVersion: ada?._v })
console.log(`update u1 plan=pro (if_version ${ada?._v}) -> v${saved.v}`)saved=$(ndb POST /v1/c/users/update "{\"update\":{\"id\":\"u1\",\"set\":{\"plan\":\"pro\"},\"if_version\":$version}}")
echo "update u1 plan=pro (if_version $version) -> v$(jq -r '.v' <<<"$saved")"Handle a version conflict
A second writer that still holds version 1 gets version_conflict (HTTP 409). Reload and try again. In Rails the error is Ndb::StaleObjectError, as in ActiveRecord.
_, err = users.Update(ctx, "u1", map[string]any{"plan": "team"}, ndb.IfVersion(ada.Version))
if errors.Is(err, ndb.ErrVersionConflict) {
fmt.Println("update u1 plan=team (if_version 1) -> version_conflict")
fresh, err := users.Get(ctx, "u1")
if err != nil {
return err
}
v, err = users.Update(ctx, "u1", map[string]any{"plan": "team"}, ndb.IfVersion(fresh.Version))
if err != nil {
return err
}
fmt.Printf("reload u1: v%d, retry with if_version %d -> v%d\n", fresh.Version, fresh.Version, v)
} else if err != nil {
return err
}begin
ndb.update("users", "u1", set: { plan: "team" }, if_version: ada["_v"])
rescue Ndb::VersionConflict
puts "update u1 plan=team (if_version #{ada['_v']}) -> version_conflict"
fresh = ndb.find("users", where: { user_id: "u1" }, versions: true).rows.first
saved = ndb.update("users", "u1", set: { plan: "team" }, if_version: fresh["_v"])
puts "reload u1: v#{fresh['_v']}, retry with if_version #{fresh['_v']} -> v#{saved['v']}"
endbegin
copy.update!(plan: "team")
rescue Ndb::StaleObjectError
puts "update u1 plan=team (if_version 1) -> version_conflict"
fresh = User.find("u1")
stale_version = fresh.version
fresh.update!(plan: "team")
puts "reload u1: v#{stale_version}, retry with if_version #{stale_version} -> v#{fresh.version}"
endtry {
await users.update('u1', { plan: 'team' }, { ifVersion: ada?._v })
} catch (err) {
if (!(err instanceof VersionConflictError)) {
throw err
}
console.log(`update u1 plan=team (if_version ${ada?._v}) -> version_conflict`)
const fresh = await users.find('u1')
const retried = await users.update('u1', { plan: 'team' }, { ifVersion: fresh?._v })
console.log(`reload u1: v${fresh?._v}, retry with if_version ${fresh?._v} -> v${retried.v}`)
}status=$(ndb_status POST /v1/c/users/update "{\"update\":{\"id\":\"u1\",\"set\":{\"plan\":\"team\"},\"if_version\":$version}}")
if [[ "$status" == "409" ]]; then
echo "update u1 plan=team (if_version $version) -> $(jq -r '.error.code' "$NDB_BODY")"
fresh=$(ndb POST /v1/c/users/find '{"where":{"user_id":"u1"},"versions":true}' | jq -r '._v')
retried=$(ndb POST /v1/c/users/update "{\"update\":{\"id\":\"u1\",\"set\":{\"plan\":\"team\"},\"if_version\":$fresh}}")
echo "reload u1: v$fresh, retry with if_version $fresh -> v$(jq -r '.v' <<<"$retried")"
fiDelete
Deleting a missing id is not_found. The programs ignore it at the start, so a second run works.
if err := users.Delete(ctx, "u2"); err != nil {
return err
}
fmt.Println("delete u2")ndb.delete("users", "u2")
puts "delete u2"User.find("u2").destroy
puts "delete u2"await users.delete('u2')
console.log('delete u2')ndb POST /v1/c/users/delete '{"delete":{"id":"u2"}}' >/dev/null
echo "delete u2"Count
count is the total of the filter, whatever the page size.
total, err := users.Where().Count(ctx)
if err != nil {
return err
}
onTeam, err := users.Where(ndb.Eq("plan", "team")).Count(ctx)
if err != nil {
return err
}
fmt.Printf("count: %d user(s), %d on plan team\n", total, onTeam)total = ndb.find("users", count: true, limit: 1).total
on_team = ndb.find("users", where: { plan: "team" }, count: true, limit: 1).total
puts "count: #{total} user(s), #{on_team} on plan team"puts "count: #{User.count} user(s), #{User.where(plan: 'team').count} on plan team"const total = await users.count()
const onTeam = await users.where({ plan: 'team' }).count()
console.log(`count: ${total} user(s), ${onTeam} on plan team`)total=$(ndb_header X-Ndb-Total POST /v1/c/users/find '{"count":true,"limit":1}')
on_team=$(ndb_header X-Ndb-Total POST /v1/c/users/find '{"where":{"plan":"team"},"count":true,"limit":1}')
echo "count: $total user(s), $on_team on plan team"Output
The four programs print the same text.
insert u1, u2 -> v1, v1
get u1: ada@example.com plan=free v1
update u1 plan=pro (if_version 1) -> v2
update u1 plan=team (if_version 1) -> version_conflict
reload u1: v2, retry with if_version 2 -> v3
delete u2
count: 1 user(s), 1 on plan teamNotes
- The Rails tab runs with
ndb-railsas a plain Ruby script. In a Rails app,config/ndb.ymldoes the connect step. See Clients. - Every SDK supports every step of this example.
- Errors and codes: Query errors.
Run it
cd examples
make run-crud LANG=go # go, ruby, ts or curl
make run-crud # every languageSource: examples/crud. The programs read NDB_URL and NDB_TOKEN, and they can run again and again.