Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

突撃!隣のリモート環境 ~推しキーボード編~

こんにちは!フロントエンドの松本です。

現在、Wizで働くエンジニアチームは殆どが在宅勤務をしております。 社内Slackの雑談チャンネルでは、ガジェットやリモート環境をシェアするチャンネルがあったりと、リモート環境のアップデートに力を入れているメンバーも少なくないです。

今回は突撃!隣のリモート環境 第1弾として、エンジニアの皆様に推しキーボードを教えてもらいました。

それではさっそくご紹介します。

【Vortexgear Tab75 茶軸】 KDMさん

f:id:yukiji_03:20211118154737j:plain

推しポイント

  • 無線
  • 84キーというちょうどいいサイズ感
  • 複数台とペアリングできる

【Niz Plum82】 SSKさん

f:id:yukiji_03:20211118155035j:plain

推しポイント

  • Win、Mac両対応
  • スイッチ : 静電容量無接点
  • キーキャップがCHERRY MX互換
  • APC機能で反応ポイントを調節できるので地味に良いです
  • サイズ感と打鍵音がスコスコという音で気に入ってます

【Keychron K8】 MTIさん

f:id:yukiji_03:20211118155232j:plain

推しポイント

  • Win、Mac両対応
  • 有線 無線切り替え可能
  • 軸も変更可能(赤、青、茶)
  • ホットスワップ対応モデルなので簡単にスイッチを交換できる
  • 別売りのパームレストが木製でかっこいい!

【Magic Keyboard(JIS)】 YKさん

f:id:yukiji_03:20211118155521j:plain

推しポイント

【HHKB HYBRID Type-S】 タコライスNさん

f:id:yukiji_03:20211119165438p:plain

推しポイント

  • 言わずもがな
  • 指が喜んでます

【7V】sevenium777さん

f:id:yukiji_03:20211119165622j:plain

推しポイント

  • やわらかい打鍵感
  • 低音で心地よい打鍵音
  • スムーズなスイッチ
  • 重い(重さは正義!)

logicool G512】DSKさん

f:id:yukiji_03:20211124101330j:plain

推しポイント

  • 光ってるw
  • うるさ過ぎない

推しじゃないポイント

  • 有線
  • テンキーあり

最後に

こうやってズラッと並べると購買欲が上がってきてしまいます。
打鍵感がいいとコードを書くのが楽しくなりそうですね。

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

Apacheのデータをrubyで整える

こんにちは。バックエンドエンジニアの河内です。

先日、Apacheのログデータの解析結果をレポートするタスクが入ってきました。われわれの業務の大半はプロダクト開発なのですが、その中には、実際に要件を策定しコードを書いていく純粋な開発タスクもあれば、その開発のための技術調査(スクラムであれば「スパイク」でしょうか)もあります。

前者はなんとなく工数を把握したうえであとは実装していく…といった感じですが(それでもスケジュール管理は欠かせません)、後者は暗中模索でおこなわなければならないタフなタスクです。

また、それとは別に、開発中の案件とは関係なく不具合などの調査が入ってくることがあるかと思います。なんとなくやることは見えていて、あとはどれだけ速くこなすか…といったタイムトライアル的なタスクです。今回のログデータ解析タスクもそんな分類になるのかな、と思っています。

作業概観

さて、ログを調べた結果、今回解析が必要なデータは以下の5行と導けました。*1

IPアドレスなど、一部の情報は適当にマスクしてます。下記の内容をlogsというファイル名で保存します。

000.000.000.000 - - [02/Nov/2021:15:41:14 +0900] "POST /awesome HTTP/1.1" 200 5009 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [04/Oct/2021:20:04:09 +0900] "POST /awesome HTTP/1.1" 200 4868 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 11; SO-03L Build/55.2.D.0.447; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/94.0.4606.61 Mobile Safari/537.36 Line/11.17.1/IAB"
000.000.000.000 - - [04/Oct/2021:19:49:50 +0900] "POST /awesome HTTP/1.1" 200 4870 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari Line/11.17.0"
000.000.000.000 - - [04/Oct/2021:18:11:32 +0900] "POST /awesome HTTP/1.1" 200 4865 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [03/Oct/2021:01:33:15 +0900] "POST /awesome HTTP/1.1" 200 4866 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 9; SO-04J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36"

前回、別のメンバーが同様の対応をおこなったときのフォーマットは以下のとおりだったそうなので、それに従います。

・アクセス日時: 2021-01-01 00:00:00
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.8
    ブラウザ: Google
    ブラウザのバージョン: 181.0.401558652
    デバイス: iPhone

あとは文字列を拾って特定フォーマットに落とし込めばよいのですが…対象は複数行あります。目で追って手で拾う…というのは不正確ですし疲れるので避けたいです。

