gRPC - connect - Render でwebサービスを作ってみる:サーバサイドCRUD実装
はじめに
gRPCとRemixを使ってサービスを公開したいと考えています。以下でサーバサイドでSQLiteを使うことができました。今回はSQLiteのテーブルの実装をしてみたいと思います。
以下のコードをベースにしています。
本記事の作業後のコードは以下です
参照情報
レコードの追加
まずはレコードを追加してみます。
backend/example_test.go
を編集します。
+ // レコード追加
+ task1, err := client.Todo.Create().Save(ctx)
+ if err != nil {
+ log.Fatalf("failed creating a todo: %v", err)
+ }
fmt.Println(task1)
// Output:
+ // Todo(id=1)
テストは成功です!
$ go test -v
=== RUN ExampleTodo
--- PASS: ExampleTodo (0.00s)
PASS
ok example 0.385s
フィールドの追加
現在のTODOはID
フィールドしかありません。より良い例とするためにフィールドを追加します。5つのカラム id
, text
, created_at
, status
, priority
をテーブルに追加し、レコードを書き込んでみます。編集するファイルはbackend/ent/schema/todo.go
です。Fields
関数を変更します。
field
, time
をインポートします。
-import "entgo.io/ent"
+import (
+ "entgo.io/ent"
+ "entgo.io/ent/schema/field"
+ "time"
+)
Field
関数を以下のように変更します。
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.Text("text").NotEmpty(),
field.Time("created_at").Default(time.Now).Immutable(),
field.Enum("status").NamedValues(
"InProgress", "IN_PROGRESS",
"Completed", "COMPLETED",
).Default("IN_PROGRESS"),
field.Int("priority").Default(0),
}
}
コード生成します。コードは./backend
で実行します。
go generate ./ent
backend/ent/todo.go
を見るとフィールドが追加されています。
今回text
以外はDefault
値が設定されていますが、text
は設定されていないので、example_tet.go
で指定する必要があります。
変更前
task1, err := client.Todo.Create().Save(ctx)
if err != nil {
log.Fatalf("failed creating a todo: %v", err)
}
fmt.Println(task1)
// Output:
// Todo(id=1)
変更後
task1, err := client.Todo.Create().SetText("Add GraphQL Example").Save(ctx)
if err != nil {
log.Fatalf("failed creating a todo: %v", err)
}
fmt.Printf("%d: %q\n", task1.ID, task1.Text)
task2, err := client.Todo.Create().SetText("Add Tracing Example").Save(ctx)
if err != nil {
log.Fatalf("failed creating a todo: %v", err)
}
fmt.Printf("%d: %q\n", task2.ID, task2.Text)
// Output:
// 1: "Add GraphQL Example"
// 2: "Add Tracing Example"
go test -v
が成功すればOKです!
スキーマにエッジを追加する
ent
では テーブル間のリレーションシップを edge
と表現するそうです。エッジとはテーブル間の関係性を表現するもので、親子関係や関連付けを定義できます。エッジを定義してTODOに親子関係をもたせることができるようにしてみます。エッジはparent
, children
で定義します。backend/ent/schema/todo.go
を変更します。
edge
をインポートします。
+ "entgo.io/ent/schema/edge"
Edges
関数を以下のように変更します。
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("parent", Todo.Type). // Todoを親とするエッジを追加
Unique(). // 親は単一
From("children"), //自分自身(今回はTodo)を子として引けるようにする
}
}
children
は必須ではないようですが、追加することで子を引くための関数が生成されるようです。
2つのTodoをつなげる
以下の接続レコードを生成します。
func ExampleTodo() {
// ...
if err := task2.Update().SetParent(task1).Exec(ctx); err != nil {
log.Fatalf("failed connecting todo2 to its parent: %v", err)
}
// Output:
// 1: "Add GraphQL Example"
// 2: "Add Tracing Example"
}
Todoをクエリする
ここまで追加してきたTodoを取得してみます。
全てのTODOを取得する
以下コードを追加します。
// すべてのtodoアイテムを取得する
items, err := client.Todo.Query().All(ctx)
if err != nil {
log.Fatalf("failed querying todos: %v", err)
}
for _, t := range items {
fmt.Printf("%d: %q\n", t.ID, t.Text)
}
結果評価にも以下を追加することを忘れないように。
// 1: "Add GraphQL Example"
// 2: "Add Tracing Example"
他のTODOを親に持つTODOを取得する
todo
に以下を追加します。todo.HasParent()
で必要になります。
+ "example/ent/todo"
以下コードを追加します。
items, err = client.Todo.Query().Where(todo.HasParent()).All(ctx)
if err != nil {
log.Fatalf("failed querying todos: %v", err)
}
for _, t := range items {
fmt.Printf("%d: %q\n", t.ID, t.Text)
}
評価結果に以下を追加します。
// 2: "Add Tracing Example"
他のTODOを親日持たず、かつ、他のTODOを子に持つTODOを取得する
以下コードを追加します。
items, err = client.Todo.Query().
Where(
todo.Not(
todo.HasParent(),
),
todo.HasChildren(),
).
All(ctx)
if err != nil {
log.Fatalf("failed querying todos: %v", err)
}
for _, t := range items {
fmt.Printf("%d: %q\n", t.ID, t.Text)
}
評価結果に以下を追加します。
// 1: "Add GraphQL Example"
子TODOを経由して親TODOを取得する
以下コードを追加します。
// 子TODOを通じて親TODOを取得し、
// クエリが正確に1つのTODOを返すことを期待します。
parent, err_parent := client.Todo.Query(). // すべてのtodoアイテムを取得する
Where(todo.HasParent()). // 親todoアイテムを持つtodoアイテムのみにフィルタリング
QueryParent(). // 親todoアイテムについて走査を続ける
Only(ctx) // 1つのtodoアイテムのみ取得する
if err != nil {
log.Fatalf("failed querying todos: %v", err_parent)
}
fmt.Printf("%d: %q\n", parent.ID, parent.Text)
評価結果に以下を追加します。
// 1: "Add GraphQL Example"
以上で参照情報で紹介されているサンプルは終わりです!
最後に、cmd/main.go
からクエリを読んでみたいと思います。テンプレートのコードから使えないと、使い方の体感ができませんので。
cmd/main.goからデータ読み書きしてみる
実際のコードは、本記事冒頭に記載の「本記事の作業後のコード」を参照してください。
最初にSQLiteをdockerでビルドするためにDockerfileの変更が必要です。以下をDockerfileのbuildステージのイメージに追記します。
RUN apk update && apk add --no-cache gcc musl-dev
RUN export CGO_ENABLED=1
cmd/main.go
を以下のように変更します。
- import追加
"example/ent"
"entgo.io/ent/dialect"
_ "github.com/mattn/go-sqlite3"
- 関数の追加
関数が呼ばれるたびにクライアントが作られる雑な実装になっています。
func (s *GreetServer) SaveRequest(
ctx context.Context,
name string,
) (string, error) {
// ファイルベースのSQLiteデータベースを持つent.Clientを作成します。
client, err := ent.Open(dialect.SQLite, "file:ent.db?_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
// 自動マイグレーションツールを実行して、すべてのスキーマリソースを作成します。
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// レコード追加
_, err = client.Todo.Create().SetText(fmt.Sprintf("Hello from %s", name)).Save(ctx)
if err != nil {
log.Fatalf("failed creating a todo: %v", err)
}
// 全てのTODOを取得する
items, err := client.Todo.Query().All(ctx)
if err != nil {
log.Fatalf("failed querying todos: %v", err)
}
// 全てのTODOを接続して返す
var combinedText string
for _, t := range items {
combinedText += fmt.Sprintf("%d: %q\n", t.ID, t.Text)
}
return combinedText, nil
}
- Greet関数に
SaveRequest
呼び出しを追加します。
// リクエストを保存して結合した結果を返す
all_request, err := s.SaveRequest(ctx, req.Msg.Name)
if err != nil {
log.Fatalf("failed SavedRequest: %v", err)
}
res := connect.NewResponse(&greetv1.GreetResponse{
Greeting: all_request,
})
以上の変更後環境を作り直します。プロジェクトルートで以下を実行。
npm run all
リクエスの履歴が返るようになりました!
まとめ
以上です!
前回のSQLiteの導入と合わせて、今回でテンプレートからSQLiteを使うことができるようになりました。これでサービスのステータス管理ができそうです 👍
Discussion