LayerX エンジニアブログ

LayerX の エンジニアブログです。

GraphQLによるLayerX インボイス ワークフロー機能のモデル設計

DX事業部でLayerX インボイスのワークフロー機能開発を担当している木戸と申します。

今回は先日リリースしたワークフロー機能について、 開発開始から2ヶ月でリリースに至るまでの流れと、採用したGraphQLでのモデル設計についてご紹介いたします。

GraphQLを採用した経緯については、こちらのエントリで @mosa_siru が紹介しておりますので御覧ください。 tech.layerx.co.jp

2021年1月の本リリース後、お客様からの声が圧倒的に多かったこともあり、いざやるぞ!と2月から以下の手順で設計・開発を行いました。

  • ワークフロー全体について機能の洗い出し
  • 機能を満たすテーブル設計
  • GraphQLのスキーマ定義
  • スキーマ定義からコード生成
  • 処理を実装

ワークフロー全体について機能の洗い出し

緊急事態宣言中だったこともあり、ワークフロー開発チーム内で認識を合わせることも困難を伴いました。
ここで我々が選択したのはmiroを利用して、それぞれが必要と思う機能を付箋でどんどん貼っていき、それに対して機能の優先度付けと対応時期を決定しました。

機能を満たすテーブル設計

機能の洗い出しが完了し、解像度が上がった状態で、次はテーブル設計を行いました。
上述の優先度付けと対応時期を決定しましたが、テーブル設計は優先度に関わらず、将来実装する機能を見越して、直近半年間、もしくは将来作る機能のために手戻りを発生させないことを意識して設計しています。

GraphQLのスキーマ定義

先日、@yyoshiki がご紹介した通り、我々はSchema Driven Developmentを採用しています。 tech.layerx.co.jp

ワークフロー機能においては、GraphQLによるスキーマ定義を採用しています。
バックエンドとフロントエンドの定義ファイル、生成ファイルなどは以下のような構成にしています。

バックエンド

graph
├── generated
│   └── generated.go    # gqlgenの生成ファイル。基本いじらない
├── gqlgen.yml          # 設定ファイル
├── model               # model.graphqlsのスキーマ定義から生成されたモデル
│   └── models_gen.go
├── model.graphqls      # スキーマ定義
├── model.resolvers.go  # モデルにembedする定義がある場合は処理の実装をここに
├── resolver.go         # 初期化処理
├── schema.graphqls     # Query(read系)、Mutation(更新系)の定義
└── schema.resolvers.go # Query、Mutationの処理の実装をここに

フロントエンド

codegen.yml      # 設定ファイル
graphql
└── query.ts     # GraphQLクエリ定義
types
└── generated.ts # graphql-codegenの生成ファイル

ここで実際に、よくあるユーザー情報の取得と作成を例にして以下に定義してみます。

バックエンド

model.graphqls

# ユーザースキーマ定義
type User {
    id: ID!
    name: String!
    email: String!
}

schema.graphqls

type Query {
    user(userId: ID!): User!  # 特定ユーザー情報取得
}

type Mutation {
    createUser(input: UserInput!): User!  # ユーザー作成
}

# ユーザー作成時のインプット用スキーマ定義
input UserInput {
    name: String!
    email: String!
}

フロントエンド

query.ts

// schema.graphqlsのQueryの内容に従って定義
export const GetUser = gql`
  query GetUser($userId: ID!) {
    user(userId: $userId) {
      id
      name
      email
    }
  }
`

// schema.graphqlsのMutationの内容に従って定義
export const CreateUser = gql`
  mutation CreateUser($input: UserInput!) {
    updateUser(input: $input) {
      id
      name
      email
    }
  }
`

こんな感じ。次はコード生成です。

スキーマ定義からコード生成

上述のユーザー定義の例をもとにコード生成してみましょう。

バックエンド

$ gqlgen

これにより生成されたコードは以下です。generated.goの中身は実装の上で意識不要なため割愛します。

models_gen.go

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

スキーマに定義したモデルがstructで出力されています。

schema.resolvers.go

func (r *queryResolver) User(ctx context.Context, userID string) (*model.User, error) {
    panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateUser(ctx context.Context, input model.UserInput) (*model.User, error) {
    panic(fmt.Errorf("not implemented"))
}

QueryとMutationに定義したメソッドが生成されます。
panic(fmt.Errorf("not implemented"))
実装時はこの部分を書き換えて処理を実装します。

フロントエンド

$ yarn graphql-codegen

以下のコードが生成されます。

generated.ts

export type User = {
  __typename?: 'User';
  id: Scalars['ID'];
  name: Scalars['String'];
  email: Scalars['String'];
};

export type GetUserQueryVariables = Exact<{
  id: Scalars['ID'];
}>;

export type UserInput = {
  name: Scalars['String'];
  email: Scalars['String'];
};

export type CreateUserMutationVariables = Exact<{
  input: UserInput;
}>;

model.graphqls、schema.graphqlsのスキーマ定義したモデルと、Query、Mutationの引数のモデルが生成されているのがわかります。

処理を実装

スキーマ定義からコードの生成まで行われたので処理を実装します。

バックエンド

バックエンドの処理はロジックをservice層、DBなどへのアクセスはrepository層で行っています。service層、repository層の実装内容は割愛します。

schema.resolvers.go

func (r *queryResolver) User(ctx context.Context, userID string) (*model.User, error) {
    user, err := r.UserService.Get(ctx, userID)
    if err != nil {
        return nil, err
    }
    return user, nil
}

func (r *mutationResolver) CreateUser(ctx context.Context, input model.UserInput) (*model.User, error) {
    user, err := r.UserService.Create(ctx, input.Name, input.Email)
    if err != nil {
        return nil, err
    }
    return user, nil
}

フロントエンド

<script lang="ts">
import Vue from 'vue'
import { GetUserQueryVariables, CreateUserMutationVariables, User, UserInput } from '~/types/generated'

export default Vue.extend({
  data() {
    return {
      user: {} as User,
      userForm: {
        name: '',
        email: '',
      } as UserInput,
    }
  },
  methods: {
    // 特定ユーザー情報取得
    getUser(id) {
      const variables: GetUserQueryVariables = {
        id,
      }
      const res = await this.$apollo
        .query({
          query: GetUser,
          variables,
        })
        .catch(e => {
          console.error(e)
          return Promise.reject(e)
        })
      this.user = res.data.user
    },
    // ユーザー作成
    createUser() {
      const variables: CreateUserMutationVariables = {
        input: {
          name: this.userForm.name,
          email: this.userForm.email,
        },
      }
      this.$apollo
        .mutate({
          mutation: CreateUser,
          variables,
        })
        .catch(e => {
          return Promise.reject(e)
        })
    },
  },
})
</script>

バックエンドのservice層、repository層、フロントエンドのUIの実装は別途しなければなりませんが、 上記の実装でフロントからバックエンドへのユーザー作成と取得の処理が実装できました。

おわりに

  • チームでワークフローのそれぞれの機能の解像度を上げ
  • それを実現するテーブル設計を行い
  • GraphQLのスキーマ定義に落とし込み
  • コードの自動生成を行う

ことで、短期間での開発を実現できました。
またスキーマ定義からコード生成でバックエンドとフロントエンドのモデルを作成することで、 型の定義が厳密になり、実装方法にブレがなく、品質とスピードの両方を手に入れることができたと考えています。
まだまだ使いたてのGraphQLではありますが、今後も使い倒してより良いサービス開発をしていければと思います。

絶賛エンジニア採用中ですので、少しでも興味のある方は一度お話をさせていただければと思います! herp.careers