また、こういうタスクは一回やりおおせても、おかわりとして同じような作業依頼が再びやってくるのが常です。

せっかくテキストは規則的に並んでいることですし、ちゃちゃっとコードを書くことにします。こういうときに自分はrubyを使うことが多いです*2

今回のようなタスクを何度かこなしているので、rbenvで特定バージョンのrubyが動作する環境が用意されています(今回の例はruby 2.6.3p62)。

作業ディレクトリの中は、以下のような感じに配備しました。logsは先ほどの5行のログが書かれたファイルで、処理をおこなうreport.rbがあり、処理後にレポート内容がREPORT.txtに吐き出されるようにします。

./
  logs
  report.rb
  REPORT.txt(report.rbにより生成される)

コードを書いていく

ログをパースする

それでは、report.rbにコードを書いていきます。Apacheログのパーサーライブラリを使い、以下のように書きました。ログフォーマットは、Apacheのものをそのまま流用できるようです。

require 'apachelogregex'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)
    puts result; exit # 出力確認
end

コードで一行目パースしたところで止めてますが、こんな感じで取得できます。

{"%h"=>"000.000.000.000", "%l"=>"-", "%u"=>"-", "%t"=>"[02/Nov/2021:15:41:14 +0900]", "%r"=>"POST /awesome HTTP/1.1", "%>s"=>"200", "%b"=>"5009", "%{Referer}i"=>"https://xxx.dev/awesome", "%{User-Agent}i"=>"Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"}

日時を読みやすくする

Apacheの日時をrubyでフォーマットする…誰かやってそうですよね? されてる方がいらっしゃったので拝借します。

require 'apachelogregex'
+ require 'time'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

+    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")
    puts datetime; exit # 出力確認
end

日時も読みやすくなりました。

2021-11-02 15:41:14

ホスト名を取得する

require 'apachelogregex'
require 'time'
+ require 'resolv'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

+     host_name = Resolv.getname(result["%h"])
    puts host_name; exit # 出力確認
end

サンプルではIPアドレスが適当ですが、実際のIPでアドレスではホスト名も取得できました。

ユーザーエージェントから各種情報を取得する

ユーザーエージェントはとくに目で見ての作業はめんどうなので、ライブラリに任せましょう。

require 'apachelogregex'
require 'time'
require 'resolv'
+ require 'user_agent_parser'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

    host_name = Resolv.getname(result["%h"])

+     user_agent_part = result["%{User-Agent}i"]
+     user_agent = UserAgentParser.parse(user_agent_part)

+     os_name, os_version = user_agent.os.to_s.split(" ")

+     browser_version = user_agent.version.to_s
+     browser_name = user_agent.family.to_s

+     device_name_family = user_agent.device.family.to_s

+     device_name = device_name_family == "iPhone" ? device_name_family : "#{user_agent.device.brand} #{user_agent.device.model}"
end

iPhoneAndroidのときの表示で違和感があったので、条件分岐してます*3

整形する

仕上げに入っていきます。フォーマットに従い出力します(ログ一行ごとのブロックで適当に仕切り線を入れて表示)。

require 'apachelogregex'
require 'time'
require 'resolv'
require 'user_agent_parser'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

    host_name = Resolv.getname(result["%h"])

    user_agent_part = result["%{User-Agent}i"]
    user_agent = UserAgentParser.parse(user_agent_part)

    os_name, os_version = user_agent.os.to_s.split(" ")

    browser_version = user_agent.version.to_s
    browser_name = user_agent.family.to_s

    device_name_family = user_agent.device.family.to_s

    device_name = device_name_family == "iPhone" ? device_name_family : "#{user_agent.device.brand} #{user_agent.device.model}"

+     text = <<~"TXT"
+         ・アクセス日時: #{datetime}
+         ・IPアドレス: #{result['%h']}
+         ・IPアドレスのホスト名: #{host_name}
+         ・UserAgentからの情報
+             OS: #{os_name}
+             OSのバージョン: #{os_version}
+             ブラウザ: #{browser_name}
+             ブラウザのバージョン: #{browser_version}
+             デバイス: #{device_name}
+     TXT
end

+ File.write('REPORT.txt', texts.join("-" * (50) + "\n"))

出力結果は以下のような感じです。

・アクセス日時: 2021-11-02 15:41:14
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.8
    ブラウザ: Google
    ブラウザのバージョン: 181.0.401558652
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 20:04:09
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: Android
    OSのバージョン: 11
    ブラウザ: LINE
    ブラウザのバージョン: 11.17.1
    デバイス: SonyEricsson SO-03L
