ESLint の no-multi-spaces で TypeScript のType 宣言の空白を許容させる

f:id:ishiyu1125:20210923145413p:plain

こんにちは。 @ishiyu です。

ESLint の no-multi-spaces ルールはすごく便利ですよね。

Function の定義で

function   (   aaaa ) {
  return   'test';
}

みたいな不要な空白だらけのコードでエラーを出してくれます。

また、--fix オプションを使えば自動でキレイに取り除いてくれます。

function (aaaa) {
  return 'test';
}

しかし、デフォルトの設定では視認性を上げるために空白を入れた配列もエラーになります。

const mens = [
  ["山田", "y.png",      25],
  ["佐藤", "sato.png",   19],
  ["鈴木", "suzuki.png", 22],
];
// => ESLint: Multiple spaces found before '25'.(no-multi-spaces)

こちらについては、以下のように eslintrc.json に定義しておけば、空白を許容し取り除かれなくなります。

"no-multi-spaces": {
  [
    "error", {
      exceptions: {
        "ArrayExpression": true
      }
    }
  ]
}

その他にも Object や変数宣言など JavaScript に関連する空白の許容は可能になっています。 (詳しくはドキュメントに載ってます)

しかし、TypeScript 特有の Type 宣言や Interface 宣言についてはドキュメントに記載がないため、空白を許容する設定ができず、このように書くしかありません。

type TestType = {
  name: string;
  genderCode: number;
  email: string;
}

ということで、 no-multi-spaces で TypeScript のType 宣言の空白を許容させる方法を調べてみました。

TypeScript のType 宣言の空白を許容させる

で、本題です。

試した環境

ちょっと古いけど。。。

typescript: 4.2.3
eslint: 7.23.0
typescript-eslint/perser: 4.20.0

設定するルール

exceptions"TSTypeAnnotation": true を追加すれば良いです。

これだけで、 Type 宣言時の空白チェックを無視するようになります。

"no-multi-spaces": {
  [
    "error", {
      exceptions: {
        "TSTypeAnnotation": true
      }
    }
  ]
}

なぜこれで空白が許容されるようになるのか

ESLint はコードをパースして AST (Abstract Syntax Tree (抽象構文木)) の集合に変換し、チェックを行なっています。

これは TypeScript でも同様です。 ただし ESLint も TypeScript も変換される AST の Type(NodeType) の種類がそれぞれ定義されており、微妙に異なっています。

その差異を吸収するのが typescript-eslint/perser です。

そのため、TypeScript+ESLint の構成の場合、typescript-eslint/perser のみ意識すれば良いことになります。

ちなみに、AST についてはこちらが分かりやすかったです。

no-multi-spaces の exceptions のキーは、この AST の Type 名になっています。

配列の空白を無視するために設定した ArrayExpression や今回紹介した TSTypeAnnotation も AST の Type の名前です。ですので、ESLint のドキュメントに記載がない設定も空白を許可させることが可能になっています。

この AST については AST Explorer で、確認ができます。

今回紹介したno-multi-spaces ルールに無視する AST Type を確認する場合には、以下のように設定し、typescript-eslint/perser でパースする必要があるので注意してください。

f:id:ishiyu1125:20210923144456p:plain
AST Explorer

おわりに

インゲージでは TypeScript を語りたいフロントエンジニアを絶賛募集中です。

ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

コマンドラインが変な状態になったら

こんにちは、masm11 です。 今回は小技を一つ紹介したいと思います。

症状

コマンドラインがこんな状態に陥って、端末を閉じるしかなかった経験、ありませんか?

  • ctrl+pctrl+h が効かない
  • Enter が効かない (押しても ^M と表示される)
  • プロンプトが左端に表示されない

等々…

解決策

そんな状態になったら、さくっと以下のコマンドを実行します。

stty sane

これだけです。たいていの症状は解決してくれます。

なお、Enter が効かない場合は、代わりに ctrl+j なら効くことが多いです。

