kakakakakku blog

Weekly Tech Blog : Keep on Learning 👍

MySQL のサンプルデータセット "world" データベース と "world_x" データベースの差とは?

MySQL 関連の検証をしたり,データベース未経験者に SQL を教えたりするときに,よく MySQL 公式の「world データベース」を使っている.「国と都市と言語」を対象にしたデータセットとなり,とても使いやすいと思う.例えば「Redash ハンズオン」でも使っている.

Example Databases

MySQL 公式のデータセットは他にもある.個人的に簡単に試すなら worldを使って,ある程度の規模を必要とするなら employee と sakila を使っている.

  • employee data(従業員データ)
  • world database(国データ)
  • world_x database(国データ)
  • sakila database(DVD レンタルショップデータ)
  • menagerie database(ペットデータ)

dev.mysql.com

world と world_x の差は?

今まで「world_x データベース」を使ったことがなく,そもそも world と world_x の差を把握できていなかった.今回は world_x を調べた結果をまとめる.なお,MySQL 8.0.19 を検証環境にした.

結論から整理すると,データサイズに差はなく,スキーマに差がある.具体的には world_x データベースでは一部のデータが「JSON 型」で,Document Store として使えるようになっている.よって,MySQL の「X DevAPI」を試せる環境となり,データベース名も world_x になっていると推測できる.やはり,ハンズオンなど簡単に使うなら今後も world で良さそう.なお,テーブル情報の概要は以下に載っている.

テーブル

まず,world と world_x のテーブルを確認する.world は「計3テーブル」となり,world_x テーブルは「計4テーブル」となる.とは言え,どちらも「国」と「都市」と「言語」のデータを持っている.

mysql> SHOW TABLES FROM world;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+
3 rows in set (0.01 sec)

mysql> SHOW TABLES FROM world_x;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city              |
| country           |
| countryinfo       |
| countrylanguage   |
+-------------------+
4 rows in set (0.01 sec)

レコード

次に,world と world_x のレコード数を確認する.既に書いた通り,データサイズに差はなかった.

  • world データベース
    • city : 4079 レコード
    • country : 239 レコード
    • countrylanguage : 984 レコード
  • world_x データベース
    • city : 4079 レコード
    • country : 239 レコード
    • countryinfo : 239 レコード
    • countrylanguage : 984 レコード

SQL も残しておく.

-- world
SELECT COUNT(*) FROM world.city;
SELECT COUNT(*) FROM world.country;
SELECT COUNT(*) FROM world.countrylanguage;

-- world_x
SELECT COUNT(*) FROM world_x.city;
SELECT COUNT(*) FROM world_x.country;
SELECT COUNT(*) FROM world_x.countryinfo;
SELECT COUNT(*) FROM world_x.countrylanguage;

スキーマ

SHOW COLUMNS を使って world と world_x のスキーマを確認する.

world は一般的なスキーマとなり,city.CountryCode と countrylanguage.CountryCode が country.Code の外部キーになっている.個人的にも使い慣れたスキーマと言える.

world スキーマ

mysql> SHOW COLUMNS FROM world.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   | MUL |         |                |
| District    | char(20) | NO   |     |         |                |
| Population  | int      | NO   |     | 0       |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world.country;
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Field          | Type                                                                                  | Null | Key | Default | Extra |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
| Code           | char(3)                                                                               | NO   | PRI |         |       |
| Name           | char(52)                                                                              | NO   |     |         |       |
| Continent      | enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') | NO   |     | Asia    |       |
| Region         | char(26)                                                                              | NO   |     |         |       |
| SurfaceArea    | decimal(10,2)                                                                         | NO   |     | 0.00    |       |
| IndepYear      | smallint                                                                              | YES  |     | NULL    |       |
| Population     | int                                                                                   | NO   |     | 0       |       |
| LifeExpectancy | decimal(3,1)                                                                          | YES  |     | NULL    |       |
| GNP            | decimal(10,2)                                                                         | YES  |     | NULL    |       |
| GNPOld         | decimal(10,2)                                                                         | YES  |     | NULL    |       |
| LocalName      | char(45)                                                                              | NO   |     |         |       |
| GovernmentForm | char(45)                                                                              | NO   |     |         |       |
| HeadOfState    | char(60)                                                                              | YES  |     | NULL    |       |
| Capital        | int                                                                                   | YES  |     | NULL    |       |
| Code2          | char(2)                                                                               | NO   |     |         |       |
+----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
15 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world.countrylanguage;
+-------------+---------------+------+-----+---------+-------+
| Field       | Type          | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+-------+
| CountryCode | char(3)       | NO   | PRI |         |       |
| Language    | char(30)      | NO   | PRI |         |       |
| IsOfficial  | enum('T','F') | NO   |     | F       |       |
| Percentage  | decimal(4,1)  | NO   |     | 0.0     |       |
+-------------+---------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