--------------------------------------------------
・アクセス日時: 2021-10-04 19:49:50
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.2
    ブラウザ: LINE
    ブラウザのバージョン: 11.17.0
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 18:11:32
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.7.1
    ブラウザ: Mobile Safari
    ブラウザのバージョン: 14.1.2
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-03 01:33:15
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: Android
    OSのバージョン: 9
    ブラウザ: Chrome Mobile
    ブラウザのバージョン: 94.0.4606.61
    デバイス: SonyEricsson SO-04J
--------------------------------------------------
・アクセス日時: 2021-10-02 13:52:21
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.7.1
    ブラウザ: Mobile Safari UI/WKWebView
    ブラウザのバージョン: 
    デバイス: iPhone

まとめ

タイムトライアル的なタスクは、ありもののライブラリや先人のナレッジ、自分の過去のメモを生かしてさくっと解決していく必要があります。一方で、一回だけでなく何度かおかわりが来ることを想定し、再現できるようにしておくとよいのかな、と思います。

最後に

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

*1:この行を特定するのにも要件からDBから特定条件でクエリを投げ、得られた日時とログデータのアクセス日時を突合しなければならない…というタスクがありました。

*2:みなさんも、手になじんだ言語があるのではないでしょうか。

*3:たとえば、iPadなどはなかったので今回は考慮してません。アプリ開発でないので、とりあえず条件を満たして早さ優先。

express(nodejs)+node.jsからmysqlに接続してデータベースを作成する

f:id:thunder_fury:20211115154050p:plain

はじめに

皆さんこんにちは、フロントエンドエンジニアのWooです。⚡️🌪

バックエンドはexpress(Node.js)を使ってMySqlに接続しクライアント側はNext(react)を使って簡単データベースを作成してみたいと思います。

自分の場合はsql文が分からないのではデータベース管理するMySQLWorkbenchを使いました。

express

expressは、Webおよびモバイルアプリケーションのための一連の強力な機能を提供する、簡潔で柔軟なNode.js Webアプリケーションフレームワークです。事実上Node.jsの標準的なWebサーバフレームワークに付けて起動だけ多くの場所で使用されているようです。

expressjs.com

MySQLWorkbenchとは

公式からの説明

MySQL Workbench は、データ・モデリングSQL 開発、およびサーバー設定、ユーザー管理、バックアップなどの包括的な管理ツールを提供します。MySQL Workbench は WindowsLinuxMac OS X で利用可能です。

データベース自分の好みで管理ツールを導入しても良いと思います。自分は「Mac MySQL Sequel Pro」と「MySQLWorkbench」どちらかで悩みましたが「MySQLWorkbench」にしました。

sequelpro.com www.mysql.com

データベーステーブル

MySQLWorkbenchを使ってmemberスキーマを作成しidは重複できないようにして user_email user_name password三つのテーブルを用意しました。

f:id:thunder_fury:20211115143710p:plain

全体ディレクトリー構成

├── client ( Next.js基本ディレクトリー)
└── api
     ├── config
     |     └──database.js
     └── server.js

パッケージinstall

 $ npm init -y
 $ npm i mysql
 $ npm i express

npm初期化の後mysqlとexpressをインストールします。

データベースconnection

データベースのconnection処理が必要です。 mysqlをimportして自分はデータベースの情報を返してくれる共通関数して使い回しできるように書いてみました。

データベース情報はセキュリティのためenvに書いた方が良いです。

api/config/database.js

const mysql = require('mysql');

const database = () => {
  const connection = mysql.createConnection({
    host: `${process.env.MYSQL_HOST}`,
    user: `${process.env.MYSQL_USER}`,
    password: `${process.env.MYSQL_PASSWORD}`,
    database: `${process.env.MYSQL_DATABASE}`,
  });
  return connection;
};

exports.database = database;

POST API用意

/api/sign_up のルートにポストする場合「member」のデータベースに格納できるように書いています。

api/server.js

const express = require('express');
const app = express();
const port = process.env.PORT || 3090;

app.post(`/api/sign_up`, (req, res) => {
    const { email, password, user_name } = req.body;
    const params = [email, password, user_name];
    const sql = `INSERT INTO member VALUES (null, ?, ?, ?)`;
    database().connect();
    database().query(sql, params, (err, rows, fields) => {
      res.header(`Content-Type`, `application/json; charset=utf-8`);
      res.status(200).send({ reow: rows });
    });
    database().end();
  });

app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

INSERT INTO member VALUES (null, ?, ?, ?) は上記MySQLWorkbenchから作成したテーブルと繋いでいます。(null, ?, ?, ?) は(id, user_mail, user_name, password )です。

ここまでバックエンドの処理は完了となります。

server立ち上げ

apiの直下で実行

 node ./server.js

これでAPIの使用が可能になります。