最後に

webpack を使ってると、エラー終了時に

  • ctrl+p が効かない (^P と表示される)

になることが多いです…

さて。弊社ではこんな小ネタもご存知なエンジニアを募集しています。 詳細は以下のページへ。

https://ingage.co.jp/recruit/

N+1問題について勉強会で発表してみた

f:id:ryohei515:20210819203336j:plain

ryohei515です。

Ruby on Railsを使う上で、N+1問題は避けては通れない問題です。

インゲージでは週に1度、社内勉強会を行っているのですが、今週は私が発表担当だったため、このN+1問題を知識整理がてら、社内勉強会で共有しました。

簡単な内容ではありますが、その資料をブログで共有したいと思います。

TL;DR

N+1問題に対しては、これが結論です。

メソッド 結合方法 使用箇所
joins 内部 内部結合で結合先の条件をフィルタしたいとき
left_joins 外部 外部結合で結合先の条件をフィルタしたいとき
preload 外部 結合先のデータを使いたいとき
eager_load 外部 結合先のデータを使いたいかつ、
結合先の抽出条件を指定したいとき
includes 外部 n+1を発生させないようにしたいとき
(できれば preloadeager_load を使う。)

ただ、勉強会では

  • sizecountlength の動きはそれぞれ違うよね。
  • インゲージが提供しているサービス Re:lation だと、 eager_loadpreload 、どっちが早い?
  • パフォーマンスを意識してpluck を使っているけど、結果を hash に加工する gem (pluck_to_hash) を使えば、より使いやすくなりそう!

といった感じで色々な話に派生し、学びの多かった勉強会となりました!

終わりに

こんな技術について語り合う場が欲しいと思っている方はぜひご応募頂けると嬉しいです!

カジュアル面談も可能なので、ぜひ以下からお願いします!

ingage.co.jp

DockerのBuildKitがヒアドキュメントをサポートしました

f:id:ingnis:20201120144028j:plain

Dockerでヒアドキュメントが使えるようになるみたいです。

DockerfileでのRUNコマンドの記述が楽になりそうです。

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y ...

参考サイト www.docker.com

ヒアドキュメントの機能を利用するには、環境変数 DOCKER_BUILDKIT=1にする必要があります。

buildコマンドで利用するイメージを変更するために、Dockerfileの冒頭に# syntax=docker/dockerfile:1.3-labsを追加します。

ヒアドキュメントが使用できるコマンドはRUNCOPYになります。

RUNコマンドでヒアドキュメントを使う

上のRUNコマンドをヒアドキュメント化するには以下のように記述します。

# syntax=docker/dockerfile:1.3-labs

FROM ubuntu:20.04

RUN <<EOF
apt-get update
apt-get upgrade -y
apt-get install -y ...
EOF

ヒアドキュメントを使うことで、RUNコマンドでの&& \の数珠つなぎから開放されDockerfileの可読性が上がりそうですね。

COPYコマンドでヒアドキュメントを使う

# syntax = docker/dockerfile:1.3-labs

FROM alpine

COPY <<-"EOF" /app/script.sh
    echo hello ${FOO}
EOF

RUN FOO=abc ash /app/script.sh >> /hello
  1. ヒアドキュメントの内容で/app/script.shを作成します。(EOFをダブルクオート囲っているので環境変数は展開されない)

  2. 次のRUNコマンドで/app/script.shを実行します。(/hellohello abcが記述される)

私には利用用途があまり思い浮かびませんが、マルチステージビルドで複雑なbuildをするときに使うときが来るかもしれません。

短いですが以上です。

インゲージではエンジニアを募集しいています。 ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

過去コミットを分割してたら、知らないファイル変更が現れた

f:id:shutooike:20210818144401p:plain

はじめに

topic-a ブランチで新たに作成した ComponentX を topic-b ブランチでも使いたくなったので、

topic-a からコンポーネントXを作成したコミットだけ cherry-pick して別PRにしたいが、