MySQL Workbench で生成した ER も載せておく.

f:id:kakku22:20200217102133p:plain

world_x スキーマ

world_x は city.Info や countryinfo.doc など,「JSON 型」のカラムを持ったテーブルがある.MySQL を Document Store として使う検証環境として最適だと思う.

mysql> SHOW COLUMNS FROM world_x.city;
+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int      | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   |     |         |                |
| District    | char(20) | NO   |     |         |                |
| Info        | json     | YES  |     | NULL    |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.country;
+---------+----------+------+-----+---------+-------+
| Field   | Type     | Null | Key | Default | Extra |
+---------+----------+------+-----+---------+-------+
| Code    | char(3)  | NO   | PRI |         |       |
| Name    | char(52) | NO   |     |         |       |
| Capital | int      | YES  |     | NULL    |       |
| Code2   | char(2)  | NO   |     |         |       |
+---------+----------+------+-----+---------+-------+
4 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.countryinfo;
+--------------+---------------+------+-----+---------+-------------------+
| Field        | Type          | Null | Key | Default | Extra             |
+--------------+---------------+------+-----+---------+-------------------+
| doc          | json          | YES  |     | NULL    |                   |
| _id          | varbinary(32) | NO   | PRI | NULL    | STORED GENERATED  |
| _json_schema | json          | YES  |     | NULL    | VIRTUAL GENERATED |
+--------------+---------------+------+-----+---------+-------------------+
3 rows in set (0.01 sec)

mysql> SHOW COLUMNS FROM world_x.countrylanguage;
+-------------+---------------+------+-----+---------+-------+
| Field       | Type          | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+-------+
| CountryCode | char(3)       | NO   | PRI |         |       |
| Language    | char(30)      | NO   | PRI |         |       |
| IsOfficial  | enum('T','F') | NO   |     | F       |       |
| Percentage  | decimal(4,1)  | NO   |     | 0.0     |       |
+-------------+---------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

MySQL Workbench で生成した ER も載せておく.テーブル定義を確認すると world_x.city には外部キーの指定がないこともわかる.

f:id:kakku22:20200217102237p:plain

なお,「JSON 型」 だとデータ構造を確認しにくいため,実際のデータを JSON_PRETTY() で載せておく.city.Info はシンプルに Population(人口) のみとなる.countryinfo.doc は Name(国名) や government(政府情報) や geography(地理情報) など,様々な情報が JSON に含まれている.

mysql> SELECT JSON_PRETTY(Info) FROM world_x.city WHERE CountryCode = 'JPN' AND NAME = 'Tokyo';
+-----------------------------+
| JSON_PRETTY(Info)           |
+-----------------------------+
| {
  "Population": 7980230
} |
+-----------------------------+
1 row in set (0.01 sec)

mysql> SELECT JSON_PRETTY(doc) FROM world_x.countryinfo WHERE JSON_EXTRACT(doc, '$.Code') = 'JPN' LIMIT 1;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| JSON_PRETTY(doc)                                                                                                                                                                                                                                                                                                                                                                                                                 |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {
  "GNP": 3787042,
  "_id": "00005de917d8000000000000006d",
  "Code": "JPN",
  "Name": "Japan",
  "IndepYear": -660,
  "geography": {
    "Region": "Eastern Asia",
    "Continent": "Asia",
    "SurfaceArea": 377829
  },
  "government": {
    "HeadOfState": "Akihito",
    "GovernmentForm": "Constitutional Monarchy"
  },
  "demographics": {
    "Population": 126714000,
    "LifeExpectancy": 80.69999694824219
  }
} |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

world_x クエリ : 東京都の Population(人口) を取得する

最後にクエリサンプルを載せておく.

まず,city テーブルから東京都の Population(人口) を取得する.今回は -> オペレータを使う.

