TOYOKUMO Tech Blog

トヨクモ株式会社の開発本部のブログです。

インフラエンジニアが育休を取りました

こんにちは。トヨクモ開発部インフラエンジニアの井上です。

昨年第一子が生まれたので一ヶ月間の育休を取りました。 弊社での育休取得者第一号ということで、今回は育休について個人的な感想などを書いていきたいと思います。

育休に入る前の準備

日頃の作業や、ネットワーク構成図、障害時対応などをドキュメントにまとめていきました。

ドキュメントを書く目的は、もちろん他のエンジニアと情報共有するためですが、気づきとして属人化されていた作業が見えたということが上げられます。 属人化して良いことは何もないので、今回のような機会に棚卸しできたのは良かったです。 「後でやる」タスクになっていた、スクリプトの改善だったり、使っていないサービスの削除や古いドキュメントの更新などはやっていて気持ちよかったです。

育休中の取り組み

僕はもともと夜型人間であり、朝起きるのがすごく苦手な人種だったので、育休中は昼夜逆転生活を開始し、夜中のミルク対応やおむつ替えなどをやってました。 だいたい朝の6時頃に寝て昼12時頃に起きるといった感じです。起きた頃には妻は昼飯の時間なので、僕がパスタを作っていたのですが、存外にうまく作れる方法を発見してしまったため、育休一ヶ月間のうち20日くらいはパスタ食べてました。太りました。😭😭😭

周りの反応

妻からは「育休取れるとかいい会社だね」と言われ、親親戚からも似たようなことを言われました。

育休は国の制度なのですが、会社の制度だと思ってる方もいるようで、うちの会社は無いからねぇとか言っているのをよく聞きます。 長期間会社を休むことにうしろめたさを感じる人もいるようですが、子育てはほんとに大変なのでワンオペしていると疲労で潰れかねません。 なので気にせずとっていきましょう。どうしても気後れするというエンジニア諸君は弊社に転職してください。

そういえば保健所に検診に行った際、周りにだいたい30名ほどの親御さんがいらっしゃったのですが、男性は一人しかいませんでした。

お金の話し

これは調べれば分かることなので改めて説明しませんが、育児休業給付金がいつ頃振り込まれるのか気になるという方もいるかと思います。 僕の場合、3月1日〜3月31日まで育休期間で4月1日に復帰しました。すぐに申請を出し、5月7日に職業安定局から振込されていました。

なお育児休業給付金は、育児休業が開始してから2ヶ月毎に、2ヶ月分を申請し支給されます。ただ2ヶ月も待ってたら貯金が底を尽きる!という方は1ヶ月毎の申請もできますのですればいいと思います。会社と相談してください。

復帰後

特に大きな問題もなくサービス提供できていたようで、復帰後は大量のメールと格闘した後、いつもの業務に戻ることができました。

所感

子育てと仕事、どちらが大変かという愚問を自身に投げてみたところ、明らかに子育ての方が大変だと分かりました。

今や核家族がほとんどで、子育てはパートナー以外に頼る人がいないというのが現状だと思います。 一人で無理して頑張ってもお互い不幸になるだけなので、育休とって夫婦とも育児に参加するのが普通の世の中になっていって欲しいと思います。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。

Elastic Beanstalk Multicontainer DockerでChromeDriverを使うJavaアプリケーションをデプロイする

こんにちは。 @makinoshi です。

トヨクモでは製品の外形監視などに、Clojureで作成したWebアプリケーションを使っています。

E2E監視に使うためにサーバー上でChrome Driverなどを動かす必要がありましたが、その環境を構成管理ツールでも、ましてや手動で構築する手間をかけたくはありません。 またその環境が正常に動作するかをローカルで確認して、そのままデプロイできれば簡単です。

そこでElastic Beanstalk Multicontainer Docker(以下、EB)Elastic Container Registry(以下、ECR)を使って構築することにしました。

Clojureのアプリケーションは1つのjarファイルにして java -jar で実行するので、ノウハウとしてはJavaアプリケーションのデプロイと変わりません。

手順としては、ECRにコンテナをデプロイし、EBの環境を構築して設定ファイルを準備し、EBにデプロイすれば完了です。

