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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
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が良さそう