mysql> SELECT Info->'$.Population' FROM world_x.city WHERE CountryCode = 'JPN' AND Name = 'Tokyo';
+----------------------+
| Info->'$.Population' |
+----------------------+
| 7980230              |
+----------------------+
1 row in set (0.01 sec)

world_x クエリ : 日本の Region(地域) と Population(人口) を取得する

今度は日本の Region(地域) と Population(人口) を取得する.world_x の場合はテーブルが分割されているため,country テーブルと countryinfo テーブルを JOIN する必要がある.今回は -> オペレータと ->> オペレータを使う.通常の char と JSON の中にある文字列を ON で紐付けた.

mysql> SELECT ci.doc->>'$.Code', ci.doc->'$.demographics.Population', c.Capital FROM world_x.country c INNER JOIN world_x.countryinfo ci ON c.Code = ci.doc->>'$.Code' WHERE c.Name = 'Japan';
+----------------------------------------------+---------------------------------------------------+---------+
| JSON_UNQUOTE(JSON_EXTRACT(c2.doc, '$.Code')) | JSON_EXTRACT(c2.doc, '$.demographics.Population') | Capital |
+----------------------------------------------+---------------------------------------------------+---------+
| JPN                                          | 126714000                                         |    1532 |
+----------------------------------------------+---------------------------------------------------+---------+
1 row in set (0.01 sec)

なお,JSON_EXTRACT() と -> や JSON_UNQUOTE(JSON_EXTRACT()) と ->> の話は昨日の記事にまとめてある.

kakakakakku.hatenablog.com

まとめ

MySQL 公式のデータセット world と world_x の差を把握するために検証環境を構築して調べた.今後も SQL を教えたりする簡単な場面では world を使おうと思う.もし「JSON 型」などを検証する必要があったら world_x を使おうと思う.差を把握できて良かった!

MySQL で JSON 型からクオートを除去した文字列を取得するなら ->> を使う

MySQL(5.7 以降)で「JSON 型」のカラムから指定したキーを取得する場合 JSON_EXTRACT() を使う.もしくは JSON_EXTRACT() と同じ動作をする -> オペレータを使うこともできる.以下に members テーブルの info カラムから name キーを取得する SQL を載せる.なお,今回は MySQL 8.0.19 を検証環境にした.

-- サンプルテーブルを作成する
mysql> CREATE TABLE members ( info json DEFAULT NULL );
Query OK, 0 rows affected (0.10 sec)

-- サンプルデータを追加する
mysql> INSERT INTO members VALUES ('{"id": 1, "name": "kakakakakku"}');
Query OK, 1 row affected (0.10 sec)

-- サンプルデータを確認する
mysql> SELECT * FROM members;
+----------------------------------+
| info                             |
+----------------------------------+
| {"id": 1, "name": "kakakakakku"} |
+----------------------------------+
1 row in set (0.01 sec)

-- JSON_EXTRACT() を使って取得する
mysql> SELECT JSON_EXTRACT(info, '$.name') FROM members;
+------------------------------+
| JSON_EXTRACT(info, '$.name') |
+------------------------------+
| "kakakakakku"                |
+------------------------------+
1 row in set (0.01 sec)

-- -> を使って取得する
mysql> SELECT info->'$.name' FROM members;
+----------------+
| info->'$.name' |
+----------------+
| "kakakakakku"  |
+----------------+
1 row in set (0.01 sec)

JSON_UNQUOTE() と組み合わせる

実際に JSON_EXTRACT() を使って文字列を取得すると "kakakakakku" のように「ダブルクオート付き」になる.例えば CHAR や VARCHAR と比較するときに困るため,JSON_UNQUOTE() と組み合わせるとクオートを除去できる.

-- JSON_UNQUOTE(JSON_EXTRACT()) を使って取得する
mysql> SELECT JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) FROM members;
+--------------------------------------------+
| JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) |
+--------------------------------------------+
| kakakakakku                                |
+--------------------------------------------+
1 row in set (0.01 sec)

->> オペレータを使う

JSON_UNQUOTE(JSON_EXTRACT()) を使うと JSON_UNQUOTE(JSON_EXTRACT(info, '$.name')) のように長くなってしまう.箇所が多いと SQL の可読性に影響する可能性もある.そこで JSON_UNQUOTE(JSON_EXTRACT()) と同じ動作をする ->> オペレータを使うこともできる.MySQL 5.7.13 で導入された ->> オペレータの存在は今まで気付いていなかった!