ディレクトリ構成

まずディレクトリ構成からです。今回の記事の主旨以外のソースコード等は省略しています。

.
├── Makefile
├── docker
│   ├── java
│   │   ├── Dockerfile
│   │   └── jar
│   │       └── app.jar
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── eb
│   ├── Dockerrun.aws.json

ECRにコンテナのリポジトリを作る

まずAWS ECRの管理画面で今回デプロイするコンテナのリポジトリを作ります。ECR > リポジトリの作成から作ることができます。

この例ではNginxとJavaアプリケーションの2つのコンテナが必要なので2つのリポジトリを作りました。

JavaアプリケーションのDockerfile

前述のように今回はJavaアプリケーションが動作するコンテナ上にChromeDriverが動作する環境が必要だったので、次のようなDockerfileを書きました。

FROM amazoncorretto:8

WORKDIR /app

COPY jar/app.jar /app/app.jar

RUN yum update -y && yum install -y \
    wget \
    unzip \
    ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts \
    https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm \
    && yum clean all \
    && rm -rf /var/cache/yum

RUN wget https://chromedriver.storage.googleapis.com/75.0.3770.90/chromedriver_linux64.zip \
    && unzip chromedriver_linux64.zip \
    && mv chromedriver /usr/local/bin/ \
    && rm chromedriver_linux64.zip

EXPOSE 5000

ENTRYPOINT ["java", "-jar", "-server", "-XX:+TieredCompilation", "-Xms512m", "-Xmx512m", "/app/app.jar"]

JDKとしてAmazon Correttoの公式Dockerコンテナを使っています。

またChromeとChromeDriverのインストールではyumのキャッシュやダウンロードしたzipファイルを削除することで、極力ビルドしたコンテナサイズを小さくするようにしています。

最後の ENTRYPOINT でJVMのオプションを渡していますが、特にメモリ設定についてはお使いのインスタンスサイズ等に合わせたものにしていただければと思います。

NginxのDockerfile

リバースプロキシとしてNginxを使います。

まず nginx.conf は次のようになります。

user  nginx;
worker_processes auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;

    sendfile  on;
    tcp_nopush on;

    upstream app_backend {
        server java:5000;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://app_backend;
        }
    }
}

ポイントは upstream app_backend の設定です。後述しますが、EBに渡す設定ファイルで別のDockerコンテナをどう参照できるか設定でき、ここでは java という名前で参照させ、またJavaのDockerfileで5000をEXPOSEしたので、 server java:5000 と設定しました。

Dockerfileはシンプルです。

FROM nginx:mainline

COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

ECRへのプッシュ方法

作ったリポジトリの詳細画面に入り、「プッシュコマンドの表示」をクリックすると、そのリポジトリにDockerイメージをプッシュする方法が表示されます。

後ほどMakefileの中でECRへのプッシュを含む、全体のデプロイ方法を紹介します。

EBの環境を構築

今回はCLIではなくマネジメントコンソールから作成しました。

環境の作成を選択し、「事前設定済みプラットフォーム」に「Multi-container Docker」を選択します。アプリケーションコードはサンプルアプリケーションのままにします。 本記事ではVPN等のネットワーク環境の構築については解説しませんが、VPNの中に入れ、さらにNAT経由で外にアクセスするようにしています。

作成が完了したら、ステータスやログを確認して、正常に環境が構築できたことを確認します。

確認できたらebcliを使ってEBの環境を参照するための設定ファイルを作ります。

$ cd eb
$ eb init
# 作成した環境を選択

Dockerrun.aws.json

EBに対して具体的なコンテナの設定をするのが Dockerrun.aws.json ファイルです。

{
  "AWSEBDockerrunVersion": 2,
  "containerDefinitions": [
    {
      "name": "java",
      "image": "<ECR上のURL>",
      "essential": true,
      "memory": 1280,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "<Resion>",
          "awslogs-group": "<ロググループ名>"
        }
      }
    },
    {
      "name": "nginx",
      "image": "<ECR上のURL>",
      "essential": true,
      "memory": 128,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "links": [
        "java"
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "<Resion>",
          "awslogs-group": "<ロググループ名>"
        }
      }
    }
  ]
}