ポスト入り口用意

nodejs.org

データベースにPOSTするため簡単なフォーム作成します。

import { useState } from 'react'
import Axios from 'axios'
import { css } from '@emotion/react'
export const SignUp = () => {
  const [ userEmeil, setUserEmail] = useState(``)
  const [ userPassword, setUserPassword] = useState(``)
  const [ userName, setUserName] = useState(``)
  const submit = async () => {
    console.log(userPassword,userEmeil )
    await Axios.post(`/api/sign_up`, { 
      email: userEmeil,
      password: userPassword,
      user_name: userName
    })
      .then(res => {
        console.log(res)
      }).catch(err =>{
        console.log(err)
      })
  }
  return (
    <div css={css`
      max-width: 300px;
      width: 100%;
      margin: 0 auto;
    `}>
    <>
      <h1>Sign Up</h1>
      <div>
        <label htmlFor={`user_email`}>Mail Address : </label>
        <input
          id={`user_email`} 
          type={`email`}
          name={`user_email`}
          onChange={(e:React.ChangeEvent<HTMLInputElement>) => {
          setUserEmail(e.target.value)
          }}
        />
      </div>
      <br />
      <div>
        <label htmlFor={`user_name`}>User Name : </label>
        <input 
          id={`user_name`} 
          type={`text`} 
          name={`user_name`}
          placeholder={``}
          onChange={(e:React.ChangeEvent<HTMLInputElement>) => {
            setUserName(e.target.value)
          }}
        />
      </div>
      <br />
      <div>
        <label htmlFor={`password`}>password : </label>
        <input 
          id={`password`}
          name={`password`}
          type={`password`}
          onChange={(e:React.ChangeEvent<HTMLInputElement>) => {
            setUserPassword(e.target.value)
          }}
        />
      </div>
      <button
        css={css`
          text-align: center;
          background: black;
          color: white;
          padding: 5px;
          margin-top: 10px;
        `}
        onClick={submit}
      >
      Sign Up
      </button>
    </>
      
    </div>
  )
}

export default SignUp

このようなサブミットフィールドになります。 f:id:thunder_fury:20211115144115p:plain

データベース確認

f:id:thunder_fury:20211115145008p:plain MySQLWorkbenchを確認してみたらちゃんとデータは格納されてるのが確認できました。⚡️🌪 今回のPOST+Nodemailerと組み合わせをして自動返信メール機能を入れても🤔良さそうな気がしました。


最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp

Nuxt Bridgeを使ってみて使用法、所感まとめ

こんにちは、フロントエンドエンジニア小玉です。

先日Nuxt3ベータ版のリリースが発表されましたね。 Nuxt3ではVue3Viteのサポートに加えて。新しいサーバエンジンが搭載されるそうです。 いくつか新しい機能やアップデートがなされた中で、今回はNuxt Bridgeについて試してみたいと思います。

Nuxt Bridge

こちらは端的に言うと、Nuxt2を使用しているプロジェクトをよりスムーズにアップグレードするためのシステムです。

公式には以下のように書かれてます

Bridge is a forward-compatibility layer that allows you to experience many of new Nuxt 3 features by simply installing and enabling a Nuxt module.