-- ->> を使って取得する
mysql> SELECT info->>'$.name' FROM members;
+-----------------+
| info->>'$.name' |
+-----------------+
| kakakakakku     |
+-----------------+
1 row in set (0.01 sec)

ドキュメントを読むと ->> は equivalent to JSON_UNQUOTE(JSON_EXTRACT()) と書いてある.

名前 説明
-> Return value from JSON column after evaluating path; equivalent to JSON_EXTRACT().
->> Return value from JSON column after evaluating path and unquoting the result; equivalent to JSON_UNQUOTE(JSON_EXTRACT()).

dev.mysql.com

まとめ

  • MySQL で「JSON 型」のカラムから指定したキーを取得する場合は JSON_EXTRACT() と JSON_UNQUOTE() を組み合わせる
  • JSON_UNQUOTE(JSON_EXTRACT()) と同じ動作をする ->> オペレータを使うと短く書ける

関連記事

4年前に JSON_EXTRACT() など JSON を操作する関数を調査した記事も参考になった.とは言え,MySQL 5.7 も MySQL 8.0 も JSON を操作する関数が増えているため,もう1度調査をしておくと良さそう.

kakakakakku.hatenablog.com

Next.js Learn (Basic) を試して学んだ Next.js の基礎の基礎

React を使ってプロトタイプを実装したりすることはあるけど,今まで Next.js を試したことがなかった.そろそろ入門しなければ!という危機感もあり,Next.js Learn (Basic) を一通り試してみた.入門する前と比較すると「Next.js でできること(基礎の基礎)」を把握することができ,非常に良質なコンテンツに仕上がっていた.今回は Next.js Learn (Basic) から学んだことを整理する.Next.js に入門するならまず試してみると良いと思う.

なお,今回は Next.js 9.2.1 を使えるようにローカル環境を構築した.

$ npm view next version
9.2.1

Next.js Learn (Basic)

Basic では「計9種類」のコンテンツが用意されている.Next.js を使ったサイト内遷移(ルーティング)もあれば,共通コンポーネントの設計もある.ページを静的に最適化したり,styled-jsx を使った CSS-in-JS を実現したり,Now を使ったデプロイも学べる.SSR (Server-Side Rendering) 以外の機能も多く試せる.素晴らしい!

  • Getting Started
  • Navigate Between Pages
  • Using Shared Components
  • Create Dynamic Pages
  • Clean URLs with Dynamic Routing
  • Fetching Data for Pages
  • Styling Components
  • API Routes
  • Deploying a Next.js App

また,手順通りに進めるだけではなく,以下のように動作確認をしながら結果を選ぶクイズも出る.写経をしながら進める価値もあるし,理解度も高まるし,よく考えられているなと思う.

f:id:kakku22:20200206142229p:plain

Next.js アプリケーション

Next.js Learn (Basic) のコンテンツは全て繋がっている.ステップバイステップに学ぶことができるし,コンテンツごとに実装する Next.js アプリケーションも異なる.具体的には,以下の「計4種類」の Next.js アプリケーションを実装する.楽しいぞ!

  • Hello World
  • Blog
  • TV Shows(番組表)
  • Famous Quotes(名言集)

参考までに「Blog アプリ」と「TV Shows アプリ」のキャプチャを載せておく.

f:id:kakku22:20200206142252p:plain

f:id:kakku22:20200206142307p:plain

コードは GitHub に公開されている.

github.com

HMR (Hot Module Replacement)

Next.js では,開発環境だとデフォルトで HMR (Hot Module Replacement) が使える.コードを修正すると,すぐにページに反映されるため,実装時のストレスがなく,便利だった.

Link コンポーネント

Next.js では pages ディレクトリ直下に JavaScript ファイルを置くと,自動的に認識される.例えば pages/about.js を作成すれば http://localhost:3000/about にアクセスできるようになる.そして,サイト内遷移を実現するときには Link コンポーネントを使う.今回は「Hello World アプリ」で以下のようなコードを実装した.react-router-dom などを使わずに Next.js で書けるのは良かった.

import Link from 'next/link';

export default function Index() {
  return (
    <div>
      <Link href="/about">
        <a title="About Page">About Page</a>
      </Link>
      <p>Hello Next.js</p>
    </div>
  );
}