logConfiguration ではawslogsドライバーを使用してコンテナのログをCloudWatchLogsに集めるための設定をしています。

linksname を指定して参照させています。リバースプロキシするために、nginxにjavaで参照できるように設定を渡しています。

Makefileでデプロイ

最後に一連のデプロイをMakefileで単一のタスクに組み上げます。

# ClojureアプリケーションをLeiningenでビルドし、
# Dockerfileから参照できるディレクトリにJARファイルを配置する
jar-build:
    @lein do clean, uberjar
    @mkdir -p docker/java/jar
    @cp -p target/uberjar/harvest.jar docker/java/jar/

docker-build:
    @cd docker/java && docker build -t <ECRのリポジトリ名> .
    @cd docker/nginx && docker build -t <ECRのリポジトリ名> .

build: jar-build docker-build

login-ecr:
    $(shell aws ecr get-login --no-include-email --region <Resion>)

push-java-docker:
    docker tag <ECRのリポジトリ名>:latest <ECRのイメージURL>:latest
    docker push <ECRのイメージURL>:latest

push-nginx-docker:
    docker tag <ECRのリポジトリ名>:latest <ECRのイメージURL>:latest
    docker push <ECRのイメージURL>:latest

push-docker: login-ecr push-java-docker push-nginx-docker

deploy: build push-docker
    @cd eb && eb deploy

JARファイルのビルド以外は、ECRのリポジトリで解説されている手順通りです。

あとは、make deploy を実行すれば完了です。

おわりに

EBを使うことで非常に簡単にDockerをデプロイすることができました。

弊社では現在運用中のPHP製品をEB+ECRの構成に変更するプロジェクトが進行中です。

Javaアプリケーションだと本記事のようにJARファイル1つを配置して起動するだけですし、起動オプション等もENTRYPOINTで java -jar コマンドの引数に渡すだけとかなり簡単にできますが、PHPのアプリケーションではそうはいきません。

次回以降の記事で、PHPの本番環境をDocker化するノウハウについて共有していこうと思います。


弊社では技術が好きなエンジニアを継続的に募集しています。

興味を持っていただいた方は、ページ下部の採用情報をご覧ください。

採用に関する情報公開をはじめました

こんにちは。 @makinoshi です。

トヨクモではこの度、scrapboxの公開プロジェクトを使い、エンジニア採用の関連情報を継続的に公開していくことにしました。

なぜ公開するのか

自分自身が就職先・転職先を探す側だった時のことを思い出しながら、求職者の立場に立ってみると、本当に知りたいのは

  • 実際に約束した給与がもらえるのか
  • 賞与を下げるような仕組みはないのか
  • 残業は本当はどれくらいあるのか、ブラック企業の心配はないか
  • 自分のキャリアにとってプラスがあるのか、技術的に成長できる場か
  • 技術的に正しいと思える、やるべきことがやられてる場か

といった実態の生の情報だと思います。

企業側が公開しているインタビュー記事や採用サイトには都合の悪いことは書かずにいいことだけを書いているように思えるのではないでしょうか。

逆に採用する立場になり、企業側として考えていることとしては、採用時点ではなく入社後こそ重要だということです。 面接で来て頂いた方にはよく話すのですが、入社後にお互いがよかったと思えるようにしたいと考えています。 そのためには実態を極力正確に把握してもらうことが重要だと考えました。

そこで今回、scrapboxで様々情報を公開していくことにしました。

なにを公開するのか

給与・年収・文化・雰囲気や開発で実際に使っているツール、社内の勉強会情報などを公開しています。

それだけでなく、弊社がまだできていないものの今後やりたいと思っていることも正直に公開してます。

筆者やその他のエンジニアに頼んで書いてもらいました。内容は社内の他のエンジニアに目を通してもらった上で、実態とズレがないことを確認してもらっています。

もしもっとこういった情報が知りたいということがあれば、こちらから質問していただければ、scrapboxに反映していきます。

おわりに

公開している情報を見て、興味を持っていただけた方は、弊社採用ページよりご応募ください。

またこういった情報公開が当たり前のものになり、エンジニアにとって企業選びがよりやりやすいものになっていけば筆者にとって望外の喜びです。