コンポーネントXを作成したコミットには他の変更(コンポーネントXの適用)も入っているため単純に cherry-pick するだけでは無理なので

コンポーネントXを作成したコミットを分割しようとしてハマった話をします。

ハマりどころ

「git 過去コミット 分割」でググるとこんな記事を見つけました。

cha-shu00.hatenablog.com

先人に感謝しつつ、なるほど git rebase -i で edit して git reset HEAD^ して、別々に commit したらいいのか・・

やってみます。

$ git log
* ccccccc (HEAD -> topic-a) fix typo
* bbbbbbb add ComponentX
* aaaaaaa fix style

git rebase -i して

$ git rebase -i aaaaaaa

pick bbbbbbb add ComponentX
pick ccccccc fix typo

# Rebase aaaaaaa..ccccccc onto aaaaaaa (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
....(omitted)

分割したいコミットを pick → edit に変更

- pick bbbbbbb add ComponentX
+ edit bbbbbbb add ComponentX

git status

$ git status
interactive rebase in progress; onto aaaaaaa
Last command done (1 command done):
   edit bbbbbbb add ComponentX
Next commands to do (105 remaining commands):
   pick ccccccc fix typo
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'topic-a' on 'aaaaaaa'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

コミットを一つ戻して、git status すると

$ git reset --soft HEAD^
$ git status
interactive rebase in progress; onto aaaaaaa
(省略)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue 
        modified:   foo/views/settings/pages/UseComponentXPage.vue 
        modified:   foo/models/A.ts
        modified:   foo/models/B.ts
        modified:   foo/models/C.ts
        .
        .
        .

ん?!??

コミット bbbbbb での変更以外のファイルまで modified になっている。

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue # <- コミット bbbbbb での変更
        modified:   foo/views/settings/pages/UseComponentXPage.vue # <- コミット bbbbbb での変更
        # 以下、知らない子たち
        modified:   foo/models/A.ts
        modified:   foo/models/B.ts
        modified:   foo/models/C.ts
        .
        .
        .        

なぜこんなことに・・・・

解決

ESLint 君の autofix の仕業でした。

webpack コンテナを落として再度同じことをやると

$ git status
interactive rebase in progress; onto aaaaaaa
(省略)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo/common/elements/CoponentX.vue 
        modified:   foo/views/settings/pages/UseComponentXPage.vue 

今度はコミット bbbbbb での変更だけが表示されました!

あとは個別にファイルをコミットして rebase --continue をしたら過去コミットの分割ができました!

さいごに

そういえば前にもこんなことあったなーと思ったので備忘録として残しておきます。

というか、適切な粒度でコミットしましょう。

弊社では適切な粒度でコミットできるエンジニアを絶賛大募集中です!

カジュアル面談も有りますのでご興味あればぜひお気軽に下記リンクからお願いします!

ingage.co.jp

後日談

コンポーネントXを作成したコミットには他の変更(コンポーネントXの適用)も入っているため単純に cherry-pick するだけでは無理なので

勝手に無理だと思い込んでいたんですが、実際やってみると

$ git log
* ccccccc (HEAD -> topic-a) fix typo
* bbbbbbb add ComponentX
* aaaaaaa fix style

$ git switch main

$ git switch -c cherry-pick-component-x
Switched to a new branch 'cherry-pick-component-x'

$ git cherry-pick bbbbbbb
error: The following untracked working tree files would be overwritten by merge:
        foo/common/elements/CoponentX.vue
Please move or remove them before you merge.
Aborting
fatal: cherry-pick failed

$ git status
On branch cherry-pick-component-x
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        foo/common/elements/CoponentX.vue

nothing added to commit but untracked files present (use "git add" to track)

と cherry-pick 自体は失敗するんですが、untracked な ComponentX.vue ファイルは生成されました。

綺麗な歴史にこだわりがないなら、これで十分だなーと思いました。

ではまた!

