🗂

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 を以下のように変更します。

  1. import追加
    "example/ent"
    "entgo.io/ent/dialect"
    _ "github.com/mattn/go-sqlite3"
  1. 関数の追加
    関数が呼ばれるたびにクライアントが作られる雑な実装になっています。
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
}
  1. 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