ClojureのWeb開発でもっとも重要なRing Handlerについて理解する

こんにちは。開発本部の @makinoshi です。

トヨクモではメインの開発言語としてClojureを採用しています。 Clojureの文法は簡潔でデータ型も少なく、比較的覚えやすい言語だと思います。

一方でWeb開発においては、Symfony/LaravelRuby on Railsのような大きなフレームワークに乗るのではなく、 1つのことをうまくやるライブラリを組み合わせてアプリを作っていくという文化であるため、 それぞれを組み合わせていくための知識が必要になります。

その知識は一見すると膨大に思えるのですが、HTTPそのものの知識を除けば実は理解するのに必要な知識は少なく、RingのHandlerがどういうものなのか理解できれば、 ほぼ理解できたといって過言ではありません。

弊社では新卒で入社した方やアルバイトの学生さんなど、徐々にClojureを書く人が増えていることもあり、 この記事で基本的な部分を理解してもらえるように解説していこうと思います。

サンプルコードのリポジトリはこちらにあります。

Ringとは何か

By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.

とあるように、HTTPのリクエスト・レスポンスを抽象化し、 仕様 を定めたものです。

抽象化層であるため、実際のサーバーの実装は他が担当します。 実装を提供するサーバーの例としては次のようなライブラリがあります。

これらのサーバーがRingが定めた仕様に沿ってHTTPリクエストを受け、HTTPレスポンスを返します。

弊社製品ではImmutantを使用しています。

Ring Handlerとは何か

Ring Handlerは単なる関数です。

ただし、Ringが定めた仕様に従ったリクエストマップを引数で受け取り、Ringが定めた仕様に従ったレスポンスマップを返す必要があります。 具体的には次のようなものです。

(defn handler [request]
  {:status 200
   :body "Hello world!"})

Handlerをどう使うか

Ring Handlerは決まった形のInput/Outputを提供する関数だということまでわかりました。

ではそれをどう使うかというと、上述したようにRingの仕様を実装しているサーバーの起動時に引数として渡すだけになります。 ring-jetty-adapter を使った例を示します。