Create Dynamic Pages

「Blog アプリ」で2種類の「サイト内遷移」を実装する.最初は以下のような実装となり,Link コンポーネントを使って href props にクエリストリングを追加している.

import Layout from '../components/MyLayout.js';
import Link from 'next/link';

const PostLink = props => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
);

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink title="Hello Next.js" />
        <PostLink title="Learn Next.js is awesome" />
        <PostLink title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  );
}

遷移したページでは以下のような実装となり,useRouter フックを使ってクエリストリングを取得している.

import { useRouter } from 'next/router'
import Layout from '../components/MyLayout.js'

const Page = () => {
  const router = useRouter()

  return (
    <Layout>
      <h1>{router.query.title}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  )
}

export default Page

ただし,この実装だと URL は http://localhost:3000/post?title=Hello%20Next.js となり,課題が残る.そこで,Next.js の「Dynamic Routes」機能を使う.以下のような実装となり,href props は /p/[id] のように [] を使った記法になる.また as を使って [id] の値を「動的に」設定する.

import Layout from '../components/MyLayout.js'
import Link from 'next/link'

const PostLink = props => (
  <li>
    <Link href="/p/[id]" as={`/p/${props.id}`}>
      <a>{props.id}</a>
    </Link>
  </li>
)

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink id="hello-nextjs" />
        <PostLink id="learn-nextjs" />
        <PostLink id="deploy-nextjs" />
      </ul>
    </Layout>
  )
}

遷移したページの実装はあまり変わらないけど,ファイルパスが pages/p/[id].js となり,[] をそのままファイル名に使う点は新しく感じた.URL は http://localhost:3000/p/hello-nextjs となる.

pages
├── about.js
├── index.js
└── p
    └── [id].js

getInitialProps を使う

「TV Shows アプリ」を実装するときに,TVmaze API を使ってテレビ番組データを取得する.そのため,初回ロード時に API を実行する必要があり,今回は getInitialProps を使った.例えば,以下のように実装すると,Index コンポーネントの props から shows を取得できるようになる.

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman');
  const data = await res.json();

  console.log(`Show data fetched. Count: ${data.length}`);

  return {
    shows: data.map(entry => entry.show)
  };
};

次にテレビ番組データを表示するページの実装をする.以下の実装に console.log があり,このログ出力を動作確認する手順は個人的には気付きが多かった.この console.log のログは「ブラウザ側」と「サーバ側」のどちらに出力されるのか?

import Layout from '../../components/MyLayout';
import fetch from 'isomorphic-unfetch';

const Post = props => (
  <Layout>
    <h1>{props.show.name}</h1>
    <p>{props.show.summary.replace(/<[/]?[pb]>/g, '')}</p>
    {props.show.image ? <img src={props.show.image.medium} /> : null}
  </Layout>
);

Post.getInitialProps = async function(context) {
  const { id } = context.query;
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
  const show = await res.json();

  console.log(`Fetched show: ${show.name}`);

  return { show };
};

export default Post;

結果としては,トップページから画面遷移をすると「ブラウザ側」にログが表示される.そして http://localhost:3000/p/481 のように URL を直接開くと「サーバ側」にログが表示される.この挙動こそ SSR だよなー!と改めて気づけて良かった.

まとめ

  • Next.js に入門するために Next.js Learn (Basic) を一通り試した
  • Next.js でできることを把握することができる
  • 次は Next.js Learn (Excel) に進むぞ!学ぶことが無限にある!

コンテンツとステップ