ChatOps: SlackからLambdaに連携してみる(5)

id:kizashi1122 です

f:id:ingage:20210620201734p:plain

いよいよ大詰めです。

Lambda の中で「Slack に投稿する」ということをします。 今回は Ruby を使っています。Ruby だと以下の gem を使いたくなります。

github.com

Lambda で gem を使うってどうするんでしょうか。 今回はこちらのやり方でいきましょう。ローカルで bundle install して zip で固めてアップロード、ですね。

docs.aws.amazon.com

他のやり方としては、SAM cli を使うだとか、CDK を使うというやり方もあると思いますがそれはまた別の機会に。

zip ファイルの作成

手元で、適当にディレクトリを作成し、その中で以下のファイルを作りましょう。

  • lambda_function.rb
  • Gemfile

lambda_function.rb は今の内容そのままでいいです。確か、今は

require 'json'

def lambda_handler(event:, context:)
    # p event
    
    req = JSON.parse(event['body'])
    if ENV['SLACK_VERIFICATION_TOKEN'] != req['token']
        return { statusCode: 401, body: "VERIFICATION TOKEN UNMATCHED" }
    end
    
    { statusCode: 200, body: 'done' }
end

こうなっていたでしょうか。 Gemfile は

source "https://rubygems.org"
gem 'slack-ruby-client'

で、よいでしょう。

次に、同じ階層で、

$ bundle install --path vendor/bundle

と実行します。Lambda の実行環境が Ruby 2.7 なので、ローカルも Ruby 2.7 系にしておいてください。 これ大事です。 実行すると、DEPRECATED とでますが気にしない。次に、ZIP で固めます。

$ zip -r function.zip lambda_function.rb vendor Gemfile*

出来上がった function.zip をアップロードします。

f:id:ingage:20210628230840p:plain

左側のファイルツリーに vendor ディレクトリがみえたでしょうか?

エコーする部分のコーディング

前回のエントリでエコーする部分にはあたりをつけました。 となるとこんなかんじでしょうか。

require 'json'
require 'slack-ruby-client'

def lambda_handler(event:, context:)
    req = JSON.parse(event['body'])
  
    if ENV['SLACK_VERIFICATION_TOKEN'] != req['token']
        return { statusCode: 401, body: "VERIFICATION TOKEN UNMATCHED" }
    end

    text = req['event']['blocks'].first['elements'].first['elements'].last['text']&.strip
    post_message(text) # 同じ内容を投稿
    
    { statusCode: 200, body: 'done' }
end

def post_message(message)
  Slack.configure do |config|
    config.token = ENV["SLACK_TOKEN"]
  end
  client = Slack::Web::Client.new
  client.chat_postMessage(channel: ENV["SLACK_CHANNEL"], text: message)
end

メッセージ送信用のトークンと投稿先のチャンネルは環境変数で指定しています。

パーミッションの追加とトークンの取得

SLACK_TOKEN は何を設定すればよいのでしょうか。

Slack の echoapp の設定画面に戻りましょう。 左メニューの「OAuth & Permissions」に移動します。

f:id:ingage:20210629091433p:plain

するとすでに上部にトークンが表示されています。これを SLACK_TOKEN として環境変数に設定しておきましょう。
SLACK_CHANNEL は投稿先のチャンネルを定義しておいてください。

ここで大事なのはこの時点ではこのトークンにはチャンネルへのメッセージ投稿権限がないことです。
chat:write の権限をつけておきます。

パーミッションを変更したことによりアプリの再インストールが必要になりますので、最初にアプリのインストールをしたのと同じ手順でインストールしましょう。

f:id:ingage:20210629091716p:plain

これで完了です。

テスト

テストしてみます。

f:id:ingage:20210629091822p:plain

やったね!

弊社では、上述の「エコーする」部分を AWS の Security Group に IP アドレスを追加する処理として運用しています。 AWSのAPIでもいいし、外部のAPIでもいいし、いろいろな使い方ができそうですね。