(Bridgeは、Nuxtモジュールをインストールして有効にするだけで、Nuxt3の新機能の多くを体験できる上位互換性レイヤーです。:google翻訳

主な機能として

  • Nitroサーバーが使用できる
  • CompositionAPIが使用できる(Nuxt3と同じ)
  • 新しいCLIとDevtoolsが使用できる

などなど。 これまでNuxt2を使用していたプロジェクトをアップグレードするためにぜひ活用したいサービスですね。

使用手順

では早速公式に則ってNuxt Bridgeを使用してNuxt2プロジェクトをアップグレードしてみたいと思います。

Nuxt Bridgeのインストール

$ yarn add --dev @nuxt/bridge@npm:@nuxt/bridge-edge
or
$ npm install -D @nuxt/bridge@npm:@nuxt/bridge-edge

※余談ですが、僕はnodeのバージョンが古くてインストールに一度失敗しました。

The engine "node" is incompatible with this module. Expected version "^14.16.0 || ^16.11.0 || ^17.0.0". Got "15.8.0"

nodeのバージョンは上げておきましょう…。

スクリプトの更新

Nuxt3では新しくnuxiというCLIが導入されました。 そちらを使用するためにpackage.jsonを以下のように更新します。

  "scripts": {
-   "dev": "nuxt-ts",
+   "dev": "nuxi dev", //nuxi のみではダメ
-   "build": "nuxt-ts build",
+   "build": "nuxi build",
+   "start": "node .output/server/index.mjs",
-   "generate": "nuxt-ts generate",
+   "generate": "nuxi generate",
  },
"dependencies": {
-   "nuxt": "^2.15.7"
+   "nuxt-edge": "latest"
  },

nuxt.config.js

module.exportsrequireなど、Common.jsがサポートされなくなるそうなので、 nuxt.config.jsを以下のように書き換える必要があります。

export default {
  ssr: false,
 ......
}
import { defineNuxtConfig } from '@nuxt/bridge'

export default defineNuxtConfig({
  ssr: false,
 ......
})

以上で設定は完了です。

まとめ

今回は先日リリースされたNuxt Bridgeを試してみると言うことで、 自分はNuxt2を使用したいくつかのプロジェクトをNuxt Bridgeでアップグレードしてみました。 大方問題なくアップグレードができましたが、もちろんベータ版ということもあり、

  • tailwindcss が非対応
  • @nuxt/content(1.x)がサポートされない、(2.x)に関しては書き換えが必要

とのことでした。 自分は@nuxt/contenttailwindcssを使用したブログも作っていたのでそれはうまくアップグレードできませんでした。

未だ対応していないモジュールもありますが、Nuxt3ではNitroエンジンの搭載やTypescriptのサポート、Auto Importなど恩恵は数々あります。正式版のリリースが期待できますね。

v3.nuxtjs.org

最後に

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

MySQLの実行計画について

こんにちはバックエンドエンジニアの小室です。

業務では主にLaravelを使って実装しています。現在担当している案件の検索機能の実装が複雑でクエリビルダーでは事足りず生SQLで実装する必要があり、改めてSQLの重要性を実感しました。そこで最近学んだSQLの実行計画について簡単にまとめてみました。

実行計画

実行計画とは、テーブルに対して検索をかけた際、どういった手順を踏んでアクセスしたかを示す実行手順書のようなものになります。

データ量や統計情報(オプティマイザ統計)などの情報をもとに、最適な実行計画がされますが、 同じSQLであればいつも同じ実行計画が作成されるとは限らず、データ量が大きく変更されたときや、統計情報が古いままだと適切な実行計画が作成されず、パフォーマンスが低下する場合もあります。

実行計画の確認方法

mysqlで実行計画を確認するにはselect文の先頭に「EXPLAIN」をつければ確認できます。

EXPLAIN 
    SELECT departments.id, departments.name d_name, companies.name c_name 
    FROM departments 
    INNER JOIN companies
    ON departments.company_id = companies.id

以下の様な表が表示されます。

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE departments ALL departments_company_id_foreign 9 100.0
1 SIMPLE companies eq_ref PRIMARY PRIMARY 8 practice_db.departments.company_id 1 100.0

DBeaverというツールを使えば、実行計画をツリー構造で表示する事もできます。「https://dbeaver.io/」からインストールできます。 ほぼ全てのDBに対応しており、実行計画の表示から、ER図の自動作成など便利な機能が豊富です。

f:id:shuto_komuro:20211106152701p:plain
DBeaver-実行計画

各カラムの説明

  • id

SELECT 識別子を表し、クエリー内の SELECT の連番になります。

  • select_type

SELECTの種類を表し、SIMPLE,PRIMARY,UNIONなどがあります。

  • table

出力の行で参照しているテーブルの名前を示します。

  • partitions

クエリーでレコードが照合されるパーティションを示します。

  • type

結合のタイプを表し、ALL,CONST,eq_refなどがあります。

  • possible_keys

テーブル内の行の検索に使用するために選択できるインデックスを示します。

  • key

実際に使用することを決定したキー (インデックス) を示します。

  • key_len

実際に使用することを決定したキーの長さを示します。

  • ref

テーブルから行を選択するために、key カラムに指定されたインデックスに対して比較されるカラムまたは定数を示します。

  • rows

クエリーを実行するために調査する行数を示します。(推定数)

  • filtered

テーブル条件によってフィルタ処理されるテーブル行の推定の割合を示します。

  • Extra

クエリーを解決する方法に関する追加情報が含まれます。(where句など)

より詳しい説明は以下の公式リファレンスを参照してください。 MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.8.2 EXPLAIN 出力フォーマット

チューニングサンプル

unionとcase分

以下のような都市別、男女別の人口を示すpopulationsテーブル(table1)があるとします。

このテーブルから、都市別に性別を1レコードにまとめた結果(table2)を

出力したいとします。

table1

id city_name sex population
1 都市1 1 63
2 都市1 2 99
3 都市2 1 39
4 都市2 2 93
5 都市3 1 42
6 都市3 2 32
7 都市4 1 38
8 都市4 2 67
9 都市5 1 79
10 都市5 2 59

table2

city_name p_men p_wom
都市1 63 99
都市2 39 93
都市3 42 32
都市4 38 67
都市5 79 59

unionを使った解

都道府県別に男性の合計値を求めた後、都道府県別に女性の合計値を求め それらの結果をマージするという手順になると思います。

sqlは以下の様になります。

SELECT city_name, sum(p_men) AS p_men, sum(p_wom) AS p_wom 
FROM (
    SELECT city_name, population AS p_men, NULL AS p_wom 
    FROM populations WHERE sex = 1
    UNION 
    SELECT city_name, null AS p_men, population AS p_wom 
    FROM populations WHERE sex = 2
) tmp 
GROUP BY city_name

以下の実行計画が出力されます。

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY ALL 4 100.0 Using temporary
2 DERIVED populations ALL 10 10.0 Using where
3 UNION populations ALL 10 10.0 Using where
UNION RESULT <union2,3> ALL Using temporary

populationsテーブルに対してフルスキャンが2回実行されていることがわかります。

UNIONを使えば、問題を小さなサブ問題に分割して考えることができますが、

内部的に複数のSELECT文を実行する実行計画として解釈されるためI/Oコストが膨らみませす。

CASE式を使った解

CASE式を使えばアクセスを1回に減らしコスト改善が可能です。

CASE式を集約関数内に収め、男性だけの人口と女性だけの人口の列を作る方法です。

sqlは以下の様になります。

SELECT
    city_name,
    sum(CASE WHEN sex = 1 THEN population ELSE 0 end) AS p_men, 
    sum(CASE WHEN sex = 2 THEN population ELSE 0 end) AS p_wom
 FROM populations
GROUP BY city_name

以下の実行計画が出力されます。

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE populations ALL 10 100.0 Using temporary

populationsテーブルに対してフルスキャンが1回のみとなり、UNIONを使った解に比べ1/2のI/Oコストで済みました。

このように、実行計画を通しアクセスパスを確認する事で冗長なSQL文を改善することができました。

今まで、フレームワークのクエリビルダに頼りきりでしたが、

SQLを遅延させないためにも、実行計画を意識する習慣をつけていけたら良いなと思います。

最後に

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

Next.jsのmiddlewareを使ってbasic認証を実装する

こんにちは、フロントエンドエンジニアの髙橋です。

先日Next.js ConfでNext.js 12は発表されましたね!

色々な新機能が追加されましたが、新機能のMiddlewareが気になって試してみたのでその使い方などを書いてみようと思います。

middlewareとは?

公式には以下のように書かれております!

Middleware enables you to use code over configuration. This gives you full flexibility in Next.js because you can run code before a request is completed.

リクエスト完了前に特定のコードを実行できる機能のようです。

*公式ドキュメントはこちら

例えば、こういった場合↓に使用することができるそうです。 f:id:sotq17:20211105150022p:plain

何を試したか

今回は1番に例に上がっているAuthentication(認証)を試してみました。 具体的に言えば、Basic認証をNext.jsにつける、という作業です。

f:id:sotq17:20211105152843g:plain

Next.jsで作ったサイトをVercelにあげる場合、コンテンツに制限をかけることは通常有料となってしまいます。

ライブラリを導入して、Basic認証をつけることも可能なようですが、公式の機能でできればそれが一番なのでは無いかと思っております!

そんなわけで、早速試してみたいと思います!

実装

プロジェクト/ファイル作成

npx create-next-app@latest --ts

create-next-appで雛形を作ったあと…

f:id:sotq17:20211105153303p:plain

pages/_middleware.tsを作成します。 こちらがmiddlewareを扱うファイルとなります。

認証処理を書いていく

早速ですが、以下のコードを貼り付けるだけでOKです! (解説はコード内に記載します)

import { NextRequest, NextResponse } from 'next/server'

export const middleware = (req: NextRequest) => {
  const basicAuth = req.headers.get('authorization')
  //HeaderにAuthorizationが定義されているかをチェック
  if (basicAuth) {
    const auth = basicAuth.split(' ')[1]
    const [user, pwd] = Buffer.from(auth, 'base64').toString().split(':')

    // basic認証のUser/Passが、envファイルにある値と同じかをチェック
    if (user ===  process.env.NEXT_PUBLIC_USER && pwd === process.env.NEXT_PUBLIC_PASS) {
      return NextResponse.next()
    }
  }

  // 同じでなければエラーを返す
  return new Response('Auth required', {
    status: 401,
    headers: {
      'WWW-Authenticate': 'Basic realm="Secure Area"',
    },
  })
}
// .env.local
NEXT_PUBLIC_USER=XXXXX
NEXT_PUBLIC_PASS=XXXXX

これでローカルを立ち上げればBasic認証がかかるはずです!

公開する

当たり前ではありますが、Vercel上にenvファイルは置けないので環境変数を設定します。

以下の通りに設定すればlocal同様にBasic認証がかかります。

f:id:sotq17:20211105154918p:plain

まとめ

実際に作ってみて、想像の何倍も簡単に実装することができたと思っています。

相変わらずのVercel依存はありますが、ここまで便利になるなら使わない手はないのでは?と最近考えるようになりました。

ちなみにこちらがサンプルのGitHubです!

Next.js 12ではmiddlewareの他にすごい機能がたくさんあります。(SWCめっちゃ早いです…!)

気になる方は他の機能もチェックしてみてはいかがでしょうか。

最後に


Wizではエンジニアとして一緒に働く仲間を絶賛募集しております。

ご興味のある方、是非ご覧下さい..!!

careers.012grp.co.jp

Mock Service Workerを使ってOpenAPIに寄り添ったテストを行う。

Tech事業部プロダクトチームの仲本です。

10月から、フロントエンドチームからプロダクトチームになりました。

今回は、現在開発しているプロダクトに、テストを導入するにあたって、Mock Service Workerを導入し、OpenAPIと組みわせてMockAPIを作成しテストを作成しました。

今回の記事は導入記録/所感的なものを書いています。

テストを導入する経緯

  • フロントエンドのテストがまずできていなかったこと
  • 社内業務案件で、リリースのたびにチームで手作業でテストを行っていたこと
  • 今後規模が大きくなると、手作業でのテストが辛くなる

こういった経験のもと、テストを導入し安全性を確保したいということになりました。

jest.mockによるmock化がかなり多くなる話

実際にjestを導入して、テストを書いていくとmock化しないと通らないテストがあること気づきました。

apiを実行する際のfetchの処理などもmock化させると、実際の動きと遠くなるなるのではないかという懸念があり調べているとMock Service Workerに出会いました。

Mock Service Workerとは

Service WorkerAPIを使用して実際のリクエストをインターセプトするAPIモックライブラリです。

ユーザーログイン画面のテスト実装

今回はログイン画面のテスト実装していきます。

Login画面で表示するコンポーネントが以下になります。

import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { jsx } from '@emotion/react'
import { useForm, UseFormReturn } from "react-hook-form"
import { BrowserRouter as Router, useHistory } from 'react-router-dom'
import { hot } from 'react-hot-loader'

import {
  fetchAsyncLogin,
  selectError,
} from '../../../stores/slices/authSlice'


// component
import { Button } from '../../components/atoms/Button'

// style
import { LoginBox, LoginTitle } from '../../../style/pages/Login'
import { Form, FormLabel, FormInput, FormButton, FormValidButton, FormValidTxt } from '../../../style/components/block/Form'


//type
import { AppDispatch } from '../../../stores';
import { LoginFormInput } from '../../../../types/user';

const Login = () => {
  const methods: UseFormReturn<LoginFormInput> = useForm<LoginFormInput>();
  const { register, handleSubmit, formState: { errors }, reset } = methods
  const dispatch: AppDispatch = useDispatch()
  const history = useHistory()

  const onSubmit = async (data: LoginFormInput) => {
    const result = await dispatch(fetchAsyncLogin(data))
    if (fetchAsyncLogin.fulfilled.match(result)) {
      if (result.payload.status == 200) {
        history.push('/')
      }
    }
    reset()
  }

  return (
    <div>
      <div css={LoginBox}>
        <form css={Form} onSubmit={handleSubmit(onSubmit)}>
          <label css={FormLabel}>
            メールアドレス
            <input
              autoComplete="email"
              type="email"
              aria-invalid={errors.mailAddress ? "true" : "false"}
              {...register("mailAddress", { required: true, pattern: /^([a-zA-Z0-9])+([a-zA-Z0-9\._+-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/ })}
              placeholder="メールアドレスを入力してください"
              css={FormInput}
            />
          </label>
          <label css={FormLabel}>
            パスワード
            <input
              autoComplete="password"
              type="password"
              {...register("passWord", { required: true, pattern: /^[a-z\d]{1,100}$/i })} aria-invalid={errors.passWord ? "true" : "false"} placeholder="パスワードを入力してください"
              css={FormInput}
            />
          </label>
          <Button name="ログイン" cssStyle={FormButton} onClick={handleSubmit(onSubmit)} dataTestId="test-2-submit-btn"  />
        </form>
      </div>
    </div>
  )
}
export default hot(module)(Login)

要件としては

  • メールアドレスが入力できる

  • パスワードが入力できる

  • ログインボタンを押すとログイン用APIにPOSTする

流れになります。

レスポンスデータをOpenAPI定義から取得する

現在携わっているプロジェクトでは、OpenAPIを使用してAPIのやりとり/認識合わせを行なっています。 OpenAPI仕様が記載されたjsonファイルを使用し、Mock Service Workerの設定を行います。

そうすることで、より使用されているAPIを忠実に再現できることや、もし仕様の変更があった際もしっかり新しい情報の入ったOpenAPIを取り込めていたら気付けるのではないかと思い使用しました。

以下がOpenAPIで定義している内容になります。

// openapi.json

"paths": {
  "/api/login": {
// ~~省略~~
  "responses": {
    "200": {
      "description": "HTTP OK",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
// ~~省略~~
            "examples": { // <= 今回レスポンスで使用するデータ
              "default": {
                "value": {
                  "status": 200,
                  "response_time": 1.1535649299621582,
                  "message": "処理が正常終了しました。",
                    "data": {
                      "login_id": "test@hoge.co.jp",
                      "token": "123|njGYLOG9EuZuIrSv83dUvnIWzFLbo6Ri5mUOLm4q",
                    }
                 }
              }
            }

上記のjsonファイルをimportし、examplesを以下のファイルでimportします。

この設定をすることにより、jsonファイルに変更があった際に変更に対応してくれます。

import schema from '../../openapi.json'

const components = {
  LoginUser: schema.paths['/api/login'].post.responses[200].content['application/json'].examples.default.value,
}
export default components 

Mock Service Workerでhandlerを設定

次にmockを作成していきます。

mswからrestをimportしgetやpostの設定を行います。

今回はpostの設定を行います。

参考: mswjs.io

以下ファイルで定義している内容としては

  • login_idがtest@hoge.co.jpかつpasswordがtest1234@

  • 上の条件を満たしていた時ステータス200と、先ほど定義したexamplesを返す設定をしています。

import { rest } from 'msw'
import components from './components'

const handlers = [
  rest.post<Record<string, any>>('http://localhost:3000/api/login', (req, res, ctx) => {
    const { login_id, password } = req.body
    if (login_id === 'test@hoge.co.jp' && password === 'test1234@') {
      return res(
        ctx.status(200),
        ctx.json(components.LoginUser)
      )
    } else {
      return res(
        ctx.status(403),
        ctx.json({
          error: 'error: invalid username or password'
        })
      )
    }
  }),
]

export { handlers } 

jest.setup.js

jest.setup.jsというファイルで、先程設定をしたmockを動かすためのserverの設定と

node環境だと、window.fetchが使えないので node-fetchをinstallして設定を行います。

github.com

import server from './src/test/lib/msw/server'
import { cleanup } from '@testing-library/react'
import fetch from "node-fetch"

beforeAll(() => server.listen())
afterEach(async () => {
  server.resetHandlers()
})
afterAll(() => server.close())

if (!globalThis.fetch) {
  globalThis.fetch = fetch
}

実際のテストコード

import React from 'react';
import '@testing-library/jest-dom/extend-expect'
import { fireEvent, getAllByText, render, screen, waitFor } from '@testing-library/react'
import { Provider } from 'react-redux';
import { act } from 'react-dom/test-utils';

import store from '../tsx/stores';

import Login from '../tsx/views/pages/login/Login'


const LoginComponent =
  <Provider store={store}>
    <Login />
  </Provider>

const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
  useHistory: () => ({
    push: mockHistoryPush, // pushメソッドをダミー関数で上書きする。
  }),
}));

〜〜省略〜〜
describe('test 2 ログイン処理', () => {
  it('ログイン確認', async () => {
    const { getByTestId } = render(LoginComponent)

    await act(async () => {
      fireEvent.change(screen.getByLabelText(/メールアドレス/i), {
        target: { value: 'test@hoge.co.jp' },
      });

      fireEvent.change(screen.getByLabelText(/パスワード/i), {
        target: { value: 'test1234@' },
      })
    });

    await act(async () => {
      fireEvent.submit(getByTestId('test-2-submit-btn'))
    });

    await waitFor(() => {
      expect(mockHistoryPush).toBeCalledWith('/');
    })
  });
}) 

ログインボタンを押した時のAPI処理のみのテストを表示しています。

ログインボタンを押すと先程handlerで設定したMockを実行するようになっています。

以上がユーザーログイン画面のテスト実装でした。

今後考えていきたいこと

  • どの範囲をテストしていくべきかを明確にする
  • 別のプロジェクトに導入するために、どういった開発手法がいいかを考えていく
  • まだ導入したてなので色々実装していきながらベストを探していきたい

上記をまず、明確にできるように頑張っていきたいと思います。

参考記事

OpenAPI定義をmswに活用してお手軽モック

Mock Service Worker で jest.mock を使わず非同期リクエストのテストを書く

最後に

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp