pdf-libを使用したPDF書き出し機能の実装

はじめに

こんにちは、プレックスの 種井 です。

プレックスジョブでは、会員ユーザー様向けに、プロフィールなどの入力情報から履歴書や職務経歴書を自動で作成できる機能を提供しています。

PDFの生成方法に関しては、サーバー側で行う方法もありましたが

  • 各クライアントのリソースに頼ることで、サーバーの負荷を抑えることができる

という観点から、クライアント側でPDF生成を行うことにしました。

今回は、履歴書・職務経歴書の自動作成機能の実装時に使用したpdf-libというライブラリの使い方、実装時に考慮した点に関して紹介していきたいと思います。

pdf-libとは?

ピュアjavascriptで書かれた、PDF生成用のライブラリであり、PDFの新規作成や、編集、フォームへの入力を行うことができます。

PDF生成用のライブラリは他にも選択肢がありますが、後述する日本語フォントへの対応のしやすさからpdf-libの使用を決めました。

環境

バージョン
Nodejs +12.22.0
React.js 17.0.2
Next.js 12.0.3
pdf-lib 1.17.1
@pdf-lib/fontkit 1.1.1

導入方法

npm

$ npm install --save pdf-lib

yarn

$ yarn add pdf-lib

基本的な使用方法の紹介

PDFの新規作成

const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage()
page.drawText('sample')
const pdfBytes = await pdfDoc.save()
// pdfBytesを使用してファイルの書き込みを行う
...

既存PDFの編集