インゲージではエンジニアを募集しいています。 ご応募お待ちしてます。

↓↓↓↓↓↓↓↓↓↓↓↓↓

ingage.co.jp

PostgreSQLでパフォーマンスを比較してみた (EXISTSとINNER JOIN)

はじめまして。5月に入社したryohei515です。

前職ではOracleを使ってSQLを書く機会がよくあり、パフォーマンスチューニング等も行ってきました。

インゲージに入社してからSQLを書く機会があったのですが、DBがPostgreSQLであるため、これまで使っていた細かなSQLのパフォーマンスの小技がPostgreSQLでも通用するのか気になり、検証してみました。

環境準備

Dockerを使って、PostgreSQLのコンテナを作成します。最新の13.3を使用しています。

ちょっと検証したい時に、ローカルを汚さずに簡単に環境が作れるので便利ですね!

docker run --rm -d -p 15432:5432 -v postgres-tmp:/var/lib/postgresql/data -e POSTGRES_HOST_AUTH_METHOD=trust postgres:13.3

※ パフォーマンスを見たいだけなので、パスワード不要となるよう、POSTGRES_HOST_AUTH_METHODtrueに設定しました。

今回の検証用のデータは、postgrteSQLのTutorialで提供されている、dvdrentalを使用しようと思います。

ER図も用意されており、SQLの動作確認や学習なんかにも使えそうですね。

PostgreSQL Sample Database Diagram

データ登録の前に、事前にDATABASEを作成しておきます。

psql -h localhost -p 15432 -U postgres -c "CREATE DATABASE dvdrental;"

以下のサイトよりzipファイルをダウンロードし、ダウンロードしたディレクトリに移動します。

PostgreSQL Sample Database

ダウンロードファイルを、unzipコマンドで展開し、.tarのファイルを作成します。

$ unzip ./dvdrental.zip
Archive:  /Users/makabe/Library/Mobile Documents/com~apple~CloudDocs/Documents/memo/091_blog/20210719_SQL_performance/dvdrental.zip
  inflating: dvdrental.tar

事前に作成しておいたDBに、以下のコマンドでデータ作成します。

(特にメッセージ等表示されず、完了します。)

pg_restore -U postgres -d dvdrental -h localhost -p 15432 ./dvdrental.tar

これで以下のコマンドを打ち込めば準備完了です。

psql -h localhost -p 15432 -U postgres -d dvdrental

パフォーマンスチェック

環境は準備できたので、パフォーマンスを測ってみたいと思います。

EXISTSではなくINNER JOINを使う」というのを、これまでよく使っていたので、実行時間やコストにどんな差が出るか見てみます。

(ざっと検索してみたところ、PostgreSQLでも同じそう…)

SQLの内容は、Rentalテーブルのデータで、length > 100Filmがどれだけあるかという内容で見ていきます。

EXISTSで書いてみるとこんな感じ。

select
  r.*
from
  rental r
where exists(
  select
    1
  from
    inventory i
  inner join
    film f
  on i.film_id = f.film_id
  where 1=1
  and r.inventory_id = i.inventory_id
  and f.length > 100
)

INNER JOINで書いてみるとこんな感じかと思います。

select
  r.*
from
  rental r
inner join
  inventory i
on r.inventory_id = i.inventory_id
inner join
  film f
on i.film_id = f.film_id
where 1=1
and f.length > 100
;

PostgreSQLでは、SQLの頭にEXPLAIN ANALYZEをつけることで、実行計画を見ることができるので、それぞれ出力してみるとこうなりました。

