Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Schema as Codeで実現する最速のスキーマ駆動開発

Avatar for 土田雄輝 土田雄輝
December 08, 2022

Schema as Codeで実現する最速のスキーマ駆動開発

Avatar for 土田雄輝

土田雄輝

December 08, 2022
Tweet

Other Decks in Programming

Transcript

  1. yyyy.mm.dd  |  Event name here  |    |  © PLAID Inc. 2 自己紹介 土田 雄輝

    Twitter : @_hokekyo1210 • 配信基盤や認証認可基盤の開発 • Go, GCP, サーバーレス yyyy.mm.dd  |  Event name here  | 
  2. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 5 Why スキーマ駆動 スキーマ駆動をなぜやるのか

    • クライアントとサーバーを非同期に開発できる • フロントとバックエンドの開発者間のコミュニケーションコスト削減 • API定義文書からライブラリの生成やサーバーのコードを生成できるので工 数とヒューマンエラーも減らせる
  3. yyyy.mm.dd  |  Event name here  |    |  © PLAID Inc. OpenAPI 6 • REST

    APIに対応したOpenAPI 👉 • エコシステムが充実 • 学習コストが低い • https://gift-tech.co.jp/articles/round-up- openapi-tools/ openapi: 3.0.0 paths: /users/{userId}: get: summary: Returns a user by ID. parameters: - in: path name: userId required: true type: integer responses: 200: schema: type: object properties: id: type: integer name: type: string
  4. yyyy.mm.dd  |  Event name here  |    |  © PLAID Inc. OpenAPI 7 • REST

    APIに対応したOpenAPI 👉 • エコシステムが充実 • 学習コストが低い • https://gift-tech.co.jp/articles/round-up- openapi-tools/ openapi: 3.0.0 paths: /users/{userId}: get: summary: Returns a user by ID. parameters: - in: path name: userId required: true type: integer responses: 200: schema: type: object properties: id: type: integer name: type: string path定義 req param定義 response 定義 method 定義
  5. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 8 OpenAPI REST API

    + OpenAPIのよくある開発フロー 1. DBの設計をする 2. API定義をOpenAPIにしたためる 3. API定義の合意を得る (PRレビュー) 4. 非同期に開発を進める a. フロントエンド b. バックエンド
  6. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 12 OpenAPI よくある問題 •

    yaml/jsonがそもそも人間に優しくない ◦ 書くのもレビューするのも大変 ◦ 最近は「stoplight」などリッチなeditorもあるけど・・・ • API定義がDBに強烈に依存する ◦ DB設計を変更すると大体API定義も直さないといけない ◦ 変更が漏れるケース • CRUDを書くのが割とToil
  7. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 13 ent 現環境最強OSS「ent」 ・goでスキーマを定義

    (Schema As Code) ・スキーマ -> DBのマイグレーション ・スキーマ -> OpenAPI自動生成 (CRUDも生成) ・スキーマ -> ORM生成 ・スキーマ -> etc… めっちゃスキーマ駆動だ
  8. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 16 ent entのスキーマについて ・Userというスキーマを定義

    ・Fields()にフィールドを定義 ・Edges()にスキーマ間の関連を定義 => RDBの中間テーブルのようなイメージ ・Goを知らなくても大体わかる package schema import "entgo.io/ent" type User struct { ent.Schema } func (User) Fields() []ent.Field { return field.String("name"), field.Int("age"). Positive(). Optional(), } func (User) Edges() []ent.Edge { return edge.To("friends", User.Type) } Userは友達(friends)を持つ
  9. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 17 entスキーマからopenapi.jsonを生成してみる ・Organization(組織) と

    User(ユーザー)を定義 ・UserはOrganizationに所属する ・UserとOrganizationはそれぞれstringの`name`フィー ルドを持つ
  10. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 18 package schema import

    "entgo.io/ent" type Organization struct { ent.Schema } func (Organization) Fields() []ent.Field { return nil } func (Organization) Edges() []ent.Edge { return nil } package schema import "entgo.io/ent" type User struct { ent.Schema } func (User) Fields() []ent.Field { return nil } func (User) Edges() []ent.Edge { return nil } ent/schema/organization.go ent/schema/user.go
  11. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 19 ent/schema/organization.go ent/schema/user.go func

    (Organization) Fields() []ent.Field { return []ent.Field{ field.String("name"). Default("unknown"), } } func (Organization) Edges() []ent.Edge { return []ent.Edge{ edge.To("users", User.Type), } } func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). Default("unknown"), } } func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From("organizations", Organization.Type) .Ref("users").Required(), } }
  12. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 23 エンドポイントを追加してみる ent/entc.go ...

    func main() { oas, err := entoas.NewExtension( entoas.Spec(spec), entoas.Mutations(func(_ *gen.Graph, spec *ogen.Spec) error { spec.AddPathItem("/organizations/{id}/addUser", ogen.NewPathItem(). SetPatch(ogen.NewOperation(). SetOperationID("addUser"). AddTags("Organization"). AddResponse("200", ogen.NewResponse()), ). AddParameters( ogen.NewParameter(). InPath(). SetName("id"). SetRequired(true). SetSchema(ogen.Int()), ogen.NewParameter(). InQuery(). SetName("user_id"). SetRequired(true). SetSchema(ogen.Int()), ), ) return nil }), ) ...
  13. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 24 エンドポイントを追加してみる ent/entc.go ...

    spec.AddPathItem("/organizations/{id}/addUser", ogen.NewPathItem(). SetPatch(ogen.NewOperation(). SetOperationID("addUser"). AddTags("Organization"). AddResponse("200", ogen.NewResponse()), ). AddParameters( ogen.NewParameter(). InPath(). SetName("id"). SetRequired(true). SetSchema(ogen.Int()), ogen.NewParameter(). InQuery(). SetName("user_id"). SetRequired(true). SetSchema(ogen.Int()), ), ) ...
  14. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 25 エンドポイントを追加してみる ent/entc.go ...

    spec.AddPathItem("/organizations/{id}/addUser", ogen.NewPathItem(). SetPatch(ogen.NewOperation(). SetOperationID("addUser"). AddTags("Organization"). AddResponse("200", ogen.NewResponse()), ). AddParameters( ogen.NewParameter(). InPath(). SetName("id"). SetRequired(true). SetSchema(ogen.Int()), ogen.NewParameter(). InQuery(). SetName("user_id"). SetRequired(true). SetSchema(ogen.Int()), ), ) ... path定義 method定義 response定義 path param定義 query param定義
  15. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 27 スキーマをDBに反映する 対応DB ・MySQL

    ・MariaDB ・TiDB ・PostgreSQL ・CockroachDB ・SQLite ・Gremlin 対応マイグレーションツール ・Atlas ・golang-migrate/migrate ・pressly/goose ・amacneil/dbmate ・Flyway ・Liquibase https://entgo.io/ja/docs/versioned-migrations
  16. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 28 -- +goose Up

    -- create "organizations" table CREATE TABLE "organizations" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL DEFAULT 'unknown', PRIMARY KEY ("id")); -- create "users" table CREATE TABLE "users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL DEFAULT 'unknown', PRIMARY KEY ("id")); -- create "organization_users" table CREATE TABLE "organization_users" ("organization_id" bigint NOT NULL, "user_id" bigint NOT NULL, PRIMARY KEY ("organization_id", "user_id"), CONSTRAINT "organization_users_organization_id" FOREIGN KEY ("organization_id") REFERENCES "organizations" ("id") ON DELETE CASCADE, CONSTRAINT "organization_users_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE); -- +goose Down -- reverse: create "organization_users" table DROP TABLE "organization_users"; -- reverse: create "users" table DROP TABLE "users"; -- reverse: create "organizations" table DROP TABLE "organizations";
  17. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 29 -- +goose Up

    -- create "organizations" table CREATE TABLE "organizations" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL DEFAULT 'unknown', PRIMARY KEY ("id")); -- create "users" table CREATE TABLE "users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL DEFAULT 'unknown', PRIMARY KEY ("id")); -- create "organization_users" table CREATE TABLE "organization_users" ("organization_id" bigint NOT NULL, "user_id" bigint NOT NULL, PRIMARY KEY ("organization_id", "user_id"), CONSTRAINT "organization_users_organization_id" FOREIGN KEY ("organization_id") REFERENCES "organizations" ("id") ON DELETE CASCADE, CONSTRAINT "organization_users_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE); -- +goose Down -- reverse: create "organization_users" table DROP TABLE "organization_users"; -- reverse: create "users" table DROP TABLE "users"; -- reverse: create "organizations" table DROP TABLE "organizations";
  18. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 30 $ PGPASSWORD=admin docker-compose

    exec postgresql psql -d db -U admin -c "\dt" List of relations Schema | Name | Type | Owner --------+--------------------+-------+------- public | goose_db_version | table | admin public | organization_users | table | admin public | organizations | table | admin public | users | table | admin (4 rows) マイグレーション結果を確認 テーブルが作られている
  19. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 31 usersテーブルを見てみる Column |

    Type | Collation | Nullable | Default --------+-------------------+-----------+----------+---------------------------------- id | bigint | | not null | generated by default as identity name | character varying | | not null | 'unknown'::character varying Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "organization_users" CONSTRAINT "organization_users_user_id" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE カラムも良さそう 外部キー制約
  20. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 32 スキーマを変更してみる func (User)

    Fields() []ent.Field { return []ent.Field{ field.String("name"). Default("unknown"), field.Int("age"). Positive(). Optional(), field.Bool("active"). Default(true), } } ent/schema/users.go
  21. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 34 usersテーブルを見てみる Column |

    Type | Collation | Nullable | Default --------+-------------------+-----------+----------+---------------------------------- id | bigint | | not null | generated by default as identity name | character varying | | not null | 'unknown'::character varying age | bigint | | | active | boolean | | not null | true Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "organization_users" CONSTRAINT "organization_users_user_id" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
  22. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 35 生成されたORMを使う main_test.go func

    CreateOrganization(ctx context.Context, client *ent.Client, name string) (*ent.Organization, error) { organization, _:= client.Organization. Create(). SetName(name). Save(ctx) return organization, nil }
  23. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 36 CreateUser()も実装する main_test.go func

    CreateUser(ctx context.Context, client *ent.Client, name string, age int, active bool, organization *ent.Organization) (*ent.User, error) { user, _:= client.User. Create(). SetAge(age). SetName(name). SetActive(active). AddOrganizations(organization). Save(ctx) return user, nil }
  24. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 37 CreateUser()も実装する main_test.go func

    CreateUser(ctx context.Context, client *ent.Client, name string, age int, active bool, organization *ent.Organization) (*ent.User, error) { user, _:= client.User. Create(). SetAge(age). SetName(name). SetActive(active). AddOrganizations(organization). Save(ctx) return user, nil } ハンズオンではこれらを使ってテストも書いています😌
  25. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 39 ent entに対する率直な感想 •

    手作業が減るので大幅に工数が削減される • スキーマをコードで記述するので可読性が高くレビューしやすい • 公式ドキュメントめちゃくちゃ分かりやすい • 途中から導入するのはハードルが高い • オールインワンなOSSなので容易にロックインしそう ◦ 全てをentに依存するのではなく部分的に採用するのがbetterか • 他言語用のORMもgenerateしてくれると嬉しい
  26. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 40 ent おまけ 対抗馬になりそうな「Prisma」について

    • スキーマからDBのマイグレーション・ORM生成などやってくれる ◦ ORMはJS/TS以外にGoも対応していたが現在は開発停止 • schema.prisma + 独自フォーマットなのでGoに依存しない • OpenAPIの自動生成は現状なさそう ◦ 一方、clientライブラリを直接生成できる • もしできるようになったら・・・ ◦ GoのORMが必要な場合以外はほぼPrismaで良さそう? ◦ entも他言語のORMが生成できるようになる可能性もある
  27. 2022.12.09  |  Developers CAREER Boost    |  © PLAID Inc. 41 ent まとめ •

    スキーマ駆動開発の最前線「ent」 ◦ スキーマをGoで記述できる ◦ OpenAPI自動生成・DBマイグレーション・ORM生成など • ハンズオンの資料に沿って簡単に使い方を紹介 ◦ https://github.com/YukiTsuchida/2022-dev-career-boost-handon • 対抗馬としては「Prisma」がいる ◦ OpenAPIを活用したい場合は現状entが良さそう