const pdfURL = '/pdfs/template.pdf'
const pdfBytes = await fetch(pdfURL).then((res) => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(pdfBytes)
const pages = pdfDoc.getPages()
pages[0].drawText('sample')
const pdfBytes = await pdfDoc.save()
// pdfBytesを使用してファイルの書き込みを行う
...

既存PDFの編集(フォーム入力)

const pdfURL = '/pdfs/template.pdf'
const pdfBytes = await fetch(pdfURL).then((res) => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(pdfBytes)
const form = pdfDoc.getForm()
const textField = this.form.getTextField('name')
textField.setText('sample name')
const pdfBytes = await pdfDoc.save()
// pdfBytesを使用してファイルの書き込みを行う
...

日本語フォントへの対応

デフォルトでは、日本語フォントが使用できないため、@pdf-lib/fontkit 使用して、カスタムフォントを設定する必要があります。

※ 以下、Next.jsへの導入を前提として説明します。

fontkitのインストールを行います。

npm

$ npm install --save @pdf-lib/fontkit

yarn

$ yarn add @pdf-lib/fontkit

例として、adobe-fonts/source-han-sansからカスタムフォントをダウンロードしてPDFドキュメントに埋め込んでみます。

public/fonts/HanSansJP ディレクトリを作成し、ダウンロードしたカスタムフォント(SourceHanSansJP-Regular.otfファイル)を配置します。

コンポーネントのマウント時にfetchメソッドでpublic/fonts/HanSansJP 以下にあるカスタムフォントを読み込んでおきます。

const [font, setFont] = useState<ArrayBuffer>(new ArrayBuffer(0))

useEffect(() => { 
    const setupFont = async () => { 
        const fontBytes = await fetch('/fonts/HanSansJP/SourceHanSansJP-Regular.otf').then((res) => res.arrayBuffer() ) 
        setFont(fontBytes) } 
    }, [])

fontkit(@pdf-lib/fontkit)と読み込んでおいたカスタムフォントの設定を行います。

import fontkit from '@pdf-lib/fontkit'
import { PDFDocument } from 'pdf-lib'
...
const onGenerate = async () => {
    const pdfDoc = await PDFDocument.create()
    pdfDoc.registerFontkit(fontkit) 
    const customFont = await pdfDoc.embedFont(font)
    const page = pdfDoc.addPage()
    page.drawText('プレックスジョブ', {
        font: customFont, // カスタムフォントの設定
    })
    const pdfBytes = await pdfDoc.save()
...
}

これで、日本語フォントへの対応が可能になりました。

バンドルサイズの削減

pdf-libやfontkitはバンドルサイズが大きいので注意が必要です。

  • pdf-lib: 758.6k (gzipped: 338k)
  • fontkit: 427.9k (gzipped: 176.7k)

Next.jsを使用しているので、ビルド時にCode Splittingされるとはいえ、上記を使用したページにおいては、初回レンダリングに不要なバンドルはDynamic importを使用して、バンドルサイズを削減しておきたいです。

以下、使用時にライブラリをインポートする例です。

...
const onGenerate = async () => {
    // Dynamic import
    const { PDFDocument } = await import('pdf-lib')
    const fontkit = await import('@pdf-lib/fontkit').then((value) => value.default)
    
    const pdfDoc = await PDFDocument.create()
    pdfDoc.registerFontkit(fontkit) 
    const customFont = await pdfDoc.embedFont(font)
    const page = pdfDoc.addPage()
    page.drawText('プレックスジョブ', {
        font: customFont, // カスタムフォントの設定
    })
    const pdfBytes = await pdfDoc.save()
...
}

PDF生成時にライブラリを読み込むようにすることで、バンドルサイズを減らすことができました。 (実際のプロジェクトに適用した際には両ライブリを使用するページで350kb程度の削減になりました)

終わりに

今回はPDFの生成をクライアントで行うためのpdf-libライブラリの使用方法、カスタムフォント対応、バンドルサイズの削減など、実装時に考慮した点について紹介してみました。

  • 動的なレイアウトを行う場合の考慮点
  • PDFのフォーム機能を使用したテンプレートの作成方法
  • 画像の埋め込み

などに関しても今後のブログ記事で紹介したいと考えています。

PDFの作成機能は業務系のアプリケーションを中心に必要になることが多いですが、いざ実装するとなると参考資料が多くないので、少しでも参考になればと思います。

最後になりますが、プレックスではソフトウェアエンジニアフロントエンドエンジニアを募集しています。

少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。

dev.plex.co.jp

参考

GraphQL::Batchを使ってよくあるユースケースをTwitterを例に実装してみる

f:id:ishitsukajun:20220314122450p:plain

こんにちは、プレックスの 石塚 です。

今回のブログでは、Ruby向けのデータローダーである GraphQL::Batch を使用して、よくアプリケーションで実装されるユースケースの実装方法をTwitterを例に紹介します。

データローダーとは?

データローダーとはGraphQLに用意されているデータを一括取得するための仕組みです。

例えば以下のようにユーザーを5件取得して、そのユーザーのツイート一覧をデータローダーを使用しないで取得しようとすると下記のように6回(5 + 1回)のクエリが発生することになります。

{
  users(first: 5) {
    id
    tweets {
      id
      content
    }
  }
}
SELECT id FROM users limit 5;
SELECT id, content FROM tweets WHERE id = 1;
SELECT id, content FROM tweets WHERE id = 2;
SELECT id, content FROM tweets WHERE id = 3;
SELECT id, content FROM tweets WHERE id = 4;
SELECT id, content FROM tweets WHERE id = 5;

これをデータローダーを使用することにより、以下のように2回のクエリでデータを取得できるようになります。

SELECT id FROM users limit 5;
SELECT id, content FROM tweets WHERE id IN (1, 2, 3, 4, 5);

GraphQLにおけるデータローダーの必要性

GraphQLではオブジェクトがネストしており、再帰的に処理を読み出せる構造になっているため、N+1が起きやすいという特徴があります。

Railsに慣れている方ならば、それってpreloadやincludesを使用すればN+1を避けることができるのではないのか?と思うかもしれません。 しかしその方法で対処してしまうと関連するモデルの呼び出しが必要のないところでも、不必要にpreloadやincludesなどが走ってしまったり、再帰的に呼び出された場合にN+1を避けることが難しくなってしまいます。そのため、基本的にはデータローダーを使うのがベターです。

本エントリーで実装したいこと

以下の3つのユースケースを実装していきます。Twitterを例にしていますが、サブテーマとして書いているように、アプリケーションを作っていると似たような機能を作ることがよくあるのではないかと思います。

  • 複数のユーザーのツイート一覧を取得する
    • ActiveRecordのリレーションをデータローダーに置き換える
  • あるユーザーのツイート一覧をいいね数と一緒に取得する
    • GROUP BYでのカウントの実装
  • あるユーザーのツイート一覧を自身がいいねしたかどうかと一緒に取得する
    • WHERE句などの条件指定をできるように拡張する

前準備

動作環境やライブラリのバージョンは下記のとおりです。

バージョン
ruby 3.0.2
rails 6.1.4
graphql-ruby 1.12.17
graphql-batch 0.4.3

Rubyにはgraphql-ruby標準のデータローダーもありますが、弊社では、スターの数が多く、使いやすいと感じた GraphQL::Batch を使っています。

以下のモデルファイルと対応するテーブルが存在すると仮定します。

class User < ApplicationRecord
  has_many :tweets
  has_many :favorites
end

class Tweet < ApplicationRecord
  has_many :favorites
  belongs_to :user
end

class Favorite < ApplicationRecord
  belongs_to :user
  belongs_to :tweet
end

実装

複数のユーザーのツイート一覧を取得する(ActiveRecordのリレーションをデータローダーに置き換える)

まずは一番基本的なユースケースとして起こり得そうな、複数のユーザーのツイート一覧を取得するQueryから実装していきます。

N+1を引き起こすコードを書いてから、それをデータローダーを使った実装にリファクタリングしていくという流れで書いていきます。

N+1を引き起こすコード

# app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    field :users, [UserType], null: false do
      description 'ユーザー一覧をツイート一覧と共に取得する'
    end

    def users
      User.limit(5)
    end
  end
end
# app/graphql/types/user_type.rb

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :tweets, [TweetType], null: false
  end
end
# app/graphql/types/tweet_type.rb

module Types
  class TweetType < Types::BaseObject
    field :id, ID, null: false
    field :content, String, null: false
  end
end

これでN+1を引き起こすQueryの実装は完了です。幸か不幸かRailsにはアソシエーションという機能があるため、たったこれだけのコードでツイートとともにユーザーを取得するAPIを書けるのですが、N+1を引き起こしてしまいます。

データローダーを使ったコード

これをデータローダーを使ったコードに書き換えていきます。まずは GraphQL::Batch のインストール。

$ gem install graphql-batch

次にスキーマGraphQL::Batch を使うように宣言します。

# app/graphql/my_schema.rb

class MySchema < GraphQL::Schema
  query MyQueryType
  mutation MyMutationType
  use GraphQL::Batch
end

GraphQL::Batch にはRailsのアソシエーションをよしなに読み込んでくれる AssociationLoader というクラスがサンプルで用意されています。 これと同じコードを app/graphql/loaders/association_loader.rb に記述します。

# app/graphql/loaders/association_loader.rb

module Loaders
  class class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      super()
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)
      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      unless @model.reflect_on_association(@association_name)
        raise ArgumentError, "No association #{@association_name} on #{@model}"
      end
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new(records: records, associations: @association_name).call
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end

そして UserType から作成した AssociationLoader を使うように変更します。

# app/graphql/types/user_type.rb

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :tweets, [TweetType], null: false

    def tweets
      Loaders::AssociationLoader.for(User, :tweets).load(object)
    end
  end
end

これでリレーションではなく、データローダーを使ってツイートを読み込んでくれるため、N+1を解消できます。

あるユーザーのツイート一覧をいいね数と一緒に取得する(GROUP BYでのカウントの実装)

次にいいね数をカウントしてツイート一覧を返す実装です。

N+1を引き起こすコード

こちらもN+1を起こす実装から作っていきましょう。

# app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    field :tweets, [TweetType], null: false do
      description 'ツイート一覧をいいね数と共に取得する'
    end

    def tweets
      Tweet.limit(5)
    end
  end
end
# app/graphql/types/tweet_type.rb

module Types
  class TweetType < Types::BaseObject
    field :id, ID, null: false
    field :content, String, null: false
    field :favorite_count, Int, null: false

    def favorite_count
      object.favorites.size
    end
  end
end

こちらで無事に?N+1クエリが発生します。

データローダーを使ったコード

次にデータローダーを作ってN+1を解消していきます。アソシエーション用のデータローダーは GraphQL::Batch で用意されていましたが、集計用のデータローダーは用意されていないため、アソシエーションを参考に自前で作っていきます。

# app/graphql/loaders/record_count_loader.rb

module Loaders
  class RecordCountLoader < GraphQL::Batch::Loader
    def initialize(model, column: model.primary_key, distinct_column: nil)
      super()
      @model = model
      @column = column.to_s
      @column_type = model.type_for_attribute(@column)
      @distinct_column = distinct_column
    end

    def load(key)
      super(@column_type.cast(key))
    end

    def perform(keys)
      query(keys).each { |key, count| fulfill(key, count) }
      keys.each { |key| fulfill(key, 0) unless fulfilled?(key) }
    end

    private

      def query(keys)
        scope = @model
        scope.where(@column => keys)
             .group(@column)
             .distinct(@distinct_column)
             .count
      end
  end
end

そして TweetType で実装したデータローダーを使うようにします。

# app/graphql/types/tweet_type.rb

module Types
  class TweetType < Types::BaseObject
    field :id, ID, null: false
    field :content, String, null: false
    field :favorite_count, Int, null: false

    def favorite_count
      Loaders::RecordCountLoader.for(Favorite,
                                     column: 'tweet_id',
                                     distinct_column: :tweet_id).load(object.id)
    end
  end
end

これでN+1を回避できます。データローダーを汎用的に使い回せる形で実装しておくと、タイプから呼び出すだけで実装が済むので便利ですね。

あるユーザーのツイート一覧を自身がいいねしたかどうかと一緒に取得する(WHERE句などの条件指定をできるように拡張する)

最後に特定のユーザーのツイート一覧を自身がいいねしたかどうかと一緒に取得するクエリを考えます。

N+1を引き起こすコード

こちらもまずはN+1を起こすクエリから書いていきます。

# app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    field :user, UserType, null: false do
      description '特定のユーザーのツイート一覧を自身がいいねしたかどうかと一緒に取得する'
      argument :id, ID, required: true
    end

    def tweet(id:)
      Tweet.find(id)
    end
  end
end

UserType には先ほど作成した AssociationLoader でツイート一覧を読み込みます。

# app/graphql/types/user_type.rb

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :tweets, [TweetType], null: false

    def tweets
      Loaders::AssociationLoader.for(User, :tweets).load(object)
    end
  end
end

TweetType に新しく is_favorited という自身がいいねしたかどうかを表すフィールドを追加します。

認証情報は context に入っていると仮定して、 自身のユーザーIDをそこから取得しています。

# app/graphql/types/tweet_type.rb

module Types
  class TweetType < Types::BaseObject
    field :id, ID, null: false
    field :content, String, null: false
    field :favorite_count, Int, null: false

    def favorite_count
      Loaders::RecordCountLoader.for(Favorite,
                                     column: 'tweet_id',
                                     distinct_column: :tweet_id).load(object.id)
    end

    def is_favorited
      object.favorites.exists?(user_id: context[:user].id)
    end
  end
end

これでいいねしたかどうかの情報が取れるようになり、N+1が発生します。

データローダーを使ったコード

RecordPresenceLoader というデータローダーを新しく作成します。

RecordCountLoader との違いとしては引数として where を受け取れるようにしている点です。 GraphQL::Batch では ActiveRecord をベースにデータローダーを拡張していけるので、このような形でWHERE句を追加したり、JOIN句を追加していくことができます。

# app/graphql/loaders/record_presence_loader.rb

module Loaders
  class RecordPresenceLoader < GraphQL::Batch::Loader
    def initialize(model, column: model.primary_key, where: nil)
      super()
      @model = model
      @column = column.to_s
      @column_type = model.type_for_attribute(@column)
      @where = where
    end

    def load(key)
      super(@column_type.cast(key))
    end

    def perform(keys)
      query(keys).each { |record| fulfill(record.public_send(@column), record.present?) }
      keys.each { |key| fulfill(key, false) unless fulfilled?(key) }
    end

    private

      def query(keys)
        scope = @model
        scope = scope.where(@where) if @where
        scope.where(@column => keys)
      end
  end
end

そして RecordPresenceLoader をタイプから使用します。

# app/graphql/types/tweet_type.rb

module Types
  class TweetType < Types::BaseObject
    field :id, ID, null: false
    field :content, String, null: false
    field :favorite_count, Int, null: false

    def favorite_count
      Loaders::RecordCountLoader.for(Favorite, column: 'tweet_id', distinct_column: :tweet_id).load(object.id)
    end

    def is_favorited
      Loaders::RecordPresenceLoader.for(Favorite,
                                        column: 'tweet_id',
                                        where: {
                                          user_id: context[:user].id
                                        }).load(object.id)
    end
  end
end

これで自身がいいねしたかどうかの情報もN+1を起こさずに取得できるようになりました。

終わりに

今回の記事はデータローダーのライブラリの使い方という小ネタでしたが、普段GraphQLの実装をしていると、日本語で書かれている情報がまだまだ少ないと感じることが多いため、少しでも参考になればと思って書いてみました。

最後になりますが、プレックスではソフトウェアエンジニアフロントエンドエンジニアを募集しています。

少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。

dev.plex.co.jp

2人目のエンジニアとして入社してオンボーディングを改善した話

f:id:kakudooo:20220208104333p:plain

はじめに

2022/1/1に株式会社プレックス(以下、プレックス)の2人目のエンジニアとして入社した種井です。

今回入社エントリを兼ねて、入社直後に行ったオンボーディング改善の取り組みを紹介しようと思います。

自己紹介

新卒でソーシャルゲームの会社の入社し、主にソーシャルゲームのサーバーサイドエンジニアとして開発・運用、新規事業部でiOSエンジニアとして開発に携わりました。

プレックス1人目のエンジニア、石塚とは新卒の同期でもあります。入社エントリ

その後、大学時代からの知人3名で会社を創業し、4年ほどスポーツ領域で事業検証や開発を繰り返しました。(スポーツ動画投稿プラットフォーム、クラブチーム向けSaaSなど)

退職した頃、石塚から声をかけてもらい、1ヶ月ほど業務委託として業務を行った後、今回プレックスに入社することを決めました。

プロフィール

出身: 三重県

実家: お寺

趣味: ランニング

今年の目標: バイクの大型免許を取ること,地元の駅伝で区間賞を取ること

入社を決めたポイント

石塚が入社エントリで紹介している内容とほぼ同意見です。

ここは、お互い一度起業した経験があることが大きいと思います。

  • 物流・インフラ領域への興味・関心
  • プロダクト開発に至るまでの事業検証のプロセスと解像度の高さへの共感
  • 理念である「日本を動かす仕組みを作る」の通り、実際の業務上でも、会社文化としての仕組みづくり(組織や業務上の)への意識が高いこと
  • 声をかけてもらった際に面談を重ねる中で、代表黒崎や石塚から、なぜ自分なのか、自分に対してどのような動きを期待しているのかを丁寧に伝えてもらったこと

などが入社を決めたポイントです。

本記事のテーマであるオンボーディング改善の取り組みも「仕組みづくり」の組織文化色が出ているかと思います。

オンボーディング改善の取り組み

今回、取り組んだオンボーディング改善は

入社後のオンボーディング期間を終えた後、1スプリント分をメインの開発から外れて、オンボーディング中で出た課題の改善時間として使ってもらう

というものです。

自分自身が受けたオンボーディングがきっかけでした。

入社前に事業や開発に関する資料やドキュメントを共有してもらい、事前に一読した上で入社後のオンボーディングを受けました。

既存のドキュメントでもスムーズにスプリントや、実装に入ることはできましたが、業務を行う中で自分がつまずいた点、次に新しい方が入った場合につまずきそうな点がいくつか出てきたので、その都度、改善案をメモに残しました。

週次の1on1(開発チームでは週次でPOやEMと1on1を実施しています。)で石塚にメモした内容を共有し、やっておいたほうがよさそうな改善項目に関してはタスクとしてバックログに残していきました。

1ヶ月間業務委託として実際に業務を行う中で、入社の意思が固まりました。最終週の石塚との1on1で入社の意思を伝え、入社が決まり次第、POや石塚にバックログに残した改善タスクをこのタイミングで実施してみたいと伝えたところ、快く「実際にやってみよう」と言ってもらうことができました。

その後、実施方法に関して議論を重ね、入社したメンバーが1スプリント分外れてオンボーディングを改善する取り組みを実際に行うことになりました。

具体的には、以下のようなステップで実施しました。

  1. 開発チーム全体でMTGを行い、オンボーディングへのフィードバックと改善タスクを共有する
  2. ビジネスサイドの改善タスクや歴史的背景の濃いものなど、自身で対応しにくいものはそれぞれ詳しいメンバーに担当を依頼する
  3. 1スプリント分、改善タスクのみを行う(2.で割り振りのあった各担当者もそのスプリント中に改善タスクを組み込みます)

その結果、開発業務が1スプリント分進まなくなるデメリットはあるものの

  • 新しいメンバーしか気づけない課題やその解決策を入社直後でも挙げやすくする
  • オンボーディングは発生する機会が少なく、改善サイクルを回しづらいので、少ない機会を最大限活用したい
  • プレックスのエンジニア組織も今後拡大していきたいため、オンボーディング改善により開発までにかかるコストをなるべく下げたい

ことからチーム内で「デメリットを考慮しても、取り組みとして今後もやっていきたい」ということになり、今後も入社するメンバーに同じくオンボーディング改善を実施してもらうことになりました。

今回実施したオンボーディング改善の取り組み

ここでは、開発にとりかかりやすい仕組みづくりの観点で、自分が実際に行った改善の一部を紹介していきます。

今回、自分の意識した点としては

  • 開発の始めやすさ
  • 開発に必要なドメイン知識の取り出しやすさ

です。

一連の開発フローのドキュメント化

現在 PlexJob(ドライバー向け求人サービス) では1週間単位のスプリントで開発を行っています。

タスクの起票、スプリントMTGからリリースまでの一連の流れや各業務で必要になるツールの導入方法などを「開発ガイド」としてドキュメントにしました。

f:id:kakudooo:20220207110443p:plain

ドメイン知識のドキュメント化

PlexJobでは、システムの提供先のステークホルダーが多いこと、複数の社内システムとの連携が必要になることから、暗黙知となっているドメイン知識が多く、都度詳しそうなメンバーから説明を受ける必要がありました。

自分が実際にヒアリングを行った内容と既存メンバーが暗黙知と認識しているドメイン知識を以下のようなドキュメントにまとめました。

  • ドメインモデル図
  • ビジネスサイドと開発のかかわる業務の背景や手順の書き出し

ここで、ドメインモデル図を使用したのは以下のような背景があります。

  • 現在社内で行っている DDD勉強会*1 で取り上げられたドメインモデル図が業務知識を表現する際に非常に分かりやすいと感じたこと
  • 勉強会で学んだ内容を実際の業務にアウトプットする機会として適当であると判断したこと

また、ドメインモデル図を表現する方法に関しては、NotionのMermaid記法を使用しました。その他の作図ツールも検討しましたが

  • 作図ツールとドキュメントツールを分けたくなかった
  • Notionが丁度、Mermaid記法に対応した

ことから、結果的にNotionで一元管理することにしました。

以下、参考例

f:id:kakudooo:20220207111809p:plain

テストデータの整備

seedデータ(PlexJobではサーバーサイドのフレームワークとしてRailsを使用)としてマスタ系のテーブルのデータは用意されていたものの、ユーザーに紐づくデータは担当した際の要件に応じて、都度ステージング環境のDBを参考に自分でデータを作成する必要があり、不便に感じました。

ドメインモデル図により、各モデルに関する知識が明らかになったので、要件毎のデータを表現しやすくなりました。

そのため、自分が開発する中で必要になったドメイン知識や今回ドキュメントにしたドメイン知識を中心に、できるだけ、db:setupした段階でユースケース毎の初期データとして制約/ルール毎のテストデータが用意され、環境構築が完了した段階で実際の運用データに近い形から触り始められるようにしました。

目新しいことはありませんが、自分がオンボーディング改善として行ったものを紹介させていただきました。

次回のオンボーディング時に新しいメンバーからまた、今回の改善に対するフィードバックや更なる改善案が出ることが楽しみです。

今後も引き続き開発内外の仕組みづくりを実施していきたいと思います💪

業務委託や副業で携わりやすい環境があること

最後にはなりますが、プレックスではソフトウェアエンジニア、フロントエンドエンジニアを募集しています。

自分自身、1ヶ月の業務委託を通じて入社を決めましたが、オンボーディングを始め、「まずは業務から、会社や開発チームを知った上で判断してみる」という方でも携わりやすい仕組みが整っています。

是非、少しでも興味を持っていただけた方は業務委託や副業からでもご応募いただけると幸いです。

dev.plex.co.jp

*1:毎週火曜日にドメイン駆動設計 モデリング/実装ガイドをテーマにDDDの勉強会を開催しています

新規事業でGraphQLを採用した振り返りと失敗談

f:id:ishitsukajun:20211220185645p:plain

この記事は GraphQL Advent Calendar 2021 の21日目の記事です。

こんにちは、プレックスの ij_spitz です。

久しぶりのブログ更新になってしまいましたが、アドベントカレンダーに登録することによりブログを書かなくてはいけない環境を強制的に作り出して、なんとかこの記事を書いています。

弊社では半年ほど前から新規事業の開発をGraphQLを使って進めているので、今回はそれの振り返りと失敗談を書いていきます。

自分も社内のメンバーもGraphQLの採用は初めてだったので、かなり初歩的なミスも犯してきたと思いますが、少しでもこれからGraphQLを採用する皆さんの参考になれば幸いです。

振り返り

GraphQLを採用して一番大きな問題となったのは、やはり学習コストがそれなりに掛かることでした。

新規事業を立ち上げる場合、エンジニアでもそれなりの事業ドメインのキャッチアップやリサーチなどのタスクをやっていく必要があると思います。そういった中で技術自体のキャッチアップもしていくとなると、なかなか学習に掛ける時間が取れなくなってしまいます。

実際自分も入社してすぐに新規事業を担当することになったのですが、入社から1ヶ月くらいは、人材紹介をしているメンバーの音声を聞いたり、物流業界についてのインプットを増やしたりと技術以外の学習に使っていた時間の方が多かったです。

加えて幸か不幸か、GraphQLは既存のRest APIを代替しているものなので、GraphQL特有の知識がなくチュートリアルを少々やった程度の理解でも、Rest APIの感覚でそれなりに開発ができてしまいます。

それによって後々修正する点が出てきたり、GraphQLの旨みがあまり活かせないと感じることが多々ありました。

自分たちがGraphQLを採用したことは後悔しておらず、今は良かったとは思っていますが、今回の教訓として得られたことは、新規サービスだから新しい技術を安易に採用するのは良くないということです。

GraphQLもAPIの技術の一種でエンドポイントごとに導入したりすることもできるので、既存の安定稼働しているサービスの方が導入はしやすいのではないかと思っています。

失敗談

次に具体的な自分たちのやらかしてきた失敗談を3つ紹介していきます。

スキーマ設計

まずGraphQLの一番の肝となるのがこのスキーマ設計です。しかしながら厄介なことに、最低限のインプットで開発を進めていると、自分のスキーマ設計が正しいのかどうかもわからないまま開発を進めることになります。

自分もそういった状態で開発を進めていたのですが、Youtubeに公開されている六本木GraphQLという勉強会のアーカイブ映像を見て、良くないスキーマ設計をしていることに気づきました。

そうならないためにもGraphQLを始める場合は、スキーマ設計についてインプットを意識的に増やしたり、知見のある人に聞いてみるなどをおすすめします。

スキーマ設計については上記の動画以外だと、以下の情報を参考にしたりしていました。

データローダーを使っていなかった

GraphQLではN+1問題が起きやすい構造になっているため、それを解決するデータローダーという仕組みがあります。

弊社ではRailsでGraphQLを使っているため、リレーションが貼ってあれば特に気にせずに関連するモデルを読み込めていたので、データローダーの存在は意識せず、開発を進めていました。

もちろんN+1が起きていたことは把握していたので、preload等のRailsの機能を使って回避していました。しかしその方法で対処してしまうと関連するモデルの呼び出しが必要のないところでも、不必要にpreload等が走ってしまってしまうので、データローダーを使うべきでした。

Rubyにはgraphql-ruby標準のデータローダーもありますが、スターの数が多く、使いやすいと感じたgraphql-batchを使っています。

github.com

ApolloClientのキャッシュについての理解が浅かった

これはGraphQLではなくてApolloClientの問題ですが、ApolloClientを使ってる方は多いと思うので紹介させてもらいました。

ApolloClientにはキャッシュの機構が用意されており、特に何も考えずにGraphQLを叩いているとよしなにキャッシュして使ってくれます(便利!)。

しかし、キャッシュの管理は自分でやらなくてはならないため、Mutationでレコードの変更や削除などを行うと、何らかの方法でキャッシュを変更したり、キャッシュを使わないように設定を変更したりする必要があります。当時の自分はこれをあまり理解しておらず、ApolloClientのfetchPolicyでno-cacheを多用してしまっていました。

no-cacheを使うこと自体は悪くないのですが、キャッシュを使って問題ない箇所ではできるだけキャッシュを使いたいので、適材適所で使う場所を選ぶことが大事です。以下の3つがMutation後にも変更されたデータを正しく取得する方法で、このような具合で使い分けをしています。

おわりに

今回は新規事業でGraphQLを採用した振り返りとやらかしてきた失敗を紹介させていただきました。細かい点まで広げると失敗談はもっとたくさんあるのですが、個人的にこのあたりは多くの人がハマってしまうんじゃないかなという3点をピックアップしました。

自分もGraphQLについての理解はまだまだだなと感じるところがたくさんあるので、これからも勉強しつつ、発信の方もできればなと思っています。今後ともブログなどご覧いただけると嬉しいです!

PLEX Product Team Blogはじめました

f:id:ishitsukajun:20210831191926p:plain

初めまして、2021/8/1から1人目の正社員のエンジニアとして株式会社プレックスに入社した石塚(@ij_spitz)です。

このたび弊社でもPLEX Product Team Blogと題したテックブログをはじめることにしたので、初回の投稿として簡単な内容ではありますが、なぜテックブログをはじめるのかなぜProduct Team Blogという名前なのかの2点について書いていきます。

なぜテックブログをはじめるのか

採用のための認知獲得

多くの会社がテックブログを書く理由として挙げるのが採用のための認知獲得ですが、弊社も同じです。

個人的にも前職の経験として、ブログによる採用への効果はとても大きかったと感じています。認知が増えることはもちろんですが、面談や面接時の候補者にとっての情報源にもなってくれるので、スムーズに選考を進められたり、会社の文化や雰囲気へのギャップを減らすことにも寄与していました。 もちろん始めてすぐに効果があるわけではありません。継続して書いていると複利で効いてくるというような印象です。

弊社は現時点ではエンジニアを大量に採用するというフェーズではありませんが、事業の拡大とともにこれから必要になってくるポジションなので、長期を見据えた仕込みとして今から少しづつ発信を増やしていこうとしています。

アウトプット場所を用意したい

ここ数年で企業のテックブログが一般的になり、多くの会社がテックブログを運用するようになりました。 一方でQiitaやZenn、noteなどの個人メディアも増え、どこに何を書くかなど迷うことも多くなっていると思います。

いろいろな考えがあると思いますが、個人的には発信したい内容に適したメディアを選ぶのが自然だと考えています。 例えば業務システムのアーキテクチャの記事など業務に密接に紐付いたものを書くとなったら企業のブログに書くという感じです。業務に深く関連した内容を個人のブログに書くことは違和感がありますし、それを許可してくれる会社もあまり多くないのではないかな思います。 そういった意味でも働いている会社に企業ブログがないということは、アウトプットの機会を損失しているということになってしまうので、今回ブログを開設することにしました。

また社内のドキュメント文化もしっかり根付かせていきたいと思っているので、うまく社内のドキュメントなどからブログに昇華させていくといったこともやっていきたいです。

なぜProduct Team Blogという名前なのか

技術ブログを書いている企業の多くはテックブログやエンジニアブログと名付けていますが、このブログはPLEX Product Team Blogという名前にしています。

もちろん書くのはエンジニアが多くなると思いますが、事業やプロダクトに関わるのはエンジニアだけではありません。 また、事業やプロダクトを創っていく上でも技術以外の要素がたくさんあります。 弊社のエンジニアもどうやってクライアントやユーザー理解を深めつつ開発を進めるかであったり、限られたリソースの中でいかに楽をして高速に開発や検証を行うかといったことに日々取り組んでいて、技術そのものというよりも、プロダクトや事業に興味のあるエンジニアが多いです。

そういった技術を含めたプロダクト全般を扱うブログの方が有益な情報を提供できるとともに、PLEXという企業自体の理解もしてもらいやすいのではないかと思い、PLEX Product Team Blogという名前を付けるに至りました。

こうした理由の一方で、個性的な名前をつけると何のブログなのかわからなくなってしまうという懸念もあります。 しかし実際にBASEさんやQuipperさんのテックブログはProduct Team Blogという名前が使われていて、界隈の人でも聞いてパッと内容が浮かびやすいのではないかということでこの名前に落ち着きました。

devblog.thebase.in

quipper.hatenablog.com

おわりに

今回は初投稿ということでテックブログをはじめる理由とProduct Team Blogという名前への思いを書きました。 月1本程度の更新頻度にはなると思いますが定期的に更新していくので、これからPLEX Product Team Blogをよろしくお願いします。