最後は付録として,コンテンツごとにステップをまとめておく.多くあるように見えるけど,実際に写経(とコピペ)をしながら進めるだけなら「2,3時間」あれば十分に終わると思う.参考までー!

  • Getting Started
    • Introduction
    • Setup
    • 404 Page
    • Creating Our First Page
    • Handling Errors
    • You are Awesome
  • Navigate Between Pages
    • Introduction
    • Setup
    • Using Link
    • Client-Side History Support
    • Adding Link Props
    • Link is Just a Wrapper Component
    • Link is Simple, but Powerful
  • Using Shared Components
    • Introduction
    • Setup
    • Create the Header Component
    • Using the Header Component
    • The Component Directory
    • The Layout Component
    • Rendering Child Components
    • Using Components
  • Create Dynamic Pages
    • Introduction
    • Setup
    • Adding a list of posts
    • Passing Data via Query Strings
    • useRouter
    • Finally
  • Clean URLs with Dynamic Routing
    • Introduction
    • Setup
    • Dynamic Routing
    • History Awareness
    • Finally
  • Fetching Data for Pages
    • Introduction
    • Setup
    • Fetching Batman Shows
    • Only on the Server
    • Implement the Post Page
    • Fetch Data in Client Side
    • Finally
  • Styling Components
    • Introduction
    • Setup
    • Styling our home page
    • Styles should go inside template strings
    • Styles and Nested Components
    • No Effect for Nested Component
    • Global Styles
    • Global Styles Work
    • What Next
  • API Routes
    • Introduction
    • Setup
    • Creating an API route
    • Fetching API Routes
    • Middlewares
    • Finally
  • Deploying a Next.js App
    • Introduction
    • Setup
    • Deploying to ▲ZEIT Now
    • Deploying to Your Own Environment
    • Build and Start
    • Run two instances
    • Build Once, Run Many Instances
    • Finally

Karabiner で「command + クリック」が動かなくなったら karabiner.json を確認しよう

数日前から,突然 Karabiner で「command + クリック」の組み合わせが動かなくなってしまった.事象としては,Karabiner 単体ではなく,以下の「日本語環境設定 (Complex Modifications)」を使っている場合に起きていた.ただし,Mac で US 配列を使っているため,左右の「command」を使って入力切替をする操作は必須となり,今まで通り使えるように戻すことを考えていた.

コマンドキーを単体で押したときに、英数・かなキーを送信する。(左コマンドキーは英数、右コマンドキーはかな) (rev 3)

前提

  • macOS Mojave
  • Karabiner-Elements 12.9.0

karabiner.json を修正したら解決した

結論を先に書くと,Karabiner の設定ファイル ~/.config/karabiner/karabiner.json を開き,to -> left_command -> lazy を false に修正したら解決した.以下の karabiner.json は一部を抜粋している.もし「右 command + クリック」を使う場合は,同様に to -> right_command -> lazy も false にする.

{
  "rules": [
    {
      "description": "コマンドキーを単体で押したときに、英数・かなキーを送信する。(左コマンドキーは英数、右コマンドキーはかな) (rev 3)",
      "manipulators": [
        {
          "from": {
            "key_code": "left_command",
            "modifiers": {
              "optional": [
                "any"
              ]
            }
          },
          "to": [
            {
              "key_code": "left_command",
              "lazy": false
            }
          ],
          "to_if_alone": [
            {
              "key_code": "japanese_eisuu"
            }
          ]
        }
      ]
    }
  ]
}

なお,ターミナルから ~/.config/karabiner/karabiner.json を直接開いても良いし,Karabiner 設定画面の Misc から,右下にある「Open config folder」をクリックして,慣れたテキストエディタを使うこともできる.

f:id:kakku22:20200203210021p:plain

EventViewer

Karabiner の動作に違和感を感じたら,EventViewer を使うと問題判別を素早くできる.例えば「command」を押しながら,EventViewer の右下にある Mouse Area をクリックすれば,イベントを確認できる.

f:id:kakku22:20200203210032p:plain

正常時は以下のように click_count:1 flags:cmd となる.

type:button_down     code:0          name:button1         misc:{x:729,y:111} click_count:1 flags:cmd
type:button_up       code:0          name:button1         misc:{x:729,y:111} click_count:1 flags:cmd

動かなくなったときは click_count:1 になっていた.ようするに「command + クリック」と操作しても,実際には「command」のイベントが送信されず,認識されていなかった.

type:button_down     code:0          name:button1         misc:{x:729,y:111} click_count:1 
type:button_up       code:0          name:button1         misc:{x:729,y:111} click_count:1 

lazy とは?

最後に Karabiner のドキュメントを読んで,lazy の意味を調べておく.個人的な意訳も含めて整理すると,lazy とは「別のキーと一緒に押すまで,該当のキーのイベントを送信しない設定」と言える.より具体的に left_command + c を例にすると,lazy: true の場合は left_command を押している間はイベントを送信せず,続いて c を押したタイミングで合わせてイベントを送信する.よって,lazy と「command + クリック」の相性が悪かったんだと思う.

pqrs.org

