ORM for Cloud Spanner to boost your productivity π
package main
import (
"cloud.google.com/go/spanner"
"context"
"github.com/kanjih/go-spnr"
)
type Singer struct {
// spnr supports 2 types of tags.
// - spanner: spanner column name
// - pk: primary key order
SingerID string `spanner:"SingerId" pk:"1"`
Name string `spanner:"Name"`
}
func main() {
ctx := context.Background()
client, _ := spanner.NewClient(ctx, "projects/{project_id}/instances/{instance_id}/databases/{database_id}")
// initialize
singerStore := spnr.New("Singers") // specify table name
// save record (spnr supports both Mutation API & DML!)
singerStore.ApplyInsertOrUpdate(ctx, client, &Singer{SingerID: "a", Name: "Alice"})
// fetch record
var singer Singer
singerStore.Reader(ctx, client.Single()).FindOne(spanner.Key{"a"}, &singer)
// fetch record using raw query
var singers []Singer
query := "select * from Singers where SingerId=@singerId"
params := map[string]any{"singerId": "a"}
singerStore.Reader(ctx, client.Single()).Query(query, params, &singers)
}
- Supports both Mutation API & DML
- Supports code generation to map records
- Supports raw SQLs for complicated cases
spnr is designed ...
- πββοΈ for reducing boliderplate codes (i.e. mapping selected records to struct or write simple insert/update/delete operations)
- π ββοΈ not for hiding queries executed in background (spnr doesn't support abstractions for complicated operations)
go get github.com/kanjih/go-spnr/v2
* v2 requires go 1.18 or later. If you use previous go versions please use v1.
spnr provides the following types of read operations πͺ
- Select records using primary keys
- Select one column using primary keys
- Select records using query
- Select one value using query
var singer Singer
singerStore.Reader(ctx, tx).FindOne(spanner.Key{"a"}, &singer)
var singers []Singer
keys := spanner.KeySetFromKeys(spanner.Key{"a"}, spanner.Key{"b"})
singerStore.Reader(ctx, tx).FindAll(keys, &singers)
tx
is the transaction object. You can get it by calling spanner.Client.ReadOnly(ReadWrite)Transaction
, or spanner.Client.Single
method.
var name string
singerStore.Reader(ctx, tx).GetColumn(spanner.Key{"a"}, "Name", &name)
var names []string
keys := spanner.KeySetFromKeys(spanner.Key{"a"}, spanner.Key{"b"})
singerStore.Reader(ctx, tx).GetColumnAll(keys, "Name", &names)
Making temporal struct to map columns is the best solution.
type cols struct {
Name string `spanner:"Name"`
Score spanner.NullInt64 `spanner:"Score"`
}
var res cols
singerStore.Reader(ctx, tx).FindOne(spanner.Key{"1"}, &res)
var singer Singer
query := "select * from `Singers` where SingerId=@singerId"
params := map[string]any{"singerId": "a"}
singerStore.Reader(ctx, tx).QueryOne(query, params, &singer)
var singers []Singer
query = "select * from Singers"
singerStore.Reader(ctx, tx).Query(query, nil, &singers)
var cnt int64
query := "select count(*) as cnt from Singers"
singerStore.Reader(ctx, tx).QueryValue(query, nil, &cnt)
FindOne
,GetColumn
method usesReadRow
method ofspanner.ReadWrite(ReadOnly)Transaction
.FindAll
,GetColumnAll
method usesRead
method.QueryOne
,Query
Method usesQuery
method.
Executing mutation API using spnr is badly simple! Here's the example π
singer := &Singer{SingerID: "a", Name: "Alice"}
singers := []Singer{{SingerID: "b", Name: "Bob"}, {SingerID: "c", Name: "Carol"}}
singerStore := spnr.New("Singers") // specify table name
singerStore.InsertOrUpdate(tx, singer) // Insert or update
singerStore.InsertOrUpdate(tx, &singers) // Insert or update multiple records
singerStore.Update(tx, singer) // Update
singerStore.Update(tx, &singers) // Update multple records
singerStore.Delete(tx, singer) // Delete
singerStore.Delete(tx, &singers) // Delete multiple records
Don't want to use in transaction? You can use ApplyXXX
.
singerStore.ApplyInsertOrUpdate(ctx, client, singer) // client is spanner.Dataclient
singerStore.ApplyDelete(ctx, client, &singers)
spnr parses struct then build DML πͺ
singer := &Singer{SingerID: "a", Name: "Alice"}
singers := []Singer{{SingerID: "b", Name: "Bob"}, {SingerID: "c", Name: "Carol"}}
singerStore := spnr.NewDML("Singers") // specify table name
singerStore.Insert(ctx, tx, singer)
// -> INSERT INTO `Singers` (`SingerId`, `Name`) VALUES (@SingerId, @Name)
singerStore.Insert(ctx, tx, &singers)
// -> INSERT INTO `Singers` (`SingerId`, `Name`) VALUES (@SingerId_0, @Name_0), (@SingerId_1, @Name_1)
singerStore.Update(ctx, tx, singer)
// -> UPDATE `Singers` SET `Name`=@Name WHERE `SingerId`=@w_SingerId
singerStore.Update(ctx, tx, &singers)
// -> UPDATE `Singers` SET `Name`=@Name WHERE `SingerId`=@w_SingerId
// -> UPDATE `Singers` SET `Name`=@Name WHERE `SingerId`=@w_SingerId
singerStore.Delete(ctx, tx, singer)
// -> DELETE FROM `Singers` WHERE `SingerId`=@w_SingerId
You don't need spnr in this case! Plain spanner SDK is enough.
sql := "UPDATE `Singers` SET `Name` = xx WHERE `Id` = @Id"
params := map[string]any
spannerClient.Update(tx, spanner.Statement{SQL: sql, Params: params})
spnr is also designed to use with embedding.
You can make structs to manipulate records for each table & can add any methods you want.
type SingerStore struct {
spnr.DML // use spnr.Mutation for mutation API
}
func NewSingerStore() *SingerStore {
return &SingerStore{DML: *spnr.NewDML("Singers")}
}
// Any methods you want to add
func (s *SingerStore) GetCount(ctx context.Context, tx spnr.Transaction, cnt any) error {
query := "select count(*) as cnt from Singers"
return s.Reader(ctx, tx).Query(query, nil, &cnt)
}
func useSingerStore(ctx context.Context, client *spanner.Client) {
singerStore := NewSingerStore()
client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {
// You can use all operations that spnr.DML has
singerStore.Insert(ctx, tx, &Singer{SingerID: "a", Name: "Alice"})
var singer Singer
singerStore.Reader(ctx, tx).FindOne(spanner.Key{"a"}, &singer)
// And you can use the methods you added !!
var cnt int
singerStore.GetCount(ctx, tx, &cnt)
return nil
})
}
Tired to write struct code to map records for every table?
Don't worry! spnr provides code generation π
go install github.com/kanjih/go-spnr/cmd/spnr@latest
spnr build -p {PROJECT_ID} -i {INSTANCE_ID} -d {DATABASE_ID} -n {PACKAGE_NAME} -o {OUTPUT_DIR}
spnr provides some helper functions to reduce boilerplates.
NewNullXXX
spanner.NullString{StringVal: "a", Valid: true}
can bespnr.NewNullString("a")
ToKeySets
- You can convert slice to keysets using
spnr.ToKeySets([]string{"a", "b"})
- You can convert slice to keysets using
Love reporting issues!