(require 'ring.adapter.jetty)

(defn- handler [request]
  {:status 200
   :body "Hello world!"})

(ring.adapter.jetty/run-jetty handler {:host "localhost"
                                       :port 8080
                                       :join? false})

これをREPLで評価して、HTTPieでHTTPリクエストを投げてみます。

*この記事ではHTTPレスポンスを綺麗に表示させるためにHTTPieを使います。

$ http http://localhost:8080
HTTP/1.1 200 OK
Content-Length: 13
Date: Tue, 02 Jul 2019 08:50:31 GMT
Server: Jetty(9.4.12.v20180830)

Hello world!

きちんとリクエストを受け、レスポンスを返せていることが確認できました。

URLによって違った結果を返すにはどうすればいいのか

ここまでで好きな形のレスポンスを返すWebサーバーができていますが、当然ながらどのURLにリクエストしても同じ結果が返ってきます。

$ http http://localhost:8080/hoge
HTTP/1.1 200 OK
Content-Length: 13
Date: Tue, 02 Jul 2019 08:50:31 GMT
Server: Jetty(9.4.12.v20180830)

Hello world!

URLによって異なった結果を返せないと使い物にならないわけですが、そのためにはリクエストマップの中の:uriの値を使います。

(require 'ring.adapter.jetty)

(defn- handler [{:as req :keys [uri]}]
  (cond
    (= uri "/hello")
    {:status 200
     :body "Hello world!"}

    (= uri "/about")
    {:status 200
     :headers {"Content-Type" "text/plain; charset=utf-8"}
     :body "弊社についてご紹介します。"}

    :else
    {:status 404
     :headers {"Content-Type" "text/plain; charset=utf-8"}
     :body "お探しのページは見つかりませんでした。"}))

(ring.adapter.jetty/run-jetty handler {:host "localhost"
                                       :port 8080
                                       :join? false})
$ http http://localhost:8080/hello
HTTP/1.1 200 OK
Content-Length: 13
Date: Tue, 02 Jul 2019 09:05:05 GMT
Server: Jetty(9.4.12.v20180830)

Hello world!

$ http http://localhost:8080/about
HTTP/1.1 200 OK
Content-Length: 39
Content-Type: text/plain;charset=utf-8
Date: Tue, 02 Jul 2019 09:05:12 GMT
Server: Jetty(9.4.12.v20180830)

弊社についてご紹介します。

$ http http://localhost:8080/foo
HTTP/1.1 404 Not Found
Content-Length: 57
Content-Type: text/plain;charset=utf-8
Date: Tue, 02 Jul 2019 09:05:23 GMT
Server: Jetty(9.4.12.v20180830)

お探しのページは見つかりませんでした。

一応URLによって異なるレスポンスを返すことはできましたが、handlerにいちいち条件を足していくのはやってられないです。

この問題を解決するために、ルーティングライブラリを使います。 ルーティングライブラリには次のようなものがありますが、やっていることはどれも上記のようなことです。

ここでは弊社の製品でも使っているbidiを使った例を示します。

(require 'ring.adapter.jetty)
(require 'bidi.ring)

(defn- hello-handler [req]
  {:status 200
   :body "Hello world!"})

(defn- about-handler [req]
  {:status 200
   :headers {"Content-Type" "text/plain; charset=utf-8"}
   :body "弊社についてご紹介します。"})

(defn- not-found-handler [req]
  {:status 404
   :headers {"Content-Type" "text/plain; charset=utf-8"}
   :body "お探しのページは見つかりませんでした。"})

(def route
  ["/" {"hello" hello-handler
        "about" {:get about-handler}
        true not-found-handler}])

(def handler
  (bidi.ring/make-handler route))

(ring.adapter.jetty/run-jetty handler {:host "localhost"
                                       :port 8080
                                       :join? false})

これで条件分岐をルーティングライブラリにやってもらいつつ、コードを綺麗にすることができました。

*ここで bidi.ring/make-handler は関数を返す関数であることに注意してください。

Middlewareとは何か

ルーティングライブラリを使うことでhandler関数を分けることができましたが、今度はhandlerで共通な処理を含めたいときに困るようになりました。

Ringにはhandlerの処理の前後に任意の処理を実行する概念としてRing Middlewareがあります。

とは言え複雑なものではなく、引数でhandler(関数)を受け取り、新しいhandler(関数)を作って返すだけです。

(require 'ring.adapter.jetty)
(require 'bidi.ring)

(defn- hello-handler [req]
  {:status 200
   :body "Hello world!"})

(defn- about-handler [req]
  {:status 200
   :headers {"Content-Type" "text/plain; charset=utf-8"}
   :body "弊社についてご紹介します。"})

(defn- not-found-handler [req]
  {:status 404
   :headers {"Content-Type" "text/plain; charset=utf-8"}
   :body "お探しのページは見つかりませんでした。"})

(def route
  ["/" {"hello" hello-handler
        "about" {:get about-handler}
        true not-found-handler}])

(defn- hoge-middleware
  "/hoge以外へのリクエストかつレスポンスが404ならいつもhogeを返すようにする"
  [handler]
  ; 関数が戻り値
  (fn [{:as req :keys [uri]}]                        ; リクエストマップの値を使える
    (let [req (assoc-in req [:params :hoge] "HOGE!") ; リクエストマップの値を自由に書き換えられる
          {:as res :keys [status]} (handler req)     ; ここで引数のhandler(= 元々のhandler)を実行
          ]                                          ; handlerの結果(= レスポンスマップ)に手を加えられる
      (if (and (= status 404) (not= uri "/hoge"))
        {:status 200
         :body "HOGE!"}
        res))))

(def handler
  (-> (bidi.ring/make-handler route) ; handlerを作り、
      (hoge-middleware)              ; middlewareの引数として渡す
      ))

(ring.adapter.jetty/run-jetty handler {:host "localhost"
                                       :port 8080
                                       :join? false})
$ http http://localhost:8080/foo
HTTP/1.1 200 OK
Content-Length: 5
Date: Tue, 02 Jul 2019 09:57:05 GMT
Server: Jetty(9.4.12.v20180830)

HOGE!

APIでJSONを返すにはどうしたらいいのか

最後にRing Middlewareのライブラリを使ってJSONを返すAPIを作ってみます。

そのためにMuuntajaを使います。 MuuntajaはJSONだけでなく様々なデータ形式に対応しており、リクエストされたContent-Typeに対応したレスポンスにしてくれるというとても便利なライブラリです。

(require 'ring.adapter.jetty)
(require 'bidi.ring)
(require 'muuntaja.middleware)

(defn- hello-handler [{:as req :keys [body-params]}]
  {:status 200
   :body body-params}) ; リクエストされたbodyの値をそのまま返すだけ

(defn- about-handler [req]
  {:status 200
   :headers {"Content-Type" "text/plain; charset=utf-8"}
   :body "弊社についてご紹介します。"})

(defn- not-found-handler [req]
  {:status 404
   :headers {"Content-Type" "text/plain; charset=utf-8"}
   :body "お探しのページは見つかりませんでした。"})

(def route
  ["/" {"hello" hello-handler
        "about" {:get about-handler}
        true not-found-handler}])

(defn- hoge-middleware
  "/hoge以外へのリクエストかつレスポンスが404ならいつもhogeを返すようにする"
  [handler]
  ; 関数が戻り値
  (fn [{:as req :keys [uri]}]                        ; リクエストマップの値を使える
    (let [req (assoc-in req [:params :hoge] "HOGE!") ; リクエストマップの値を自由に書き換えられる
          {:as res :keys [status]} (handler req)     ; ここで引数のhandler(= 元々のhandler)を実行
          ]                                          ; handlerの結果(= レスポンスマップ)に手を加えられる
      (if (and (= status 404) (not= uri "/hoge"))
        {:status 200
         :body "HOGE!"}
        res))))

(def handler
  (-> (bidi.ring/make-handler route) ; handlerを作り、
      (hoge-middleware)              ; middlewareの引数として渡す
      (muuntaja.middleware/wrap-format) ; 追加
      ))

(ring.adapter.jetty/run-jetty handler {:host "localhost"
                                       :port 8080
                                       :join? false})
# リクエストも表示させてみます
$ http -v POST http://localhost:8080/hello foo=bar
POST /hello HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 14
Content-Type: application/json
Host: localhost:8080
User-Agent: HTTPie/1.0.2

{
    "foo": "bar"
}

HTTP/1.1 200 OK
Content-Length: 13
Content-Type: application/json;charset=utf-8
Date: Tue, 02 Jul 2019 10:11:05 GMT
Server: Jetty(9.4.12.v20180830)

{
    "foo": "bar"
}

おわりに

ここまでで、RingのHandlerとMiddlewareについてご理解いただけましたでしょうか。 これらが理解できれば、あとはWebアプリケーションに共通して必要なMiddlewareを足していけば、すぐに通常のWebアプリケーションに必要な機能が揃います。

次回以降、定番のMiddlewareについて解説する記事も書いていこうと思います。


トヨクモ株式会社ではClojureを書きたいエンジニア、PHP/Vue.jsを書きたいエンジニア、技術が好きなエンジニアを募集しております。

よろしければ採用ページをご覧ください。

kintone REST API向けのPHPのクエリビルダを作りました

開発本部の齊藤です。kintone REST API向けのPHPのクエリビルダkintone-query-builderを作成しました。 レコードの取得 (GET) にある、「レコードの一括取得(クエリで条件を指定)」のパラメータqueryのためのクエリビルダーです。 ソースコードはMITライセンスで公開してありますので、kintone連携製品の開発者の方はよかったら使ってみてください。 packagistに登録してありますので、composer require toyokumo/kintone-query-builderで使うことができます。

ユースケース

サイボウズスタートアップスでは、kViewerで実際に使用しています。既存のパラメータqueryを文字列連結で地道に組み立てるモジュールを、kintone-query-builderで書き直しました。リファクタリングの結果として、より可読性の高いコードにすることができました。改変してありますが、クエリビルダー導入前のコードと、導入後のコードを比較してみたいと思います。

比較

以前のコード

<?php
foreach ($filters as $filter) {

    $with = $filter['conj'] ?? 'and';
    if (substr($query, -1) === '(') {
        $with = '';
    }
    $query .= " {$with} " . $this->makeQuery($filter);
}

この if(substr($query,-1)==='(')は今まで貯めてきたクエリがあるかどうかを判定していますが、こういった地道な文字列操作をなくすことができました。

<?php
foreach ($filters as $filter) {

    if ($with === 'and') {
        $builder->andWhere($subBuilder);
    } else {
        // $with=='or'
        $builder->orWhere($subBuilder);
    }
}

また、kintone apiの仕様では、time = NOW()のような構文で関数を使うことができるのですが、クエリビルダを使用しない場合、右辺の値が関数かどうかを調べるコードが必要になります。

<?php
        if (
            preg_match(
                '/^(NOW\(\))|(TODAY\(\))|.../',
                $filter['val']
            )
        ) {
            $val = $filter['val'];
        } else {
            $val = "\"{$filter['val']}\"";
        }

この処理はクエリビルダがやってくれるので、すべて消すことができました。

例(レコードの全取得)

kintone apiの仕様として、1回のリクエストにつき500件のレコードしか取得できない、という制約があります。kintone query builderを使用して特定の上限を満たすレコードを全取得するコードは以下になります。

<?php
$builder = (new KintoneQueryBuilder())->where(...);
$records = $api->fetch($builder.build());
// do something
$offset = 0;
$records_max = 500;
while(!\empty($records)) {
    // do something
    $offset+=$records_max;
    $records = $api->fetch($builder->offset($offset)->build());
}

offsetの指定が違うだけで、条件指定の部分が同じクエリを複数発行することになりますが、$builderを使いまわすことができています。

詳しい使い方

READMEを引用します。

<?php
use KintoneQueryBuilder\KintoneQueryBuilder;
use KintoneQueryBuilder\KintoneQueryExpr;
// example
// すべての演算子(=, !=, like, not like, <, >, <=, >=, in, not in)が使えます
(new KintoneQueryBuilder())->where('name', '=', 'hoge')->build();
// => 'name = "hoge"'
(new KintoneQueryBuilder())
    ->where('favorite', 'in', ['apple', 'banana', 'orange'])
    ->build();
// => 'favorite in ("apple","banana","orange")'
(new KintoneQueryBuilder())
    ->where('age', '>', 10)
    ->andWhere('name', 'like', 'banana') // かわりにwhereと書くことができます(where = andWhere)
    ->andWhere('name', '!=', 'banana')
    ->build();
// => 'age > 10 and name like "banana" and name != "banana"'
(new KintoneQueryBuilder())
    ->where('age', '>', 20)
    ->orderBy('$id', 'desc')
    ->limit(50)
    ->build();
// => 'age > 20 order by $id desc limit 50'
(new KintoneQueryBuilder()) // ネストしたクエリには、KintoneQueryExprを$builder->whereの引数として渡してください。
->where(
    (new KintoneQueryExpr())
        ->where('a', '<', 1)
        ->andWhere('b', '<', 1)
)->orWhere(
    (new KintoneQueryExpr())
        ->where('c', '<', 1)
        ->andWhere('d', '<', 1)
)->build();
// => '(a < 1 and b < 1) or (c < 1 and d < 1)'
(new KintoneQueryBuilder())->where('x', '=','ho"ge')->build()
// ダブルクオートはエスケープされます
// => 'x = "ho\"ge"'

今後も継続的にメンテナンスしていきます。また、今後JavaとJavascriptへポートする予定です。バグがありましたら、GitHubリポジトリのIssueで報告していただけるとありがたいです。

与太話

kintoneのapiの仕様が明確に書かれていないところがあり、テスト環境でクエリを投げてapiと対話しながら、仕様を明確にしていく作業が必要でした。 ダブルクオートのエスケープが必要であること、order byを複数のカラムについてかける方法(ordrer by x desc, y asc などとする)、like検索の仕様など、いくつか気になる点がありました。 ドキュメント のコメント欄を漁る必要があり調べるのが手間だったので、kintone本体の開発者の方には、コメントの内容を必要であればドキュメントにも追加していただきたいです。