まとめ

  • Karabiner で違和感を感じたら EventViewer を使うと問題判別を素早くできる
  • もし「command + クリック」の組み合わせが動かなくなってしまった場合は karabiner.json で lazy の設定を確認する

Mackerel : check-http コマンドの --status オプションとは?

Mackerel の go-check-plugins で check-http を使うと,エンドポイントの「チェック監視」を設定できる.なお,単純に外形監視をする場合は,Mackerel の機能で「URL 外形監視」を使える.ただし,ローカルエンドポイントを対象にするなど,check-http を使う場面もあるので,目的次第なところではある.

github.com

mackerel.io

check-http を使う

以下のように check-http コマンドを使うと,監視結果を取得できる.リターンコード 200 の場合は OK となり,リターンコード 404 の場合は WARNING となる.

# 200 の場合(サンプル)
$ check-http --url https://kakakakakku.hatenablog.com
HTTP OK: HTTP/1.1 200 OK - 214805 bytes in 0.295717 second response time

# 404 の場合(サンプル)
$ check-http --url https://kakakakakku.hatenablog.com/xxxxx
HTTP WARNING: HTTP/1.1 404 Not Found - 73031 bytes in 0.417951 second response time

なお,実際に「チェック監視」を行う場合は /etc/mackerel-agent/mackerel-agent.conf に以下(サンプル)を設定すると,Mackerel 管理画面に警告が出る.

[plugin.checks.check-http-kakakakakku-blog-200]
command = ["check-http", "--url", "https://kakakakakku.hatenablog.com/"]

[plugin.checks.check-http-kakakakakku-blog-404]
command = ["check-http", "--url", "https://kakakakakku.hatenablog.com/xxxxx"]

f:id:kakku22:20200203105724p:plain

check-http で使えるオプション

check-http には多くのオプションが実装されている.ヘルプを表示すると以下のようになる.

$ check-http --help
Usage:
  check-http [OPTIONS]

