Toyokumo Tech Blog

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

Clojureエンジニアが育休を4ヶ月とりました

こんにちは。開発本部の飯塚です。 4月に第2子が生まれ、4ヶ月の育児休暇を取得しました。

育休の入るまでにしたこと

開発本部では毎週目標を決めて翌週頭に何ができたか/できなかったなどを振り返るのですが、出産予定日がある程度確定した時点で、育児休暇に入るまでの計画を作成して共有・レビューしました。

この計画作成でありがたかったのが、育休に入る予定の2週間前から待機期間として雑務を中心とした計画とし、いつでも育休に入れる状態にさせていただいたことです。 これにより予定日が近づいてきても途中の業務や引き継ぎに関する不安を抱えなくて済み、精神衛生上とても良かったです。 なお実際に生まれたのが予定日の5日前だったので(私目線では)スムーズに育休に入れたと思っています。

育休を取得して良かったこと

この手の話はよくある話になりがちで、すぐ大きくなってしまって貴重な赤ちゃん時期を、短いながら夫婦2人で育児できたことはとても素敵なことでした。

それ以外だと上の子の幼稚園入園が出産とちょうど重なったことで、入園後3ヶ月ほど幼稚園イベントは私1人で参加していたのですが、以下のようなことができたのは貴重だったかなと思います。

  • 母の日イベントに唯一父1人だけで参加する
  • 顔を覚えられているので幼稚園近くの公園に1人でいても通報されない

理想と現実

元々、上の子が幼稚園に行っている間の1時間程度を使って少しだけ業務を行う予定だった(そういう命令があったわけではなく妻とも相談して了承済みの話)のですが、幼稚園の送迎・買い物・家事・下の子の面倒を見るなどしていると全然時間が取れず、また体力の消耗も激しかったので結局業務は全くできませんでした。 これは完全に2人目の育児の大変さを甘く見ていた結果です。

育休の長さについて

制度的には1年取得しても問題なかったのですが、私は妻とも相談して4ヶ月で復帰しました。 ただこの4ヶ月という期間も、元々暫定で2ヶ月という申請をした上で2回延長した結果になります。 下の子の成長具合や上の子の幼稚園の様子、また妻の体力などを考慮しつつ夫婦納得の上で期間が決められたのは良かったなぁと思います。

復帰に際しての不安

上の子の時は転職前かつ出産時の諸々のトラブル関連で育児休暇を取らずに終わってしまったので、長期間の育児休暇をとるのは初めてでした。 休暇中に取り残される・ちゃんと復帰できるだろうかといった不安が無いわけでは無かったのですが、それ以上に家事・育児が大変だったので考える余裕もあまり無かったのが正直なところです。

実際に復帰してみて(まだ数日しか経っていませんが)ですが、仕事っぽいことはできているつもりであることと、4ヶ月いなくても温かく迎えてくれる職場・メンバーに恵まれたので杞憂だったなという感想です。

最後に

上の子の幼稚園入園に伴って生活リズムが早くなったので、復帰後は 09:00-18:00 勤務から 07:00-16:00 の時差勤務に変更しました。 これまでだと朝に数分顔を合わせて、家に着いたら夕食が終わっているという状態になってしまいますが、時差勤務により子供と顔を合わせる時間が増え、帰宅後に家事も手伝えて、満員電車も(多少)回避できてとみんなハッピーになりました。

育児休暇に入るまでの待機期間に始まり、育児休暇期間の延長、復帰後の勤務形態の変更などいろいろと父として育児しやすい環境が整いつつあり助かったので、育休をとったからと言って幼稚園でママさんたちに「すごーい!」と言われないようになると良いですね。


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

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

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

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

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

育休に入る前の準備

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

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

育休中の取り組み

僕はもともと夜型人間であり、朝起きるのがすごく苦手な人種だったので、育休中は昼夜逆転生活を開始し、夜中のミルク対応やおむつ替えなどをやってました。 だいたい朝の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について解説する記事も書いていこうと思います。


追記

続きはこちらです。

tech.toyokumo.co.jp


トヨクモ株式会社では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本体の開発者の方には、コメントの内容を必要であればドキュメントにも追加していただきたいです。