EXISTS

                                                          QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
 Hash Semi Join  (cost=191.88..653.14 rows=9771 width=36) (actual time=4.001..12.163 rows=9723 loops=1)
   Hash Cond: (r.inventory_id = i.inventory_id)
   ->  Seq Scan on rental r  (cost=0.00..310.44 rows=16044 width=36) (actual time=0.014..2.067 rows=16044 loops=1)
   ->  Hash  (cost=157.00..157.00 rows=2790 width=4) (actual time=3.915..3.918 rows=2785 loops=1)
         Buckets: 4096  Batches: 1  Memory Usage: 130kB
         ->  Hash Join  (cost=74.11..157.00 rows=2790 width=4) (actual time=0.564..2.816 rows=2785 loops=1)
               Hash Cond: (i.film_id = f.film_id)
               ->  Seq Scan on inventory i  (cost=0.00..70.81 rows=4581 width=6) (actual time=0.016..0.662 rows=4581 loops=1)
               ->  Hash  (cost=66.50..66.50 rows=609 width=4) (actual time=0.512..0.514 rows=610 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 30kB
                     ->  Seq Scan on film f  (cost=0.00..66.50 rows=609 width=4) (actual time=0.007..0.354 rows=610 loops=1)
                           Filter: (length > 100)
                           Rows Removed by Filter: 390
 Planning Time: 2.396 ms
 Execution Time: 12.817 ms
(15 rows)

INNER JOIN

------------------------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=172.62..596.63 rows=5343 width=36) (actual time=3.954..10.271 rows=9723 loops=1)
   Hash Cond: (r.inventory_id = i.inventory_id)
   ->  Seq Scan on rental r  (cost=0.00..310.44 rows=16044 width=36) (actual time=0.011..1.635 rows=16044 loops=1)
   ->  Hash  (cost=153.55..153.55 rows=1525 width=4) (actual time=3.892..3.895 rows=2785 loops=1)
         Buckets: 4096 (originally 2048)  Batches: 1 (originally 1)  Memory Usage: 130kB
         ->  Hash Join  (cost=70.66..153.55 rows=1525 width=4) (actual time=0.609..3.106 rows=2785 loops=1)
               Hash Cond: (i.film_id = f.film_id)
               ->  Seq Scan on inventory i  (cost=0.00..70.81 rows=4581 width=6) (actual time=0.013..0.632 rows=4581 loops=1)
               ->  Hash  (cost=66.50..66.50 rows=333 width=4) (actual time=0.572..0.574 rows=610 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 30kB
                     ->  Seq Scan on film f  (cost=0.00..66.50 rows=333 width=4) (actual time=0.012..0.409 rows=610 loops=1)
                           Filter: (length > 100)
                           Rows Removed by Filter: 390
 Planning Time: 1.070 ms
 Execution Time: 10.788 ms
(15 rows)

Rentalの件数が16000件しかないので、大した違いはありませんが、Planning TimeExecution TimeともにINNER JOINのSQLの方が速いことがわかります。

各SQLの実行計画で主に違う点は、一番上の計画のようです。

Hash Semi Join  (cost=191.88..653.14 rows=9771 width=36) # EXISTS
Hash Join  (cost=172.62..596.63 rows=5343 width=36)      # INNER JOIN
  • cost
    • 左側の数字: 初期処理の推定コスト。 出力段階が開始できるようになる前に消費される時間。
    • 右側の数字: 全体推定コスト。
  • rows: 行の推定数。
  • width: 行のバイト単位の長さ。

ここでもコストが初期処理の推定コスト・全体推定コスト共にINNER JOINの方が小さく、これが処理時間の違いにつながったのかと思います。

rowsが異なるのは、あくまで推定値だからということでしょうか。

まとめ

環境構築して実行したSQLの実行計画より、INNER JOINの方がEXISTSより早いことを確認することができました。

ただINNER JOINだと、SQLを読む側にとって抽出条件と分かりにくいので、件数が少ないようなテーブルであれば、EXISTSの方がよいでしょう。ケースバイケースですね。

また、PostgreSQLのTutorialのデータ量だと、パフォーマンスも問題にならず、時間がかかると実感できなかったので、同様のことをする機会があれば、今度はデータ量をもっと多くして試してみたいと思います。

参考サイト