Application Options:
  -u, --url=                                          A URL to connect to
  -s, --status=                                       mapping of HTTP status
      --no-check-certificate                          Do not check certificate
  -i, --source-ip=                                    source IP address
  -H=                                                 HTTP request headers
  -p, --pattern=                                      Expected pattern in the content
      --max-redirects=                                Maximum number of redirects followed (default: 10)
      --connect-to=HOST1:PORT1:HOST2:PORT2            Request to HOST2:PORT2 instead of HOST1:PORT1
  -x, --proxy=[PROTOCOL://][USER:PASS@]HOST[:PORT]    Use the specified proxy. PROTOCOL's default is http, and PORT's
                                                      default is 1080.
      --user=USER[:PASSWORD]                          Basic Authentication user ID and an optional password.

Help Options:
  -h, --help                                          Show this help message

check-http --status とは?

ヘルプを見ただけだと,--status オプションをどう使えば良いのかわからなかった.コメントには「mapping of HTTP status」としか書かれていない.なお,GitHub の README.md を見ると,以下のようなサンプルは載っていた.何やら「特殊な構文」がありそうだとわかる.

check-http -s 404=ok -u http://example.com
check-http -s 200-404=ok -u http://example.com

整理をすると,check-http の --status オプションは,Mackerel の「チェック監視」として「特定のリターンコードの監視結果を個別に設定するとき」に使う.例えば「404 を正常 (OK) とする」など.

今回は Go の実装を読みながら --status オプションの動作を整理した.

--status オプションに設定できるフォーマット

--status オプションには,以下の通り,2種類のフォーマットを設定できる.「特定のリターンコード」を設定することもできるし,ハイフンを使って「リターンコードの範囲(以上/以下)」を設定することもできる.

${コード}=${ステータス名}
${コード}-${コード}=${ステータス名}

${コード} には 200 や 404 や 500 など,リターンコードを数値で設定し,${ステータス名} には以下の4種類から固定値を設定する.

  • OK
  • WARNING
  • CRITICAL
  • UNKNOWN

よって,以下のように設定できる.

--status 200=OK
--status 500=WARINNG
--status 200-499=OK
--status 400-599=CRITICAL

さらに --status オプションを複数設定することもできる.

--status 200=OK --status 500=WARINNG
--status 200=OK --status 400-599=CRITICAL

--status オプションのデフォルト値

なお,重要な「デフォルト値」は以下となる.ただし,ドキュメントには書いてなく,Go の実装を読んで確認したため,ドキュメントに書いてもらえると良さそう.正直言って,わかりにくかった.プルリクエストを送ることもできるけど,どに書くべきだろう?

  • < 400 なら OK
  • < 500 なら WARNING
  • それ以外 なら CRITICAL

検証環境

--status オプションの動作確認をするために,Sinatra で雑に API を書いた.指定したリターンコードを返すだけだけど,以下に app.rb の実装を載せておく.200 / 202 / 401 / 404 / 500 / 503 を返す.

require 'sinatra'

get '/200' do
  status 200
end

get '/202' do
  status 202
end

get '/401' do
  status 401
end

get '/404' do
  status 404
end

get '/500' do
  status 500
end

get '/503' do
  status 503
end

実際に curl で実行すると,適切にリターンコードを返せている.

$ curl -s -I http://localhost:4567/200 | grep HTTP
HTTP/1.1 200 OK

$ curl -s -I http://localhost:4567/202 | grep HTTP
HTTP/1.1 202 Accepted

$ curl -s -I http://localhost:4567/401 | grep HTTP
HTTP/1.1 401 Unauthorized

$ curl -s -I http://localhost:4567/404 | grep HTTP
HTTP/1.1 404 Not Found

$ curl -s -I http://localhost:4567/500 | grep HTTP
HTTP/1.1 500 Internal Server Error

$ curl -s -I http://localhost:4567/503 | grep HTTP
HTTP/1.1 503 Service Unavailable

検証

404 はデフォルトでは WARNING だけど,--status 404=OK を設定すると OK になる.

$ check-http --url http://localhost:4567/404
HTTP WARNING: HTTP/1.1 404 Not Found  - 0 bytes in 0.021076 second response time

$ check-http --url http://localhost:4567/404 --status 404=OK
HTTP OK: HTTP/1.1 404 Not Found  - 0 bytes in 0.010714 second response time

401 も 404 もデフォルトでは WARNING だけど,--status 400-499=OK を設定すると OK になる.

$ check-http --url http://localhost:4567/401
HTTP WARNING: HTTP/1.1 401 Unauthorized  - 0 bytes in 0.004647 second response time

$ check-http --url http://localhost:4567/404
HTTP WARNING: HTTP/1.1 404 Not Found  - 0 bytes in 0.021076 second response time

$ check-http --url http://localhost:4567/401 --status 400-499=OK
HTTP OK: HTTP/1.1 401 Unauthorized  - 0 bytes in 0.010508 second response time

$ check-http --url http://localhost:4567/404 --status 400-499=OK
HTTP OK: HTTP/1.1 404 Not Found  - 0 bytes in 0.013790 second response time

404 はデフォルトでは WARNING だけど,--status 400-499=CRITICAL を設定すると CRITICAL になる.

$ check-http --url http://localhost:4567/404
HTTP WARNING: HTTP/1.1 404 Not Found  - 0 bytes in 0.009807 second response time

$ check-http --url http://localhost:4567/404 --status 400-499=CRITICAL
HTTP CRITICAL: HTTP/1.1 404 Not Found  - 0 bytes in 0.006045 second response time

検証(矛盾のある設定をした場合)

最後は個人的に気になって「矛盾のある設定」を検証してみた.具体的には --status 200=OK --status 200-599=CRITICAL と --status 200-599=CRITICAL --status 200=OK のように 200 の設定を重複させてみた.OK と CRITICAL のどちらになるのだろう?

結論としては「順番通りに優先される」となる.ようするに1個目の --status が優先される.実装を読んでもそうなっている.基本的にこんな設定はしないと思うけど,警告などは出なかった.

$ check-http --url http://localhost:4567/200 --status 200=OK --status 200-599=CRITICAL
HTTP OK: HTTP/1.1 200 OK  - 0 bytes in 0.026561 second response time

$ check-http --url http://localhost:4567/200 --status 200-599=CRITICAL --status 200=OK
HTTP CRITICAL: HTTP/1.1 200 OK  - 0 bytes in 0.007202 second response time

まとめ

  • 最近 Mackerel の check-http を使う機会があった
  • 使えるオプションを確認したところ --status をどう使えば良いのかわからなかった
  • 特定のリターンコードの監視結果を個別に設定するときに使えることがわかった

今年も「Mackerel アンバサダー」として,ブログを軸にアウトプットしていくぞ!